我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情
🔥 Hi,我是小余。
本文已收录到 GitHub · Androider-Planet 中。这里有 Android 进阶成长知识体系,关注公众号 [小余的自习室] ,在成功的路上不迷路!
前言
想必大家都知道,在 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 中的代码呢? 这就需要引入插件化相关概念:
插件化相关概念:
- 1.插件:一个插件其实就是一个apk
- 2.插件工程:能够编译生成插件apk的工程
- 3.插件化:将一个大的apk拆分成多个小apk的过程
插件化结构图:
插件化框架中的apk一般都有自己的插件ClassLoader,插件AssetManager,插件Context
而最底层的插件框架类似于我们的Android Framework层
插件化优缺点:
优点:
- 1.较小apk的体积,可以根据需要下载对应的插件模块
- 2.插件可以单独作为apk进行调试,且互相解耦,可以多模块同时开发,提升开发效率
- 3.可以动态更新插件或插件补丁
缺点:
- 1.对于已经成型的项目重构成本较大
- 2.很多插件化框架做不到对所有版本兼容。
插件化和组件化区别
- 组件化:是将一个App分成多个模块,每个模块都是一个组件(module), 开发过程中可以让这些组件相互依赖或独立编译、调试部分组件,但是这些组件最终会合并成一个完整的Apk去发布到应用市场。
- 插件化:是将整个App拆分成很多模块,每个模块都是一个Apk(组件化的每个模块是一个lib), 最终打包的时候将宿主Apk和插件Apk分开打包,只需发布宿主Apk到应用市场,插件Apk通过动态按需下发到宿主Apk。
插件化改造需要解决哪些问题
- 1.插件类加载
- 2.插件资源加载
- 3.插件四大组件通讯
- 4.插件动态部署
1.插件类加载:
这里我们首先得了解下类加载的双亲委派原则:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去加载, 每一个层次的加载器都是如此, 因此所有的加载请求最终都会传送到最底层的启动类加载器, 只有父加载器无法完成加载的时候才会将加载任务向下传递个子类进行。
Android中的ClassLoader类关系:
由于双亲委派原则存在,其加载过程如下:
这里我们主要来看PathClassLoader
和DexClassLoader
这两个类加载器是我们Android中最重要的类加载器 查看源码:
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}
DexClassLoader
类中就只有一个构造方法,构造方法中直接调用了父类的构造,DexClassLoader继承了BaseDexClassLoader,构造方法中的参数的含义是:
- dexPath:dex文件路径
- optimizedDirectory:dex文件首次加载时会进行优化操作,这个参数即为优化后的odex文件的存放目录,官方推荐使用应用私有目录来缓存优化后的dex文件,dexOutputDir = context.getDir(“dex”, 0);
- librarySearchPath:动态库路径
- parent:当前类加载器的父类加载器
继续看PathClassLoader
:
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}
PathClassLoader有两个构造方法,同样也是直接调用了父类的构造方法,从构造方法上来看,DexClassLoader和PathClassLoader的区别只有第二个参数optimizedDirectory,在PathClassLoader中optimizedDirectory默认传入的是null。
从源码中看这两个类的作用也是因为optimizedDirectory参数的不同而不同,在源码中看使用PathClassLoader由于没有传入optimizedDirectory,系统会自动生成以后缓存目录,即/data/dalvik-cache/,在这个目录存放优化以后的dex文件。
所以PathClassLoader只能加载已安装的apk的dex,即加载系统的类和已经安装的应用程序(安装的apk的dex文件会存储在/data/dalvik-cache中),而DexClassLoader可以加载指定路径的apk、dex,也可以从sd卡中进行加载。
基于上面的分析,在做插件化改造过程中只要创建一个DexClassLoader 对象,然后使用这个对象去加载外部路径的class文件即可: 简化过程如下:
private void loadClass() {
init();
try {
// 从优化后的dex文件中加载APK_HELLO_CLASS_PATH类
clazz = pluginClassLoader.loadClass("com.iflytek.test.HelloWorld");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
private fun init() {
extractPlugin()
pluginPath = File(filesDir.absolutePath, "plugin.apk").absolutePath
nativeLibDir = File(filesDir, "pluginlib").absolutePath
dexOutPath = File(filesDir, "dexout").absolutePath
// 生成 DexClassLoader 用来加载插件类
pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)
}
// 从 assets 中拿出插件 apk 放到内部存储空间
private fun extractPlugin() {
var inputStream = assets.open("plugin.apk")
File(filesDir.absolutePath, "plugin.apk").writeBytes(inputStream.readBytes())
}
获取到插件中的类加载器后,就可以通过反射的方式去调用插件中类的方法:
val loadClass = pluginClassLoader.loadClass(activityName)
loadClass.getMethod("hello",null).invoke(loadClass)
最后在需要加载插件中的类时:需要根据插件名称获取对应的插件ClassLoader即可:
简化代码如下:
PluginClassLoader.load(“插件名”,"需要加载的插件类权限定名")
2.插件资源加载:
资源注入,其实这一点相当重要,Android 应用的开发其实崇尚的是逻辑与资源分离的理念, 所有资源(layout、values 等)都会被打包到 Apk 中,然后生成一个对应的 R 类,其中包含对所有资源的引用 id。
资源的注入并不容易,好在 Android 系统给我们留了一条后路,最重要的是这两个接口:
PackageManager#getPackageArchiveInfo:根据 Apk 路径解析一个未安装的 Apk 的 PackageInfo
PackageManager#getResourcesForApplication:根据 ApplicationInfo 创建一个 Resources 实例
我们要做的就是在加载插件 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);
}
}
// ...
}
当然你也可以也可以把插件资源独立出来,第一次使用时通过插件名去对应路径下寻找apk,然后根据apk路径创建一个插件的PluginResources对象 最后缓存在内存中,下次就不用再重新创建插件资源
结合ClassLoader和资源加载过程,我们可以使用一个Context来包裹住插件中的这些对象:
public class PluginContext extends ContextThemeWrapper {
//插件ClassLoader
private final ClassLoader pluginClassLoader;
//插件Resources
private final Resources pluginResource;
//插件名称,一般根据插件的apk名称来
private final String mPlugin;
...
@Override
public ClassLoader getClassLoader() {
if (pluginClassLoader != null) {
return pluginClassLoader;
}
return super.getClassLoader();
}
@Override
public Resources getResources() {
if (pluginResource != null) {
return pluginResource;
}
return super.getResources();
}
@Override
public AssetManager getAssets() {
if (pluginResource != null) {
return pluginResource.getAssets();
}
return super.getAssets();
}
}
我们在使用插件中类和资源的时候,就可以通过PluginContext来获取ClassLoader和Resources, 得到插件中的类和资源
3.插件中四大组件通讯
在讲解插件中四大组件通讯前我们先来了解下Activity的启动过程:
Activity的启动过程主要分为两种,一种是根Activity的启动过程,一种是普通Activity的启动过程。关于根Activity的启动过程在前面文章介绍过,这里来简单回顾下,如下图所示。
首先Launcher进程向AMS请求创建根Activity,AMS会判断根Activity所需的应用程序进程是否存在并启动,如果不存在就会请求Zygote进程创建应用程序进程。应用程序进程启动后,AMS会请求应用程序进程创建并启动根Activity。
普通Activity和根Activity的启动过程大同小异,但是没有这么复杂,因为不涉及应用程序进程的创建,跟Laucher也没关系,如下图所示。
上图抽象的给出了普通Acticity的启动过程。在应用程序进程中的Activity向AMS请求创建普通Activity(步骤1),AMS会对 这个Activty的生命周期管和栈进行管理,校验Activity等等。如果Activity满足AMS的校验,AMS就会请求应用程序进程中的ActivityThread去创建并启动普通Activity.
可以看出:
我们需要启动一个Activity,就需要经过AMS校验,AMS会检测当前Activity是否在AndroidManifest.xml中注册过,如果没有注册就会报ActivityNotFoundException
下面给出方案:
- 1.需要在AndroidManifest.xml中注册Activity进行占坑,使用占坑的方式骗过AMS的校验
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.music.anna.pluginactivity">S
<application
...
<activity android:name=".StubActivity"/>
</application>
</manifest>
- 2.在AMS返回到宿主工程后,还原需要启动的插件Activity,然后创建对应的Activity实例。
上面的方案有三种实现方式:
方式1:Hook Instrumentation
public class Instrumentation {
//启动Activity的时候,调用此方法时,替换调Intent
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
}
//AMS检测后,创建Activity之前替换回Intent
public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
}
}
Instrumentation代理类如下:
public class InstrumentationProxy extends Instrumentation {
private Instrumentation mInstrumentation;
private PackageManager mPackageManager;
public InstrumentationProxy(Instrumentation instrumentation, PackageManager packageManager) {
mInstrumentation = instrumentation;
mPackageManager = packageManager;
}
//启动Activity的时候,调用此方法时,替换调Intent
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
List<ResolveInfo> infos = mPackageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL);
if (infos == null || infos.size() == 0) {
intent.putExtra(HookHelper.TARGET_INTENsT_NAME, intent.getComponent().getClassName());//1
intent.setClassName(who, "com.music.anna.pluginactivity.StubActivity");//2
}
try {
Method execMethod = Instrumentation.class.getDeclaredMethod("execStartActivity",
Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class);
return (ActivityResult) execMethod.invoke(mInstrumentation, who, contextThread, token,
target, intent, requestCode, options);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
//AMS检测后,创建Activity之前替换回Intent
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException,
IllegalAccessException, ClassNotFoundException {
String intentName = intent.getStringExtra(HookHelper.TARGET_INTENT_NAME);
if (!TextUtils.isEmpty(intentName)) {
return super.newActivity(cl, intentName, intent);
}
return super.newActivity(cl, className, intent);
}
}
最后使用HookHelper工具类将InstrumentationProxy替换系统中的mInstrumentation 代码如下:
public static void hookInstrumentation(Context context) throws Exception {
Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
Field mMainThreadField =FieldUtil.getField(contextImplClass,"mMainThread");//1
Object activityThread = mMainThreadField.get(context);//2
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Field mInstrumentationField=FieldUtil.getField(activityThreadClass,"mInstrumentation");//3
FieldUtil.setField(activityThreadClass,activityThread,"mInstrumentation",new InstrumentationProxy((Instrumentation) mInstrumentationField.get(activityThread),
context.getPackageManager()));
}
方式2:Hook IActivityManager.startActivity和ActivityThread.mH.mCallback
- IActivityManager:用于应用进程和AMS进行通讯的binder对象,在调用AMS校验Activity前使用占坑Activity骗过AMS
- ActivityThread.mH.mCallback:用于处理AMS校验后,返回到宿主的ApplicationThread线程中,处理Activity创建请求。 ActivityThread会通过H将代码的逻辑切换到主线程中,H类是ActivityThread的内部类并继承自Handler,如下所示。
frameworks/base/core/java/android/app/ActivityThread.java
private class H extends Handler {
public static final int LAUNCH_ACTIVITY = 100;
public static final int PAUSE_ACTIVITY = 101;
...
public void handleMessage(Message msg) {
if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
switch (msg.what) {
case LAUNCH_ACTIVITY: {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
} break;
...
}
...
}
H中重写的handleMessage方法会对LAUNCH_ACTIVITY类型的消息进行处理,最终会调用Activity的onCreate方法。那么在哪进行替换呢?接着来看Handler的dispatchMessage方法:
frameworks/base/core/java/android/os/Handler.java
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
Handler的dispatchMessage用于处理消息,看到如果Handler的Callback类型的mCallback不为null,就会执行mCallback的handleMessage方法。因此,mCallback可以作为Hook点,我们可以用自定义的Callback来替换mCallback,自定义的Callback如下所示。
HCallback.java
public class HCallback implements Handler.Callback{
public static final int LAUNCH_ACTIVITY = 100;
Handler mHandler;
public HCallback(Handler handler) {
mHandler = handler;
}
@Override
public boolean handleMessage(Message msg) {
if (msg.what == LAUNCH_ACTIVITY) {
Object r = msg.obj;
try {
//得到消息中的Intent(启动SubActivity的Intent)
Intent intent = (Intent) FieldUtil.getField(r.getClass(), r, "intent");
//得到此前保存起来的Intent(启动TargetActivity的Intent)
Intent target = intent.getParcelableExtra(HookHelper.TARGET_INTENT);
//将启动SubActivity的Intent替换为启动TargetActivity的Intent
intent.setComponent(target.getComponent());
} catch (Exception e) {
e.printStackTrace();
}
}
mHandler.handleMessage(msg);
return true;
}
}
HCallback实现了Handler.Callback,并重写了handleMessage方法,当收到消息的类型为LAUNCH_ACTIVITY时,将启动SubActivity的Intent替换为启动TargetActivity的Intent。接着我们在HookHelper中定义一个hookHandler方法如下所示。
public static void hookHandler() throws Exception {
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Object currentActivityThread= FieldUtil.getField(activityThreadClass ,null,"sCurrentActivityThread");//1
Field mHField = FieldUtil.getField(activityThread,"mH");//2
Handler mH = (Handler) mHField.get(currentActivityThread);//3
FieldUtil.setField(Handler.class,mH,"mCallback",new HCallback(mH));
}
ActivityThread类中有一个静态变量sCurrentActivityThread,用于表示当前的ActivityThread对象, 因此
- 在注释1处获取ActivityThread中定义的sCurrentActivityThread对象。
- 注释2处获取ActivityThread类的mH字段,
- 接着在注释3处获取当前ActivityThread对象中的mH对象,
- 最后用HCallback来替换mH中的mCallback。
在MyApplication的attachBaseContext方法中调用HookHelper的hookHandler方法,运行程序,当我们点击启动插件按钮,发现启动的是插件TargetActivity。
方式3:Hook住ClassLoader
这里我们使用RePlugin
框架的原理来讲解下:
RePlugin全局只hook了一个点,那就是ClassLoader
原理图:
从原理图中我们看到Replugin hook了两个ClassLoader
RePluginClassLoader
:继承PathClassLoader
所以只能用于加载宿主中的已经安装的类。PluginDexClassLoader
:继承自DexClassLoader
,前面我们分析过,DexClassLoader可以加载外部路径上的类
在步骤2
中:找到对应的插件中四大组件信息
插件信息使用下面方式获取:
- 2.1:获取插件的
mPackageInfo
mPackageInfo = pm.getPackageArchiveInfo(mPath,
PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES | PackageManager.GET_PROVIDERS | PackageManager.GET_RECEIVERS | PackageManager.GET_META_DATA);
- 2.2:通过
mPackageInfo
获取四大组件信息以及配置清单文件信息
/**
* 初始化ComponentList对象 <p>
* 注意:仅框架内部使用
*/
public ComponentList(PackageInfo pi, String path, PluginInfo pli) {
if (pi.activities != null) {
for (ActivityInfo ai : pi.activities) {
if (LOG) {
LogDebug.d(PLUGIN_TAG, "activity=" + ai.name);
}
ai.applicationInfo.sourceDir = path;
// todo extract to function
if (ai.processName == null) {
ai.processName = ai.applicationInfo.processName;
}
if (ai.processName == null) {
ai.processName = ai.packageName;
}
mActivities.put(ai.name, ai);
}
}
if (pi.providers != null) {
for (ProviderInfo ppi : pi.providers) {
if (LOG) {
LogDebug.d(PLUGIN_TAG, "provider=" + ppi.name + "; auth=" + ppi.authority);
}
if (ppi.processName == null) {
ppi.processName = ppi.applicationInfo.processName;
}
if (ppi.processName == null) {
ppi.processName = ppi.packageName;
}
mProvidersByName.put(ppi.name, ppi);
mProvidersByAuthority.put(ppi.authority, ppi);
}
}
if (pi.services != null) {
for (ServiceInfo si : pi.services) {
if (LOG) {
LogDebug.d(PLUGIN_TAG, "service=" + si.name);
}
if (si.processName == null) {
si.processName = si.applicationInfo.processName;
}
if (si.processName == null) {
si.processName = si.packageName;
}
mServices.put(si.name, si);
}
}
if (pi.receivers != null) {
for (ActivityInfo ri : pi.receivers) {
if (LOG) {
LogDebug.d(PLUGIN_TAG, "receiver=" + ri.name);
}
if (ri.processName == null) {
ri.processName = ri.applicationInfo.processName;
}
if (ri.processName == null) {
ri.processName = ri.packageName;
}
mReceivers.put(ri.name, ri);
}
}
// 解析 Apk 中的 AndroidManifest.xml
String manifest = getManifestFromApk(path);
}
在步骤3
中:给插件中的Activity分配占位Activity,用于欺骗AMS
// 远程分配坑位
container = client.allocActivityContainer(plugin, process, ai.name, intent);
在步骤8和9
中:这两个步骤就是取出坑位Activity中真正要启动的插件Activity,通过映射获取
前面说过Activity在回调的时候会启动Instrumentation
的newActivity
创建Activity:
public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
String pkg = intent != null && intent.getComponent() != null
? intent.getComponent().getPackageName() : null;
return getFactory(pkg).instantiateActivity(cl, className, intent);
}
public @NonNull Activity instantiateActivity(@NonNull ClassLoader cl, @NonNull String className,
@Nullable Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return (Activity) cl.loadClass(className).newInstance();
}
最终调用cl.loadClass(className).newInstance()创建一个实例对象:
这个时候的:
- cl:还是宿主的PluginClassLoader,
- className:占坑Activity类名
要实现启动插件Activity,我们就需要来看宿主的RePluginClassLoader的loadClass方法是如何操作的:
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
//
Class<?> c = null;
c = PMF.loadClass(className, resolve);
if (c != null) {
return c;
}
//
try {
c = mOrig.loadClass(className);
// 只有开启“详细日志”才会输出,防止“刷屏”现象
if (LogDebug.LOG && RePlugin.getConfig().isPrintDetailLog()) {
LogDebug.d(TAG, "loadClass: load other class, cn=" + className);
}
return c;
} catch (Throwable e) {
//
}
//
return super.loadClass(className, resolve);
}
进入PMF.loadClass(className, resolve);
final Class<?> loadClass(String className, boolean resolve) {
// 加载Service中介坑位
if (className.startsWith(PluginPitService.class.getName())) {
return PluginPitService.class;
}
//
if (mContainerActivities.contains(className)) {
Class<?> c = mClient.resolveActivityClass(className);
if (c != null) {
return c;
}
return DummyActivity.class;
}
//
if (mContainerServices.contains(className)) {
Class<?> c = loadServiceClass(className);
if (c != null) {
return c;
}
return DummyService.class;
}
//
if (mContainerProviders.contains(className)) {
Class<?> c = loadProviderClass(className);
if (c != null) {
return c;
}
return DummyProvider.class;
}
...
return loadDefaultClass(className);
}
可以看到这里面获取的是映射表中的插件四大组件class类,返回的是插件中的类
这里class类就是通过前面说的插件ClassLoader:PluginDexClassLoader进行加载。
PluginDexClassLoader中重写了loadClass方法,我们就入源码看看:
@Override
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
// 插件自己的Class。从自己开始一直到BootClassLoader,采用正常的双亲委派模型流程,读到了就直接返回
Class<?> pc = null;
ClassNotFoundException cnfException = null;
try {
pc = super.loadClass(className, resolve);
if (pc != null) {
...
return pc;
}
} catch (ClassNotFoundException e) {
if (PluginDexClassLoaderPatch.need2LoadFromHost(className)) {
try {
return loadClassFromHost(className, resolve);
} catch (ClassNotFoundException e1) {
}
}
}
// 若插件里没有此类,则会从宿主ClassLoader中找,找到了则直接返回
// 注意:需要读取isUseHostClassIfNotFound开关。默认为关闭的。可参见该开关的说明
if (RePlugin.getConfig().isUseHostClassIfNotFound()) {
try {
return loadClassFromHost(className, resolve);
} catch (ClassNotFoundException e) {
}
}
// At this point we can throw the previous exception
if (cnfException != null) {
throw cnfException;
}
return null;
}
可以看到其在加载类的时候,会优先加载当前插件中的类,如果没找到,再去加载宿主中的类 来看。这样就可以成功找到插件中的Activity,之后就可以跳转了。
这里借鉴下恋猫de小郭的图来描述下Replugin中的ClassLoader调用关系:
通过这几个步骤及实现了只hook ClassLoader实现插件和宿主之间的通讯。
4.运行时容器技术(ProxyActivity代理)
四大组件通讯除了上面的Activity占坑方式外,还可以使用一种运行时容器技术。
运行时容器技术,简单来说就是在宿主 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();
}
// ...
}
大概原理就是这么简单,启动插件组件需要依赖容器,容器负责加载插件组件并且完成双向转发,转发来自系统的生命周期回调至插件组件,同时转发来自插件组件的系统调用至系统。
最后来介绍下几种主流插件化开源框架:
阿里系Atlas
:github.com/alibaba/atl…
Atlas容器框架: 支持特性:
功能 | 说明 |
---|---|
四大组件支持 | 支持运行bundle中的四大组件 |
共享代码资源 | bundle可以直接使用host中的代码和资源 |
bundle按需加载 | 业务需要时,才会去加载对应bundle中的代码和资源 |
远程bundle | 减少包体积。不常用的bundle放在云端,需要时按需下载。当用户设备空间紧张时,可以清理掉一些长期不用的组件 |
解释执行 | 为了降低用户等待时间,Atlas框架在dalivk系统上首次使用bundle时关闭了verify,在ART系统上首次使用时关闭了dex2oat走解释执行。同时后台通过异步任务走原生的dexopt过程,为下次使用做好准备 |
360系RePlugin
:github.com/Qihoo360/Re…
RePlugin是一套完整的、稳定的、适合全面使用的,占坑类插件化方案:
- 完整的:让插件运行起来“像单品那样”,支持大部分特性
- 稳定的:如此灵活完整的情况下,其框架崩溃率仅为业内很低的“万分之一”
- 适合全面使用的:其目的是让应用内的“所有功能皆为插件”
- 占坑类:以稳定为前提的Manifest占坑思路
- 插件化方案:基于Android原生API和语言来开发,充分利用原生特性
支持特性:
Feature | Description |
---|---|
Components | Activity, Service, Provider, Receiver(Including static) |
Not need to upgrade when brand a new Plug-in | Supported |
Android Feature | Supported almost all features |
TaskAffinity & Multi-Process | Perfect supported! |
Support Plug-in Type | Built-in (Only Two Step) and External(Download) |
Plug-in Coupling | Binder, Class Loader, Resources, etc. |
Interprocess communication | Sync, Async, Binder and Cross-plug-in broadcast |
User-Defined | Theme & AppComat Supported |
DataBinding | Supported |
Safety check when installed | Supported |
Resources Solution | Independent Resources + Context pass(No Adaptation ROM) |
Android Version | API Level 9 (Android 2.3 and above) |
高中生罗迪VirtualApp
:github.com/asLody/Virt…
VirtualApp 作者是高中生罗迪,据说这个 Android 大牛初三的时候就开始研究双开、插件化的技术,相当了不起。 项目的思路与DroidPlugin 相似,不过他没有提供 Service 的代理,而是使用 ContentProvider 来代替 Service 在宿主中作为真正的运行体。 这款框架在2017年12月份已经作废,不过商用版本在更新。
原理:hook了AMS
腾讯系Shadow
:github.com/Tencent/Sha…
支持特性:
- 四大组件
- Fragment(代码添加和Xml添加)
- DataBinding(无需特别支持,但已验证可正常工作)
- 跨进程使用插件Service
- 自定义Theme
- 插件访问宿主类
- So加载
- 分段加载插件(多Apk分别加载或多Apk以此依赖加载)
- 一个Activity中加载多个Apk中的View 等等……
关于如何选择插件化框架,这个见仁见智,可以根据自身项目需求和框架特性进行选择。
总结
讲了那么多这里是该总结下了: 本文主要讲解了当前主流插件化使用到的插件化技术
主要有: 1.类的加载过程以及原理 2.资源的注入过程以及原理 3.插件和宿主之间四大组件通讯机制。说到了几种hook方式 4.介绍了几种主流框架的特性。
一般大厂都有自己的开源框架,而开源出来的部分只是冰山一角,但我们也希望通过这一角来窥探道插件化内部的奥秘。
参考资料