插件化技术
- 一.概述
- 二.原理
- 三.好处
- 四.插件化涉及到的技术以及常用的插件化框架
- 五.详细说明
- 1.第一个问题:类加载
- (1)Android 项目中,动态加载技术按照加载的可执行文件的不同大致可以分为两种:
- (2)在 Android 中的 ClassLoader 机制主要用来加载 dex 文件,系统提供了两个 API 可供选择:
- (3)插件化类加载原理
- (3)核心代码,未适配
- 2.第二个问题:组件生命周期管理
- (1)问题描述
- (2)hook技术
- (3)activity的启动过程如何偷梁换柱
- (4)hook AMS
- (5)hook Handler
- 3. 第三个问题:资源加载
- (1)资源加载思路解析
- (2)代码实现
一.概述
Android插件化技术,可以实现功能模块的按需加载和动态更新,其本质是动态加载未安装的apk。分为宿主apk和插件apk:
(1)所谓宿主,就是需要能提供运行环境,给资源调用提供上下文环境,一般也就是我们主 APK ,要运行的应用,它作为应用的主工程所在,实现了一套插件的加载和管理的框架,插件都是依托于宿主的APK而存在的。
(2)所谓插件,可以想象成每个独立的功能模块封装为一个小的 APK ,可以通过在线配置和更新实现插件 APK 在宿主 APK 中的上线和下线,以及动态更新等功能。
二.原理
插件化要解决的三个核心问题:类加载、组件生命周期管理、资源加载
三.好处
(1) 让用户不用重新安装APK 就能升级应用功能,减少发版本频率,增加用户体验。
(2) 提供一种快速修复线上 BUG 和更新的能力。
(3) 按需加载不同的模块,实现灵活的功能配置,减少服务器对旧版本接口兼容压力。
(4)模块化、解耦合、并行开发、 65535 问题。
四.插件化涉及到的技术以及常用的插件化框架
- 反射机制
- 类加载过程
- activity的启动流程
- 资源文件的加载流程
- hook技术
- 代理模式:动态代理静态代理
五.详细说明
1.第一个问题:类加载
(1)Android 项目中,动态加载技术按照加载的可执行文件的不同大致可以分为两种:
a。动态加载 .so库(c/c++通过jni技术调用)
b. 动态加载 dex/jar/apk文件(现在动态加载普遍说的是这种)
(2)在 Android 中的 ClassLoader 机制主要用来加载 dex 文件,系统提供了两个 API 可供选择:
a. PathClassLoader:只能加载已经安装到 Android 系统中的 APK 文件。因此不符合插件化的需求,不作考虑。
b. DexClassLoader:支持加载外部的 APK、Jar 或者 dex 文件,正好符合文件化的需求,所有的插件化方案都是使用 DexClassloader 来加载插件 APK 中的 .class文件的
(3)插件化类加载原理
①通过DexClassLoader加载插件apk中的文件,通过反射技术获得dexElements
②通过PathClassLoader获得已经加载宿主apk中的dexElements,同样利用反射
③将1、2步获得dexElements数组合并成,并将新数组通过反射为PathClassLoader的dexElements重新赋值
(3)核心代码,未适配
private void loadApk() throws NoSuchFieldException, IllegalAccessException {//0。插件apk位置以及缓存位置String pluginStr = mContext.getExternalFilesDir(null).getAbsolutePath()+"/pluginapp-debug.apk";String cache_plugin = mContext.getDir("cache_plugin", Context.MODE_PRIVATE).getAbsolutePath();//1:通过DexClassLoader获得插件apk中dexElementsDexClassLoader dexClassLoader = new DexClassLoader(pluginStr,cache_plugin,null,mContext.getClassLoader());Class<?> superclass = dexClassLoader.getClass().getSuperclass();Field pathListField = superclass.getDeclaredField("pathList");pathListField.setAccessible(true);Object pathListObject = pathListField.get(dexClassLoader);Field dexElementsField = pathListObject.getClass().getDeclaredField("dexElements");dexElementsField.setAccessible(true);Object dexElementsObject = dexElementsField.get(pathListObject);//2:通过pathClassLoader获得宿主dexElementsClassLoader pathClassLoader =mContext.getClassLoader();Object hostPathListObject = pathListField.get(pathClassLoader);Object hostDexElementsObject = dexElementsField.get(hostPathListObject);//3:合并int pluginLength = Array.getLength(dexElementsObject);int hostLength = Array.getLength(hostDexElementsObject);int new_dexElementsLength = pluginLength + hostLength;Object newDexElements = Array.newInstance(hostDexElementsObject.getClass().getComponentType(),hostLength + pluginLength);for (int i = 0; i < new_dexElementsLength; i++) {if (i < pluginLength) {Array.set(newDexElements, i, Array.get(dexElementsObject, i));} else {Array.set(newDexElements, i, Array.get(hostDexElementsObject, i - pluginLength));}}//4.最后为类加载器通过反射将新的数组设置回pathClassLoaderdexElementsField.set(hostPathListObject,newDexElements);}
2.第二个问题:组件生命周期管理
(1)问题描述
插件中有activity,通过第一步类已经加载进宿主app中,但是清单文件中未注册该activity。想要在宿主app的activity页面点击跳转到插件app中的activity页面,就会有问题,提示未注册该activity
(2)hook技术
钩子,勾住系统的程序逻辑,在某段SDK源码逻辑执行的过程中,通过代码手段(其实就是反射)拦截执行该逻辑,加入自己的代码逻辑。
(3)activity的启动过程如何偷梁换柱
activity1跳转到activity2过程中app和AMS交互2次
①第一次app和AMS通信,AMS会检查activity2是否在清单文件中注册,所以说我们要使用hook技术将要跳转的activity2换成已经在宿主清单文件中注册的RegisterActivity,越过AMS检查;通过hook AMS 实现
②第二次AMS和app通信,可以启动activity2,我们需要使用hook技术将activity2替换回去;通过hook handler 实现
(4)hook AMS
①思路源码解析
②核心代码(未进行适配)
/*** hook AMS对象* 对AMS对象的startActivity方法拦截*/public static void hookAms(Context context) throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException {//1.获得AMS对象//1.1获得静态属性IActivityManagerSingletonClass<?> activityManagerClass = Class.forName("android.app.ActivityManager");Field iActivityManagerSingletonField = activityManagerClass.getDeclaredField("IActivityManagerSingleton");iActivityManagerSingletonField.setAccessible(true);Object iActivityManagerSingletonObject = iActivityManagerSingletonField.get(null);//静态变量通过null直接获取//1.2获得Single的mInstance属性值Class<?> singletonClazz = Class.forName("android.util.Singleton");Field mInstanceField = singletonClazz.getDeclaredField("mInstance");mInstanceField.setAccessible(true);Object AMSSubject = mInstanceField.get(iActivityManagerSingletonObject);//这就是AMS对象//2.对AMS对象进行代理动态代理:代理对象和被代理对象同一个接口IActivityManagerClass<?> IActivityManagerInterface = Class.forName("android.app.IActivityManager");AMSInvocationHandler handler = new AMSInvocationHandler(context, AMSSubject);Object AMSProxy = Proxy.newProxyInstance(//动态代理,交给AMSInvocationHandler处理Thread.currentThread().getContextClassLoader(),new Class[]{IActivityManagerInterface},handler);mInstanceField.set(iActivityManagerSingletonObject,AMSProxy);//3.InvocationHandler对AMS对象的方法进行拦截}
动态代理拦截,此处偷梁换柱
/*** @Author : yaotianxue* @Time : On 2023/6/6 08:27* @Description : AMSInvocationHandler*/
public class AMSInvocationHandler implements InvocationHandler {private Context mContext;private Object subject;public AMSInvocationHandler(Context context, Object subject) {mContext = context;this.subject = subject;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {//拦截startActivity方法if("startActivity".equals(method.getName())) {Log.d("ytx","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("ytx","replaced startActivity intent");}}}return method.invoke(subject,args);}
}
(5)hook Handler
①思路源码解析
②代码实现
/*** 获得到handler特定消息中的Intent进行处理* 将Intent对象的RegisteredActivity替换成PluginActvity*/public static void hookHandler(Context context) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {//1。获取到handler对象Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");sCurrentActivityThreadField.setAccessible(true);Object activityThreadObject = sCurrentActivityThreadField.get(null);//静态直接给null即可Field mHField = activityThreadClass.getDeclaredField("mH");mHField.setAccessible(true);Object mHObject = mHField.get(activityThreadObject);//2.给handler的mCallback的属性进行赋值,静态代理实现Field mCallbackField = Handler.class.getDeclaredField("mCallback");mCallbackField.setAccessible(true);mCallbackField.set(mHObject,new HookHmCallback());//3.在callback中将Intent对象的RegisberedActivity替换成PluginActvity}
静态代理拦截,此处将插件activity换回去
/*** @Author : yaotianxue* @Time : On 2023/6/6 08:50* @Description : MyCallBack*/
public class HookHmCallback implements Handler.Callback {private static final int LAUNCH_ACTIVITY = 100;private static final int EXECUTE_TRANSACTION = 159;private static final String TAG = "ytx" ;@Overridepublic boolean handleMessage(@NonNull Message msg) {switch (msg.what){// API 21 ~ 27 启动Activity的消息是LAUNCH_ACTIVITYcase LAUNCH_ACTIVITY:Log.d(TAG,"HookHmCallback handleMessage LAUNCH_ACTIVITY enter !!!");// 消息对象是ActivityClientRecord对象,其中包含Intent// 获取intent对象Object intentObject = ReflectUtils.getFieldValue(msg.obj,"intent");if(intentObject instanceof Intent){Intent intent = (Intent) intentObject;// 将之前替换缓存下来的插件Intent替换回去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;// API 28 ~ 32,添加了事务管理,启动Activity的消息是EXECUTE_TRANSACTIONcase EXECUTE_TRANSACTION:Log.d(TAG,"HookHmCallback handleMessage EXECUTE_TRANSACTION enter !!!");// 启动Activity之中EXECUTE_TRANSACTION其中一条消息,需要找到属于启动Activity的那条消息// 消息对象是ClientTransaction对象,其中有ClientTransactionItem列表// 启动Activity的Item是LaunchActivityItem,其中包含Intent// 获取mActivityCallbacks,Item列表对象Object mActivityCallbacksObject = ReflectUtils.getFieldValue(msg.obj,"mActivityCallbacks");if(mActivityCallbacksObject instanceof List){List mActivityCallbacks = (List) mActivityCallbacksObject;// 循环列表for (Object callbackItem : mActivityCallbacks) {// 找到LaunchActivityItem对象if(TextUtils.equals(callbackItem.getClass().getName(),"android.app.servertransaction.LaunchActivityItem")){// 获取LaunchActivityItem的Intent对象Object mIntentObject = ReflectUtils.getFieldValue(callbackItem,"mIntent");if(mIntentObject instanceof Intent){Intent mIntent = (Intent) mIntentObject;// 将之前替换缓存下来的插件Intent替换回去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;}
}
3. 第三个问题:资源加载
(1)资源加载思路解析
a.独立运行时,宿主中只有自己资源
b.插件架构后,application中加载插件中的资源,插件activity中使用资源时通过application获得即可
(2)代码实现
①获得插件apk中的资源
/*** 获取插件的Resources* @return* @throws IllegalAccessException* @throws InstantiationException*/public Resources loadResources() throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {AssetManager assetManager = AssetManager.class.newInstance();Method addAssetPathMethod = AssetManager.class.getMethod("addAssetPath", String.class);addAssetPathMethod.setAccessible(true);String pluginStr = mContext.getExternalFilesDir(null).getAbsolutePath()+"/pluginapp-debug.apk";addAssetPathMethod.invoke(assetManager, pluginStr);return new Resources(assetManager,mContext.getResources().getDisplayMetrics(),mContext.getResources().getConfiguration());}
②application中获得插件的资源
class App: Application() {private lateinit var loadResources:Resources//插件的资源override fun onCreate() {super.onCreate()val pluginManager = PluginManager.getInstance(this).init()loadResources = pluginManager.loadResources()//获得插件的资源文件}fun getLoadResources():Resources{return if(loadResources == null){//如果是null 就返回宿主资源super.getResources()}else{//如果不是null 就返回插件资源loadResources}}
}
③插件apk中调用appliction的资源,找到对应的资源
open class BaseActivity:AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)}override fun getResources(): Resources {// 因为插件的全部Activity都继承于这个类,所以当Activity需要加载资源的时候,会访问这个getResources方法// 如果获取application的resources不为空// 如果当前app以插件形式在宿主中运行,那得到的便是宿主Application中的Resources对象// 又因为宿主的Application返回的是插件的Resources对象,所以最终加载的仍然是插件的资源// 如果当前app独立运行,那么得到的便是是自身的Application,那么返回的将是自身的Resources对象// 否则返回自身的Resources对象val pluginResources = application?.resourcesif(pluginResources != null){return pluginResources}return super.getResources()}
}