搞懂插件化,看这一篇就够了
背景
历史和现状
发展历史
- 2012年,大众点评的屠毅敏推出了AndroidDynamicLoader框架,可以算是第一个插件化框架。
- 2014年初,阿里一位员工做了一次技术分享,专门讲淘宝的Altas技术,以及这项技术的大方向。但是很多技术细节没有分享,也没有开源。
- 2014年底,任玉刚发布了一个Android插件化项目,起名为dynamic-load-apk,它没有Hook太多的系统底层方法,而是从上层,即App应用层解决问题,创建一个继承自Activity的ProxyActivity类,然后让插件中的所有Activity都继承自ProxyActivity,并重写Activity所有的方法。
- 2015年8月,张勇发布DroidPlugin,有非常多的Hook,能把任意的App都加载到宿主里。目前已经不维护了,但很多思路值得借鉴。
- 2017年3月,Atlas开源。
- 2017年6月,VirtualAPK 是滴滴开源的一套插件化框架,支持几乎所有的 Android 特性,四大组件等。
- 2018年,Google在IO大会上发布了Android App Bundle方案,是一个依赖于GooglePlay Service的官方插件化方案,同时在Android P及以上系统中开始加入了系统级的支持。不过因为国内没有PlayService服务,我们需要在其基础上进行魔改。
- 2019年6月,Qigsaw正式开源,基于Android App Bundle,同时其API与官方兼容,即在国内可以走插件化方式,在海外走GooglePlay渠道,思路非常不错。
现状
- 各插件框架,都是基于自身App的业务来开发的,目标或多或少都有区别,所以很难有一个插件框架能一统江湖解决所有问题。
- Android每次版本升级都会给各个插件化框架带来不少冲击,都要费劲心思适配一番,更别提国内各个厂商对在ROM上做的定制了,正如VirtualAPK的作者任玉刚所说:完成一个插件化框架的 Demo 并不是多难的事儿,然而要开发一款完善的插件化框架却并非易事。
- 在2020年,Android插件化依旧是一个高风险的技术,涉及到各个Android SDK版本、各个OEM厂商的兼容性适配问题。
笼统的分类
免安装型
- 宿主和插件单独编译。
- 加载一个与主app无业务关系的独立apk,实现不安装即可使用功能。
自解耦型
- 宿主和插件共同完成编译。
- 偏向于在“组件化”的基础上,将“组件”从主app中剥离为“插件”。
插件化关注点
- 加载&管理插件
- ClassLoader
- Resources
- 四大组件
- 编译打包
- 宿主&插件升级
VirtualApk源码
VirtualApk作为一个开创性的插件化框架,源码非常具有借鉴意义。下面从VirtualApk源码的角度,从各个方面来讨论,如何实现一个插件化框架。
加载&管理插件
- PluginManager:负责加载和管理插件,保存一些全局信息
- LoadedPlugin:加载插件后,保存了插件的全部信息
加载流程
PluginManager
public void loadPlugin(File apk) throws Exception {
// ... 检查apk文件是否存在
// 创建LoadedPlugin
LoadedPlugin plugin = createLoadedPlugin(apk);
// 缓存到mPlugins map 中
this.mPlugins.put(plugin.getPackageName(), plugin);
synchronized (mCallbacks) {
for (int i = 0; i < mCallbacks.size(); i++) {
// 插件Load成功回调
mCallbacks.get(i).onAddedLoadedPlugin(plugin);
}
}
}
创建LoadedPlugin大致流程:
public LoadedPlugin(PluginManager pluginManager, Context context, File apk) throws Exception {
// 反射调用PackageParser.parsePackage解析apk,获取Package对象
this.mPackage = PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK);
this.mPackage.applicationInfo.metaData = this.mPackage.mAppMetaData;
// 构造PackageInfo对象
this.mPackageInfo = new PackageInfo();
// ... 将package中内容复制到PackageInfo中
// 构造PluginPacakgeManager对象
this.mPackageManager = createPluginPackageManager();
// 构造PluginContext对象
this.mPluginContext = createPluginContext(null);
// 构造Resources对象
this.mResources = createResources(context, getPackageName(), apk);
// 构造ClassLoader
this.mClassLoader = createClassLoader(context, apk, this.mNativeLibDir, context.getClassLoader());
// 拷贝so库。将CPU-ABI对应的SO拷贝到宿主的目录下。
tryToCopyNativeLib(apk);
// 缓存Manifest中的Activities/Services/Content Provider
// ...
// 将静态广播转为动态
// ...
// 实例化插件的Application,并调用onCreate
invokeApplication();
}
具体做法
parsePacakge
// 不同的Android版本,hook的方式不一样
static final PackageParser.Package parsePackage(Context context, File apk, int flags) throws Throwable {
PackageParser parser = new PackageParser();
// 从apk中解析出Package,包含packageName/versionCode/versionName/四大组件等信息
PackageParser.Package pkg = parser.parsePackage(apk, flags);
// 通过collectCertificates方法获取应用的签名信息mSignatures
Reflector.with(parser)
.method("collectCertificates", PackageParser.Package.class, int.class)
.call(pkg, flags);
return pkg;
}
创建插件的PackageManager
// 创建PluginPackageManager
protected class PluginPackageManager extends PackageManager {
@Override
public PackageInfo getPackageInfo(String packageName, int flags) throws NameNotFoundException {
// 使用包名从PluginManager中获取插件LoadedPlugin
LoadedPlugin plugin = mPluginManager.getLoadedPlugin(packageName);
if (null != plugin) {
// 获取插件PackageInfo
return plugin.mPackageInfo;
}
// 如果没找到,则使用宿主的PackageInfo
return this.mHostPackageManager.getPackageInfo(packageName, flags);
}
@Override
public ActivityInfo getActivityInfo(ComponentName component, int flags) throws NameNotFoundException {
LoadedPlugin plugin = mPluginManager.getLoadedPlugin(component);
if (null != plugin) {
// 从LoadedPlugin获取ActivityInfo
return plugin.mActivityInfos.get(component);
}
return this.mHostPackageManager.getActivityInfo(component, flags);
}
// ...
}
创建插件的Context
class PluginContext extends ContextWrapper{
private final LoadedPlugin mPlugin;
public PluginContext(LoadedPlugin plugin) {
super(plugin.getPluginManager().getHostContext());
this.mPlugin = plugin;
}
@Override
public ClassLoader getClassLoader() {
// 获取插件ClassLoader
return this.mPlugin.getClassLoader();
}
@Override
public PackageManager getPackageManager() {
// 获取插件PackageManager
return this.mPlugin.getPackageManager();
}
@Override
public Resources getResources() {
// 获取插件的Resources
return this.mPlugin.getResources();
}
@Override
public AssetManager getAssets() {
// 获取插件的AssetManager
return this.mPlugin.getAssets();
}
@Override
public void startActivity(Intent intent) {
// 启动插件的activity
ComponentsHandler componentsHandler = mPlugin.getPluginManager().getComponentsHandler();
componentsHandler.transformIntentToExplicitAsNeeded(intent);
super.startActivity(intent);
}
}
创建插件的PackageManger和Context的用处,是为了在后续的使用中,更方便得使用插件的ClassLoader,Resources等资源。比如创建Activity后Hook掉Context。
Resources
打包aapt做了什么
image- 为assets res目录的每个资源,生成一个资源id常量,把id值和资源名称的对应关系,存放在resources.arsc文件中
- 把这些资源id常量,都定义在R.java文件中
- 将文本的xml转化成二进制xml文件,占用空间更小,解析更快
运行时获取资源
- 运行过程中通过Resource来获取资源,Resource内部通过AssetManager来读取打包到apk中的资源文件。
- 调用AssetManager的addAssetPath,将resDir传入,将资源加入到assetmanager中
VirtualApk提供两种资源处理方式
protected Resources createResources(Context context, String packageName, File apk) throws Exception {
if (Constants.COMBINE_RESOURCES) {
// 合并宿主和插件资源
return ResourcesManager.createResources(context, packageName, apk);
} else {
Resources hostResources = context.getResources();
// 新创建一个assetmanager
AssetManager assetManager = createAssetManager(context, apk);
// 使用新的assetmanager和宿主的屏幕参数信息(DPI、屏幕宽高等),初始化resource。
return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
}
}
- 如果设置了COMBINE_RESOURCES标志,会将宿主与插件的资源合并,宿主和插件可以互相访问,插件也可访问其他插件的资源(不推荐)
- 否则,插件使用独立的Resources,宿主和插件,插件和插件之间都无法互相访问
1. 资源独立
protected AssetManager createAssetManager(Context context, File apk) throws Exception {
AssetManager am = AssetManager.class.newInstance();
// 调用addAssetPath将插件apk的资源加入到assetManager
Reflector.with(am).method("addAssetPath", String.class).call(apk.getAbsolutePath());
return am;
}
2. 资源合并
ResourceManager.createResources
public static synchronized Resources createResources(Context hostContext, String packageName, File apk) throws Exception {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// N之后可以直接hook mSplitResDirs,原理一样
return createResourcesForN(hostContext, packageName, apk);
}
// 创建合并资源后的newResources
Resources resources = ResourcesManager.createResourcesSimple(hostContext, apk.getAbsolutePath());
// 替换宿主的mResources
ResourcesManager.hookResources(hostContext, resources);
return resources;
}
ResourceManager.createResourcesSimple
private static Resources createResourcesSimple(Context hostContext, String apk) throws Exception {
Resources hostResources = hostContext.getResources();
Reflector reflector = Reflector.on(AssetManager.class).method("addAssetPath", String.class);
// 获取宿主的AssetManager
AssetManager assetManager = hostResources.getAssets();
reflector.bind(assetManager);
// 调用addAssetPath添加插件资源
final int cookie2 = reflector.call(apk);
// 添加所有插件的资源
List<LoadedPlugin> pluginList = PluginManager.getInstance(hostContext).getAllLoadedPlugins();
for (LoadedPlugin plugin : pluginList) {
final int cookie3 = reflector.call(plugin.getLocation());
}
// 创建newResources
Resources newResources = new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());an
// 更新所有插件的Resources
for (LoadedPlugin plugin : pluginList) {
plugin.updateResources(newResources);
}
return newResources;
}
ResourceManager.hookResources
// 用newResources替换宿主的Resources
public static void hookResources(Context base, Resources resources) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return;
}
try {
// 替换宿主context中的mReources
Reflector reflector = Reflector.with(base);
reflector.field("mResources").set(resources);
// 替换宿主PackageInfo中的mResources
Object loadedApk = reflector.field("mPackageInfo").get();
Reflector.with(loadedApk).field("mResourtces").set(resources);
Object activityThread = ActivityThread.currentActivityThread();
Object resManager;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
resManager = android.app.ResourcesManager.getInstance();
} else {
resManager = Reflector.with(activityThread).field("mResourcesManager").get();
}
// 替换ResourceManager中的resource缓存,将newResources的弱引用放入map中,新创建的context会使用newResources
Map<Object, WeakReference<Resources>> map = Reflector.with(resManager).field("mActiveResources").get();
Object key = map.keySet().iterator().next();
map.put(key, new WeakReference<>(resources));
} catch (Exception e) {
Log.w(TAG, e);
}
}
创建context的时候,会调用ResourceManager.getTopLevelResources()来获取Resources,所有context里面的资源都来自于此处。
public Resources getTopLevelResources(String resDir, int displayId, Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {
final float scale = compatInfo.applicationScale;
// 创建key
ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale, token);
Resources r;
synchronized (this) {
// 是否在mActiveResources存在
WeakReference<Resources> wr = mActiveResources.get(key);
r = wr != null ? wr.get() : null;
if (r != null && r.getAssets().isUpToDate()) {
return r;
}
}
// 创建Resources
AssetManager assets = new AssetManager();
r = new Resources(assets, dm, config, compatInfo, token);
synchronized (this) {
WeakReference<Resources> wr = mActiveResources.get(key);
// 将新创建的Resources的弱引用存入ActiveResources
mActiveResources.put(key, new WeakReference<Resources>(r));
return r;
}
}
合并资源的问题
1. 资源id重复
资源打包时,会对res目录下资源文件分配一个唯一Id。
image
- Id前两位PP为Package Id,代表应用类型。是系统应用、第三方应用、Instant App或Dynamic Feature等。
- Id中间两位TT为Type,代表资源类型。是drawable、layout或string等。
- Id后四位EE为Entry,代表该资源顺序。
解决方法:
重写AAPT命令,在插件apk打包过程中,通过指定资源id的前缀PP字段,来保证宿主和插件的资源id永远不会冲突。
2. 宿主升级
宿主升级,旧版本插件配新版本宿主,需要保证原来插件调用的资源id不能改变,否则宿主升级后,加载的插件还是拿取的旧版本资源id,会导致资源找不到和错乱情况。
所以,宿主中被插件使用的资源要保证:
- 旧的资源不能删除
- 需要保持旧版本资源的id不变(参考Tinker的实现方案)
ClassLoader
类加载源码
loadClass
// 双亲委派机制
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 首先,检查类是否已被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// parent未找到类
}
if (c == null) {
// 自己去加载类
c = findClass(name);
}
}
return c;
}
findClass
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
//通过DexPathList查找类
Class c = pathList.findClass(name, suppressedExceptions);
// ...
return c;
}
public Class findClass(String name, List<Throwable> suppressed) {
// 遍历dexElements查找
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
DexClassLoader & PathClassLoader 区别
- DexClassLoader和PathClassLoader都继承自BaseDexClassLoader。
- optmizedDirectory 不为空时,使用用户定义的目录作为 DEX 文件优化后产物 .odex 的存储目录,为空时,会使用默认的 /data/dalvik-cache/ 目录。
- 指定的optimizedDirectory必须是内部存储
BaseDexClassLoader构造函数:
// dex文件路径/odex文件输出目录/动态库路径/parent classloader
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
this(dexPath, librarySearchPath, parent, null, false);
}
PathClassLoader构造函数:
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
DexClassLoader构造函数
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
多ClassLoader & 单ClassLoader
多ClassLoader
多 ClassLoader 的方案,还可以细分为两种:一种是每个自定义 ClassLoader 的 parent 为当前宿主应用的 ClassLoader 即是 PathClassLoader,这种方案将宿主视为运行环境,插件需依赖宿主运行,插件之间互相隔离,如下图:
image
一种是每个自定义 ClassLoader 的 parent 为 BootClassLoader,这种方案类似原生应用隔离的方案,宿主与插件、插件与插件互相独立,如下图:
image
单ClassLoader
这种方案是委托给应用的PathClassLoader加载.dex,宿主与插件共享同一个 ClassLoader。 BaseDexClassLoader 在构造时生创建一个DexPathList,而DexPathList内部有一个叫做dexElements数组,我们要做的就是将 dex 文件插入到这个dexElements数组中,在 PathClassLoader 中查找类时,就会遍历这个数组中 DexFile 的信息,完成插件类的加载。
VirtualApk插件ClassLoader的创建
protected ClassLoader createClassLoader(Context context, File apk, File libsDir, ClassLoader parent) throws Exception {
File dexOutputDir = getDir(context, Constants.OPTIMIZE_DIR);
String dexOutputPath = dexOutputDir.getAbsolutePath();
// dex文件路径 优化后的odex文件路径 动态库路径 宿主的classloader
DexClassLoader loader = new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent);
if (Constants.COMBINE_CLASSLOADER) {
// 合并到宿主, 宿主能访问插件的类
DexUtil.insertDex(loader, parent, libsDir);
}
return loader;
}
合并DexElements
DexUtils.insertDex
public static void insertDex(DexClassLoader dexClassLoader, ClassLoader baseClassLoader, File nativeLibsDir) throws Exception {
// 宿主的dexElements
Object baseDexElements = getDexElements(getPathList(baseClassLoader));
// 插件dexElements
Object newDexElements = getDexElements(getPathList(dexClassLoader));
// 合并后,宿主的dexElements在插件前面
Object allDexElements = combineArray(baseDexElements, newDexElements);
// 将合并后的dexElements设置到宿主PathClassloader中
Object pathList = getPathList(baseClassLoader);
Reflector.with(pathList).field("dexElements").set(allDexElements);
// 插入so库
insertNativeLibrary(dexClassLoader, baseClassLoader, nativeLibsDir);
}
总结
- 如果设置COMBINE_CLASSLOADER,宿主可以访问插件的Class,不设置,则宿主无法访问插件的Class
- 插件可以且优先访问宿主的Class,因为将宿主的classLoader作为parent
- 单ClassLoader和多ClassLoader并存的方式,更灵活。如果不知道class属于哪个插件,使用PathClassLoader加载。如果知道属于哪个插件,直接使用插件的ClassLoader加载,效率更高。
四大组件
四大组件没有在宿主的Manifest中注册,所以需要做一些Hook操作来绕过系统的检查。
Activity启动
几个问题:
- 插件Activity没有在Manifest中注册,会报ActivityNotFoundException异常,怎么解决?
- 插件Activity创建 以及 创建完之后资源问题
- LaunchMode如何处理?
Hook流程
- 在启动Activity的请求到达AMS前,替换成已在Manifest中注册的Activity,来通过AMS的检查
- 在AMS调用回来的路径上,将Activity替换回来
Activity启动流程
image去程Hook
// 动态代理
public static void hookActivityManagerService() throws Reflector.ReflectedException{
Object gDefaultObj = null;
// API 29 及以后hook android.app.ActivityTaskManager.IActivityTaskManagerSingleton
// API 26 及以后hook android.app.ActivityManager.IActivityManagerSingleton
// API 25 以前hook android.app.ActivityManagerNative.gDefault
if(Build.VERSION.SDK_INT >= 29){
gDefaultObj = Reflector.on("android.app.ActivityTaskManager").field("IActivityTaskManagerSingleton").get();
}else if(Build.VERSION.SDK_INT >= 26){
gDefaultObj = Reflector.on("android.app.ActivityManager").field("IActivityManagerSingleton").get();
}else{
gDefaultObj = Reflector.on("android.app.ActivityManagerNative").field("gDefault").get();
}
Object amsObj = Reflector.with(gDefaultObj).field("mInstance").get();
// 本地的类加载器;
// 代理类的对象所继承的接口(用Class数组表示,支持多个接口)
// 代理类的实际逻辑,封装在new出来的InvocationHandler内
Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
amsObj.getClass().getInterfaces(), new IActivityManagerHandler(amsObj));
Reflector.with(gDefaultObj).field("mInstance").set(proxy);
}
IActivityManagerHandler
public class IActivityManagerHandler implements InvocationHandler {
Object mBase;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Log.d(TAG, "invoke " + method.getName());
// 如果是启动Activity,替换Intent
if("startActivity".equals(method.getName())){
hookStartActivity(args);
return method.invoke(mBase, args);
}else if("startService".equals(method.getName())){
// 将所有的操作进行拦截,都改为startService,然后统一在onStartCommand中分发
}
return method.invoke(mBase, args);
}
}
替换Activity
// 替换为占位Activity
private void hookStartActivity(Object[] args){
int index = getIntentIndex(args);
Intent intent = (Intent) args[index];
// 将插件的隐式intent转化为显式intent,host的intent不变
ComponentName component = intent.getComponent();
// component为空,且非host
if(component == null){
// host resolveinfo 为null
ResolveInfo info = mPluginManager.resolveActivity(intent);
if(info != null && info.activityInfo != null){
component = new ComponentName(info.activityInfo.packageName, info.activityInfo.name);
intent.setComponent(component);
}
}
// Component不为空,且非host
if(intent.getComponent() != null
&& !intent.getComponent().getPackageName().equals(mPluginManager.getHostContext().getPackageName())){
Intent newIntent = new Intent();
String stubPackage = mPluginManager.getHostContext().getPackageName();
// 占位Activity的名称
ComponentName componentName = new ComponentName(stubPackage,
mPluginManager.getComponentsHandler().getStubActivityClass(intent));
newIntent.setComponent(componentName);
// 将之前的intent存起来
newIntent.putExtra(Constants.KEY_IS_PLUGIN, true);
newIntent.putExtra(Constants.EXTRA_TARGET_INTENT, intent);
args[index] = newIntent;
Log.d(TAG, "hook succeed");
}
}
回程Hook
// Hook ActivityThread 中的 mH
public void hookActivityThreadCallback() throws Exception {
ActivityThread activityThread = ActivityThread.currentActivityThread();
Handler handler = Reflector.with(activityThread).field("mH").get();
Reflector.with(handler).field("mCallback").set(new ActivityThreadHandlerCallback(handler));
}
ActivityThreadHandlerCallback
@Override
public boolean handleMessage(@NonNull Message msg) {
Log.d(TAG, "handle Message " + msg.what);
if(what == 0){
try{
// Hook获取到EXECUTE_TRANSACTION的值
ActivityThread activityThread = ActivityThread.currentActivityThread();
Handler handler = Reflector.with(activityThread).field("mH").get();
what = Reflector.with(handler).field("EXECUTE_TRANSACTION").get();
}catch (Reflector.ReflectedException e){
e.printStackTrace();
what = EXECUTE_TRANSACTION;
}
}
// 如果是EXECUTE_TRANSACTION
if(msg.what == what){
handleLaunchActivity(msg);
}
return false;
}
private void handleLaunchActivity(Message msg){
try{
List list = Reflector.with(msg.obj).field("mActivityCallbacks").get();
if(list == null || list.isEmpty()) return;
Class<?> launchActivityItemClz = Class.forName("android.app.servertransaction.LaunchActivityItem");
if(launchActivityItemClz.isInstance(list.get(0))) {
// 从LaunchActivityItem中获取到待启动的intent
Intent intent = Reflector.with(list.get(0)).field("mIntent").get();
// 待启动的intent中保存的target,就是插件activity的信息
Intent target = intent.getParcelableExtra(Constants.EXTRA_TARGET_INTENT);
if(target != null){
// 替换回原来的activity
intent.setComponent(target.getComponent());
}
}
}catch (Reflector.ReflectedException e){
e.printStackTrace();
}catch (ClassNotFoundException e){
e.printStackTrace();
}
}
创建插件Activity
VAInstrumentation
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent)
throws ClassNotFoundException, IllegalAccessException, InstantiationException {
try {
// 宿主的ClassLoader
cl.loadClass(className);
} catch (ClassNotFoundException e) {
ComponentName component = intent.getComponent();
if (component != null) {
String targetClassName = component.getClassName();
LoadedPlugin loadedPlugin = mPluginManager.getLoadedPlugin(component.getPackageName());
if (loadedPlugin != null) {
// 使用插件的classLoader加载
Activity activity =
mBase.newActivity(loadedPlugin.getClassLoader(), targetClassName, intent);
return activity;
}
}
}
return super.newActivity(cl, className, intent);
}
替换Activity中的Resources & Context
@Override
public void callActivityOnCreate(Activity activity, Bundle icicle) {
injectActivity(activity);
mBase.callActivityOnCreate(activity, icicle);
}
protected void injectActivity(Activity activity) {
final Intent intent = activity.getIntent();
if (PluginUtil.isIntentFromPlugin(intent)) {
Context base = activity.getBaseContext();
try {
LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
// 替换插件Activity context的mResources
Reflector.with(base).field("mResources").set(plugin.getResources());
Reflector reflector = Reflector.with(activity);
// 替换插件Activity的Context
reflector.field("mBase").set(plugin.createPluginContext(activity.getBaseContext()));
// 替换插件Activity的Application
reflector.field("mApplication").set(plugin.getApplication());
// set screenOrientation
ActivityInfo activityInfo = plugin.getActivityInfo(PluginUtil.getComponent(intent));
if (activityInfo.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
activity.setRequestedOrientation(activityInfo.screenOrientation);
}
// for native activity
ComponentName component = PluginUtil.getComponent(intent);
Intent wrapperIntent = new Intent(intent);
wrapperIntent.setClassName(component.getPackageName(), component.getClassName());
activity.setIntent(wrapperIntent);
} catch (Exception e) {
Log.w(TAG, e);
}
}
}co
LaunchMode
在Manifest中注册各种LaunchMode的Activity,根据LaunchMode来按顺序匹配到不同的StubActivity
imageBroadcastReceiver
- 动态注册不需要特殊处理,可以正常使用
- 静态注册在loadPlugin时会被转化为动态注册
// BroadcastReceiver静态转动态,将插件的静态receiver动态注册到host中
Map<ComponentName, ActivityInfo> receivers = new HashMap<>();
for(PackageParser.Activity receiver : this.mPackage.receivers){
receivers.put(receiver.getComponentName(), receiver.info);
BroadcastReceiver br = BroadcastReceiver.class.cast(
// 用插件的classloader去加载
getClassLoader().loadClass(receiver.getComponentName().getClassName()).newInstance());
for(PackageParser.ActivityIntentInfo aii : receiver.intents){
mHostContext.registerReceiver(br, aii);
}
}
- 问题:插件的静态广播会被当作动态处理,如果插件没有运行(即没有插件进程运行),其静态广播也永远不会被触发。意思就是,不能通过监听系统的一些事件把插件的相关功能拉起
总结
优点
- 插件可自由增加四大组件,不受宿主约束。
- 提供COMBINE_RESOURCE和COMBINE_CLASSLOADER两种选择,满足更多应用场景。
缺点
- Hook点多,每发新的Android版本都需要重新适配
Qigsaw
概况
依赖于Dynamic Features & Split Apks
image安装split apks的方法
adb install-multiple [base.apk, split.apk]
-
PackageInstaller
,会弹出授权窗,安装完成后,默认再次启动生效。setDontKillApp(系统Api)可决定当APK安装完成后是否杀死应用进程 - 第三方应用利用PackageInstaller安装split APKs体验不友好,且某些国产手机对split APKs功能支持不完善,所以Qigsaw最终还是按照一般插件化方式安装加载split APKs。
与其他框架的区别
四大组件
- 打包的时候,会进行Manifest合并,将split apk的manifest合并到base apk中
- split apk的四大组件不能动态更新
ClassLoader
- 可选单ClassLoader和多ClassLoader
- 多ClassLoader模式下,每个插件会生成一个SplitDexClassLoader。插件可以访问宿主的类,宿主不可访问插件的类
Resources
- 打包时,Android Gradle Plugin会将split apks的资源id与base apk的id分开,不会产生冲突,也是通过自定义PP字段
- 使用时,使用ASM在getResource的地方插入SplitInstallHelper.loadResources(),合并所有插件的资源。
- 宿主默认不能访问插件的资源,需要加白名单。加入白名单后,使用ASM将resource插入目标Activity。
/**
* Activities of base apk which would load split's fragments or resources.
*/
baseContainerActivities = [
// gamecenterplugin
"com.yxcorp.gifshow.gamecenter.cloudgame.ZtGameCloudPlayActivity"
]
多进程问题
子进程需要初始化qigsaw,但是子进程未加载过插件Split Apks。
qigsaw的解决方案:
- 子进程启动时,加载所有已安装的splits
- 修改ClassLoader findClass,如果出现ClassNotFound,加载所有已安装的splits。因为可能在进程运行的过程中,加载了新的插件。
private Class<?> onClassNotFound(String name) {
// 加载所有已安装的splits
SplitLoadManagerService.getInstance().loadInstalledSplits();
ret = findClassInSplits(name);
if (ret != null) {
SplitLog.i(TAG, "Class %s is found in Splits after loading all installed splits.", name);
return ret;
}
return null;
}
插件更新
- 插件不能完全动态更新,可接Tinker热修复
- 每次升级宿主,都有重新下载插件
总结
优点
- 可以无缝切换国内和国外发布
- 站在Google肩膀上,稳定且实现简单
缺点
- 插件和宿主一起打包,每次更新宿主,插件都要重新下载