浅谈 Android 插件化原理

我们称这个过程叫做 ClassLoader 注入。完成注入后,所有来自宿主的类使用宿主的 ClassLoader 进行加载,所有来自插件 Apk 的类使用插件 ClassLoader 进行加载,而由于 ClassLoader 的双亲委派机制,实际上系统类会不受 ClassLoader 的类隔离机制所影响,这样宿主 Apk 就可以在宿主进程中使用来自于插件的组件类了。

Runtime Container


上面说到只要做到 ClassLoader 注入后,就可以在宿主进程中使用插件 Apk 中的类,但是我们都知道 Android 组件都是由系统调用启动的,未安装的 Apk 中的组件,是未注册到 AMSPMS 的,就好比你直接使用 startActivity 启动一个插件 Apk 中的组件,系统会告诉你无法找到。

我们的解决方案很简单,即运行时容器技术,简单来说就是在宿主 Apk 中预埋一些空的 Android 组件,以 Activity 为例,我预置一个 ContainerActivity extends Activity 在宿主中,并且在 AndroidManifest.xml 中注册它。

它要做的事情很简单,就是帮助我们作为插件 Activity 的容器,它从 Intent 接受几个参数,分别是插件的不同信息,如:

  • pluginName

  • pluginApkPath

  • pluginActivityName

等,其实最重要的就是 pluginApkPathpluginActivityName,当 ContainerActivity 启动时,我们就加载插件的 ClassLoaderResource,并反射 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,是不是感觉有点看懂了,虽然真正搞的时候还有很多小坑,但大概原理就是这么简单,启动插件组件需要依赖容器,容器负责加载插件组件并且完成双向转发,转发来自系统的生命周期回调至插件组件,同时转发来自插件组件的系统调用至系统。

Resource Injection


最后要说的是资源注入,其实这一点相当重要,Android 应用的开发其实崇尚的是逻辑与资源分离的理念,所有资源(layoutvalues 等)都会被打包到 Apk 中,然后生成一个对应的 R 类,其中包含对所有资源的引用 id

资源的注入并不容易,好在 Android 系统给我们留了一条后路,最重要的是这两个接口:

  • PackageManager#getPackageArchiveInfo:根据 Apk 路径解析一个未安装的 ApkPackageInfo

  • PackageManager#getResourcesForApplication:根据 ApplicationInfo 创建一个 Resources 实例

我们要做的就是在上面 ContainerActivity#onCreate 中加载插件 Apk 的时候,用这两个方法创建出来一份插件资源实例。具体来说就是先用 PackageManager#getPackageArchiveInfo 拿到插件 ApkPackageInfo,有了 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 {}

文末

很多人在刚接触这个行业的时候或者是在遇到瓶颈期的时候,总会遇到一些问题,比如学了一段时间感觉没有方向感,不知道该从那里入手去学习,对此我整理了一些资料,需要的可以免费分享给大家

这里笔者分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

【视频教程】

天道酬勤,只要你想,大厂offer并不是遥不可及!希望本篇文章能为你带来帮助,如果有问题,请在评论区留言。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

[外链图片转存中…(img-YIp1Swtk-1715334942318)]

[外链图片转存中…(img-tWv1lZhN-1715334942319)]

【视频教程】

[外链图片转存中…(img-YGuEU53x-1715334942319)]

天道酬勤,只要你想,大厂offer并不是遥不可及!希望本篇文章能为你带来帮助,如果有问题,请在评论区留言。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 17
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值