Android插件化原理(二):Activity的插件化

  上一节插件类的加载中我们解决了插件类加载的问题,插件中的类在需要的时候可以正常被加载并实例化,但是对于四大组件类而言,成功加载并创建出实例它们仍不能正常工作,因为它们的工作需要频繁与ActivityManagerService(下面简称AMS)进行通信,有复杂的生命周期需要处理。因此如果想要实现插件化,还需要让四大组件能够正常工作。
  解决四大组件插件化问题,目前主要有两种思路,一是通过Hook的方式欺骗系统绕过系统检查,目前大多框架采用的都是这种思路,像DroidPlugin、VirtualApk等;另一种是基于代理模式对四大组件的请求进行分发,像早期的dynamic_load_apk以及前段时间才开源的Shadow采用的都是这种思路。两种思路各有优劣,因为我们是基于VirtualApk进行的源码分析,所以会讲解第一种思路,但我稍微了解了下Shadow框架的实现,实现思路还是很不错的,解决了很多早期dynamic_load_apk框架存在的问题,感兴趣的同学推荐了解一下。

  要理解四大组件插件化的原理,需要先对四大组件的工作流程有一定的了解,涉及到相关的内容我会稍微提一下,还不了解的同学推荐先阅读下相关资料。这一节我们分析一下四大组件之一Activity的插件化。

Activity插件化思路

  Activity是四大组件用的最频繁的组件,Activity的插件化也是各大插件化框架必须实现的功能。Activity插件化与Activity的启动有着密切的关系,Activity的启动过程需要由应用进程与AMS共同完成,当要启动一个Activity时,应用进程会向AMS发起请求,AMS收到这个包含要启动的Activity信息的请求后会进行一些列的处理以及权限校验,处理校验完成后回调到应用进程,由Activity所属的应用进程完成Activity的启动。
  AMS进行的一系列权限校验中,有一个非常关键的校验就是会检查要启动的Activity是否在AndroidManifest文件中声明,在我们刚入门Android开发的时候有时会忘了将Activity在AndroidManifest里注册,启动这个Activity的时候就会抛出ActivityNotFoundException异常,所以没注册的Activity是没办法启动的。而在我们的插件化中,虽然我们可以将插件Activity在宿主的AndroidManifest中预注册,但是这种方案非常不灵活,插件化的一个思想就是可以在宿主不改变的情况下实现功能的动态化,如果采用这种方案,当我们的插件Activity有改动时就不可避免的要修改宿主,所以这种方案不是最优解。因此Activity插件化需要解决的主要问题就是如何启动一个没有在宿主AndroidManifest中注册的Activity。
  一个巧妙的思路是可以在宿主中注册一个占位Activity,这个Activity不会被真正启动只是在宿主中占个坑位,当要启动插件Activity的时候,在向AMS发起请求之前我们将目标Activity从插件Activity替换成占位Activity,这样AMS收到的请求就是启动占位Activity的请求了,显然校验不会有问题,接下来在AMS校验成功以后回调到应用进程,此时应用进程收到的回调里的目标Activity还是占位Activity,因此我们还需要在创建Activity实例之前将其替换成插件Activity。通过占位Activity,就可以以“偷梁换柱,瞒天过海”的方式绕过AMS的检查,完成插件Activity的启动。
  Activity启动过程中,应用进程的处理可以分成向AMS发起请求以及Activity启动两部分,对于上面讲到的思路,我们分别看看这两部分要如何处理。

向AMS发起请求

  首先看下Activity向AMS发起请求的部分,时序图如下:


