上面说到只要做到 ClassLoader 注入后,就可以在宿主进程中使用插件 Apk 中的类,但是我们都知道 Android 组件都是由系统调用启动的,未安装的 Apk 中的组件,是未注册到 AMS 和 PMS 的,就好比你直接使用 startActivity 启动一个插件 Apk 中的组件,系统会告诉你无法找到。
我们的解决方案很简单,即运行时容器技术,简单来说就是在宿主 Apk 中预埋一些空的 Android 组件,以 Activity 为例,我预置一个 ContainerActivity extends Activity 在宿主中,并且在 AndroidManifest.xml 中注册它。
它要做的事情很简单,就是帮助我们作为插件 Activity 的容器,它从 Intent 接受几个参数,分别是插件的不同信息,如:
-
pluginName -
pluginApkPath -
pluginActivityName
等,其实最重要的就是 pluginApkPath 和 pluginActivityName,当 ContainerActivity 启动时,我们就加载插件的 ClassLoader、Resource,并反射 pluginActivityName 对应的 Activity 类。当完成加载后,ContainerActivity 要做两件事:
-
转发所有来自系统的生命周期回调至插件
Activity -
接受
Activity方法的系统调用,并转发回系统
我们可以通过复写 ContainerActivity 的生命周期方法来完成第一步,而第二步我们需要定义一个 PluginActivity,然后在编写插件 Apk 中的 Activity 组件时,不再让其集成 android.app.Activity,而是集成自我们的 PluginActivity,后面再通过字节码替换来自动化完成这部操作,后面再说为什么,我们先看伪代码。
public class ContainerActivity extends Activity {
private PluginActivity pluginActivity;
@Override
protected void onCreate(Bundle savedInstanceState) {
String pluginActivityName = getIntent().getString("pluginActivityName", "");
pluginActivity = PluginLoader.loadActivity(pluginActivityName, this);
if (pluginActivity == null) {
super.onCreate(savedInstanceState);
return;
}
pluginActivity.onCreate();
}
@Override
protected void onResume() {
if (pluginActivity == null) {
super.onResume();
return;
}
pluginActivity.onResume();
}
@Override
protected void onPause() {
if (pluginActivity == null) {
super.onPause();
return;
}
pluginActivity.onPause();
}
// ...
}
public class PluginActivity {
private ContainerActivity containerActivity;
public PluginActivity(ContainerActivity containerActivity) {
this.containerActivity = containerActivity;
}
@Override
public T findViewById(int id) {
return containerActivity.findViewById(id);
}
// ...
}
// 插件 Apk 中真正写的组件
public class TestActivity extends PluginActivity {
// ......
}
Emm,是不是感觉有点看懂了,虽然真正搞的时候还有很多小坑,但大概原理就是这么简单,启动插件组件需要依赖容器,容器负责加载插件组件并且完成双向转发,转发来自系统的生命周期回调至插件组件,同时转发来自插件组件的系统调用至系统。
最后要说的是资源注入,其实这一点相当重要,Android 应用的开发其实崇尚的是逻辑与资源分离的理念,所有资源(layout、values 等)都会被打包到 Apk 中,然后生成一个对应的 R 类,其中包含对所有资源的引用 id。
资源的注入并不容易,好在 Android 系统给我们留了一条后路,最重要的是这两个接口:
-
PackageManager#getPackageArchiveInfo:根据Apk路径解析一个未安装的Apk的PackageInfo -
PackageManager#getResourcesForApplication:根据ApplicationInfo创建一个Resources实例
我们要做的就是在上面 ContainerActivity#onCreate 中加载插件 Apk 的时候,用这两个方法创建出来一份插件资源实例。具体来说就是先用 PackageManager#getPackageArchiveInfo 拿到插件 Apk 的 PackageInfo,有了 PacakgeInfo 之后我们就可以自己组装一份 ApplicationInfo,然后通过 PackageManager#getResourcesForApplication 来创建资源实例,大概代码像这样:
PackageManager packageManager = getPackageManager();
PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(
pluginApkPath,
PackageManager.GET_ACTIVITIES
| PackageManager.GET_META_DATA
| PackageManager.GET_SERVICES
| PackageManager.GET_PROVIDERS
| PackageManager.GET_SIGNATURES
);
packageArchiveInfo.applicationInfo.sourceDir = pluginApkPath;
packageArchiveInfo.applicationInfo.publicSourceDir = pluginApkPath;
Resources injectResources = null;
try {
injectResources = packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo);
} catch (PackageManager.NameNotFoundException e) {
// ...
}
拿到资源实例后,我们需要将宿主的资源和插件资源 Merge 一下,编写一个新的 Resources 类,用这样的方式完成自动代理:
public class PluginResources extends Resources {
private Resources hostResources;
private Resources injectResources;
public PluginResources(Resources hostResources, Resources injectResources) {
super(injectResources.getAssets(), injectResources.getDisplayMetrics(), injectResources.getConfiguration());
this.hostResources = hostResources;
this.injectResources = injectResources;
}
@Override
public String getString(int id, Object... formatArgs) throws NotFoundException {
try {
return injectResources.getString(id, formatArgs);
} catch (NotFoundException e) {
return hostResources.getString(id, formatArgs);
}
}
// ...
}
然后我们在 ContainerActivity 完成插件组件加载后,创建一份 Merge 资源,再复写 ContainerActivity#getResources,将获取到的资源替换掉:
public class ContainerActivity extends Activity {
private Resources pluginResources;
@Override
protected void onCreate(Bundle savedInstanceState) {
// ...
pluginResources = new PluginResources(super.getResources(), PluginLoader.getResources(pluginApkPath));
// ...
}
@Override
public Resources getResources() {
if (pluginActivity == null) {
return super.getResources();
}
return pluginResources;
}
}
这样就完成了资源的注入。
=================================================================================
上面其实说到了,我们被迫改变了插件组件的编写方式:
class TestActivity extends Activity {}
->
class TestActivity extends PluginActivity {}
有没有什么办法能让插件组件的编写与原来没有任何差别呢?
Shadow 的做法是字节码替换插件,我认为这是一个非常棒的想法,简单来说,Android 提供了一些 Gradle 插件开发套件,其中有一项功能叫 Transform Api,它可以介入项目的构建过程,在字节码生成后、dex 文件生成钱,对代码进行某些变换,具体怎么做的不说了,可以自己看文档。
实现的功能嘛,就是用户配置 Gradle 插件后,正常开发,依然编写:
class TestActivity extends Activity {}
然后完成编译后,最后的字节码中,显示的却是:
class TestActivity extends PluginActivity {}
到这里基本的框架就差不多结束了。
========================================================================
结尾
我还总结出了互联网公司Android程序员面试涉及到的绝大部分面试题及答案,并整理做成了文档,以及系统的进阶学习视频资料,免费分享给大家。 (包括Java在Android开发中应用、APP框架知识体系、高级UI、全方位性能调优,NDK开发,音视频技术,人工智能技术,跨平台技术等技术资料),希望能帮助到你面试前的复习,且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。