写了一个月应用层代码,感觉写呕了,最近在研究插件化动态加载方面的东西。
没错就是360的开源库:DroidPluginTeam
还有一位大神写的很好的源码分析总结:understand-plugin-framework
本文主要对第一篇:Android插件化原理解析——Hook机制之动态代理 遇到的一些问题以及解决最后的作业部分,并记录下作为学习心得笔记。对于刚接触这个的学者,可以起到一定作用避免少走弯路吧~建议先下载其源代码,然后结合本文来看。
本文需要解决的作业:在Activity自身的跳转中进行Hook。
先简要说下遇到的几个坑以及后面的学习整理:
Activity 启动流程和Context类详解:
关于Context的理解可以先参考以下两篇博客:
Android中Context详解 ---- 你所不知道的Context
比较重要的点是要理清Context的继承关系:
由图可一目了然Activity的继承关系,由于Activity是继承自Context的包装类ContextWrapper的,在我们的Activity得到Context对象时,会通过重写的attachBaseContext方法得到Context实例。
这也就是在第一个例子中为什么在attachBaseContext中执行hook方法的原因:
然而这里要特别注意一点,作者的tainn的例子中使用的是ApplicationContext,然而作业使用到的并不是ContextImpl的mInstrumentation而是自己的mInstrumentation,我们来看看Activity中调用attachBaseContext调用的地方:
可以看到attachBaseContext是在Activity的成员变量mInstrumentation初始化之前进行调用的,所以不能像作者一样在attachBaseContext中执行hook方法,这里我选择了在执行startActivity之前调用以保证能hook到。
作业流程:
1.首先修改MainActivity中的点击事件和hook触发方法的:
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // TODO: 16/1/28 支持Activity直接跳转请在这里Hook // 家庭作业,留给读者完成. LinearLayout linearLayout = new LinearLayout(this); linearLayout.setOrientation(LinearLayout.HORIZONTAL); LinearLayout.LayoutParams btnParams = new LinearLayout.LayoutParams(300, 100); btnParams.setMargins(10, 10, 10, 10); // Button tv = new Button(this); // tv.setLayoutParams(btnParams); // tv.setText("测试1"); // linearLayout.addView(tv); Button tv2 = new Button(this); tv2.setLayoutParams(btnParams); tv2.setText("家庭作业"); linearLayout.addView(tv2); setContentView(linearLayout); // tv.setOnClickListener(new View.OnClickListener() { // @Override // public void onClick(View v) { // Intent intent = new Intent(Intent.ACTION_VIEW); // intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // intent.setData(Uri.parse("http://www.baidu.com")); // // 注意这里使用的ApplicationContext 启动的Activity // // 因为Activity对象的startActivity使用的并不是ContextImpl的mInstrumentation // // 而是自己的mInstrumentation, 如果你需要这样, 可以自己Hook // // 比较简单, 直接替换这个Activity的此字段即可. // getApplicationContext().startActivity(intent); // } // }); tv2.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent i = new Intent(MainActivity.this,OtherActivity.class); // 在这里进行Hook Log.d("xiaonangua","开始hook"); try { MyHookHelper.attachContext(MainActivity.this); } catch (Exception e) { Log.d("xiaonangua",e.getMessage().toString()); e.printStackTrace(); } startActivity(i); } }); } @Override protected void attachBaseContext(Context newBase) { super.attachBaseContext(newBase); try { // HookHelper.attachContext(); } catch (Exception e) { Log.d("xiaonangua",e.getMessage().toString()); e.printStackTrace(); } } }
2.与tainn作者分析Context的调用链不同,我们先来看看Activity.startActivity()源码:
public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) { if (mParent == null) { Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity( this, mMainThread.getApplicationThread(), mToken, this, intent, requestCode, options); <span style="white-space:pre"> </span>.....接下来要做的就是把 mInstrumentation换成我们修改过的代理对象,通过反射得到Activity的Instrumentation字段然后创建代理对象并偷梁换柱QAQ:
public class MyHookHelper { public static void attachContext(Activity mActivity) throws Exception{ // 先获取到当前的ActivityThread对象 Class<?> activityClass = Class.forName("android.app.Activity"); // 拿到原始的 mInstrumentation字段 Field mInstrumentationField = activityClass.getDeclaredField("mInstrumentation");//获取属性 //打破封装 mInstrumentationField.setAccessible(true); Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(mActivity); // 创建代理对象 Instrumentation myEvilInstrumentation = new MyEvillnstrumentation(mInstrumentation); // 偷梁换柱 mInstrumentationField.set(mActivity, myEvilInstrumentation); } }代理类基本和作者的一样,加了一个小字段而已:
public class MyEvillnstrumentation extends Instrumentation { private static final String TAG = "xiaonangua"; // ActivityThread中原始的对象, 保存起来 Instrumentation mBase; public MyEvillnstrumentation(Instrumentation base) { mBase = base; } public ActivityResult execStartActivity( Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) { // Hook之前, XXX到此一游! Log.d(TAG, "\n南瓜执行了startActivity, 参数如下: \n" + "who = [" + who + "], " + "\ncontextThread = [" + contextThread + "], \ntoken = [" + token + "], " + "\ntarget = [" + target + "], \nintent = [" + intent + "], \nrequestCode = [" + requestCode + "], \noptions = [" + options + "]"); // 开始调用原始的方法, 调不调用随你,但是不调用的话, 所有的startActivity都失效了. // 由于这个方法是隐藏的,因此需要使用反射调用;首先找到这个方法 try { Method execStartActivity = Instrumentation.class.getDeclaredMethod( "execStartActivity", Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class); execStartActivity.setAccessible(true); Log.d("xiaonangua",execStartActivity.toString()); return (ActivityResult) execStartActivity.invoke(mBase,who, contextThread, token, target, intent, requestCode, options); } catch (Exception e) { Log.d("xiaonangua",e.getMessage().toString()); return null; // 某该死的rom修改了 需要手动适配 //throw new RuntimeException("do not support!!! pls adapt it"); } } }然后启动app,运行,打印log如下:
10-28 16:51:02.520 26242-26242/com.weishu.upf.dynamic_proxy_hook.app2 D/xiaonangua: 开始hook 10-28 16:51:02.530 26242-26242/com.weishu.upf.dynamic_proxy_hook.app2 D/xiaonangua: 南瓜执行了startActivity, 参数如下: who = [com.weishu.upf.dynamic_proxy_hook.app2.MainActivity@3ec0c1af], contextThread = [android.app.ActivityThread$ApplicationThread@2f6ff9bc], token = [android.os.BinderProxy@2222360], target = [com.weishu.upf.dynamic_proxy_hook.app2.MainActivity@3ec0c1af], intent = [Intent { cmp=com.weishu.upf.dynamic_proxy_hook.app2/.OtherActivity }], requestCode = [-1], options = [null]
Success~成功地完成了第一篇家庭作业哈哈哈。