代理 Hook
如果我们自己创建代理对象, 然后把原始对象替换为我们的代理对象, 那么就可以在这个代理对象为所欲为了; 修改参数, 替换返回值, 我们称之为 Hook.
下面我们 Hook 掉 startActivity 这个方法, 使得每次调用这个方法之前输出一条日志;(当然, 这个输入日志有点点弱, 只是为了展示原理; 只要你想, 你想可以替换参数, 拦截这个 startActivity 过程, 使得调用它导致启动某个别的 Activity, 指鹿为马!)
首先我们得找到被 Hook 的对象, 我称之为 Hook 点; 什么样的对象比较好 Hook 呢? 自然是容易找到的对象. 什么样的对象容易找到? 静态变量和单例; 在一个进程之内, 静态变量和单例变量是相对不容易发生变化的, 因此非常容易定位, 而普通的对象则要么无法标志, 要么容易改变. 我们根据这个原则找到所谓的 Hook 点.
然后我们分析一下 startActivity 的调用链, 找出合适的 Hook 点. 我们知道对于 Context.startActivity(Activity.startActivity 的调用链与之不同), 由于 Context 的实现实际上是 ContextImpl; 我们看 ConetxtImpl 类的 startActivity 方法:
- @Override
- public void startActivity(Intent intent, Bundle options) {
- ...
- mMainThread.getInstrumentation().execStartActivity(
- getOuterContext(), mMainThread.getApplicationThread(), null,(Activity)null, intent, -1, options);
- }
这里, 实际上使用了 ActivityThread 类的 mInstrumentation 成员的 execStartActivity 方法; 注意到, ActivityThread 实际上是主线程, 而主线程一个进程只有一个, 因此这里是一个良好的 Hook 点.
接下来就是想要 Hook 掉我们的主线程对象, 也就是把这个主线程对象里面的 mInstrumentation 给替换成我们修改过的代理对象; 要替换主线程对象里面的字段, 首先我们得拿到主线程对象的引用, 如何获取呢? ActivityThread 类里面有一个静态方法 currentActivityThread 可以帮助我们拿到这个对象类; 但是 ActivityThread 是一个隐藏类, 我们需要用反射去获取, 代码如下:
- // 先获取到当前的 ActivityThread 对象
- Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
- Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
- currentActivityThreadMethod.setAccessible(true);
- Object currentActivityThread = currentActivityThreadMethod.invoke(null);
拿到这个 currentActivityThread 之后, 我们需要修改它的 mInstrumentation 这个字段为我们的代理对象, 我们先实现这个代理对象, 由于 JDK 动态代理只支持接口, 而这个 Instrumentation 是一个类, 没办法, 我们只有手动写静态代理类, 覆盖掉原始的方法即可.(cglib 可以做到基于类的动态代理, 这里先不介绍)
- public class EvilInstrumentation extends Instrumentation {
- private static final String TAG = "EvilInstrumentation";
- // ActivityThread 中原始的对象, 保存起来
- Instrumentation mBase;
- public EvilInstrumentation(Instrumentation base) {
- mBase = base;
- }
- public ActivityResult execStartActivity(
- Context who, IBinder contextThread, IBinder token, Activity target,
- Intent intent, int requestCode, Bundle options) {
- // Hook 之前, XXX 到此一游!
- Log.e("sck", "执行了 startActivity, 参数如下:" + "who = [" + who + "]," +
- "contextThread = [" + contextThread + "], token = [" + token + "]," +
- "target = [" + target + "], intent = [" + intent +
- "], requestCode = [" + requestCode + "], options = [" + 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);
- return (ActivityResult) execStartActivity.invoke(mBase, who,
- contextThread, token, target, intent, requestCode, options);
- } catch (Exception e) {
- // 某该死的 rom 修改了 需要手动适配
- throw new RuntimeException("do not support!!! pls adapt it");
- }
- }
- }
Ok, 有了代理对象, 我们要做的就是偷梁换柱! 代码比较简单, 采用反射直接修改:
- public static void attachContext() throws Exception {
- // 先获取到当前的 ActivityThread 对象
- Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
- Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
- currentActivityThreadMethod.setAccessible(true);
- //currentActivityThread 是一个 static 函数所以可以直接 invoke, 不需要带实例参数
- Object currentActivityThread = currentActivityThreadMethod.invoke(null);
- // 拿到原始的 mInstrumentation 字段
- Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
- mInstrumentationField.setAccessible(true);
- Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);
- // 创建代理对象
- Instrumentation evilInstrumentation = new EvilInstrumentation(mInstrumentation);
- // 偷梁换柱
- mInstrumentationField.set(currentActivityThread, evilInstrumentation);
- }
Activity 中的代码:
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- // TODO: 16/1/28 支持 Activity 直接跳转请在这里 Hook
- // 家庭作业, 留给读者完成.
- Button tv = new Button(this);
- tv.setText("测试界面");
- setContentView(tv);
- 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);
- }
- });
- }
- @Override
- protected void attachBaseContext(Context newBase) {
- super.attachBaseContext(newBase);
- Log.e("sck", "attachBaseContext");
- try {
- // 在这里进行 Hook
- HookHelper.attachContext();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
整个 Hook 过程简要总结如下:
寻找 Hook 点, 原则是静态变量或者单例对象, 尽量 Hook pulic 的对象和方法, 非 public 不保证每个版本都一样, 需要适配.
选择合适的代理方式, 如果是接口可以用动态代理; 如果是类可以手动写代理也可以使用 cglib.
偷梁换柱 -- 用代理对象替换原始对象
自己是从事了七年开发的 Android 工程师, 不少人私下问我, 2019 年 Android 进阶该怎么学, 方法有没有?
没错, 年初我花了一个多月的时间整理出来的学习资料, 希望能帮助那些想进阶提升 Android 开发, 却又不知道怎么进阶学习的朋友.[包括高级 UI, 性能优化, 架构师课程, NDK,Kotlin, 混合式开发 (ReactNative+Weex),Flutter 等架构技术资料] , 希望能帮助到您面试前的复习且找到一个好的工作, 也节省大家在网上搜索资料的时间来学习.
来源: http://www.jianshu.com/p/b513da9f2c0b