向AMS发起请求

  当我们启动一个Activity时,会调用Activity的startActivity(),内部经过重载会调用到Instrumentation的execStartActivity(),该方法内部会通过AMS在应用进程的代理向AMS发起请求,这里的表现在8.0前后有不同表现,以8.0版本为例,会调用ActivityManager.getService()获取到IActivityManager类型的代理对象并调用其startActivity()向AMS发起请求。
  根据上面的思路,我们要在向AMS发送请求之前,把请求中的插件Activity替换成占位Activity,方法有很多种,例如我们可以让Activity继承一个基类Activity并在基类中重写startActivity()方法进行替换,但这种方法需要继承特定的基类相对没有那么方便,有没有其它更好地实现方式呢?
  这种方式就是Hook,Hook就是把一些系统类替换成我们自己的类,这样在正常执行系统流程的时候,就会执行我们的代码,在这里我们就可以做一些替换的操作来欺骗系统。就像上面Activity的启动流程中,向AMS发起请求前,系统会调用到Instrumentation、IActivityManager等类,那么如果我们Hook这些类,就可以在向AMS请求之前完成Activity的替换了。
  那么是不是所有类都可以Hook呢?并不是,类要能Hook必须满足一个条件:要么这个类实现了某个接口,且我们要Hook的方法声明在这个接口中,这种形式的可以通过动态代理的方式进行Hook,例如IActivityManager,我们要调用的startActivity()就在这个接口中;要么就是这个类是个可访问的类,这种形式的我们可以继承这个类并重写方法,例如Instrumentation,我们可以继承它并重写startActivity()。如果以上两个条件都不满足,那我们是没法Hook这个类的,这两种形式Hook的实现在VirtualApk源码中都有体现,我们后面再细看。那如果一个满足了上面的条件是否就一定可以Hook呢?理论上是可以Hook但不一定是好的Hook点,这里就是Hook点选择的问题了,一般情况下我们会选取单例模式的对象或者是静态变量作为Hook点,因为Hook这样的对象可以做到一次Hook一直生效,例如Instrumentation,它是ActivityThread的一个成员变量,但是我们知道ActivityThread一个进程就只有一个实例,因此也可以认为Instrumentation只有一个实例,另外看过源码的都知道IActivityManager是个单例模式的对象,所以Instrumentation和IActivityManager都是一个比较好的Hook点。

  上面讲了实现的思路以及方式,接下来就跟着VirtualApk源码看一下VirtualApk是如何实现的。

  我们一般会在Application的onCreate()中调用PluginManager.getInstance(this).init();进行VirtualApk的初始化,PluginManager是一个懒加载的单例模式,如果实例不存在会调用PluginManager的构造方法创建PluginManager实例,在构造方法中会调用hookCurrentProcess()进行Hook,我们看下这个方法实现,如下所示:

protected void hookCurrentProcess() {
    hookInstrumentationAndHandler();
    hookSystemServices();
    hookDataBindingUtil();
}

  这里调用不同方法hook不同的对象,第一个的方法hook了Instrumentation以及主线程(ActivityThread)的Handler,hook这两个对象用于实现Activity的插件化;第二个方法hook了IActivityManager,用于实现Service的插件化,这个方法我们在Service的插件化中再分析。我们看一下hookInstrumentationAndHandler(),如下所示:

protected void hookInstrumentationAndHandler() {
    try {
        ActivityThread activityThread = ActivityThread.currentActivityThread();
        Instrumentation baseInstrumentation = activityThread.getInstrumentation();
        final VAInstrumentation instrumentation = createInstrumentation(baseInstrumentation);
        Reflector.with(activityThread).field("mInstrumentation").set(instrumentation);
        Handler mainHandler = Reflector.with(activityThread).method("getHandler").call();
        Reflector.with(mainHandler).field("mCallback").set(instrumentation);
        this.mInstrumentation = instrumentation;
        Log.d(TAG, "hookInstrumentationAndHandler succeed : " + mInstrumentation);
    } catch (Exception e) {
        Log.w(TAG, e);
    }
}

  首先获取到ActivityThread的实例,并获取到ActivityThread的成员Instrumentation对象,接着创建了一个VAInstrumentation类的实例,并且把原本的Instrumentation对象传入,VAInstrumentation继承自Instrumentation并且实现了Handler.Callback接口,其内部有一个Instrumentation类型的mBase字段,原本的Instrumentation对象就会通过mBase字段持有;最后再通过反射将ActivityThread的mInstrumentation设置成刚刚创建的VAInstrumentation对象。这样子在启动Activity的时候,调用就不再是Instrumentation的execStartActivity(),而是VAInstrumentation的execStartActivity()hookInstrumentationAndHandler()接着通过反射获取到ActivityThread的Handler并将其Handler.Callback类型的成员mCallback也设置成刚创建的VAInstrumentation对象,这部在AMS回调以后会使用到,我们在下半部分再进行分析。我们还是看一下VAInstrumentation的execStartActivity()做了什么,方法如下所示:

