Android 开发进阶

Android 插件化的原理,对 startActivity 方

2020-10-12  本文已影响0人  JeffreyWorld

Hook Activity 的 startActivity 方法(Activity startActivity)

先定义一个 InstrumentationProxy 继承 Instrumentation ,然后完善里面的 execStartActivity 方法。

public class InstrumentationProxy extends Instrumentation {

    private static final String TAG = "InstrumentationProxy";

    Instrumentation mInstrumentation;

    public InstrumentationProxy(Instrumentation instrumentation) {
        mInstrumentation = instrumentation;
    }

    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        Log.d(TAG, "Hook 成功" + " who :" + who);
        // 开始调用原始的方法, 调不调用随你,但是不调用的话, 所有的startActivity都失效了.
        // 由于这个方法是隐藏的,因此需要使用反射调用;首先找到这个方法
        Class[] pareTyples = {Context.class, IBinder.class, IBinder.class,
                Activity.class, Intent.class, int.class, Bundle.class};
        Object[] pareVaules = {who, contextThread, token, target,
                intent, requestCode, options};
        String methodName = "execStartActivity";
        try {
            Method execStartActivity = mInstrumentation.getClass().getDeclaredMethod(methodName, pareTyples);
            execStartActivity.setAccessible(true);
            return (ActivityResult) execStartActivity.invoke(mInstrumentation, pareVaules);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}
public class HookMainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_hook_main);
        Class clazz = Activity.class;
        try {
            Field field = clazz.getDeclaredField("mInstrumentation");//通过Activity.class 拿到 mInstrumentation字段
            field.setAccessible(true);
            Instrumentation mInstrumentation = (Instrumentation) field.get(this); //根据activity内mInstrumentation字段 获取Instrumentation对象
            Instrumentation instrumentationProxy = new InstrumentationProxy(mInstrumentation); ////创建代理对象
            field.set(this, instrumentationProxy); //进行替换
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Hook Context 的 startActivity 方法 (getApplicationContext startActivity)

public class HookMainActivity extends AppCompatActivity {

    @Override
    protected void attachBaseContext(Context context) {
        super.attachBaseContext(context);
        try {
            // 在这里进行Hook
            String className = "android.app.ActivityThread";
            String methodName = "currentActivityThread";
            Class[] pareTyples = new Class[]{};
            Object[] pareVaules = new Object[]{};
            // 先获取到当前的ActivityThread对象
            Class clazz = Class.forName(className);
            Method method = clazz.getDeclaredMethod(methodName, pareTyples);
            method.setAccessible(true);
            Object currentActivityThread = method.invoke(null, pareVaules);// 先获取到当前的ActivityThread对象
            String filedName = "mInstrumentation";
            // 拿到原始的 mInstrumentation字段
            Field field = clazz.getDeclaredField(filedName);
            field.setAccessible(true);
            Instrumentation mInstrumentation = (Instrumentation) field.get(currentActivityThread); // 拿到原始的 mInstrumentation字段
            // 创建代理对象
            Instrumentation instrumentationProxy = new InstrumentationProxy(mInstrumentation);
            // 偷梁换柱
            field.set(currentActivityThread, instrumentationProxy);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_hook_main);
    }
}

Hook startActivity 总结

Hook Context 的 startActivity 方法和 Hook Activity 的 startActivity 方法最大的区别就是替换的 Instrumentation 不同 , 前者是 ActivityThread 中的 Instrumentation ,后者是Activity 中的 Instrumentation。另外有一点需要注意的是,在 MainActivity 的onCreate 方法中进行Instrumentation替换的,未必是最佳的替换时间点 ,所以在 Activity 的 attachBaseContext 方法中进行 Instrumentation 替换,因为这个方法要先于 Activity 的 onCreate 方法被调用。讲到这里,我们知道了如何使用代理来 Hook startActivity 方法,简单说就是找到 Hook 点,再用代理对象来替换 Hook 点 。

Hook 的要点

我们来梳理下 Activity startActivity 方法的调用流程。

@Override
public void startActivity(Intent intent, @Nullable Bundle options) {
    if (options != null) {
        startActivityForResult(intent, -1, options);
    } else {
        // Note we want to go through this call for compatibility with
        // applications that may have overridden the method.
        startActivityForResult(intent, -1);
    }
}
    public void startActivityForResult(Intent intent, int requestCode) {
        startActivityForResult(intent, requestCode, null);
    }
    /**
     * Launch an activity for which you would like a result when it finished.
     * When this activity exits, your
     * onActivityResult() method will be called with the given requestCode.
     * Using a negative requestCode is the same as calling
     * {@link #startActivity} (the activity is not launched as a sub-activity).
     *
     * <p>Note that this method should only be used with Intent protocols
     * that are defined to return a result.  In other protocols (such as
     * {@link Intent#ACTION_MAIN} or {@link Intent#ACTION_VIEW}), you may
     * not get the result when you expect.  For example, if the activity you
     * are launching uses {@link Intent#FLAG_ACTIVITY_NEW_TASK}, it will not
     * run in your task and thus you will immediately receive a cancel result.
     *
     * <p>As a special case, if you call startActivityForResult() with a requestCode
     * >= 0 during the initial onCreate(Bundle savedInstanceState)/onResume() of your
     * activity, then your window will not be displayed until a result is
     * returned back from the started activity.  This is to avoid visible
     * flickering when redirecting to another activity.
     *
     * <p>This method throws {@link android.content.ActivityNotFoundException}
     * if there was no Activity found to run the given Intent.
     *
     * @param intent The intent to start.
     * @param requestCode If >= 0, this code will be returned in
     *                    onActivityResult() when the activity exits.
     * @param options Additional options for how the Activity should be started.
     * See {@link android.content.Context#startActivity(Intent, Bundle)}
     * Context.startActivity(Intent, Bundle)} for more details.
     *
     * @throws android.content.ActivityNotFoundException
     *
     * @see #startActivity
     */
    public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
            @Nullable Bundle options) {
        if (mParent == null) {
            options = transferSpringboardActivityOptions(options);
            Instrumentation.ActivityResult ar =
                mInstrumentation.execStartActivity(
                    this, mMainThread.getApplicationThread(), mToken, this,
                    intent, requestCode, options);
            if (ar != null) {
                mMainThread.sendActivityResult(
                    mToken, mEmbeddedID, requestCode, ar.getResultCode(),
                    ar.getResultData());
            }
            if (requestCode >= 0) {
                // If this start is requesting a result, we can avoid making
                // the activity visible until the result is received.  Setting
                // this code during onCreate(Bundle savedInstanceState) or onResume() will keep the
                // activity hidden during this time, to avoid flickering.
                // This can only be done when a result is requested because
                // that guarantees we will get information back when the
                // activity is finished, no matter what happens to it.
                mStartedActivity = true;
            }

            cancelInputsAndStartExitTransition(options);
            // TODO Consider clearing/flushing other event sources and events for child windows.
        } else {
            if (options != null) {
                mParent.startActivityFromChild(this, intent, requestCode, options);
            } else {
                // Note we want to go through this method for compatibility with
                // existing applications that may have overridden it.
                mParent.startActivityFromChild(this, intent, requestCode);
            }
        }
    }

