插件化的理念
将应用分为多个模块,分出宿主与插件
用户安装宿主,动态加载插件
插件化的优点
按需加载、可插拔、动态更新
减小apk体积,解决方法是超过65535的问题
插件分开开发与编译,提高效率,降低耦合度
插件化的缺点
提升项目复杂度
插件化框架
特性 | dynamic-load-apk | DynamicApk | Small | DroidPlugin | VirtualAPK |
---|
作者 | 任玉刚 | 携程 | wequick | 360 | 滴滴 | 四大组件支持 | 只支持Activity | 只支持Activity | 只支持Activity | 全支持 | 全支持 | 插件无需在清单文件预注册 | √ | x | √ | √ | √ | 插件可以依赖宿主 | √ | √ | √ | x | √ | 支持PendingIntent | x | x | x | √ | √ | Android特性支持 | 大部分 | 大部分 | 大部分 | 几乎全部 | 几乎全部 | 兼容性适配 | 一般 | 一般 | 中等 | 高 | 高 | 插件构建 | 无 | 部署AAPT | Gradle插件 | 无 | Gradle插件 |
插件的架构思路
Demo地址
如何加载插件?
通过Android的类加载机制,和几个ClassLoader分别的作用可知,想要加载插件,就需要将插件项目打成dex或apk,然后根据DexClassLoader指定插件路径,即可加载
apk是一个完整的Android包,其中包含了dex代码部分,res资源部分,AndroidManifest清单部分,作为插件时,其资源也可以被用到
dex是一个纯代码的包,有多个class文件组合而成
dex可作为热修复,apk可以作为完整的插件使用,这里讲的是插件的使用,就以apk为例
独立声明一个插件工程,在其中编写代码,然后编译成apk,其过程和正常apk工程一样
将apk放入手机存储,然后通过DexClassLoader加载,此时,可以调用没有使用的资源的类的方法
插件中的类
public class PluginTest {
public static void doSomeThing(){
Log.d("hahaha", "PluginTest doSomeThing ");
}
}
拷贝插件到私有目录,并创建插件的类加载器
private boolean copyPlugin() {
String pluginName = "plugin.apk";
mPluginPath = mContext.getExternalFilesDir(null).getAbsolutePath() + File.separator + pluginName;
String targetPath = mContext.getDir(PLUGIN_CACHE_DIR_NAME,Context.MODE_PRIVATE).getAbsolutePath();
if(!new File(mPluginPath).exists()){
Log.e(TAG,"plugin not exists !!!");
return false;
}
File targetFile = new File(targetPath);
if(targetFile.exists()){
targetFile.delete();
}
if(!targetFile.getParentFile().exists()){
targetFile.getParentFile().mkdirs();
}
try {
FileInputStream in = new FileInputStream(mPluginPath);
FileOutputStream out = new FileOutputStream(targetPath);
FileUtil.copy(in, out);
}catch (Exception e){
Log.e(TAG,"plugin copy failed !!!");
e.printStackTrace();
return false;
}
mPluginClassloader = new DexClassLoader(targetPath,PLUGIN_UNCOMPRESS_DIR,null,mContext.getClassLoader());
return true;
}
通过插件的DexClassLoader,执行插件中的方法
val pluginTestClass = PluginManager.get(this).pluginClassloader?.loadClass("com.ls.pluginapp.PluginTest")?:return
ReflectUtils.reflectStaticMethod(pluginTestClass,"doSomeThing")
不使用DexClassLoader加载插件类
如果只是反射几个类方法,可以使用插件的类加载器执行,但是一个插件中有很多类,而且还有组件,不可能全用插件的ClassLoader.loadClass
参考Android的类加载逻辑可知,ClassLoader的loadClass方法,加载类是在pathList属性中的dexElements数组中查找类的
根据这个,我们可以通过反射的方式,将插件DexClassLoader中的pathList.dexElements数组,复制到宿主APK的PathClassLoader的pathList.dexElements中去
这样,PathClassLoader在查找类的时候,也就可以找到插件中的类了,而不需要借助DexClassLoader去加载了
热修复的思路也是如此,将需要热修复的代码打成dex包,然后把dex包下载到本地,通过DexClassLoader解析,然后将DexClassLoader中的pathList.dexElements数组中的元素,复制到PathClassLoader中的pathList.dexElements数组的最前面,这样,PathClassLoader在寻找类的时候,会先找到最前面的类,也就是热修复过的类进行加载,而在后面的原有类就被忽略掉了
合并dexElements的方法
private boolean dexMerge(){
if(mPluginClassloader == null){
return false;
}
String pathList = "pathList";
Object systemPathList = ReflectUtils.getFieldValue(mContext.getClassLoader(),pathList);
Object pluginPathList = ReflectUtils.getFieldValue(mPluginClassloader,pathList);
String dexElements = "dexElements";
Object systemDexElements = ReflectUtils.getFieldValue(systemPathList,dexElements);
Object pluginDexElements = ReflectUtils.getFieldValue(pluginPathList,dexElements);
if(systemDexElements == null || pluginDexElements == null){
Log.e(TAG,"dexElements not found, systemDexElements = " + systemDexElements + ", pluginDexElements = " + pluginDexElements);
return false;
}
Object newElements = combineArray(pluginDexElements,systemDexElements);
if(newElements == null){
Log.e(TAG,"dexMerge failed !!!");
return false;
}
if(!ReflectUtils.setField(systemPathList,dexElements,newElements)){
Log.e(TAG,"new dexElements set failed !!!");
return false;
}
return true;
}
直接在PathClassLoader中调用插件方法
val pluginTestClass = classLoader?.loadClass("com.ls.pluginapp.PluginTest")?:return
ReflectUtils.reflectStaticMethod(pluginTestClass,"doSomeThing")
如何启动组件?
Activity的启动时通过请求AMS检查通过之后,然后再启动并调用器生命周期
未在AMS中注册的Activity是无法通过检查的,也就是没有在AndroidManifest.xml中注册的Activity,无法启动
而插件中的组件是没有在宿主的AndroidManifest.xml中注册的,也就是无法正常启动组件
而根据Activity启动逻辑可以得出如下思路,来绕过AMS检查,从而启动Activity
启动思路
因为Activity在启动前会请求AMS检查,那么可以在AMS检查前将包含插件Activity的Intent替换为已经在宿主清单文件中注册过的Activity的Intent
待AMS检查完毕之后,在Activity真正启动之前,再将包含注册过的Activity的Intent替换会包含插件Activity的Intent
如此狸猫换太子,插件的Activity就得以正常启动了
第一次替换
根据AMS跨进程通讯源码可知,在启动Activity之前,会将Intent传给ActivityManager的一个静态属性ActivityManagerService单例对象的startActivity方法
那么通过动态代理将其单例对象的方法执行代理过来,然后在startActivity方法执行之前,将其Intent参数,替换为已经注册过的Activity的Intent,然后交给AMS去检查
第一次替换的版本适配
ActivityManager的代码在API26的时候改版过,所以,动态代理时,其属性名有所变动
通过源码对比可知,其改变了获取单例的静态方法与静态属性的名称,而类型没有改变,所以我们只需要将反射的名称改变即可
API29之后,启动Activity就不是调用ActivityManager了,而是独立出了一个ActivityTaskManager来管理Activity
private boolean hookAMS(){
if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.P){
Log.e(TAG,"api <= 25,start hook AMS !!!");
Object singleton = null;
if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1){
Class<?> clazz = ReflectUtils.getClass("android.app.ActivityManagerNative");
Object gDefault = ReflectUtils.getStaticFieldValue(clazz,"gDefault");
singleton = gDefault;
if(singleton == null){
Log.e(TAG,"api <= 25,get gDefault failed !!!");
return false;
}
}
else{
Log.e(TAG,"api <= 28,start hook AMS !!!");
Object IActivityManagerSingleton = ReflectUtils.getStaticFieldValue(ActivityManager.class,"IActivityManagerSingleton");
singleton = IActivityManagerSingleton;
if(singleton == null){
Log.e(TAG,"api <= 28,get IActivityManagerSingleton failed !!!");
return false;
}
}
Object AMSObject = ReflectUtils.getFieldValue(singleton,"mInstance");
Class<?> IActivityManagerClazz = ReflectUtils.getClass("android.app.IActivityManager");
Object AMSProxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{IActivityManagerClazz}, (proxy, method, args) -> {
if("startActivity".equals(method.getName())) {
Log.d(TAG,"Proxy IActivityManager startActivity invoke...");
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Intent) {
Intent intent = new Intent();
intent.setClass(mContext, RegisteredActivity.class);
intent.putExtra("actionIntent", (Intent) args[i]);
args[i] = intent;
Log.d(TAG,"replaced startActivity intent");
}
}
}
return method.invoke(AMSObject,args);
});
boolean success = ReflectUtils.setField(singleton,"mInstance",AMSProxy);
if(!success){
Log.e(TAG,"api <= 28,AMS hook failed !!!");
}
return success;
}
else{
Log.e(TAG,"api <= 32,start hook A(T)MS !!!");
Class<?> clazz = ReflectUtils.getClass("android.app.ActivityTaskManager");
Object IActivityTaskManagerSingleton = ReflectUtils.getStaticFieldValue(clazz,"IActivityTaskManagerSingleton");
Object AMSObject = ReflectUtils.getFieldValue(IActivityTaskManagerSingleton,"mInstance");
Class<?> IActivityTaskManagerClazz = ReflectUtils.getClass("android.app.IActivityTaskManager");
Object AMSProxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{IActivityTaskManagerClazz}, (proxy, method, args) -> {
if("startActivity".equals(method.getName())) {
Log.d(TAG,"Proxy IActivityTaskManager startActivity invoke...");
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Intent) {
Intent intent = new Intent();
intent.setClass(mContext, RegisteredActivity.class);
intent.putExtra("actionIntent", (Intent) args[i]);
args[i] = intent;
Log.d(TAG,"replaced startActivity intent");
}
}
}
return method.invoke(AMSObject,args);
});
boolean success = ReflectUtils.setField(IActivityTaskManagerSingleton,"mInstance",AMSProxy);
if(!success){
Log.e(TAG,"api > 28,AMS hook failed !!!");
}
return success;
}
}
第二次替换
通过AMS的第二次跨进程通讯可知,在AMS检查完成之后,是在Binder线程,需要通过Handler发送消息到UI线程去启动Activity,而Handler执行机制会先判断并执行成员属性mCallback
这里将Handler的mCallback赋值为自己写的Callback,在其中处理LAUNCHER_ACTIVITY消息,将Intent替换回包含插件Activity的Intent,让系统去启动
如此,一次插件Activity的启动就完成了
第二次替换的版本适配
ActicityThread的源码造API28是经过一次改变,所以其发送Handler消息的方式和消息ID都改变了
通过源码对比可知,其消息名称从LAUNCHER_ACTIVITY变成了EXECUTE_TRANSACTION,而且因为添加了事务的原因,其msg.obj不再是简单的ActivityClientRecord对象,而是ClientTransaction对象,而Intent就会放在ClientTransaction中的callbacks集合中的某一个Item中,而callbacks集合是一个ClientTransactionItem集合,其中的LauncherActivityItem是ClientTransaction的子类,也是启动Activity的Item,Intent就在其中
此时通过遍历ClientTransaction的callbacks,找到LauncherActivityItem,将其中的Intent替换回包含插件Activity的Intent,让系统去启动
private boolean hookHandler(){
Class<?> activityThreadClazz = ReflectUtils.getClass("android.app.ActivityThread");
Object activityThreadObject = ReflectUtils.getStaticFieldValue(activityThreadClazz,"sCurrentActivityThread");
Object mHObject = ReflectUtils.getFieldValue(activityThreadObject,"mH");
boolean success = ReflectUtils.setField(mHObject,"mCallback",new HookHmCallback());
if(!success){
Log.e(TAG,"api > 28,Handler hook failed !!!");
}
return success;
}
private class HookHmCallback implements Handler.Callback {
private static final int LAUNCH_ACTIVITY = 100;
private static final int EXECUTE_TRANSACTION = 159;
@Override
public boolean handleMessage(@NonNull Message msg) {
switch (msg.what){
case LAUNCH_ACTIVITY:
Log.d(TAG,"HookHmCallback handleMessage LAUNCH_ACTIVITY enter !!!");
Object intentObject = ReflectUtils.getFieldValue(msg.obj,"intent");
if(intentObject instanceof Intent){
Intent intent = (Intent) intentObject;
Parcelable actionIntent = intent.getParcelableExtra("actionIntent");
if(actionIntent != null){
boolean success = ReflectUtils.setField(msg.obj,"intent",actionIntent);
if(success){
Log.d(TAG,"HookHmCallback handleMessage LAUNCH_ACTIVITY replaced !!!");
}
}
}
break;
case EXECUTE_TRANSACTION:
Log.d(TAG,"HookHmCallback handleMessage EXECUTE_TRANSACTION enter !!!");
Object mActivityCallbacksObject = ReflectUtils.getFieldValue(msg.obj,"mActivityCallbacks");
if(mActivityCallbacksObject instanceof List){
List mActivityCallbacks = (List) mActivityCallbacksObject;
for (Object callbackItem : mActivityCallbacks) {
if(TextUtils.equals(callbackItem.getClass().getName(),"android.app.servertransaction.LaunchActivityItem")){
Object mIntentObject = ReflectUtils.getFieldValue(callbackItem,"mIntent");
if(mIntentObject instanceof Intent){
Intent mIntent = (Intent) mIntentObject;
Parcelable actionIntent = mIntent.getParcelableExtra("actionIntent");
if(actionIntent != null){
boolean success = ReflectUtils.setField(callbackItem,"mIntent",actionIntent);
if(success){
Log.d(TAG,"HookHmCallback handleMessage EXECUTE_TRANSACTION replaced !!!");
}
}
}
}
}
}
break;
}
return false;
}
}
如何加载插件资源?
资源的加载是通过Resource对象完成的,获取Resource对象可以直接使用Context的getResource方法即可,而如此获取的Resource对象是宿主的Resource,也只能加载宿主apk中的资源
通过资源加载流程可知,Resource对象的创建传入了AssetManager对象,和一些手机配置,手机配置无可厚非,直接使用Context获取就好了
而AssetManager是管理资源的类,其中有个方法addAssetPath,是添加资源路径的,那么想要加载插件资源,只需要创建一个新的AssetManager对象,将插件的资源路径传入addAssetPath方法,然后根据新的AssetManager对象创建新的Resource对象,作为插件的资源加载入口,即可完成资源的加载
而因为插件有时也可以独立运行,所以其Resource对象不能只用新创建的,需要根据场景使用系统的Resource或新创建的Resource
其实,在宿主的Application中新创建一个Resource,作为插件的Resource,然后在插件中使用时使用getApplication().getResource(),作为资源加载入口
那么此时,如果插件独立运行,其Application就不是宿主的Application,获取到的Resource对象自然就是系统的Resource
而作为插件运行时,其Application就是宿主的Application,其Resource是通过插件路径新创建的Resource
如此,在不同场景使用都不会有问题了
创建插件的Resources对象
private boolean createPluginResource() {
Log.e(TAG, "createPluginResource: enter !!!");
try {
AssetManager pluginAssetManager = AssetManager.class.newInstance();
boolean success = ReflectUtils.reflectMethod(pluginAssetManager,"addAssetPath",mPluginPath);
if(!success){
Log.e(TAG, "createPluginResource: addAssetPath failed !!!");
}
mPluginResources = new Resources(pluginAssetManager,mContext.getResources().getDisplayMetrics(),mContext.getResources().getConfiguration());
return success;
} catch (Exception e) {
Log.e(TAG, "createPluginResource: AssetManager instantiation failed !!!");
e.printStackTrace();
return false;
}
}
重写宿主Application的getResources方法
@Override
public Resources getResources() {
Resources pluginResources = PluginManager.get(getApplicationContext()).getPluginResources();
if(pluginResources != null){
return pluginResources;
}
return super.getResources();
}
插件中写BasePluginActivity,所有的Activity都继承它,然后重写它的getResources方法
abstract class BasePluginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun getResources(): Resources {
val pluginResources = application?.resources
if(pluginResources != null){
return pluginResources
}
return super.getResources()
}
}
Android类加载器
BootClassLoader :系统启动时用于加载系统常用类,ClassLoader内部类
PathClassLoader :加载系统类和已安装的应用程序类
DexClassLoader :在未安装的情况下加载dex文件及包含dex的apk或jar,热修复和插件化的基础
双亲委托机制
对于类加载的过程,使用到了双亲委托机制
- 可以很好地避免重复加载
- 可以提高类使用的安全性,将不同的类交给不同的ClassLoader
类加载逻辑
类的加载首先是通过双亲委托机制找到可以加载类的ClassLoader
然后ClassLoader会先找缓存,如果没有,就会调用DexPathList类型的dexPathList属性的findClass方法
而DexPathList的findClass方法,实质上就是在一个Element[] dexElement数组中查找对应的类,然后返回
Activity启动逻辑
两次跨进程访问
Activity的启动,考虑到安全性的问题,需要请求系统的AMS(ActivityManagerService)进行检查,如果传入的Activity没有在apk的清单文件中注册过,那么apk安装之后,AMS便无法解析到其Activity的信息,在检查时,自然就是无法通过的
因为AMS是系统进程,其他apk进程想要跨进程访问,需要通过Binder机制,也就是跨进程内存拷贝机制,而AMS检查完成之后需要通知apk进程,也是跨进程通讯,也需要用到Binder机制,所以是两次跨进程通讯
第一次,检查Activity跨进程
第一次,启动Activity,通过startActivity方法,其方法最终调到ActivityManager中,然后根据其中的AMS静态属性,访问AMS进程,并调用器startActivity方法
第二次,传回Intent跨进程
第二次,在AMS检查Activity完成之后,想要通知apk,就需要通过Binder,而在Binder线程中无法访问UI,所以会通过ActivityThread中的mH属性,也就是Handler对象发送消息,回调到apk的UI线程,然后去启动Activity,并调用其生命周期
在API28是,系统源码经过了改版,添加了事务,在代码层面,其调用复杂度成倍正价,不过其原理是万变不离其宗
API28以前
API28及以后
资源加载过程
在Activity启动过程中,ActivityThread在启动Activity之前,会为其创建对应的Context对象,而Context中自然也就带有Resource对象,Resource对象的创建,需要AssetManager作为参数,AssetManager是管理apk资源的类,其中有addAssetPath方法可以添加资源所在路径
AssetManger根据资源路径创建完成,Resource根据AssetManager创建完成,Context也就创建完成,Activity正式启动,调用onCreate方法
面试题
|