public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) {
    injectIntent(intent);
    return mBase.execStartActivity(who, contextThread, token, target, intent, requestCode, options);
}

  方法首先调用injectIntent()将原始的Intent对象传入,接着调用了原本的Instrumentation对象的execStartActivity()完成向AMS的请求,因此我们看一下injectIntent()做了什么处理,如下所示:

protected void injectIntent(Intent intent) {
    mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent);   // 1
    if (intent.getComponent() != null) {
        Log.i(TAG, String.format("execStartActivity[%s : %s]", intent.getComponent().getPackageName(), intent.getComponent().getClassName()));
        mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);  // 2
    }
}

  注释1的代码主要是将Intent里包名转换成插件的包名,后面AMS回调以后通过包名找到对应的ClassLoader加载并创建Activity实例;我们看一下注释2的代码,如下所示:

public void markIntentIfNeeded(Intent intent) {
    if (intent.getComponent() == null) {
        return;
    }
    String targetPackageName = intent.getComponent().getPackageName();
    String targetClassName = intent.getComponent().getClassName();
    if (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) {
        intent.putExtra(Constants.KEY_IS_PLUGIN, true);
        intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName);
        intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);
        dispatchStubActivity(intent);
    }
}

  在上一步已经将intent中的包名转化成了插件的包名,该方法首先根据包名看看是否有匹配的插件,如果没有则不做转换,否则则将要启动的目标Activity的包名和类名作为参数添加到Intent中,再调用dispatchStubActivity(),该方法如下所示:

private void dispatchStubActivity(Intent intent) {
    ComponentName component = intent.getComponent();
    String targetClassName = intent.getComponent().getClassName();
    LoadedPlugin loadedPlugin = mPluginManager.getLoadedPlugin(intent);  // 1
    ActivityInfo info = loadedPlugin.getActivityInfo(component);  // 2
    if (info == null) {
        throw new RuntimeException("can not find " + component);
    }
    int launchMode = info.launchMode;  // 3
    Resources.Theme themeObj = loadedPlugin.getResources().newTheme();
    themeObj.applyStyle(info.theme, true);  // 4
    String stubActivity = mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj);  // 5
    Log.i(TAG, String.format("dispatchStubActivity,[%s -> %s]", targetClassName, stubActivity));
    intent.setClassName(mContext, stubActivity);  //6
}

  注释1处根据包名获取到加载插件时生成的LoadedPlugin对象,注释2处获取到包含要启动的Activity信息的ActivityInfo对象,在插件类的加载中我们说到了插件加载的时候会通过PackageParser去解析插件apk及其AndroidManifest文件,得到包含四大组件信息的列表,上面的ActivityInfo就是这时候生成的,注释3和注释4处获取到插件Activity在其AndroidManifest文件中声明的启动模式以及主题,并根据启动模式及样式在注释5处调用StubActivityInfo的getStubActivity()匹配一个最合适的占位Activity并返回它的类名,在注释6处将Intent中要启动的Activity设置成刚刚获取到的占位Activity。我们看一下getStubActivity()如何匹配占位Activity的,如下所示:

