美文网首页Android 开发进阶
Android 插件化的原理,对 startActivity 方

Android 插件化的原理,对 startActivity 方

作者: JeffreyWorld | 来源:发表于2020-10-12 14:42 被阅读0次

    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 的要点

    • Hook 的选择点:静态变量和单例,因为一旦创建对象,它们不容易变化,非常容易定位。
    • Hook 的过程:
      1.寻找 Hook 点,原则是静态变量或者单例对象,尽量 Hook public 的对象和方法。
      2.选择合适的代理方式,如果是接口可以用动态代理。
      3.偷梁换柱——用代理对象替换原始对象。
    • Android 的 API 版本比较多,方法和类可能不一样,所以要做好 API 的兼容工作。

    我们来梳理下 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 初始化和启动逻辑。

    相关文章

      网友评论

        本文标题:Android 插件化的原理,对 startActivity 方

        本文链接:https://www.haomeiwen.com/subject/fgiupktx.html