转自 Kindem 的博客,欢送转载,但要注明出处
???? 意识插件化
想必大家都晓得,在 Android
零碎中,利用是以 Apk
的模式存在的,利用都须要装置能力应用。但实际上 Android
零碎装置利用的形式相当简略,其实就是把利用 Apk
拷贝到零碎不同的目录下、而后把 so
解压进去而已。
常见的利用装置目录有:
/system/app
:零碎利用/system/priv-app
:零碎利用/data/app
:用户利用
那可能大家会想问,既然装置这个过程如此简略,Android
是怎么运行利用中的代码的呢,咱们先看 Apk
的形成,一个常见的 Apk
会蕴含如下几个局部:
classes.dex
:Java
代码字节码res
:资源目录lib
:so
目录assets
:动态资产目录AndroidManifest.xml
:清单文件
其实 Android
零碎在关上利用之后,也只是开拓过程,而后应用 ClassLoader
加载 classes.dex
至过程中,执行对应的组件而已。
那大家可能会想一个问题,既然 Android
自身也是应用相似反射的模式加载代码执行,凭什么咱们不能执行一个 Apk
中的代码呢?
这其实就是插件化的目标,让 Apk
中的代码(次要是指 Android
组件)可能免装置运行,这样可能带来很多收益,最不言而喻的劣势其实就是通过网络热更新、热修复,设想一下,你的利用领有 Native
利用个别极高的性能,又能获取诸如 Web
利用一样的收益。
嗯,现实很美妙不是嘛?
???? 难点在哪
大家其实都晓得,Android
利用自身是基于魔改的 Java
虚拟机的,动静加载代码几乎不要太简略,只须要应用 DexClassLoader
加载 Apk
,而后反射外面的代码就能够了。
然而光能反射代码是没有意义的,插件化真正的魅力在于,能够动静加载执行 Android
组件(即 Activity
、Service
、BroadcastReceiver
、ContentProvider
、Fragment
)等。
认真想一下,其实要做到这一点是有难度的,最次要的妨碍是,四大组件是在零碎外面中注册的,具体来说是在 Android
零碎的 ActivityManagerService (AMS)
和 PackageManagerService (PMS)
中注册的,而四大组件的解析和启动都须要依赖 AMS
和 PMS
,如何坑骗零碎,让他抵赖一个未装置的 Apk
中的组件,就是插件化的最大难点。
另外,资源(特指 R
中援用的资源,如 layout
、values
等)也是一大问题,设想一下你在宿主过程中应用反射加载了一个插件 Apk
,代码中的 R
对应的 id
却无奈援用到正确的资源,会产生什么结果。
总结一下,其实做到插件化的要点就这几个:
- 反射并执行插件
Apk
中的代码(ClassLoader Injection
) - 让零碎能调用插件
Apk
中的组件(Runtime Container
) - 正确辨认插件
Apk
中的资源(Resource Injection
)
当然还有其余一些小问题,但可能不是所有场景下都会遇到,咱们前面再独自说。
???? 解决方案
首先来谈一谈常见插件化框架的整体架构,市面上的插件化框架理论很多,如 Tecent
的 Shadow、Didi
的 VirtualApk、360
的 RePlugin,我本人也写了一个简略的插件化框架 Zed。他们各有各的短处,不过大体上差不多,如果要具体学习,我举荐 Shadow
,它的劣势在于 0 Hook
,没有应用公有 API
意味着能够走的很远,不会被 Google
搞。
他们大体原理其实都差不多,运行时会有一个宿主 Apk
在过程中跑,宿舍 Apk
是真正被装置的利用,宿主 Apk
能够加载插件 Apk
中的组件和代码运行,插件 Apk
能够任意热更新。
接下来咱们依照下面要点的程序,大抵讲一下每一个方面怎么攻克。
ClassLoader Injection
简略来说,插件化场景下,会存在同一过程中多个 ClassLoader
的场景:
- 宿主
ClassLoader
:宿主是装置利用,运行即主动创立 - 插件
ClassLoader
:应用new DexClassLoader
创立
咱们称这个过程叫做 ClassLoader
注入。实现注入后,所有来自宿主的类应用宿主的 ClassLoader
进行加载,所有来自插件 Apk
的类应用插件 ClassLoader
进行加载,而因为 ClassLoader
的双亲委派机制,实际上零碎类会不受 ClassLoader
的类隔离机制所影响,这样宿主 Apk
就能够在宿主过程中应用来自于插件的组件类了。
Runtime Container
下面说到只有做到 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 extends View> T findViewById(int id) {return containerActivity.findViewById(id);
}
// ...
}
// 插件 `Apk` 中真正写的组件
public class TestActivity extends PluginActivity {// ......}
Emm,是不是感觉有点看懂了,尽管真正搞的时候还有很多小坑,但大略原理就是这么简略,启动插件组件须要依赖容器,容器负责加载插件组件并且实现双向转发,转发来自零碎的生命周期回调至插件组件,同时转发来自插件组件的零碎调用至零碎。
Resource Injection
最初要说的是资源注入,其实这一点相当重要,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 {}
到这里根本的框架就差不多完结了。
✨ 写在最初
插件化是一门很有意思的学识,Emm,怎么说呢,用一句话来形容就是偷天换日灯下黑,在各种坑的限度下一直跟零碎博弈寻找前途。随着理解的深刻,大家必定能了解我这句话,本文也只是抛砖引玉,更多的乐趣还是要本人去挖掘。