public String getStubActivity(String className, int launchMode, Theme theme) {
    String stubActivity= mCachedStubActivity.get(className);  // 1
    if (stubActivity != null) {
        return stubActivity;
    }
    TypedArray array = theme.obtainStyledAttributes(new int[]{
            android.R.attr.windowIsTranslucent,
            android.R.attr.windowBackground
    });
    boolean windowIsTranslucent = array.getBoolean(0, false);  // 2
    array.recycle();
    if (Constants.DEBUG) {
        Log.d(Constants.TAG_PREFIX + "StubActivityInfo", "getStubActivity, is transparent theme ? " + windowIsTranslucent);
    }
    stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
    switch (launchMode) {  // 3
        case ActivityInfo.LAUNCH_MULTIPLE: {
            stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
            if (windowIsTranslucent) {  // 4
                stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, 2);
            }
            break;
        }
        case ActivityInfo.LAUNCH_SINGLE_TOP: {
            usedSingleTopStubActivity = usedSingleTopStubActivity % MAX_COUNT_SINGLETOP + 1;  // 5
            stubActivity = String.format(STUB_ACTIVITY_SINGLETOP, corePackage, usedSingleTopStubActivity);
            break;
        }
        case ActivityInfo.LAUNCH_SINGLE_TASK: {
            usedSingleTaskStubActivity = usedSingleTaskStubActivity % MAX_COUNT_SINGLETASK + 1;  //6
            stubActivity = String.format(STUB_ACTIVITY_SINGLETASK, corePackage, usedSingleTaskStubActivity);
            break;
        }
        case ActivityInfo.LAUNCH_SINGLE_INSTANCE: {
            usedSingleInstanceStubActivity = usedSingleInstanceStubActivity % MAX_COUNT_SINGLEINSTANCE + 1;  //7
            stubActivity = String.format(STUB_ACTIVITY_SINGLEINSTANCE, corePackage, usedSingleInstanceStubActivity);
            break;
        }
        default:break;
    }
    mCachedStubActivity.put(className, stubActivity);  // 8
    return stubActivity;
}

  注释1处先判断是否已经为目标Activity匹配过占位Activity,如果有则直接从缓存中取;注释2处获取目标主题,VirtualApk对标准启动模式内置了两种主题的占位Activity,分别是普通主题和半透明主题,windowIsTranslucent参数在注释4处用到,根据这个参数选取对应主题占位的Activity;注释3处就根据目标Activity的启动模式选取对应启动模式的占位Activity,其中因为标准模式的Activity是可以重复创建实例的,所以标准模式的占位Activity可以重用(一个占位Activity可以对应多个目标Activity),因此标准模式的占位Activity一种主题只需要一个即可,但其他的启动模式在一定条件下是会复用而不会重新创建实例,因此这种占位Activity不能重用,VirtualApk需要内置一定数量的该启动模式的占位Activity,且如果我们启动的这种模式插件Activity的数量大于VirtualApk内置的数量,是可能出现问题的,SingleTop、SingleTask、SingleInstance这三种启动模式的数量都是8个。我们看到注释5、6、7处的代码就是循环找对应启动模式的空闲占位Activity,但这种方式不保证找到的占位Activity是可用的,因为有可能还未释放。最后在注释8处将匹配的结果加入缓存中。
  既然VirtualApk内置了这么多的占位Activity,那么AndroidManifest应该可以看到它们的声明,我们看下VirtualApk core库中的Activity声明:

<activity android:exported="false" android:name=".A$1" android:launchMode="standard"/>
<activity android:exported="false" android:name=".A$2" android:launchMode="standard"
    android:theme="@android:style/Theme.Translucent" />

<activity android:exported="false" android:name=".B$1" android:launchMode="singleTop"/>
<activity android:exported="false" android:name=".B$2" android:launchMode="singleTop"/>
<activity android:exported="false" android:name=".B$3" android:launchMode="singleTop"/>
<activity android:exported="false" android:name=".B$4" android:launchMode="singleTop"/>
<activity android:exported="false" android:name=".B$5" android:launchMode="singleTop"/>
<activity android:exported="false" android:name=".B$6" android:launchMode="singleTop"/>
<activity android:exported="false" android:name=".B$7" android:launchMode="singleTop"/>
<activity android:exported="false" android:name=".B$8" android:launchMode="singleTop"/>

<activity android:exported="false" android:name=".C$1" android:launchMode="singleTask"/>
<activity android:exported="false" android:name=".C$2" android:launchMode="singleTask"/>
<activity android:exported="false" android:name=".C$3" android:launchMode="singleTask"/>
<activity android:exported="false" android:name=".C$4" android:launchMode="singleTask"/>
<activity android:exported="false" android:name=".C$5" android:launchMode="singleTask"/>
<activity android:exported="false" android:name=".C$6" android:launchMode="singleTask"/>
<activity android:exported="false" android:name=".C$7" android:launchMode="singleTask"/>
<activity android:exported="false" android:name=".C$8" android:launchMode="singleTask"/>

