安卓动态加载之Hook(一)

时间:2024-04-09 20:59:20

最近在研究安卓插件技术,系列文章用来记录一下自己的学习经历以及遇到的坑。

文章通过反射动态替换的方式,修改activity的startActivity()方法的具体实现。

这里需要先了解一下java的反射机制:

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任

意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。

具体可以参考:http://blog.csdn.net/liujiahan629629/article/details/18013523这篇文章,作者写的很清楚的。

首先我们来看看activity的startActivity()方法的真面目。

通过调用activity的startActivity()方法最后都是调用了带三个参数的startActivityForResult()方法。在方法体内部,最终实际调用的还是Activity成员变量mInstrumentation的execStartActivty()方法。到了这里我们可以修改startActivityForResult方法或者mInstrumentation的execStartActivty方法,但是我发现startActivityForResult方法是一个公开的方法,而mInstrumentation是一个私有成员变量(同时考虑到还可以通过context启动activity)。为了不影响外界主动调用startActivityForResult产生不一致的结果,决定修改mInstrumentation这个成员变量。

首先,我们需要拿到这个私有成员变量进行替换。。

通过反射,我们或许可以这么拿:

        Class<?> activityClass = null;
        activityClass = Class.forName("android.app.Activity");
        // 拿到原始的 mInstrumentation变量
        Field mInstrumentationField = activityClass.getDeclaredField("mInstrumentation");
        mInstrumentationField.setAccessible(true);
        Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(activityClass);

但是实际测试过程中,我发现不行。抛了一个异常:

java.lang.IllegalArgumentException: Expected receiver of type android.app.Activity, but got java.lang.Class<android.app.Activity>

然后回头看我们的代码:发现

mInstrumentationField.get()

这个方法传递的参数是一个Class类型的,这里应该传递一个Activity类型的参数,因为mInstrumentation是Activity的一个成员变量。

于是乎,下面遇到了一个问题,就是怎么拿到Activity的实例,我们知道Activity对外是没有提供获取实例的静态方法,也没有提供显示的创建实例的方法。

通过Class.newInstance()方法会重新创建一个实例对象,显然是不行的。

通过反射获取构造器调用newInstance()也相当于重新创建了一个对象,也是不行的。

秉着万物皆对象的想法,我试着获取默认无参的构造方法:

activityClass.getDeclaredMethod("Activity"),发现抛了NoSuchMethodFound异常。

于是乎,我决定把需要hook的activity作为参数穿给hook对象。(毕竟不是本文要说的重点啊~~)

改写后的代码是这样的:

public static void replaceIsr(Activity activity) {
    try {
        Class<?> activityClass = Class.forName("android.app.Activity");
        // 拿到原始的 mInstrumentation字段
        Field mInstrumentationField = activityClass.getDeclaredField("mInstrumentation");
        mInstrumentationField.setAccessible(true);
        Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(activity);
        // 创建代理对象
        Instrumentation hookInstrumentation = new HookInstrumentation(mInstrumentation);
        // 更换instrumentation对象
        mInstrumentationField.set(activity, hookInstrumentation);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    }
}

这里来看。我们貌似可以更换Instrumentation对象了。接下来,我们看看我们这个假的Instrumentation怎么写。

public class HookInstrumentation extends Instrumentation {
    // Activity中原始的对象, 保存起来
    Instrumentation mBase;

    public HookInstrumentation(Instrumentation base) {
        mBase = base;
    }

    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        Log.d("wh", "HookInstrumentation:execStartActivity");
        // 调用原生的方法正常启动activity,由于这个方法是隐藏的,且有多个重载形式,选择一个最终调用的方法进行反射获取
        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) {
            throw new RuntimeException("do not support!!! pls adapt it");
        }
    }
}

这里我们直接继承自Instrumentation来创建一个我们自己的类,因为我们需要改造启动activity的方法,所以需要改造execActivity()方法。为了不影响系统正常的activity启动,我们需要维持一个系统的Instrumentation对象来启动activity。具体见注释。这里就不在赘述了。我只是在系统调用这个hook的对象的execStartActivity()方法时,打了一段日志。不影响正常的activity正常启动。

写完这个假的Instrumentatinon,接下来就是需要替换Activity的成员变量mInstrumentation了。。观察Activity给mInstrumentation赋值的地方:


安卓动态加载之Hook(一)


是个常量方法,无法被重写。再往前面看看。然后发现了这个:

安卓动态加载之Hook(一)


不知道大家有没有注意到第一行的注释,在构造器调用之后,onCreate()调用之前进行设置这些参数,那么我们可以在Activity已经设置完参数后,进行成员变量的替换;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    HookHelper.replaceIsr(this);
    this.startActivity(new Intent(this, JumpActivity.class));
}
运行程序,可以看到日志的确输出了:安卓动态加载之Hook(一)


到此,一次对Activity的startActivity()方法的hook操作就完成了。文章是对weishu的博文学习后的小结,博文内容非常完整,流程清晰。

具体参考:http://weishu.me/2016/01/28/understand-plugin-framework-overview/