插件化技术
- 一.概述
- 二.原理
- 三.好处
- 四.插件化涉及到的技术以及常用的插件化框架
- 五.详细说明
- 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中dexElements
DexClassLoader 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获得宿主dexElements
ClassLoader 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.最后为类加载器通过反射将新的数组设置回pathClassLoader
dexElementsField.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获得静态属性IActivityManagerSingleton
Class<?> 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对象进行代理动态代理:代理对象和被代理对象同一个接口IActivityManager
Class<?> 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;
}
@Override
public 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" ;
@Override
public boolean handleMessage(@NonNull Message msg) {
switch (msg.what){
// API 21 ~ 27 启动Activity的消息是LAUNCH_ACTIVITY
case 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_TRANSACTION
case 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?.resources
if(pluginResources != null){
return pluginResources
}
return super.getResources()
}
}