<activity android:exported="false" android:name=".D$1" android:launchMode="singleInstance"/>
<activity android:exported="false" android:name=".D$2" android:launchMode="singleInstance"/>
<activity android:exported="false" android:name=".D$3" android:launchMode="singleInstance"/>
<activity android:exported="false" android:name=".D$4" android:launchMode="singleInstance"/>
<activity android:exported="false" android:name=".D$5" android:launchMode="singleInstance"/>
<activity android:exported="false" android:name=".D$6" android:launchMode="singleInstance"/>
<activity android:exported="false" android:name=".D$7" android:launchMode="singleInstance"/>
<activity android:exported="false" android:name=".D$8" android:launchMode="singleInstance"/>

  可以看到VirtualApk注册了非常多的占位Activity,且它们的类字和匹配逻辑中的一致,需要注意的是,VirtualApk虽然注册了这些Activity,但它们对应的类是不存在的,这一点我们后面还会再关注到。
  上面就是占位Activity的匹配逻辑了,这里总结下,启动一个Activity时会调用VAInstrumentation的execStartActivity(),其内部会调用injectIntent()处理Intent,这个方法会进行Intent包名的转化,再根据包名看是否匹配插件,如不匹配则说明要启动的是宿主Activity,那么无需处理直接启动即可,否则将要启动的目标Activity包名和类名作为附加参数添加到Intent中,再根据目标Activity的启动模式、主题匹配一个空闲(不绝对)的占位Activity,将Intent的启动的Activity设置成该占位Activity,最后调用原Instrumentation的execStartActivity()向AMS发送请求,这样AMS收到的就是启动占位Activity的请求了,因此AMS的校验也就不会出现问题,在AMS校验完成后会回调到启动Activity所在的应用进程。那么Activity向AMS发起请求的部分就完成了,下面看一下Activity启动部分。

Activity的启动

  Activity启动部分的时序图如下:

Activity的启动

  AMS会调用ApplicationThread的scheduleLaunchActivity()回调到应用进程,再通过主线程的Handler发送一个LAUNCH_ACTIVITY类型的消息,Handler处理这个消息时调用ActivityThread的handlerLaunchActivity(),其内部又会调用performLaunchActivity(),这个方法会调用Instrumentation的newActivity()创建Activity实例,并调用Activity的attach()完成Activity的初始化,最后调用Instrumentation的callActivityOnCreate(),其内部会调用Activity的performCreate(),再调用onCreate()完成Activity的启动。
  前面我们说到AMS回调到应用进程时参数中要启动的Activity还是占位Activity,因此我们需要在Activity创建实例之前将Activity替换回来。这里依然采用Hook的方式,可选的Hook点有很多,例如主线程的Handler,在Handler处理消息时可以进行替换;另外还有Instrumentation,在调用newActivity()创建Activity实例实例时也可以进行替换。VirtualApk这两个点都进行了Hook,但是Hook主线程Handler是为了处理一下插件的主题,而Hook Instrumentation完成Activity的替换;在前面分析hookInstrumentationAndHandler()的时候我们说到了方法还hook了主线程Handler,因此我们现在再看看它如何Hook的:

protected void hookInstrumentationAndHandler() {
    ...
    Handler mainHandler = Reflector.with(activityThread).method("getHandler").call();
    Reflector.with(mainHandler).field("mCallback").set(instrumentation);
    ...
}

  方法反射调用ActivityThread的getHandler()获得了ActivityThread的H类型成员mH对象,H是ActivityThread的一个内部类,外部无法访问,继承自Handler,用于处理主线程的消息。接着将前面创建的VAInstrumentation类型的对象通过反射赋值给Handler的mCallback,VAInstrumentation实现了Callback接口。当Handler处理消息时,会判断mCallback是否不为空,若不为空则先调用mCallback进行处理,这样就会调用到VAInstrumentation的handlerMessage()进行处理,也就完成了hook。
  可能会有疑问为什么需要绕个弯去Hook Handler的mCallback而不直接HookActivityThread的mH,这就涉及我前面说到的Hook的条件了,mH是Hook不到的,因为我们要Hook的方法handlerMessage()并不属于一个接口,因此无法通过动态代理的方式去Hook,同时H是ActivityThread内部类访问权限是private的,因此我们也没法通过继承的方式去Hook,所以我们只能转而去Hook Handler的mCallback,因为其handlerMessage()是在接口Callback中。
  VAInstrumentation的handlerMessage()主要进行Activity主题的替换,这里就不深入分析了。

  我们看看插件Activity是如何被还原的,前面流程中已经说到了会调用Instrumentation的newActivity()创建实例,而VirtualApk已经通过Hook编程了VAInstrumentation且重写了newActivity(),因此我们看看VAInstrumentation的newActivity()实现,如下所示:

