介绍
定义
插件化从APK中拆分出单独的业务或功能模块,可独立的进行下载、安装、运行。
优势
- 动态化:快速功能更新、快速修复问题、按需拉取功能包
- 包大小:大大缩减宿主包体积,提高下载安装速度,提高用户激活率,减少投放成本
应用
Case 1
Case 2
原理
如下图,Apk中有classes.dex、res资源、assets资源、resources.arsc、naitve lib、AndroidManifest.xml等,这些也都是用来承载我们需求功能的文件,那我们一个完整的插件化框架就需要支持以下文件的单独运行。
类
1. 双亲委派模型
Android中可以通过ClassLoader加载类,每个ClassLoader构造的时候都会传入一个parentClassLoader作为父亲,大家最熟知的就是loadClass方法,ClassLoader默认加载类的逻辑也都遵循“双亲委派机制”,即优先尝试用parent来加载类,加载不到再从自己的ClassLoader中加载类
public abstract class ClassLoader {
...
public ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
}
protected Class<?> loadClass(String name, boolean resolve) {
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
c = findClass(name);
}
}
return c;
}
...
}
最上层的ParentClassLoader通常为系统核心类,这样可以避免因为加载自定义同名类,与系统核心类冲突。
2. DexClassLoader
Android提供了ClassLoader的具体实现类DexClassLoader类,这个类可以帮我们加载一个apk中的类,我们只需要传入apkFile,就可以通过loadClass加载类,我们可以通过这种方式,给每个插件新建一个PluginClassLoader
public class PluginClassLoader extends DexClassLoader {
public PluginClassLoader(String apkFile, String optimizedDirectory, String libraryPath, ClassLoader systemClassLoader) {
this(apkFile, optimizedDirectory, libraryPath, systemClassLoader, false);
}
@Override
protected Class<?> loadClass(String name, boolean resolve) {
...
}
}
3. 实现对插件类的加载
Java每个类对象都有一个classLoader属性,表示当前类是由哪个ClassLoader加载,当我们用宿主的A类去调用插件的B类时,会尝试用A类的classLoader去loadClass B,显然是加载不到的。
但如果我们想让宿主类可以直接加载插件类,那需要宿主类的classloader具备加载插件类的能力。在Android中呢,默认用PathClassLoader来加载宿主APK中的类,其Parent为BootClassLoader来加载系统的类。SDK自定义一个新的类DelegateClassLoader实现对插件类的加载,并将其插到PathClassLoader与BootClassLoader之间
反射替换parent
public static DelegateClassLoader installHook() {
try {
ClassLoader pathClassLoader = Utils.getAppContext().getClassLoader();
ClassLoader parentClassLoader = pathClassLoader.getParent();
if (!(parentClassLoader instanceof DelegateClassLoader)) {
DelegateClassLoader DelegateClassLoader = new DelegateClassLoader(parentClassLoader, pathClassLoader);
Field parentField = ClassLoader.class.getDeclaredField("parent");
parentField.setAccessible(true);
parentField.set(pathClassLoader, DelegateClassLoader);
return DelegateClassLoader;
}
} catch (Exception var4) {
}
return null;
}
我们通过DelegateClassLoader中的逻辑来加载插件的类,分为几步:
- 首先调用PathClassLoader的findLoadedClass()从宿主中查找
- 交给BootClassLoader处理
- DelegateClassLoader先调用PathClassLoader#findClass查找
- 遍历所有已加载启动的插件Dex中查找
- 最后按类名匹配到目标插件,尝试加载插件并查找
public class DelegateClassLoader extends ClassLoader {
private Method findClassMethod;
private Method findLoadedClassMethod;
private ClassLoader pathClassLoader;
public DelegateClassLoader(ClassLoader parent, ClassLoader pathClassLoader) {
super(parent);
this.findClassMethod = MethodUtils.getAccessibleMethod(ClassLoader.class, "findClass", String.class);
this.findLoadedClassMethod = MethodUtils.getAccessibleMethod(ClassLoader.class, "findLoadedClass", String.class);
this.pathClassLoader = pathClassLoader;
}
@Override
protected Class<?> findClass(String className) {
// pathClassLoader.findLoadedClass(name)
if (clazz == null && findLoadedClassMethod != null) {
try {
clazz = (Class<?>) findLoadedClassMethod.invoke(pathClassLoader, className);
} catch (Throwable e) {
exception = e;
}
}
// pathClassLoader.findClass(name)
if (clazz == null && findClassMethod != null) {
try {
clazz = (Class<?>) findClassMethod.invoke(pathClassLoader, className);
} catch (Throwable e) {
exception = e;
}
}
// PluginClassLoader.findClassFromCurrent(name) from loaded Plugin
if (clazz == null) {
Map<String, PluginClassLoader> pluginClassLoaders = new ConcurrentHashMap<>(PluginLoader.sCachedPluginClassLoader);
Iterator<Map.Entry<String, PluginClassLoader>> iterator = pluginClassLoaders.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, PluginClassLoader> entry = iterator.next();
PluginClassLoader loader = entry.getValue();
try {
clazz = loader.findClassFromCurrent(className);
} catch (Throwable e) {
exception = e;
}
if (clazz != null) {
break;
}
}
}
// PluginClassLoader.findClassFromCurrent(name) from unloaded Plugin
if (clazz == null) {
for (Plugin plugin : PluginManager.getInstance().listPlugins()) {
if (needLoadPlugin(plugin, className)) {
// preload
if (PluginLoader.sCachedPluginClassLoader.get(plugin.mPackageName) == null) {
PluginManager.getInstance().preload(plugin.mPackageName);
}
// find
PluginClassLoader pluginClassLoader = PluginLoader.sCachedPluginClassLoader.get(plugin.mPackageName);
if (pluginClassLoader != null) {
try {
clazz = pluginClassLoader.findClassFromCurrent(className);
} catch (Throwable e) {
exception = e;
}
if (clazz != null) {
break;
}
}
}
}
}
...
}
}
四大组件
1. Manifest检查
AMS是Android中最核心的服务,主要负责系统中四大组件的启动、切换、调度及应用进程的管理和调度等工作。其中在PackageManagerService.java打开一个Activity时,会先通过Package查找此APK是否Manifest中声明过此Activity,如果没声明就无法打开目标Activity。
2. Activity-偷梁换柱
Android是通过Intent告诉AMS要打开的Activity的,当我们要启动插件的Activity时,如果我们假装告诉系统要启动一个宿主Activity,就可以通过系统的检测了
- startActivity最终都会统一经过Instrumention,我们可以通过Hook Instrumention来做一些定制化操作
- 在HookInstrumention中,我们在执行execStartActivity之前对Intent内容替换为SubActivity
private Intent wrapIntent(Object target, Intent intent, int requestCode) {
boolean shouldInterrupt = false;
if (intent != null && !intent.getBooleanExtra("start_only_for_android", false)) {
String packageName = this.getPluginPackageName(intent);
if (PluginPackageManager.isPluginPackage(packageName)) {
HookManager.getInstance().installHookActivityManagerProxy();
shouldInterrupt = !PluginPackageManager.checkPluginInstalled(packageName) || !PluginManager.getInstance().loadPlugin(packageName);
}
}
if (shouldInterrupt) {
Intent wrapIntent = new Intent(Utils.getAppContext(), PluginLoaderActivity.class);
wrapIntent.putExtra("target_intent", intent);
wrapIntent.putExtra("request_code", requestCode);
wrapIntent.putExtra("plugin_package_name", this.getPluginPackageName(intent));
return wrapIntent;
} else {
List<ResolveInfo> hostActivities = Utils.getAppContext().getPackageManager().queryIntentActivities(intent, 16842752);
if (hostActivities != null && hostActivities.size() > 0) {
return intent;
} else {
//真实要启动的组件
Intent targetIntent = intent;
if (intent != null) {
intent.putExtra("hookInstrumentationHasWrapIntent", true);
}
if (intent != null && !intent.getBooleanExtra("start_only_for_android", false)) {
List<ResolveInfo> resolveInfos = PluginPackageManager.queryIntentActivities(intent, 0);
if (resolveInfos != null && resolveInfos.size() > 0) {
ActivityInfo targetActivityInfo = ((ResolveInfo)resolveInfos.get(0)).activityInfo;
if (targetActivityInfo != null) {
//插桩的组件
ActivityInfo stubActivityInfo = PluginActivityManager.selectStubActivity(targetActivityInfo);
if (stubActivityInfo != null) {
intent.putExtra("target_activityinfo", targetActivityInfo);
intent.putExtra("stub_activityinfo", stubActivityInfo);
Intent wrapIntent = new Intent();
//设置代理className
wrapIntent.setClassName(stubActivityInfo.packageName, stubActivityInfo.name);
wrapIntent.setFlags(intent.getFlags());
wrapIntent.putExtra("target_intent", intent);
wrapIntent.putExtra("target_activityinfo", targetActivityInfo);
wrapIntent.putExtra("stub_activityinfo", stubActivityInfo);
String stubProcessName = "";
if (stubActivityInfo.applicationInfo != null) {
stubProcessName = stubActivityInfo.applicationInfo.processName;
}
String createInfo = System.currentTimeMillis() + "#" + Process.myPid() + "#" + Utils.getAppContext().getApplicationInfo().processName + "#" + stubProcessName;
wrapIntent.putExtra("stub_createinfo", createInfo);
targetIntent = wrapIntent;
wrapIntent.putExtra("hookInstrumentationHasWrapIntent", true);
} else {
Logger.w("hook/activity", "HookInstrumentation wrapIntent selectStubActivityInfo null");
}
}
} else {
Logger.w("hook/activity", "HookInstrumentation wrapIntent queryIntentActivities from plugin empty");
}
}
return targetIntent;
}
}
}
- AMS处理完系统会通过ActivityThreadHandler的消息的方式打开目标Activity
- 这时候我们可以用同样的方法Hook ActivityThreadHandler对传回来的Intent中SubActivity替换为原本的插件Activity
private boolean handleLaunchPluginActivity(Message message) {
Intent stubIntent = null;
try {
Object activityClientRecord = message.obj;
stubIntent = (Intent)FieldUtils.readField(activityClientRecord, "intent");
stubIntent.setExtrasClassLoader(this.getClass().getClassLoader());
//从Intent中获取要启动的真实activity
Intent targetIntent = (Intent)stubIntent.getParcelableExtra("target_intent");
ActivityInfo targetActivityInfo = (ActivityInfo)stubIntent.getParcelableExtra("target_activityinfo");
ActivityInfo stubActivityInfo = (ActivityInfo)stubIntent.getParcelableExtra("stub_activityinfo");
ActivityInfo stubActivityInfoFromSystem = (ActivityInfo)FieldUtils.readField(activityClientRecord, "activityInfo");
if (stubActivityInfo == null || stubActivityInfoFromSystem == null || stubActivityInfo.exported || stubActivityInfoFromSystem.exported || !TextUtils.equals(stubActivityInfo.packageName, stubActivityInfoFromSystem.packageName) || !TextUtils.equals(stubActivityInfo.name, stubActivityInfoFromSystem.name)) {
Logger.d("hook/activity", "HookHandlerCallbackStart source verification failed.");
return false;
}
if (targetIntent != null && targetActivityInfo != null) {
if (targetActivityInfo.applicationInfo != null && !(new File(targetActivityInfo.applicationInfo.sourceDir)).exists()) {
Logger.e("hook/activity", "HookHandlerCallback handleLaunchPluginActivity, targetActivityInfo.applicationInfo.sourceDir is not exists, " + targetActivityInfo.applicationInfo.sourceDir);
targetIntent.putExtra("extra_stub_intent", stubIntent);
targetActivityInfo.applicationInfo = Utils.getAppContext().getApplicationInfo();
targetIntent.setClassName(Utils.getAppContext(), ErrorBackupActivity.class.getName());
} else {
this.reportIfTimeout(stubIntent);
Logger.d("hook/activity", "HookHandlerCallback handleLaunchPluginActivity, then launchPluginApp applicationInfo = " + targetActivityInfo.applicationInfo);
if (targetActivityInfo.applicationInfo != null) {
PluginLoader.launchPluginApp(targetActivityInfo.applicationInfo.packageName, targetActivityInfo);
}
String contextPackageName = PluginPackageManager.generateContextPackageName(targetActivityInfo.packageName);
targetIntent.setClassName(contextPackageName, targetActivityInfo.name);
}
FieldUtils.writeField(activityClientRecord, "intent", targetIntent);
FieldUtils.writeField(activityClientRecord, "activityInfo", targetActivityInfo);
Logger.w("hook/activity", "HookHandlerCallback handleLaunchPluginActivity, " + String.format("Target[%s] <<< Stub[%s]", targetActivityInfo.name, stubActivityInfo.name));
}
if (OSUtil.isAndroidO()) {
try {
ViewRootImpl.ActivityConfigCallback callback = (ViewRootImpl.ActivityConfigCallback)FieldUtils.readField(activityClientRecord, "configCallback");
ViewRootImpl.ActivityConfigCallback newCallback = new ActivityConfigCallbackProxy(callback);
FieldUtils.writeField(activityClientRecord, "configCallback", newCallback);
Logger.i("hook/activity", "HookHandlerCallback hook replace ViewRootImpl.ActivityConfigCallback");
} catch (Exception var11) {
Logger.e("hook/activity", "HookHandlerCallback hook replace ViewRootImpl.ActivityConfigCallback failed.", var11);
}
}
} catch (Exception var12) {
Logger.e("hook/activity", "HookHandlerCallback handleLaunchPluginActivity failed.", var12);
if (stubIntent != null && var12 instanceof BadParcelableException) {
try {
Object mExtras = FieldUtils.readField(stubIntent, "mExtras");
FieldUtils.writeField(mExtras, "mParcelledData", (Object)null);
} catch (Throwable var10) {
stubIntent.replaceExtras(new Bundle());
}
}
}
return false;
}
3. Service-偷梁换柱
与Activity同样的方式:
- Hook ActivityManagerNative 拦截Service的start/stop/bind/unBind方法,将Intent换成SubService
- Hook ActivityThreadHandler 将Intent目标还原
4. Receiver注册
广播分为静态广播和动态广播,动态广播不需要注册,静态广播只需要在插件启动时,按照动态广播的方式手动进行注册即可。
private static void registerReceivers(ApplicationInfo pluginAppInfo) {
List<ReceiverInfo> receiverInfos = PluginPackageManager.getReceivers(pluginAppInfo.packageName, 0);
if (receiverInfos != null && receiverInfos.size() > 0) {
ClassLoader pluginClassLoader = (ClassLoader)sCachedPluginClassLoader.get(pluginAppInfo.packageName);
PackageManager packageManager = Utils.getAppContext().getPackageManager();
Iterator var4 = receiverInfos.iterator();
while(true) {
ReceiverInfo receiverInfo;
List hostReceiversList;
do {
if (!var4.hasNext()) {
return;
}
receiverInfo = (ReceiverInfo)var4.next();
String packageName = Utils.getAppContext().getPackageName();
Intent intent = new Intent();
intent.setComponent(new ComponentName(packageName, receiverInfo.name));
hostReceiversList = packageManager.queryBroadcastReceivers(intent, 16842752);
} while(hostReceiversList != null && hostReceiversList.size() > 0);
try {
BroadcastReceiver receiver = (BroadcastReceiver)pluginClassLoader.loadClass(receiverInfo.name).newInstance();
Iterator var10 = receiverInfo.intentFilters.iterator();
while(var10.hasNext()) {
IntentFilter intentFilter = (IntentFilter)var10.next();
Utils.getAppContext().registerReceiver(receiver, intentFilter);
}
Logger.i("hook/load", "PluginLoader registerReceivers, " + receiver + ", " + pluginAppInfo.packageName);
} catch (Exception var12) {
Logger.e("hook/load", "PluginLoader registerReceivers failed, " + receiverInfo.name + "pkg = " + pluginAppInfo.packageName, var12);
}
}
}
}
5. ContentProvider安装
类似Receiver的静态改态的方式,相同进程的数据访问只需要将Provider执行ActivityThread.installContentProvider即可(反射调用)
private static boolean installContentProviders(Context context, String pluginPkgName, String processName) {
List<ProviderInfo> providers = PluginPackageManager.getProviders(pluginPkgName, processName, 0);
if (providers != null && providers.size() > 0) {
Iterator<ProviderInfo> iterator = providers.iterator();
while(iterator.hasNext()) {
ProviderInfo providerInfo = (ProviderInfo)iterator.next();
if (context.getPackageManager().resolveContentProvider(providerInfo.authority, 16777216) != null) {
iterator.remove();
}
if (providerInfo != null && !TextUtils.equals(providerInfo.applicationInfo.packageName, context.getPackageName())) {
providerInfo.applicationInfo.packageName = context.getPackageName();
}
}
ActivityThreadHelper.installContentProviders(context, providers);
Logger.i("hook/load", "PluginLoader installContentProviders, " + providers + ", " + pluginPkgName);
}
return true;
}
资源
1. 加载插件资源
Android是通过Resources对象加载资源的,Resources有个核心实现类AssetManager,他负责具体的资源读取,它有一个很重要的方法addAssetPath(path)可以将一个资源路径添加到当前Resources中,当我们加载一个插件时,调用它就能把插件资源添加到Resources对象中
private AssetManager appendAssetPathSafely(AssetManager assetManager, String sourceDir, boolean asSharedLibrary) {
int tryCount = 3;
while(tryCount-- >= 0) {
String errorMsg = null;
try {
synchronized(assetManager) {
int cookie = 0;
for(int i = 0; i < 3; ++i) {
if (OSUtil.isAndroidLM()) {
cookie = (Integer)MethodUtils.invokeMethod(assetManager, "addAssetPathNative", new Object[]{sourceDir}, new Class[]{String.class});
} else if (OSUtil.isAndroidNMR1()) {
cookie = (Integer)MethodUtils.invokeMethod(assetManager, "addAssetPathNative", new Object[]{sourceDir, asSharedLibrary}, new Class[]{String.class, Boolean.TYPE});
}
if (cookie != 0) {
break;
}
}
if (cookie == 0) {
errorMsg = "cookie == 0";
break;
}
Object seed = FieldUtils.readField(assetManager, "mStringBlocks");
int seedNum = seed != null ? Array.getLength(seed) : 0;
int num = (Integer)MethodUtils.invokeMethod(assetManager, "getStringBlockCount", new Object[0]);
Object newStringBlocks = Array.newInstance(seed.getClass().getComponentType(), num);
for(int i = 0; i < num; ++i) {
if (i < seedNum) {
Array.set(newStringBlocks, i, Array.get(seed, i));
} else {
long nativeStringBlockObj = (Long)MethodUtils.invokeMethod(assetManager, "getNativeStringBlock", new Object[]{i}, new Class[]{Integer.TYPE});
Object stri = MethodUtils.invokeConstructor(seed.getClass().getComponentType(), new Object[]{nativeStringBlockObj, true}, new Class[]{Long.TYPE, Boolean.TYPE});
Array.set(newStringBlocks, i, stri);
}
}
FieldUtils.writeField(assetManager, "mStringBlocks", newStringBlocks);
}
Logger.w("hook/load", "AssetManagerProcessorCompat appendAssetPathSafely success, sourceDir = " + sourceDir);
break;
} catch (Exception var18) {
Logger.e("hook/load", "AssetManagerProcessorCompat appendAssetPathSafely failed, sourceDir = " + sourceDir, var18);
}
}
return assetManager;
}
2. 插件与宿主的资源ID冲突
Java代码通常通过资源ID(Int值)来访问具体资源,资源id是由4个字节组成,用十六进制表示:0x PPTTEEEE,其中PP代表命名空间,TT代表资源类型(animator、anim、color、drawable等),EEEE代表的是每一个资源所出现的顺序。我们打包时的资源PP值默认都是7F,就会导致插件和宿主的资源冲突:
恰好aapt工具支持我们通过命令参数的方式设置PP值 : aapt2 --package-id 0xPP ,我们给每个插件的PP段设置不同的值,就能保证每个插件和宿主的资源都不冲突。
3. 插件访问宿主资源
如果插件使用了宿主的资源,是否将资源单独在插件中打一份呢?如下图,会带来包体积的增加:
针对这个问题,正好Android官方提供了public.xml来帮助我们固定资源名与资源ID
Native so
当我们执行System.loadLibrary(libName)的时候,会调用VMStack.getCallingClassLoader()返回当前类的classLoader对象,用于加载so文件,插件类的classLoader就是PluginClassLoader
而我们在创建PluginClassLoader的时候,通过librarySearchPath参数传入so的路径。