1插件化面临的问题

图解

graph LR
subgraph 解决插件组件生命周期调用问题
插件生命周期
end

subgraph 反射AssetManager.assetPath,再通过AssetManager来创建一个新的Resources对象
加载插件资源
end

subgraph newDexClassLoader加载插件apk,从插件ClassLoader中load指定的插件Activity名字,newInstance之后强转为Activity类型使用
加载插件类
end

subgraph 上下文如BaseContext,mResources,activityInfo在attach时反射替换为插件的
直接提供插件Activity给系统
end


插件生命周期-->启动插件Activity

启动插件Activity-->HookInstrumentation的execStartActivity
启动插件Activity-->|Shadow|更改启动Activity方式,调用自定义方法到插件进程,转成插代理Activity
插件生命周期-->AMS回到App端

AMS回到App端-->直接提供插件Activity给系统-->Hack修改宿主PathClassLoader
直接提供插件Activity给系统-->HackActivityThread主线程Handler

AMS回到App端-->提供代理Activity给系统-->生命周期转发给插件Activity
提供代理Activity给系统-->|Shadow|生命周期转发给插件Activity,再转调回代理Activity

加载插件类

Shadow的全动态设计原理解析

Java还有两个和动态化相关的特性,一个是接口,另一个是向上转型。

Class<?> implClass = classLoader.loadClass("com.xxx.AImpl");
Object implObject = implClass.newInstance();
A a = (A) implObject;

这里假设classLoader动态加载了一些Java类,其中就有一个类叫做com.xxx.AImpl,AImpl继承自A,或者AImpl实现了A接口。注意这里用了强制类型转换,是因为代码层面是将Object类型向下转型成了A。但实际上我们知道implObject的类型是AImpl,AImpl转换成A是一个向上转型。向上转型总是安全的。所以用这种方法总是可以先定义出接口,精心设计接口,让接口足够通用和稳定。只要接口不变,它的实现总是可以修改的。我们将接口打包在宿主中,接口就轻易不能更新了。但是它的实现总是可以更新的。

加载插件资源

传统方式

https://github.com/singwhatiwanna/dynamic-load-apk/

我们知道,activity的工作主要是由ContextImpl来完成的, 它在activity中是一个叫做mBase的成员变量。注意到Context中有如下两个抽象方法,看起来是和资源有关的,实际上context就是通过它们来获取资源的,这两个抽象方法的真正实现在ContextImpl中。也即是说,只要我们自己实现这两个方法,就可以解决资源问题了。

/** Return an AssetManager instance for your application's package. */
public abstract AssetManager getAssets();
/** Return a Resources instance for your application's package. */
public abstract Resources getResources();

下面看一下如何实现这两个方法 首先要加载apk中的资源:

protected void loadResources() {  
    try {  
        AssetManager assetManager = AssetManager.class.newInstance();  
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);  
        addAssetPath.invoke(assetManager, mDexPath);  
        mAssetManager = assetManager;  
    } catch (Exception e) {  
        e.printStackTrace();  
    }  
    Resources superRes = super.getResources();  
    mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),  
            superRes.getConfiguration());  
    mTheme = mResources.newTheme();  
    mTheme.setTo(super.getTheme());  
}

说明:加载的方法是通过反射,通过调用AssetManager中的addAssetPath方法,我们可以将一个apk中的资源加载到Resources中,由于addAssetPath是隐藏api我们无法直接调用,所以只能通过反射,下面是它的声明,通过注释我们可以看出,传递的路径可以是zip文件也可以是一个资源目录,而apk就是一个zip,所以直接将apk的路径传给它,资源就加载到AssetManager中了,然后再通过AssetManager来创建一个新的Resources对象,这个对象就是我们可以使用的apk中的资源了,这样我们的问题就解决了。

Shadow使用的方式

val packageManager = hostAppContext.packageManager
val packageArchiveInfo = packageManager.getPackageArchiveInfo(
        archiveFilePath,
                 PackageManager.GET_ACTIVITIES
                    or PackageManager.GET_META_DATA
                    or PackageManager.GET_SERVICES
                    or PackageManager.GET_PROVIDERS
                    or PackageManager.GET_SIGNATURES
)
packageArchiveInfo.applicationInfo.nativeLibraryDir = installedApk.libraryPath
packageArchiveInfo.applicationInfo.dataDir = dataDir.absolutePath
packageArchiveInfo.applicationInfo.publicSourceDir = archiveFilePath
packageArchiveInfo.applicationInfo.sourceDir = archiveFilePath

packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo)