public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
    try {
        cl.loadClass(className);  // 1
    } catch (ClassNotFoundException e) {
        ComponentName component = PluginUtil.getComponent(intent);  // 2
        if (component == null) {
            return newActivity(mBase.newActivity(cl, className, intent));
        }
        String targetClassName = component.getClassName();
        LoadedPlugin plugin = mPluginManager.getLoadedPlugin(component);  // 3
        if (plugin == null) {
            boolean debuggable = false;
            try {
                Context context = mPluginManager.getHostContext();
                debuggable = (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
            } catch (Throwable ex) {}
            if (debuggable) {
                throw new ActivityNotFoundException("error intent: " + intent.toURI());
            }
            return newActivity(mBase.newActivity(cl, StubActivity.class.getName(), intent));
        }
        Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);  // 4
        activity.setIntent(intent);
        Reflector.QuietReflector.with(activity).field("mResources").set(plugin.getResources());
        return newActivity(activity);
    }
    return newActivity(mBase.newActivity(cl, className, intent));
}

  注释1处通过ClassLoader去加载目标Activity类,这里的ClassLoader是从ActivityThread传递过来的,是宿主的PathClassLoader,如果要启动的是宿主类,那么PathClassLoader是可以正常加载,就不会进入catch中的逻辑,最后调用原Instrumentation的newActivity()完成Activity创建;如果要启动的是插件类,我们知道目前还没有进行插件Activity的替换,所以这里的className还是占位Activity的类名,但我们前面提到过,VirtualApk虽然注册了这些占位Activity但并没有对应的类,所以PathClassLoader是无法加载的,就会进入到catch中。注释2处会从Intent中提取目标Activity的包名和类名并重新构造一个ComponentName对象,这里提取的包名和类名正是我们在上半部分中作为附加参数添加到Intent中的。接着在注释3处根据插件的包名得到了插件对应的LoadedPlugin对象,最后在注释4处调用原Instrumentation的newActivity()创建Activity实例,但不同的是这里传递的类名变成我们要启动的插件Activity,ClassLoader变成了从LoadedPlugin对象获取的ClassLoader,在插件类的加载中我们已经说过了这个ClassLoader就是用于加载对应插件的DexClassLoader,接着我们看一下Instrumentation的newActivity()是如何创建Activity实例的,如下所示:

public Activity newActivity(ClassLoader cl, String className, Intent intent)
        throws InstantiationException, IllegalAccessException,
        ClassNotFoundException {
    String pkg = intent != null && intent.getComponent() != null
            ? intent.getComponent().getPackageName() : null;
    return getFactory(pkg).instantiateActivity(cl, className, intent);
}

  方法主要是调用了AppComponentFactory的instantiateActivity(),我们看看这个方法,如下所示:

public Activity instantiateActivity(ClassLoader cl, String className,Intent intent)
        throws InstantiationException, IllegalAccessException, ClassNotFoundException {
    return (Activity) cl.loadClass(className).newInstance();
}

  代码非常简单,就是通过我们传递的ClassLoader,调用其loadClass()加载类并调用Class类的newInstance()创建出Activity实例,我们传递的DexClassLoader显然是可以加载并创建出插件Activity实例的,这样就完成了插件Activity的创建了。

  在插件类的加载中我们留下了一个问题,就是当Constants.COMBINE_CLASSLOADER为false时,这时候插件的DexFile是不会合并入宿主的PathClassLoader中的,在这种场景下在宿主无法加载普通的插件类,却可以正常启动包括Activity在内的四大组件,这是为什么呢?其实看到这里应该就很明白了,在宿主中加载插件类会通过宿主的PathClassLoader,因为DexFile没有合并所以PathClassLoader并不能加载,但是在启动四大组件时,并不是通过宿主的PathClassLoader去加载四大组件类,而是会找到插件对应的DexClassLoader,由它去加载四大组件类,所以插件中的四大组件可以被加载并启动。

  在创建出Activity实例后,系统会继续执行后续的流程,接着会执行Instrumentation的callActivityOnCreate(),VAInstrumentation同样重写了这个方法,其内部主要是调用了VAInstrumentation的injectActivity()并调用原Instrumentation的callActivityOnCreate(),因此我们看一下injectActivity()的实现,如下所示:

