插件化学习 - 知识总结
资源访问
res里的每一个资源都会在R.Java里生成一个对应的Integer类型的id,APP启动时会先把R.java注册到当前的上下文环境,我们在代码里以R文件的方式使用资源时正是通过使用这些id访问res资源,然而插件的R.java并没有注册到当前的上下文环境,所以插件的res资源也就无法通过id使用了。
平时我们所访问资源一般是通过getResources().getXXX()
的方式来获取的。
因为宿主程序中并没有插件的资源,所以通过R来加载插件的资源是行不通的,程序会抛出异常:无法找到某某id所对应的资源。
所以我们需要先获取到插件的Resources:
private Resources getPlugResources() {
// 获取插件包中的资源
AssetManager assetManager = null;
try {
assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, mDexPath);
} catch (Exception e) {
e.printStackTrace();
}
if (assetManager == null) {
return null;
}
Resources superRes = super.getResources();
mPlugResources = new Resources(assetManager, superRes.getDisplayMetrics(),
superRes.getConfiguration());
return mPlugResources;
}
public int getColor(String colorName) {
if (mPlugResources == null) {
getPlugResources();
}
try {
return mPlugResources.getColor(mPlugResources.getIdentifier(colorName, COLOR, PLUG_NAME));
} catch (Resources.NotFoundException e) {
e.printStackTrace();
return -1;
}
}
调用插件中的Activity
apk被宿主程序调起以后,apk中的activity其实就是一个普通的对象,不具有activity的性质,因为系统启动activity是要做很多初始化工作的,而我们在应用层通过反射去启动activity是很难完成系统所做的初始化工作的,所以activity的大部分特性都无法使用包括activity的生命周期管理,这就需要我们自己去管理。当然还需要我们常说的上下文Context
需要先简单的了解一下Activity的启动流程。
在此就直接记录重要的结论:
在
ActivityThread.java
的performLaunchActivity
方法中,该方法通过Instrumentation的newActivity创建了activity类,接着完成了application的创建(没有创建Application的情况下,在该方法中就创建了Application的context),接着通过createBaseContextForActivity方法为该activity创建context,再调用attach方法进行绑定。
正常的的Activity被AMS反射调用,在attach后就有了Context,那我们自己反射的Activity要想有Context,就要模拟AMS调用方式,构造Context,但是这相当于再写个系统,不可实现,那怎么办?
遇到问题,解决问题。
插件中被反射的activity没有了Context,我们可以把主apk的Acitvity的Context传递给插件Acitivity。
在宿主APK注册一个ProxyActivity(代理Activity),就是作为占坑使用。每次打开插件APK里的某一个Activity的时候,都是在宿主里使用启动ProxyActivity,然后在ProxyActivity的生命周期里方法中,调用插件中的Activity实例的生命周期方法,从而执行插件APK的业务逻辑。所以思路就来了:
第一、ProxyActivity中需要保存一个Activity实例,该实例记录着当前需要调用插件中哪个Activity的生命周期方法。
第二、ProxyActivity如何调用插件apk中Activity的所有生命周期的方法,使用反射呢?还是其他方式(接口)。
缺点:
- 插件Activity不能使用this关键字,比如this.finish()方法是无效的,真正掌管生命周期的是proxy应该调用proxy.finish(),所以百度开源框架 dynamic-load-apk使用that指向proxy,约定插件中使用that来代替this。
- 插件Activity无法深度演绎真正的Activity组件,可能有些高级特性无法使用。
- 启动新activity的约束:启动外部activity不受限制,启动apk内部的activity有限制,首先由于apk中的activity没注册,所以不支持隐式调用,其次必须通过BaseActivity中定义的新方法startActivityByProxy和startActivityForResultByProxy,还有就是不支持LaunchMode。
生命周期管理
- 反射
- 接口
动态创建Activity
使用代理Activity有一些限制:
- 实际运行的Activity实例其实都是ProxyActivity,并不是真正想要启动的Activity;
- ProxyActivity只能指定一种LaunchMode,所以插件里的Activity无法自定义LaunchMode;
- 不支持静态注册的BroadcastReceiver;
- 往往不是所有的apk都可作为插件被加载,插件项目需要依赖特定的框架,还有需要遵循一定的”开发规范”;
解决对策就是,在需要启动插件的某一个Activity(比如PlugActivity)的时候,动态创建一个TargetActivity,新创建的TargetActivity会继承PlugActivity的所有共有行为,而这个TargetActivity的包名与类名刚好与我们事先注册的TargetActivity一致,我们就能以标准的方式启动这个Activity。
启动Activity是一个复杂的过程,有很多环节:Activity.startActivity()->Activity.startActivityForResult()->Instrument.excuteStartActivity()->ASM.startActivity()。大概又这么几个环节,详细了解可以参考文章:《深入理解Activity的启动过程》。 所谓“占坑”在宿主端的AndroidManifest.xml注册一个不存在的Activity,可以取名为StubActivity,同样启动插件的Activity都是启动StubActivity,然后在启动Activity的某个环节,我们找个“临时”演员来代替StubActivity,这个临时演员就是插件中定义的Activity,这叫“瞒天过海”。如何找“临时”演员?本章要讲的重点:使用 dexmaker 临时改造插件Activity。
插件化研究之dexmaker动态生成Activity
Android插件化学习之路(六)之动态创建Activity
HOOK Activity
在了解了Activity的启动流程之后,我们得知是mInstrumentation.newActivity()
创建的Activity。
我们要做的就是通过替换掉Instrumentation类,达到定制插件运行环境的目的。
简单的HOOK
- 替换Instrumentation类
// 先获取到当前的ActivityThread对象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
// 拿到原始的 mInstrumentation字段
Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
mInstrumentationField.setAccessible(true);
Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);
//如果没有注入过,就执行替换
if (!(mInstrumentation instanceof PluginInstrumentation)) {
PluginInstrumentation pluginInstrumentation = new PluginInstrumentation(mInstrumentation);
mInstrumentationField.set(currentActivityThread, pluginInstrumentation);
}
- 替换newActivity()
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
if (intent != null) {
isPlugin = intent.getBooleanExtra(PluginCons.FLAG_ACTIVITY_FROM_PLUGIN, false);
}
if (isPlugin && intent != null) {
className = intent.getStringExtra(PluginCons.FLAG_ACTIVITY_CLASS_NAME);
}
return super.newActivity(cl, className, intent);
}
HOOK:
8个类搞定插件化——Activity实现方案
Android 插件化原理解析——Activity生命周期管理
动态代理:
知识总结 插件化学习 Activity加载分析
Android插件化系列第(五)篇---Activity的插件化方案(代理模式)
Demo
各类开源库比较
特性 | DynamicLoadApk | DynamicAPK | Small | DroidPlugin | VirtualAPK |
---|---|---|---|---|---|
支持四大组件 | 只支持Activity和普通Service | 只支持Activity | 只支持Activity | 全支持 | 全支持 |
组件无需在宿主manifest中预注册 | √ | × | √ | √ | √ |
插件可以依赖宿主 | √ | √ | √ | × | √ |
支持PendingIntent | × | × | × | √ | √ |
Android特性支持 | 大部分 | 大部分 | 大部分 | 几乎全部 | 几乎全部 |
兼容性适配 | 一般 | 一般 | 中等 | 高 | 高 |
插件构建 | 无 | 部署aapt | Gradle插件 | 无 | Gradle插件 |
各类分析文章
插件化框架android-pluginmgr全解析
Dynamic-Load-Apk源码解析
Android 全面插件化 RePlugin 流程与源码解析
Android插件化快速入门与实例解析(VirtualApk)