getResourcesForApplication控制AssetManager加载插件路径里的资源:
createAssetManager(ResourcesKey):358, ResourcesManager (android.app), ResourcesManager.java
createResourcesImpl(ResourcesKey):490, ResourcesManager (android.app), ResourcesManager.java
getOrCreateResources(IBinder, ResourcesKey, ClassLoader):787, ResourcesManager (android.app), ResourcesManager.java
getResources(IBinder, String, String[], String[], String[], int, Configuration, CompatibilityInfo, ClassLoader):853, ResourcesManager (android.app), ResourcesManager.java
getTopLevelResources(String, String[], String[], String[], int, LoadedApk):1940, ActivityThread (android.app), ActivityThread.java
getResourcesForApplication(ApplicationInfo):1420, ApplicationPackageManager (android.app), ApplicationPackageManager.java
create(PackageInfo, String, Context):32, CreateResourceBloc (com.tencent.shadow.core.loader.blocs), CreateResourceBloc.kt
call():109, LoadPluginBloc$loadPlugin$buildResources$1 (com.tencent.shadow.core.loader.blocs), LoadPluginBloc.kt

上述创建的插件Resource存放在ShadowPluginLoader中PluginPartsMap对应的entry value也就是pluginParts中,pluginParts保存了插件application,classLoader,resources等信息

上述resource被用来构造mixResource并在ShadowActivityDelegate的getResource方法中返回mixResource,默认使用插件resource


不同插件之间,宿主和插件之间的资源管理方面:

  • 公用一套资源,需要采用固定资源id及ID分段机制避免冲突
  • 独立资源方案,不同插件管理自己的资源

解决插件组件生命周期调用问题

插件Activity生命周期

  1. 动态代理和Binder hook(AMS和PMS)方式,如360的DroidPlugin方案等

以下是静态代理,占坑方式偷梁换柱

启动插件Activity替换成代理Activity

  • 1.1 通过Hook Instrumentation的execStartActivity()把intent中的插件Activity替换成代理Activity,让系统去启动代理Activity
  • 1.2 宿主启动插件,更改启动Activity的方式,通过调用FastPluginManager的startPluginActivity方法到插件进程将插件Activity转成成代理ContainerActivity(Shadow中宿主启动插件的方式)正常启动
  • 1.3 插件Activity跳转内部Activity,在super类内部控制转调代理Activity

再次回到App端初始化Activity时,需要将代理Activity替换成(或转调到)插件Activity

有几种Hook方式可以实现

  • 2.1:直接提供插件Activity给系统,让系统回调插件Activity的生命周期(VirtualAPk, Replugin,Neptune新版)

面临插件上下文信息使用的是宿主信息的问题:

需要在Activity attach方法调用后(Instrumentation#callActivityOnCreate)反射替换刚刚attach到Activity里的BaseContext,mResources,activityInfo等属性,这里可以优化的是可以在对应属性的getter方法中拦截处理,而不是反射替换到属性值,因此需要字节码插桩让插件的Activity继承自PluginActivity(插件Activity基类),在这个基类中实现各种对插件包依赖的转调(Neptune新的方式),通过装饰者模式实现

  • 2.2:提供给系统的是代理Activity
    • 2.2.1 需要将所有生命周期方法(还有attach方法)全部转调给插件Activity,目的是让插件Activity自身有活力
    • 2.2.2 (Shadow)生命周期方法由代理Activity转调到插件Activity,并在插件Activity调用super方法时转调到代理Activity,插件Activity可以不继承Activity仅仅是一个Object,插件Activity完全不需要有活力,只要代理Activity有活力就行

面临插件上下文信息使用的是宿主信息的问题: Shadow的处理方式和上面装饰者的方式类似,区别在于动态插桩的插件基类不是真的Activity,是一个ContextWrapper,也就是说,本身就是一个装饰功能类,并非真的Activity,中间的双向转调层可以获取插件信息并通过HostActivityDelegator转调给宿主Activity

Tencent Shadow—零反射全动态Android插件框架正式开源 那么一个重要的原则就是,如果一个组件需要安装才能使用,那么就别在没安装的情况下把它交给系统。我们已知的插件框架中,做的最好的也不符合这个原则,所以尽管它的Hook点少,但就是由于它将没有安装的Activity交给系统了,所以后面就不得不做一些Hack的事修补

Shadow解决Activity等组件生命周期的方法解析

实际上通过前面的分析,我们发现其实根本不需要插件Activity执行super.onCreate()方法。明确了这个方案原本的目的,就是在宿主中注册并启动一个壳子Activity,这个壳子Activity什么都不自己做,想办法让插件Activity的各个生命周期方法实现代码成为壳子Activity的各个生命周期实现方法的代码。因此我们根本不需要插件Activity是一个系统Activity的子类。我们只是因为需要插件Activity还能正常安装运行,才导致它是一个真正的系统Activity子类的。

class ShadowActivity {
    ContainerActivity containerActivity;
    public void onCreate(Bundle savedInstanceState) {
        containerActivity.superOnCreate(savedInstanceState);
    }
}

class XXXPluginActivity extends ShadowActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        savedInstanceState.clear();
        super.onCreate(savedInstanceState);
    }
}

class ContainerActivity extends Activity {
    ShadowActivity pluginActivity;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        pluginActivity.onCreate(savedInstanceState);
    }

    public void superOnCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
}
仔细思考下现在的调用结果是不是PluginActivity在正常安装运行和插件环境下运行时行为就一致了