protected void injectActivity(Activity activity) {
    final Intent intent = activity.getIntent();
    if (PluginUtil.isIntentFromPlugin(intent)) {
        Context base = activity.getBaseContext();
        try {
            LoadedPlugin plugin = mPluginManager.getLoadedPlugin(intent);
            Reflector.with(base).field("mResources").set(plugin.getResources());  // 1
            Reflector reflector = Reflector.with(activity);
            reflector.field("mBase").set(plugin.createPluginContext(activity.getBaseContext()));  // 2
            reflector.field("mApplication").set(plugin.getApplication());  //3
            ActivityInfo activityInfo = plugin.getActivityInfo(PluginUtil.getComponent(intent));
            if (activityInfo.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
                activity.setRequestedOrientation(activityInfo.screenOrientation);
            }
            ComponentName component = PluginUtil.getComponent(intent);
            Intent wrapperIntent = new Intent(intent);
            wrapperIntent.setClassName(component.getPackageName(), component.getClassName());
            activity.setIntent(wrapperIntent);
        } catch (Exception e) {
            Log.w(TAG, e);
        }
    }
}

  在注释1处通过反射对Activity的资源进行了替换,替换后Activity就可以加载插件中的资源,关于资源的创建我们在后边资源的插件化章节中再细讲。注释2和注释3分别将Activity的Context、Application替换成了表示插件的PluginContext以及Application,替换后可以保证一些场景能获取到正确的数据。
  Instrumentation的callActivityOnCreate()调用以后最终会调用到Activity的onCreate(),到这里就完成了插件Activity的启动了。

  我们可以通过adb shell dumpsys activity activities看看在AMS中Activity栈的情况是怎样的,我启动了两个插件Activity,得到如下结果:

  可以发现在AMS中记录的Activity并不是我们实际的Activity,而依旧是占位Activity。其实这很好理解,因为我们在向AMS发起请求时是用的占位Activity,在AMS回调以后再将Activity替换回来,AMS一直认为的都是启动的占位Activity,它甚至没有感知到插件Activity的存在,所以AMS中记录的也都是占位Activity。这也就是为什么启动其他启动模式的Activity超过最大数量时会有问题的原因了,因为AMS会一直当成要启动占位Activity,如果同一个占位Activity它就会复用而不会创建实例,那么使用同一个占位Activity的所有插件Activity中,后启动的插件Activity就可能会无法被启动。
  那么新的问题来了,如果AMS中记录的是占位Activity,那后续Activity的生命周期插件Activity能够正常执行吗?我们以destroy为例,当我们调用finish()关闭Activity时,其内部会调用以下方法与AMS通信:

ActivityManager.getService().finishActivity(mToken, resultCode, resultData, finishTask)

  调用时会携带token,AMS回调后最终会调用到ActivityThread的performDestroyActivity(),如下所示:

private ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing, int configChanges, boolean getNonConfigInstance) {
    ActivityClientRecord r = mActivities.get(token);
    ...
        performPauseActivityIfNeeded(r, "destroy");
        ...
                r.activity.performStop(r.mPreserveWindow);
                ...
            mInstrumentation.callActivityOnDestroy(r.activity);
            ...
    mActivities.remove(token);
    StrictMode.decrementExpectedActivityCount(activityClass);
    return r;
}

  首先根据上面的方法可以看出,应用与AMS通信并没有指定具体的Activity,而是通过Token进行标识的。方法首先根据token取出ActivityClientRecord,并对其类型为Activity的成员变量activity调用onPause()、onStop()以及onDestroy(),那么这个Activity是占位Activity还是插件Activity呢?在ActivityThread的performLaunchActivity()中,会先调用Instrumentation的newActivity()创建Activity实例,再将创建的Activity对象赋值给ActivityClientRecord的成员activity,所以这里取出的Activity是我们的插件Activity对象,也就是说会调用插件Activity的onDestroy()等生命周期方法,因此即使在AMS中并不知道我们插件Activity的存在,但是插件Activity是有生命周期的。

  这一节我们分析了四大组件中最重要的一个组件Activity的插件化,另外这一节讲到的一些思路,像Hook技术,对后面的其他几个组件的插件化都是有用的。下一节我们再一起分析下Service的插件化原理。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349