腾讯零反射全动态Android插件框架,一文解析
/ 今日科技快讯 /
近日,人工智能开发领域顶尖企业的高管和员工将开会讨论制定关于如何使用人工智能技术的标准。据外媒报道,OpenAI、微软、谷歌、苹果、英伟达、Stability AI、Hugging Face和Anthropic等公司的代表预计将出席此次会议。这次会议由顶级投资公司SV Angel召集,与会代表计划讨论如何以最负责任的态度继续开发人工智能。
/ 作者简介 /
本篇文章来自a5right的投稿,文章主要分享了他对腾讯插件化框架Shadow的解析,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。
a5right的博客地址:
https://lyldalek.top/about/
/ 其一 /
项目编译问题
首先这个工程是可以直接在命令行 build 的,但是我的台式机(windows) sync/rebuild 等可能会有一些奇葩问题。后来又发现我的笔记上clone一个新的项目没有以下问题,神奇!!!
GBK 编码问题,gradle build 可以通过,然后再次 sync 就没问题了…
projects - sdk 下找不到 android.jar 中的代码问题,咨询了作者暂时不知道问题在哪里,其核心代码逻辑在 CommonJarSettingsPlugin 里面的 project.dependencies.add("compileOnly", androidJar) **,但是没生效,试了下,直接在 dependence 里面使用也不行,但是将 android.jar 拷贝到工程目录下就可以了,所以临时解决方案就是拷贝改文件,然后将这个路径写死,再 attach 一下 source 即可。
DocumentBuilderFactory.newDefaultInstance() 报错问题,有的设置了 jdk 为 11 就行,可以看项目里面的 issue。但是我这不行,改成 DocumentBuilderFactory.newInstance() 即可。
项目结构概览
红框部分
sample-constant是一些字符串常量(host 与 plugin 通用)。
sample-host是宿主应用。
sample-manager是插件管理器的动态实现。
绿框部分
sample-plugin/sample-loader是loader的动态实现,业务主要在这里定义插件组件和壳子代理组件的配对关系等。
sample-plugin/sample-runtime是runtime的动态实现,业务主要在这里定义壳子代理组件的实际类。
sample-plugin/sample-base-lib是插件App的一部分基础代码,是一个aar库。
sample-plugin/sample-base是一个apk模块壳子,将sample-base-lib打包在其中。既用于正常安装运行开发sample-base-lib。sample-plugin/sample-app 是依赖 sample-base-lib 开发的更多业务代码。它编译出的插件 apk 没有打包 sample-base-lib ,会在插件运行时依赖 sample-base 插件。
黄框部分
coding 处理模板代码生成,android.jar 等问题(因为 module 使用的是 java-library)。
core 核心代码。
dynamic 动态相关实现(manager,loader,runtime)都是动态的。
这一块是通过 includeBuild 来进行编译的。
插件内容说明
宿主发布出去后,可以动态更新两部分内容。
可以看到宿主有两个插件包,一个是 pluginmanager.apk,这个是属于宿主的,也可以动态更新。一个是 plugin.zip,这个就是纯插件内容,里面包含 loader/runtime,还有业务相关的 app 与 base 内容。
/ 其二 /
全动态框架
插件部分自然不用说,当然是动态的。全动态的意思是插件框架本身设计的也是动态的。
这里我们介绍一个前置知识:
Java 编译的字节码中保存的就是其他类的名称,其他类的实现是在运行时才去查找的。所以 Java 是一个“动态”语言。Java 有两个和动态化相关的特性,一个是接口,另一个是向上转型:
Class<?> implClass = classLoader.loadClass("com.xxx.AImpl");
Object implObject = implClass.newInstance();
A a = (A) implObject;
使用上面的方式,我们就可以先定义出接口,精心设计接口,让接口足够通用和稳定。只要接口不变,它的实现总是可以修改的。我们将接口打包在宿主中,接口就轻易不能更新了。但是它的实现总是可以更新的。
所有的插件框架在解决的问题都不是如何动态加载类,而是解决动态加载的 Activity 没有在 AndroidManifest 中注册,该如何能正常运行的问题。如果 Android 系统没有 AndroidManifest 的限制,那么所有插件框架都没有存在的必要了。因为 Java 语言本身就支持动态更新实现的能力。
这也就是为什么几乎所有的插件化框架,都在到处找 hook 点来欺骗系统。这种插件化框架即使是开源的,看起来也很令人蛋疼,因为它的知识点很偏。一般人的知识范围是一个圆,这些知识点就完全在圆外面,而且还是散开的,就算你看了一遍,当时想通了,过不了多久也会忘记,因为这些知识点与你现有的知识链接的很弱,跟一条蛛丝一样,想要完全搞懂,你需要一条高速公路的链接,所以,通常写这个框架的人才能真正搞懂里面的东西,而看这个框架的人学不到什么东西。
但是 shadow 相对而言就好得多,它没有 hook 点,不需要你去翻源码搞清楚每个点为啥要 hook,甚至是翻每个版本的源码差异来搞清楚里面的每个 if-elseif 的作用。在这里我们可以专心的深入架构设计,顺便看一下插件加载与运行的流程。本篇虽然是深入理解,但是并不会每个细节都讲到,正如前面所说的,有些逻辑如果自己没有遇到过,基本是搞不明白为啥要这样写的,写插件化框架的人也是一个一个坑踩出来的。
有了前置知识,我们来看看如何实现插件框架的动态化。首先,插件框架分为3大部分:
manager
runtime
loader
核心有3大部分,但是能动态化的只有两部分,一个是 manager,一个是 loader。除此之外,还有一个 container 也是动态的。
Manager
Manager 负责管理插件,包括插件的下载逻辑、入口逻辑,预加载逻辑等。由于 Manager 就是一个普通类,不是 Android 系统规定要在 Manifest 中注册才能使用的类,所以 Manager 的动态化就是一般性的动态加载实现。
上面这个工程就是我们开发一个插件的时候,Manager 具体的实现,逻辑很简单,继承 PluginManagerThatUseDynamicLoader ,实现一个自己的 Manager就行。实际上 FastPluginManager 里面基本上也是模板代码,所以最终我们只需要实现一下 SamplePluginManager 。其核心方法是:
/**
* @return 宿主中注册的PluginProcessService实现的类名
*/
@Override
protected String getPluginProcessServiceName(String partKey) {
if (PART_KEY_PLUGIN_MAIN_APP.equals(partKey)) {
return "com.tencent.shadow.sample.host.PluginProcessPPS";
} else if (PART_KEY_PLUGIN_BASE.equals(partKey)) {
return "com.tencent.shadow.sample.host.PluginProcessPPS";
} else if (PART_KEY_PLUGIN_ANOTHER_APP.equals(partKey)) {
return "com.tencent.shadow.sample.host.Plugin2ProcessPPS";//在这里支持多个插件
} else {
//如果有默认PPS,可用return代替throw
throw new IllegalArgumentException("unexpected plugin load request: " + partKey);
}
}
这个方法的作用就是要声明插件与宿主里面的 Service 的对应关系。宿主声明的 Service 如下:
<service
android:name="com.tencent.shadow.sample.host.PluginProcessPPS"
android:process=":plugin" />
插件一般是运行在一个单独的进程,也可以多个插件运行在一个进程。比如 sample 里面的,将一个业务插件拆成了两部分,一个是 base,一个 main-app,这虽然是两个插件包,但是实际上是一个业务整体,这两个插件运行在同一个进程。
回到 manager 部分,我们实现了 SamplePluginManager 后,打包成一个插件 apk,发布出去。那么宿主需要将这个 manager 插件加载进来,其流程相当简单:
// 这里的 apk 参数就是 manager 插件 apk 的路径
Shadow.getPluginManager(apk);
这里调用链接如下:
com.tencent.shadow.test.dynamic.host.manager.Shadow#getPluginManager
com.tencent.shadow.dynamic.host.DynamicPluginManager#DynamicPluginManager
会实例化一个 DynamicPluginManager 出来,这个类主要处理pluginManager的动态升级问题。由两部分构成,一个是 PluginManagerUpdater,一个是 PluginManagerImpl。PluginManagerUpdater 就是去查询是否有更新插件 apk 的存在,有就去加载这个对应的文件。
PluginManagerImpl 就是我们写的 SamplePluginManager,是通过 ManagerImplLoader 来获取的。细节就是使用工厂模式去实例化一个com.tencent.shadow.dynamic.impl.ManagerFactoryImpl 对象出来。这个类名是固定的,我们的 Manager 插件包里面必须包含这个类,然后里面返回我们的 SamplePluginManage。
/**
* 此类包名及类名固定
*/
public final class ManagerFactoryImpl implements ManagerFactory {
@Override
public PluginManagerImpl buildManager(Context context) {
return new SamplePluginManager(context);
}
}
这样流程就清楚了,我们看一下 ManagerImplLoader 的核心实现:
PluginManagerImpl load() {
// 创建 manager 插件的 classLoader,就是一个普通的 classLoder,只不过支持访问在白名单里面的宿主类
ApkClassLoader apkClassLoader = new ApkClassLoader(...);
// 这个 context 传递给了 SamplePluginManager,里面做了资源等的替换
Context pluginManagerContext = new ChangeApkContextWrapper(...);
try {
ManagerFactory managerFactory = apkClassLoader.getInterface(...);
return managerFactory.buildManager(pluginManagerContext);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
Loader
Loader 就是负责加载插件 Activity,然后实现插件 Activity 的生命周期等功能的那部分核心逻辑了。Loader 的动态化稍微蛋疼一些,虽然核心流程与 Manager 类似,但是由于 loader 与插件的依赖关系较为强烈,比如代理壳子PluginContainerActivity 需要和 PluginActivity 通过 Loader 相互调用。所以 Shadow 应用前面提到的动态化原理时,做了双向的接口,可以看到代码中的 HostActivityDelegate 和 HostActivityDelegator。通过定义出这两个接口,可以避免 PluginContainerActivity 和 Loader 相互加载对方时还需要加载对方所依赖的其他类。定义成接口,就只需要加载这个接口就行了。
我们举个例子说明一下,看一下 PluginContainerActivity 的代码,它是插件的一部分:
public PluginContainerActivity() {
HostActivityDelegate delegate;
DelegateProvider delegateProvider = DelegateProviderHolder.getDelegateProvider(getDelegateProviderKey());
if (delegateProvider != null) {
delegate = delegateProvider.getHostActivityDelegate(this.getClass());
delegate.setDelegator(this);
} else {
Log.e(TAG, "PluginContainerActivity: DelegateProviderHolder没有初始化");
delegate = null;
}
super.hostActivityDelegate = delegate;
hostActivityDelegate = delegate;
}
如果没有 HostActivityDelegate 这个接口,那么这里必然要先一个具体的实现类,也就是 ShadowActivityDelegate,而这个类是在 loader 中的,所以插件就与 loader 强关联了。我们需要尽可能的让 loader 单独加载。
通过定义接口进行解耦,是很常见的一种方式,正所谓,计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决。后面我们还会遇到同样的设计方式。
loader 插件工程的结构几乎与manager 插件工程一样,不做过多介绍。
Runtime
runtime 实际上不能动态化,因为它必须和插件绑定在一起。从 sample 工程可以看出,动态化的是 container:
里面实际上就是那些注册在宿主 AndroidManifest 中的代理壳子。由于 Activity 的创建是系统根据 Activity 的名字直接通过宿主的 PathClassLoader 构造的,所以这些 Activity 必须打包在宿主中才能处于 PathClassLoader,才能被系统找到。
之所以要做这个设计,是因为 tencent 业务的宿主对合入代码的增量要求极其严格,是要求0增量合入的。而代理壳子 Activity 上需要提前 Override 非常多的方法。同时由于定义了 Delegate和Delegator 接口,还在 Delegator 接口上又添加了 superOnCreate 等方法,导致 Activity 上每有一个需要 Override 的方法,就要增加4个方法数,而 Activity 上大概有350个方法。
所以就迫于无奈做了 Container 的动态化,通过修改 ClassLoader 的 parent,为 ClassLoader 新增一个 parent。将原本的 BootClassLoader <- PathClassLoader 结构变为 BootClassLoader <- DexClassLoader <- PathClassLoader,插入的 DexClassLoader 加载了 ContainerActivity 就可以使得系统在向 PathClassLoader 查找 ContainerActivity 时能够正确找到实现。这里在这个动态化中使用了唯一一次反射修改私有变量。
需要注意的是,工程的清单文件里面是没有 Activity 的声明的,是在宿主里面声明的。
/ 其三 /
一直到这里,都比较简单,因为没设计到生命周期等问题,我们只需要把类加载进来就好了。下面我们就开始分析 Shadow 是如何处理 Activity 的生命周期问题的。
其实实现原理属于一层窗户纸,一捅就破了。重要的在于思路上的转变。以前的插件框架总是想用一些 Hack 手段去修改系统行为,找到系统的漏洞达到目的。Shadow 的原则是不去跟系统对抗。
如果一个组件需要安装才能使用,那么就别在没安装的情况下把它交给系统。只要我们不说谎,不去欺骗系统,那就不用去到处堵漏洞,做一些Hack的事情。
所以套一个壳子的方案就非常好。这种思路其他框架很早就有了,但是它们一直想把一个插件 Activity 套在一个宿主 Activity 之中,然后想办法实现一个转调关系。如果插件 Activity 是一个真的 Activity,那这个插件就可以正常编译安装运行,对开发插件或者直接上架插件 App 非常有利。
但是由于它是个系统的 Activity 子类,它就有很多方法不能直接调用,甚至还可能需要避免它的 super 方法被调用。如果插件 Activity 不是一个真的 Activity,只是一个跟 Activity 有差不多方法的普通类,这件事就简单多了,只需要让壳子 Activity 持有它,转调它就行了。但这种插件的代码正常编译成独立 App 安装运行会比较麻烦,代码中可能会出现很多插件相关的 if-else,也不好。
Shadow 做了一个非常简单事,通过运用 AOP 思想,利用字节码编辑工具,在编译期把插件中的所有 Activity 的父类都改成一个普通类,然后让壳子持有这个普通类型的父类去转调它就不用 Hack 任何系统实现了。虽然说是非常简单的事,实际上这样修改后还带来一些额外的问题需要解决,比如 getActivity() 方法返回的也不是 Activity 了。不过 Shadow 的实现中都解决了这些问题。
Activity
插件 Activity 的 onCreate 方法的代码就相当于是壳子 Activity 的 onCreate 方法的代码的一部分了。比如:
class ShadowActivity {
public void onCreate(Bundle savedInstanceState) {
}
}
class PluginActivity extends ShadowActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
System.out.println("Hello World!");
}
}
class ContainerActivity extends Activity {
ShadowActivity pluginActivity;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
pluginActivity.onCreate(savedInstanceState);
}
}
但是有一个问题,如果插件里面的代码在 Activity 的 super.onCreate(savedInstanceState); 之前有逻辑的话,就会出问题:
class PluginActivity extends ShadowActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
savedInstanceState.clear();
super.onCreate(savedInstanceState);
}
}
显然这种代码在正常安装运行时和插件环境运行时就不一样了。因为变成了:
class ContainerActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
savedInstanceState.clear();
}
}
我刚开始想的是给 ShadowActivity 加几个方法,比如:beforeOnCreate 等等,但是 Shadow 的做法更好些。我们要的不是插件 Activity 的super.onCreate() 调用不执行,我们是希望插件 Activity 的super.onCreate() 能够直接指挥壳子 Activity 什么时候调用super.onCreate()。
假如 PluginActivity 是继承自 ContainerActivity 的,运行时系统调用的是 PluginActivity 的实例,那么 PluginActivity的super.onCreate() 就会直接指导 ContainerActivity 什么时候调用 super.onCreate() 了。所以我们在这里的真正需求是如何把原本的继承关系用持有关系实现了。所以 Shadow 是这样实现的:
class ShadowActivity {
ContainerActivity containerActivity;
public void onCreate(Bundle savedInstanceState) {
containerActivity.superOnCreate(savedInstanceState);
}
}
class PluginActivity 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);
}
}
这里我们就又用到中间层,计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决。Shadow 实际的选择不是一个单纯的类而已,而是一个继承至 ContextThemeWrapper 的类,因为这样可以很轻松的处理文件相关的问题,比如在插件里面使用了 sp,那么 sp 应该储存到插件目录下,而不是宿主目录下,具体可看 SubDirContextThemeWrapper。还有就是 Activity 本身就是一个 context,它不过是通过 AMS 有了生命周期,那么我们继承 context,将生命周期委托给宿主壳子也非常的合适,覆写一些方法,处理插件的资源加载/so加载/classLoader 等问题。
发现一个问题,就是 Shadow 的设计里面非常频繁的使用了继承,将一个抽象类的职责尽可能的单一化,但是继承关系长了之后,带来的阅读性下降很多。而且有些抽象类就只是为了单一职责,没有第二个继承它的。会不会使用组合会更好点呢。
Fragment
里面的 getActivity 等方法,通过 FragmentSupportTransform 的变换后,都变成了com.tencent.shadow.core.runtime.ShadowFragmentSupport#fragmentGetActivity 这个方法。里面的 onAttach(Activity) 等方法:
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
}
运行时,这里的 activity 显然是 PluginContainerActivity,而逻辑上我们是把它当作 PluginActivity 的,编译时还让他继承了 ShadowActivity。所以需要做一个转换,简单来说,编译后变成了这样:
private void superOnAttach(ShadowActivity shadowActivity) {
super.onAttach((Activity) ShadowFragmentSupport.toOriginalContext(shadowActivity));
}
@Override // android.app.Fragment
public void onAttach(Activity activity) {
onAttachShadowActivity((ShadowActivity) ShadowFragmentSupport.toPluginContext(activity));
}
private void onAttachShadowActivity(ShadowActivity activity) {
superOnAttach(activity);
}
Service
service 的处理很简单,由于插件的 service 都是走到了 ShadowContext 的对应方法:
@Override
public boolean bindService(Intent service, ServiceConnection conn, int flags) {
..
Pair<Boolean, Boolean> ret = mPluginComponentLauncher.bindService(this, service, conn, flags);
...
}
最终会走到 PluginServiceManager 的方法里面:
fun bindPluginService(intent: Intent, conn: ServiceConnection, flags: Int): Boolean {
...
val service = createServiceAndCallOnCreate(intent)
...
mServiceBinderMap[componentName] = service.onBind(intent)
...
// 回调onServiceConnected
conn.onServiceConnected(componentName, it)
...
}
所以这里是将 service 当成了一个普通类。当然 Transform 阶段也做了一些事情,具体看 ShadowService。可以想到,插件里面的Service不支持多进程,因为插件本来就是一个进程了。
ContentProvider
ContentProvider 的处理采用的是转发的方式,就是宿主声明了一个 Provider:
<provider
android:authorities="${applicationId}.contentprovider.authority.dynamic"
android:name="com.tencent.shadow.core.runtime.container.PluginContainerContentProvider"
android:grantUriPermissions="true"
android:process=":plugin" />
对于插件中的 ContentProvider 的 url,将涉及到 url 解析的方法都重定向了一下,让他们都走到 PluginContentProviderManager 里面的方法去:
override fun parse(uriString: String): Uri {
if (uriString.startsWith(CONTENT_PREFIX)) {
val uriContent = uriString.substring(CONTENT_PREFIX.length)
val index = uriContent.indexOf("/")
val originalAuthority = if (index != -1) uriContent.substring(0, index) else uriContent
val containerAuthority = getContainerProviderAuthority(originalAuthority)
if (containerAuthority != null) {
return Uri.parse("$CONTENT_PREFIX$containerAuthority/$uriContent")
}
}
return Uri.parse(uriString)
}
这里就相当于将插件 ContentProvider 的 Authority 改成了宿主的 Authority,就会走到宿主的 ContentProvider 的逻辑,由他来负责分发。
ShadowContentProviderDelegate 又会将 Authority 替换为插件的,最终走到 PluginContentProviderManager 里面的方法,然后和Service的处理一样。
BroadcastReceiver
广播的实现也比较常规,在插件中动态注册和发送广播,直接调用系统的方法即可,因为广播不涉及生命周期等复杂的内容。需要处理的就是在 Manifest 中静态注册的广播。
因为插件是我们自己编译的,所以在编译的时候,我们可以获取到四大组件的所有信息,然后将这写信息写入一个类(com.tencent.shadow.core.manifest_parser.PluginManifest)里面,放入到插件包一起编译。这样加载插件的时候。我们直接 load 这个类,就知道有哪些 Receiver 了,我们直接全部动态注册一下就 ok 了。
for (Map.Entry<String, String[]> entry : mBroadcasts.entrySet()) {
try {
String receiverClassname = entry.getKey();
BroadcastReceiver receiver = mAppComponentFactory.instantiateReceiver(
mPluginClassLoader,
receiverClassname,
null);
IntentFilter intentFilter = new IntentFilter();
String[] receiverActions = entry.getValue();
if (receiverActions != null) {
for (String action : receiverActions) {
intentFilter.addAction(action);
}
}
registerReceiver(receiver, intentFilter);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
系统回调 BroadcastReceiver 的 onReceive(Context context, Intent intent) 方法时,还有需要处理的地方,具体逻辑见 ReceiverSupportTransform:
1. 传回的 context 是宿主的,需要修改为插件的。
2. intent 的 ExtrasClassLoader 是宿主的,需要改为插件的。
/ 其四 /
插件的启动流程。首先,点击宿主的一个按钮:
Intent intent = new Intent(MainActivity.this, PluginLoadActivity.class);
intent.putExtra(Constant.KEY_PLUGIN_PART_KEY, PART_KEY_PLUGIN_BASE);
intent.putExtra(Constant.KEY_ACTIVITY_CLASSNAME, "com.tencent.shadow.sample.plugin.app.lib.gallery.splash.SplashActivity");
可以看到,是通过 PluginLoadActivity 做了二次处理。指定了要启动哪个插件,以及插件的哪个页面。PluginLoadActivity 里面开了一个单线程线程池去处理:
// 获取到 Manager,因为我们的 Manager 是动态的,需要加载
HostApplication.getApp().loadPluginManager(PluginHelper.getInstance().pluginManagerFile);
// 指定插件包的位置,插件包里面的哪个插件,插件里面的页面
Bundle bundle = new Bundle();
bundle.putString(Constant.KEY_PLUGIN_ZIP_PATH, PluginHelper.getInstance().pluginZipFile.getAbsolutePath());
bundle.putString(Constant.KEY_PLUGIN_PART_KEY, getIntent().getStringExtra(Constant.KEY_PLUGIN_PART_KEY));
bundle.putString(Constant.KEY_ACTIVITY_CLASSNAME, getIntent().getStringExtra(Constant.KEY_ACTIVITY_CLASSNAME));
// 调用了 Manager 的 enter 方法,这里有一个回调,主要用来展示一个 loading 页面
// loading 页面是 Manager 插件包里面的资源,因为 Manager 管理插件的入口
HostApplication.getApp().getPluginManager()
.enter(PluginLoadActivity.this, Constant.FROM_ID_START_ACTIVITY, bundle, new EnterCallback() {
}
然后会进入到 SamplePluginManager 里面的 onStartActivity 方法:
// mCurrentContext 是 Manager 插件的 context
final View view = LayoutInflater.from(mCurrentContext).inflate(R.layout.activity_load_plugin, null);
// 触发回调
callback.onShowLoadingView(view);
// 下面的逻辑运行在一个单线程线程池里面
// 安装插件
InstalledPlugin installedPlugin = installPlugin(pluginZipPath, null, true);
// 可以看到,如果多个插件有依赖关系,需要手动处理加载顺序,
// 这里就先加载了 base 插件,然后加载 main-app 插件。
// 加载完插件之后,调用插件 application 的 onCreate 方法,这样插件就跑起来了。
loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_BASE);
loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_MAIN_APP);
callApplicationOnCreate(PART_KEY_PLUGIN_BASE);
callApplicationOnCreate(PART_KEY_PLUGIN_MAIN_APP);
// application 处理完毕后,还需要启动目标插件 activity
Intent intent = mPluginLoader.convertActivityIntent(pluginIntent);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mPluginLoader.startActivityInPluginProcess(intent);
上面的逻辑中,installPlugin 与 loadPlugin 与 callApplicationOnCreate 与 startActivityInPluginProcess 都要细说,我们一个一个道来。
installPlugin
首先将插件包里面的 loader 与 runtime 两个插件包进行 odex 处理:
// ODexBloc 的命名是仿的 dart ???
ODexBloc.oDexPlugin(apkFile, oDexDir, AppCacheFolderManager.getODexCopiedFile(oDexDir, key));
里面的处理很简单,而且只对 27 一下才有效,就是用一个 DexClassLoader 加载一下这个插件包就好了,指定一下 odex 的路径,里面会自动触发 odex 优化。然后将其他业务插件的 so 解压出来,注意这里将所有的业务插件的 so 都放入到了同一个目录:
// so 解压路径 ShadowPluginManager/UnpackedPlugin/test-dynamic-manager/lib/${uuid}/xxx.so
CopySoBloc.copySo(apkFile, soDir, AppCacheFolderManager.getLibCopiedFile(soDir, partKey), filter);
copy so 的时候,是先查询宿主支持哪个,具体可看 getPluginSupportedAbis ,然后只解压插件对应的 so。最后对每个业务插件也做一下 odex。这样插件就安装好了。
loadPlugin
首先启动插件进程,就是 bind 一个宿主的 service,也就是 PluginProcessPPS。想要指定哪个插件对应哪个进程,可以覆写 FastPluginManager 的 getPluginProcessServiceName 方法。把这个 service 返回的 IBinder,经过包装后变为 :
mPpsController = PluginProcessService.wrapBinder(service);
所以,mPpsController 就是一个 IBinder,用来与 PluginProcessPPS 通信的。那么为啥要包一下呢?是因为,PpsController 是手写了AIDL 相关的代码,那么为啥要手写呢?因为在此基础上做了一些异常的处理,比如:
public void loadRuntime(String uuid) throws RemoteException, FailedException {
Parcel _data = Parcel.obtain();
Parcel _reply = Parcel.obtain();
try {
_data.writeInterfaceToken(PpsBinder.DESCRIPTOR);
_data.writeString(uuid);
mRemote.transact(PpsBinder.TRANSACTION_loadRuntime, _data, _reply, 0);
int i = _reply.readInt();
if (i == TRANSACTION_CODE_FAILED_EXCEPTION) {
throw new FailedException(_reply);
} else if (i != TRANSACTION_CODE_NO_EXCEPTION) {
throw new RuntimeException("不认识的Code==" + i);
}
} finally {
_reply.recycle();
_data.recycle();
}
}
可以看到都是 AIDL 的模板代码,只不过是多了一些异常的处理。拿到了 Service 的 Binder 之后,又通过这个 Binder 获取了另外一个 Binder:
IBinder iBinder = mPpsController.getPluginLoader();
mPluginLoader = new BinderPluginLoader(iBinder);
所以,mPluginLoader 也是一个Binder的包装,它最终会调用到 Service 进程的 DynamicPluginLoader 类里面去。插件的加载核心流程都在 DynamicPluginLoader 里面,所以通过传递 Binder 的方式,提供一些接口方法给宿主调用,触发插件的加载。mPpsController 提供负责加载 loader 与 runtime 的接口。
mPpsController.loadRuntime(uuid);
mPpsController.loadPluginLoader(uuid);
mPpsController.loadRuntime(uuid);说的是加载,实际只是处理了一下 ClassLoader,核心逻辑在 DynamicRuntime.loadRuntime(installedRuntimeApk); 里面,插入了一层ClassLoader :
BootClassLoader
|—RuntimeClassLoader
|——PathClassLoader
这样是为了让宿主能访问到 runtime 插件包中的类,比如 PluginContainerActivity等,前面也说过原因了。mPpsController.loadPluginLoader(uuid); 这个会去加载 loader 插件包,它也创建了一个 ClassLoader:
BootClassLoader
|—RuntimeClassLoader
|——PathClassLoader
|———ApkClassLoader
mPluginLoader 负责提供加载插件的接口。
mPluginLoader.loadPlugin(partKey);
它也创建了一个 ClassLoader:
BootClassLoader
|—RuntimeClassLoader
|——PathClassLoader
|———PluginClassLoader
我们可以看到,loader 插件包与 plugin 插件包都有自己的 ClassLoader,这样会有问题,因为 plugin 插件里面的类,比如 Activity,它是继承 ShadowActivity 的,这个类在 loader 插件包里面。肯定是访问不到的,所以 Shadow 做了这样一个特殊处理:
// 插件依赖跟loader一起打包的runtime类,如ShadowActivity,从loader的ClassLoader加载
// loaderClassLoader 实际上是 ApkClassLoader
if (className.subStringBeforeDot() == "com.tencent.shadow.core.runtime") {
return loaderClassLoader.loadClass(className)
}
通过这种方式,强行让 PluginClassLoader 访问 ApkClassLoader 加载的类。类加载器说完了,我们继续看 mPluginLoader.loadPlugin(partKey); 里面的其他逻辑。
// 加载 com.tencent.shadow.core.manifest_parser.PluginManifest 这个类,
// 做一个检查,保证插件和宿主包名一致
CheckPackageNameBloc.check(pluginManifest, hostAppContext)
为啥要保证插件与宿主包名一致呢?
ApplicationId 一般是在 build.gradle 中设置的,在编译时这个字符串会被记录在2个位置。第1是记录在应用的 AndroidManifest.xml 中,第2是记录在应用的 resources.arsc 文件中。
记录在 AndroidManifest.xml 中的包名主要用来构造应用的 Context 对象。在我们开发中一般通过 context.getPackageName() 方法获得到当前应用设置的 ApplicationId。但是重要的是,系统也会通过 context 获取包名来识别 context 来自于哪个安装的应用。我们知道系统不允许安装多个相同 ApplicationId 的应用,这也是因为系统就是根据这个包名来区分安装的应用的。通过这个 ApplicationId,系统也可以反向查找到应用安装在系统中的 apk 文件路径。问题在于系统不是非常简单的和我们一样只会调用 context.getPackageName() 方法获得应用的 ApplicationId。还会调用一些私有 API 获取,例如 getOpPackageName() 方法。
所以以 Hook 方式实现的插件框架中,只能不停的兼容各种 OEM 系统、各种 Android 版本。
Shadow 始终坚持一个原则来避免使用私有 API,就是通过一层中间件将插件代码变成宿主代码的一部分。实际上这个过程是可以通过手工将中间件和插件代码都写在宿主中达到相同效果的。所以在这个设计中,插件代码实际上就是宿主代码的一部分。既然是一部分,ApplicationId 怎么会不一样呢?所以要求插件和宿主的 ApplicationId 保持一致,就永远不会将插件代码没有安装这件事暴露给系统。Shadow 代码中并没有什么对包名的特殊处理逻辑,只有一处检查包名是否一致的逻辑com.tencent.shadow.core.loader.blocs.ParsePluginApkBloc#parse。
// 创建一个 ApplicationInfo
CreatePluginApplicationInfoBloc.create()
// 创建插件的 PackageManager
PluginPackageManagerImpl()
// 创建插件的 Resource
CreateResourceBloc.create()
// 处理 AppComponentFactory
ShadowAppComponentFactory::class.java.cast(clazz.newInstance())
// 创建 Application
CreateApplicationBloc.createShadowApplication()
到了这里,插件包就处理完了。
callApplicationOnCreate
由于mPluginLoader 负责插件接口,所以这个方法由 mPluginLoader.callApplicationOnCreate(partKey); 触发,最终调用到 ShadowPluginLoader 里面:
fun callApplicationOnCreate(partKey: String) {
...
val application = pluginParts.application
application.attachBaseContext(mHostAppContext)
mPluginContentProviderManager.createContentProviderAndCallOnCreate(
application, partKey, pluginParts
)
application.onCreate()
...
}
前面已经创建了 application,这里处理很简单,调用 attachBaseContext 与 onCreate 这两个生命周期方法,注意,还处理了 ContentProvider。
startActivityInPluginProcess
这个方法也是通过 mPluginLoader 来触发,最终调用到 DynamicPluginLoader 的 startActivityInPluginProcess 方法里面:
@Synchronized
fun startActivityInPluginProcess(intent: Intent) {
mUiHandler.post {
// 这个 mContext 是由宿主的 applicationContext
mContext.startActivity(intent)
}
}
由于我们已将将 pluginIntent 转换为了 containerIntent,所以直接交给宿主来启动宿主的壳子 Activity。壳子Activity 启动后,就要处理 delegate 等信息,然后同步触发插件 Activity 的生命周期,具体可看 ShadowActivityDelegate。壳子 Activity 与插件 Activity 的对应关系实现,由 Manager 的 onBindContainerActivity 方法实现,Sample 里面只实现了标准模式的 Activity 对应关系,标准模式可以多对一,其余的模式不行:
public ComponentName onBindContainerActivity(ComponentName pluginActivity) {
switch (pluginActivity.getClassName()) {
/**
* 这里配置对应的对应关系
*/
}
return new ComponentName(context, DEFAULT_ACTIVITY);
}
/ 其五 /
资源的处理其实比较简单,因为每个插件包都有自己的 Context,而且也没有访问宿主的需求,所以只需要处理一下资源冲突就好了。资源冲突通过资源分包解决,但是引发了其他问题,具体可看 CreateResourceBloc 逻辑。因为我没遇到过,所以也不懂里面为啥会有这样的问题,不深究。
/ 其六 /
WebView 的处理,WebView 本身也是动态加载的,但是在插件里面实例化一个 WebView 的时候,它的资源路径会被添加到壳子的Resource里面,而不是 ShadowContext 的 Resource 里面,会有问题,处理方式就是,使用宿主 Context 主动实例化一个 WebView,将里面的路径拷贝出来:
WebView(hostAppContext)
val hostApplicationInfo = hostAppContext.applicationInfo
val hostSharedLibraryFiles = hostApplicationInfo.sharedLibraryFiles
还有一个问题,就是 WebView 加载本地资源的问题。
在正常的 App 开发中,我们可以用这样的代码加载 App 的 Assets 中打包的Web页面。
webview.loadUrl("file:///android_asset/index.html");
Android 系统实现这个功能时,并没有像我们想象的使用 webview 对象的 Context 去查找 Assets 资源。而是通过当前应用的 ApplicationId,反查了当前安装的应用的 apk 路径。所以查到的自然是安装的宿主应用。在宿主的 apk 中自然是找不到插件的 Assets。我们可以模仿“Web离线包”方案。对 WebView loadUrl 中 file:///android_asset/ 协议进行修改。先通过 LayoutInflater 将插件中用到的 WebView 都换成 ShadowWebView。再Override ShadowWebView 的 loadUrl 方法。将请求来的file:///android_asset/ 协议都修改成 http://android.asset/ 协议。
关于这部分代码,请查看com.tencent.shadow.core.runtime.ShadowWebView类的实现。
/ 其七 /
还有一个多插件自定义控件重名问题。
在Android 7 及以下,LayoutInflater 创建 View 的时候,会缓存其构造函数,提升效率,但是它没有校验 ClassLoader。也就是说,如果有不同的 ClassLoader 来创建同一个 View,那么就会出现提示信息类似于“RecycleView cannot cast to RecycleView”的Crash异常。Shadow 的处理是如果 ClassLoder 校验没通过,那就将缓存移除,重新创建一个,其实这里可以参考高版本的逻辑。
if (constructor != null && !verifyClassLoader(context, constructor)) {
constructor = null;
sConstructorMap.remove(cacheKey);
}
/ 其八 /
画了一些不伦不类的类图…
推荐阅读:
微软的人工智能 Copilot 到底有多强大?带你来体验一下
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注