05 项目架构-插件化-Activity

作者: 凤邪摩羯 | 来源:发表于2021-10-12 09:07 被阅读0次

    背景

    上文在我们实现了最简单的插件化,也介绍了插件化实现过程中需要用到的知识点,最后我们实现了从app中加载sd卡中的dex文件,调用dex中Test类的方法。今天我们将实现Activity的插件化,Activity是需要在清单文件中注册的,插件中的Activity没有在宿主的清单文件中注册,那么我们如何启动它呢?

    Activity的插件化思路

    Activity是四大组件用的最频繁的组件,Activity的插件化也是各大插件化框架必须实现的功能。Activity插件化与Activity的启动有着密切的关系。

    Activity的启动过程需要由应用进程与AMS共同完成,当要启动一个Activity时,应用进程会向AMS发起请求,AMS收到这个包含要启动的Activity信息的请求后会进行一些列的处理以及权限校验,处理校验完成后回调到应用进程,由Activity所属的应用进程完成Activity的启动。

    因此现有的插件化框架都会有一套越过AndroidMainfest.xml注册而启动Activity的机制,本文就带你们实现和分析这一套机制

    因为AMS会进行Activity的处理和权限校验(是否注册),处理校验完会回到应用进程,由Activity所属的应用进程完成Activity的启动。那么思路就来了,我们可以在宿主App中创建一个ProxyActivity继承自Activity,并且在清单中注册,当启动插件中的Activity的时候,在系统检测前,找到一个Hook点,然后通过Hook将插件Activity替换成ProxyActivity,等到AMS检测完之后,再找一个Hook点将它们换回来,这样就实现了插件Activity的启动。思路虽然简单,但是需要熟悉Activity启动流程,动态代理,反射,Handler等原理,所以其实并不简单,需要很深的功力

    先来看一下Activity的启动流程

    image

    通过这张图我们可以确定Hook点的大致位置。

    1. 在进入AMS之前,找到一个Hook点,用来将插件Activity替换为ProxyActivity。
    2. 从AMS出来后,再找一个Hook点,用来将ProxyActivity替换为插件Activity。

    Hook Activity启动入口

    我们在启动Activity一般通过Intent包装后调用startActivity来启动,我们可以在AMS检查之前将Intent中的要启动的Activity替换为我们本地已经注册过的ProxyActivity,同时把我们要启动的插件Activity保存在Intent中,然后在经过AMS校验之后,再把Intent中的ProxyActivity再替换为插件中的Activity并启动,也就是说能够修改Intent的地方就可以作为Hook点

    这里要强调一下,查找Hook点应该尽量找静态变量或者单例对象,尽量Hook public的对象和方法。为什么呢?因为静态变量好获取,不容易被改变,而且静态变量只要找一个,不是静态变量就可能有多个对象,需要进一步的判断;为什么要找public方法呢,因为private方法可能被内部调用,影响该类的多个方法,当然这不是主要原因(public也有可能),主要是public是提供给外部使用的,一般是不容易改变。

    下面我们进入源码

    //Activity.java
    @Override
        public void startActivity(Intent intent) {
            this.startActivity(intent, null);
        }
    
        @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(@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);
                            ···
        }
    复制代码
    

    然后我们进入Instrumentation的execStartActivity方法中

    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        IApplicationThread whoThread = (IApplicationThread) contextThread;
        Uri referrer = target != null ? target.onProvideReferrer() : null;
        if (referrer != null) {
            intent.putExtra(Intent.EXTRA_REFERRER, referrer);
        }
            ···
        try {
            intent.migrateExtraStreamToClipData();
            intent.prepareToLeaveProcess(who);
            //这里就是我们的Hook点,替换传入startActivity方法中的Intent参数
            int result = ActivityManager.getService()
                .startActivity(whoThread, who.getBasePackageName(), intent,
                        intent.resolveTypeIfNeeded(who.getContentResolver()),
                        token, target != null ? target.mEmbeddedID : null,
                        requestCode, 0, null, options);
            checkStartActivityResult(result, intent);
        } catch (RemoteException e) {
            throw new RuntimeException("Failure from system", e);
        }
        return null;
    }
    复制代码
    

    为什么就能直接看出这是个Hook点呢,因为ActivityManager.getService().startActivity这个调用中含有参数Intent,同时getService()方法是一个静态public方法,方便hook

    public static IActivityManager getService() {
        return IActivityManagerSingleton.get();
    }
    复制代码
    

    找到该Hook点,通过动态代理(IActivityManager是个接口),我们要生成一个代理对象,我们要代理的是ActivityManager.getService()返回的对象,同时替换它的参数Intent

    //创建动态代理对象
    Class<?> iActivityManagerClass = Class.forName("android.app.IActivityManager");
    
    Object proxyInstance = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
            new Class[]{iActivityManagerClass}, new InvocationHandler() {
                @Override
                public Object invoke(Object o, Method method, Object[] args) throws Throwable {
                    // do something
                    // Intent的修改 -- 过滤
                    /**
                     * IActivityManager类的方法
                     * startActivity(whoThread, who.getBasePackageName(), intent,
                     *                         intent.resolveTypeIfNeeded(who.getContentResolver()),
                     *                         token, target != null ? target.mEmbeddedID : null,
                     *                         requestCode, 0, null, options)
                     */
                    //过滤
                    if ("startActivity".equals(method.getName())) {
                        int index = -1;
                        //获取Intent参数在args数组中的index值
                        for (int i = 0; i < args.length; i++) {
                            if (args[i] instanceof Intent) {
                                index = i;
                                break;
                            }
                        }
                        //得到原始的Intent对象
                        Intent intent = (Intent) args[index];
    
                        //生成代理proxyIntent
                        Intent proxyIntent = new Intent();
                        proxyIntent.setClassName("com.jackie.plugingodemo", ProxyActivity.class.getName());
                        //保存原始的Intent对象
                        proxyIntent.putExtra(TARGET_INTENT, intent);
                        //使用proxyIntent替换数组中的Intent
                        args[index] = proxyIntent;
                    }
    
                    //args method需要的参数  --不改变原有的执行流程
                    //mInstance 系统的IActivityManager对象
                    return method.invoke(mInstance, args);
                }
            });
    复制代码
    

    接着我们再使用反射将系统中的IActivityManager对象替换为我们的代理对象proxyInstance,如何替换?我们来看一下源码。

        //ActivityManager.class
        public static IActivityManager getService() {
            return IActivityManagerSingleton.get();
        }
    
        private static final Singleton<IActivityManager> IActivityManagerSingleton =
                new Singleton<IActivityManager>() {
                    @Override
                    protected IActivityManager create() {
                        final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
                        final IActivityManager am = IActivityManager.Stub.asInterface(b);
                        return am;
                    }
                };
    复制代码
    

    再来看看SingleTon的源码

    public abstract class Singleton<T> {
        private T mInstance;
    
        protected abstract T create();
    
        public final T get() {
            synchronized (this) {
                if (mInstance == null) {
                    mInstance = create();
                }
                return mInstance;
            }
        }
    }
    复制代码
    

    可以看到,IActivityManagerSingleton.get()实际上返回的就是mInstance对象,接下来我们要替换的就是这个对象,代码如下:

    //获取Singleton<T>对象
    Field singletonField = null;
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { //8.0
        Class<?> clazz = Class.forName("android.app.ActivityManagerNative");
        singletonField = clazz.getDeclaredField("gDefault");
    } else {
        Class<?> clazz = Class.forName("android.app.ActivityManager");
        singletonField = clazz.getDeclaredField("IActivityManagerSingleton");
    }
    singletonField.setAccessible(true);
    Object singleton = singletonField.get(null); //静态的可以直接获取,传入null
    
    //获取mInstance对象,mInstance是非静态的,mInstance对象是系统的IActivityManager对象,也就是ActivityManager.getService()
    Class<?> singletonClass = Class.forName("android.util.Singleton");
    Field mInstanceField = singletonClass.getDeclaredField("mInstance");
    mInstanceField.setAccessible(true);
    
    final Object mInstance = mInstanceField.get(singleton);
    
    //创建动态代理对象
    ···
    //替换
    mInstanceField.set(singleton, proxyInstance);
    复制代码
    

    到此为止,我们的第一次Hook就已经实现了,下面我们来看第二次Hook点。

    Hook Activity启动出口

    从前面的那张图我们可以看到在出来的时候,会调用H(handler)的handleMessage方法,在handleMessage方法中(注意这里是android 7.0,和8.0/9.0的源码不同)

    public void handleMessage(Message msg) {
    1452            if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
    1453            switch (msg.what) {
    1454                case LAUNCH_ACTIVITY: {
    1455                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
    1456                    final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
    1457
    1458                    r.packageInfo = getPackageInfoNoCheck(
    1459                            r.activityInfo.applicationInfo, r.compatInfo);
    1460                    handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
    1461                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    1462                } break;
    复制代码
    

    在这里我们并没有看到我们的Intent,继续玩下看handleLaunchActivity

    private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
    2688        // If we are getting ready to gc after going to the background, well
    2689        // we are back active so skip it.
    2690        unscheduleGcIdler();
    2691        mSomeActivitiesChanged = true;
    2692
    2693        if (r.profilerInfo != null) {
    2694            mProfiler.setProfiler(r.profilerInfo);
    2695            mProfiler.startProfiling();
    2696        }
    2697
    2698        // Make sure we are running with the most recent config.
    2699        handleConfigurationChanged(null, null);
    2700
    2701        if (localLOGV) Slog.v(
    2702            TAG, "Handling launch of " + r);
    2703
    2704        // Initialize before creating the activity
    2705        WindowManagerGlobal.initialize();
    2706
    2707        Activity a = performLaunchActivity(r, customIntent);
                        ···
    复制代码
    

    注意这个方法的参数customIntent并不是我们想要的Intent,因为上面该参数传的是null。继续看performLaunchActivity

      private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    2515        // System.out.println("##### [" + System.currentTimeMillis() + "] ActivityThread.performLaunchActivity(" + r + ")");
    2516
    2517        ActivityInfo aInfo = r.activityInfo;
    2518        if (r.packageInfo == null) {
    2519            r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
    2520                    Context.CONTEXT_INCLUDE_CODE);
    2521        }
    2522
    2523        ComponentName component = r.intent.getComponent();
    2524        if (component == null) {
    2525            component = r.intent.resolveActivity(
    2526                mInitialApplication.getPackageManager());
    2527            r.intent.setComponent(component);
    2528        }
    复制代码
    

    可以看到该方法中有(ActivityClientRecord)r.intent方法,注意,不是说有看到Intent的可以Hook,也要看这个intent所属的是什么对象,也就是说你要熟悉系统中的一些常见类,ActivityRecord和ActivityClientRecord都是保存Activity信息的对象。只不过,ActivityRecord归system_server进程使用,ActivityClientRecord归App进程使用

    所以这里可以对ActivityClientRecord的intent进行hook,ActivityClientRecord方法中的intent(非静态)

     static final class ActivityClientRecord {
    310        IBinder token;
    311        int ident;
    312        Intent intent;
    复制代码
    

    要获取非静态的intent,首先我们要获取ActivityClientRecord对象,那么如果获取该对象呢?倒推回去,performLaunchActivity被handleLaunchActivity调用,然后handleLaunchActivity在处理LAUNCH_ACTIVITY消息时被调用

    final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
    复制代码
    

    可以看到,我们的这个r(ActivityClientRecord)实际上是个msg.obj,也就是说能拿到msg(Message)就可以拿到r对象了,那怎么拿到msg呢,也就是我们上面说的mCallback,将mCallback作为hook点,替换或创建整个mCallback,我们就可以拿到该消息了

    下面我们来看一下Handler的源码:

    /**
     * Handle system messages here.
     */
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
    复制代码
    

    如果不了解Handler的源码,可以看看我之前写的文章,消息的发送最终会调用dispatchMessage方法,然后分发给handleMessage方法(如果该方法有被调用的话),仔细看该方法,如果Handle.mCallback不为空的话,会首先执行mCallback.handleMessage(msg)方法,同时只有在mCallback.handleMessage(msg)返回为false的时候,才会继续执行下面的handleMessage方法,这个非常重要。我们再来看系统的H(Handler)

    //ActivityThread.java
    final H mH = new H();
    
    //Handler.java
    113    public Handler() {  //第一个参数是callback
    114        this(null, false);
    115    }
    复制代码
    

    也就是说系统的这个Handler在传callback参数时是空的,没有Callback,那么我们需要自己创建一个Callback

    // 创建的 callback
    Handler.Callback callback = new Handler.Callback() {
    
        @Override
        public boolean handleMessage(@NonNull Message msg) {
            // 通过msg  可以拿到 Intent,可以换回执行插件的Intent
    
            // 找到 Intent的方便替换的地方  --- 在这个类里面 ActivityClientRecord --- Intent intent 非静态
            // msg.obj == ActivityClientRecord
            switch (msg.what) {
                case 100:
                    try {
                        Field intentField = msg.obj.getClass().getDeclaredField("intent");
                        intentField.setAccessible(true);
                        // 启动代理Intent
                        Intent proxyIntent = (Intent) intentField.get(msg.obj);
                        // 启动插件的 Intent
                        Intent intent = proxyIntent.getParcelableExtra(TARGET_INTENT);
                        if (intent != null) {
                            intentField.set(msg.obj, intent);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    break;
                case 159:
                    try {
                        // 获取 mActivityCallbacks 对象
                        Field mActivityCallbacksField = msg.obj.getClass()
                                .getDeclaredField("mActivityCallbacks");
    
                        mActivityCallbacksField.setAccessible(true);
                        List mActivityCallbacks = (List) mActivityCallbacksField.get(msg.obj);
    
                        for (int i = 0; i < mActivityCallbacks.size(); i++) {
                            if (mActivityCallbacks.get(i).getClass().getName()
                                    .equals("android.app.servertransaction.LaunchActivityItem")) {
                                Object launchActivityItem = mActivityCallbacks.get(i);
    
                                // 获取启动代理的 Intent
                                Field mIntentField = launchActivityItem.getClass()
                                        .getDeclaredField("mIntent");
                                mIntentField.setAccessible(true);
                                Intent proxyIntent = (Intent) mIntentField.get(launchActivityItem);
    
                                // 目标 intent 替换 proxyIntent
                                Intent intent = proxyIntent.getParcelableExtra(TARGET_INTENT);
                                if (intent != null) {
                                    mIntentField.set(launchActivityItem, intent);
                                }
                            }
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    break;
            }
            // 必须 return false
            return false;
        }
    };
    复制代码
    

    同时在该方法中从Intent中拿出插件的Activity,最终启动该Activity。然后通过反射给系统的H(Handler)设置一个Callback

    // 获取 ActivityThread 类的 Class 对象
                Class<?> clazz = Class.forName("android.app.ActivityThread");
    
                // 获取 ActivityThread 对象
                Field activityThreadField = clazz.getDeclaredField("sCurrentActivityThread");
                activityThreadField.setAccessible(true);
                Object activityThread = activityThreadField.get(null);
    
                // 获取 mH 对象
                Field mHField = clazz.getDeclaredField("mH");
                mHField.setAccessible(true);
                final Handler mH = (Handler) mHField.get(activityThread);
    
                Field mCallbackField = Handler.class.getDeclaredField("mCallback");
                mCallbackField.setAccessible(true);
    
                // 创建的 callback
                            ···
                // 替换系统的 callBack
                mCallbackField.set(mH, callback);
    复制代码
    

    到这来我们就实现了Activity的插件化,当然Hook点不止这些,有兴趣的读者可以自己寻找,同时在不同版本上源码的实现方式也不同,需要进行适配。在Android10上,系统对源码做了较大的修改,有兴趣的可以自己实现一波。

    最后你可能会碰到这么一个异常

    2020-11-29 12:27:33.247 19124-19124/com.jackie.plugingodemo D/AppCompatDelegate: Exception while getting ActivityInfo
        android.content.pm.PackageManager$NameNotFoundException: ComponentInfo{com.jackie.plugingodemo/com.jackie.plugin.PluginActivity}
            at android.app.ApplicationPackageManager.getActivityInfo(ApplicationPackageManager.java:435)
            at androidx.appcompat.app.AppCompatDelegateImpl.isActivityManifestHandlingUiMode(AppCompatDelegateImpl.java:2649)
            at androidx.appcompat.app.AppCompatDelegateImpl.updateForNightMode(AppCompatDelegateImpl.java:2499)
            at androidx.appcompat.app.AppCompatDelegateImpl.applyDayNight(AppCompatDelegateImpl.java:2374)
            at androidx.appcompat.app.AppCompatDelegateImpl.onCreate(AppCompatDelegateImpl.java:494)
            at androidx.appcompat.app.AppCompatActivity.onCreate(AppCompatActivity.java:114)
            at com.jackie.plugin.PluginActivity.onCreate(PluginActivity.java:12)
            at android.app.Activity.performCreate(Activity.java:7136)
            at android.app.Activity.performCreate(Activity.java:7127)
            at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1271)
            at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2893)
    复制代码
    

    该异常提示我们找不到ActivityInfo,然后我们到抛出异常的方法看一下

    private boolean isActivityManifestHandlingUiMode() {
        if (!mActivityHandlesUiModeChecked && mHost instanceof Activity) {
            final PackageManager pm = mContext.getPackageManager();
            if (pm == null) {
                // If we don't have a PackageManager, return false. Don't set
                // the checked flag though so we still check again later
                return false;
            }
            try {
                int flags = 0;
                // On newer versions of the OS we need to pass direct boot
                // flags so that getActivityInfo doesn't crash under strict
                // mode checks
                if (Build.VERSION.SDK_INT >= 29) {
                    flags = PackageManager.MATCH_DIRECT_BOOT_AUTO
                            | PackageManager.MATCH_DIRECT_BOOT_AWARE
                            | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
                } else if (Build.VERSION.SDK_INT >= 24) {
                    flags = PackageManager.MATCH_DIRECT_BOOT_AWARE
                            | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
                }
                final ActivityInfo info = pm.getActivityInfo(
                        new ComponentName(mContext, mHost.getClass()), flags);
                mActivityHandlesUiMode = info != null
                        && (info.configChanges & ActivityInfo.CONFIG_UI_MODE) != 0;
            } catch (PackageManager.NameNotFoundException e) {
                // This shouldn't happen but let's not crash because of it, we'll just log and
                // return false (since most apps won't be handling it)
                Log.d(TAG, "Exception while getting ActivityInfo", e);
                mActivityHandlesUiMode = false;
            }
        }
        // Flip the checked flag so we don't check again
        mActivityHandlesUiModeChecked = true;
    
        return mActivityHandlesUiMode;
    }
    复制代码
    

    然后进如pm.getActivityInfo方法

    /**
     * Retrieve all of the information we know about a particular activity
     * class.
     *
     * @param component The full component name (i.e.
     *            com.google.apps.contacts/com.google.apps.contacts.
     *            ContactsList) of an Activity class.
     * @param flags Additional option flags to modify the data returned.
     * @return An {@link ActivityInfo} containing information about the
     *         activity.
     * @throws NameNotFoundException if a package with the given name cannot be
     *             found on the system.
     */
    public abstract ActivityInfo getActivityInfo(ComponentName component,
            @ComponentInfoFlags int flags) throws NameNotFoundException;
    复制代码
    

    通过查看该方法注释,可以看到当找不到该Activity的包名,也就是在系统中找不到,就会抛出异常了,因为我们的插件包名和宿主App的包名不一致导致的,不过系统也为我们捕获了该异常了。

    我们为什么要学插件化

    Activity插件化的实现很重要的一点是寻找Hook点,如何寻找Hook点需要我们对Activity启动流程非常熟悉。插件化涉及到的技术其实很多,四大组件的启动流程,AMS/PKMS等系统服务启动流程,Handler,反射,动态代理等等,里面运用到很多Android自身的知识,而不仅仅是Java的知识,有点像Android技术的“集大成者”。所以如果你想成为一个高级开发,就应该懂得像插件化,热修复这样的技术难点。最后,看完本文喜欢的点个赞和关注吧。

    相关文章

      网友评论

        本文标题:05 项目架构-插件化-Activity

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