首先,我们先来看一下 startActivityForResult 方法,当 mParent 为 null 的时候,会调用到 mInstrumentation.execStartActivity 方法。当 mParent 不为 null 时,都会调用到 mParent.startActivityFromChild 方法。而 mParent 为 Activity 实例,接下来我们一起看一下 startActivityFromChild 方法。

    /**
     * This is called when a child activity of this one calls its
     * {@link #startActivity} or {@link #startActivityForResult} method.
     *
     * <p>This method throws {@link android.content.ActivityNotFoundException}
     * if there was no Activity found to run the given Intent.
     *
     * @param child The activity making the call.
     * @param intent The intent to start.
     * @param requestCode Reply request code.  < 0 if reply is not requested.
     * @param options Additional options for how the Activity should be started.
     * See {@link android.content.Context#startActivity(Intent, Bundle)}
     * Context.startActivity(Intent, Bundle)} for more details.
     *
     * @throws android.content.ActivityNotFoundException
     *
     * @see #startActivity
     * @see #startActivityForResult
     */
    public void startActivityFromChild(@NonNull Activity child, @RequiresPermission Intent intent,
            int requestCode, @Nullable Bundle options) {
        options = transferSpringboardActivityOptions(options);
        Instrumentation.ActivityResult ar =
            mInstrumentation.execStartActivity(
                this, mMainThread.getApplicationThread(), mToken, child,
                intent, requestCode, options);
        if (ar != null) {
            mMainThread.sendActivityResult(
                mToken, child.mEmbeddedID, requestCode,
                ar.getResultCode(), ar.getResultData());
        }
        cancelInputsAndStartExitTransition(options);
    }

可以看到 startActivityFromChild 中也会调用 mInstrumentation.execStartActivity 方法。因此,即我们通过 Activity startActivity 的方法启动 activity,最终都会调用到 mInstrumentation.execStartActivity 方法。因此,如果我们想要拦截的话,可以 hook 住 mInstrumentation。

由于 mInstrumentation 是类,不是 interface,不能使用动态代理的方式,因此,这里我们使用静态代理的方式。

ok,这里我们重新理一下 Activity 大概的启动流程:

app 调用 startActivity 方法 -> Instrumentation 类通过 ActivityManagerNative 或者 ActivityManager( API 26以后)将启动请求发送给 AMS -> AMS 进行一系列检查并将此请求通过 Binder 派发给所属 app -> app 通过 Binder 收到这个启动请求 -> ActivityThread 中的实现将收到的请求进行封装后送入 Handler -> 从 Handler 中取出这个消息,开始 app 本地的 Activity 初始化和启动逻辑。

上一篇下一篇

猜你喜欢

热点阅读