1:替换DexElements流程:
插件化原理:
插件生成apk,宿主通过反射机制和类加载器(传入插件apk),获取到插件的dexElements,并将dexElements合并到宿主的类加载器的dexElements,
这样插件所有的class都位于宿主的类加载器里面,达到宿主可以启动插件的目的。
启动插件普通类代码流程:
1)Plugin module:
public class Test {
public int add(int a, int b){
return a + b;
};
}
编译生成plugin.apk,放到sdk目录下
2)Host module:
public class LoadUtil {
private static final String apkpath = "/sdcard/plugin.apk";
public static void loadClass(Context context) {
//反射流程
//1)获取class
//2)获取class中我们需要的那个属性Filed
//3)Field.get(实例化对象),得到属性对应的那个实例
//4)通过以上方法分别获取host的dexElements对象和plugin的dexElements
//两层:classLoader得到pathList实例,pathList实例得到DexPathList实例
//BaseDexClassLoader->pathList->DexPathList
try {
// 获取DexPathList的class
Class<?> dexPathListClass = Class.forName("dalvik.system.DexPathList");
//获取DexPathList的dexElements属性
Field dexElementField = dexPathListClass.getDeclaredField("dexElements");
//将dexElements属性设置为public
dexElementField.setAccessible(true);
//获取BaseDexClassLoader的class
Class<?> classLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
//获取pathList属性
Field pathListField = classLoaderClass.getDeclaredField("pathList");
pathListField.setAccessible(true);
//获取数组的类加载器,get(实例化对象)可以获取到对象的值
//1.获取宿主的类加载器
ClassLoader pathClassLoader = context.getClassLoader();
//通过BaseClassLoader的实例化对象获取到pathList的实例化对象
Object hostPathList = pathListField.get(pathClassLoader);
//通过pathList的实例得到elements的对象
Object[] hostDexElements = (Object[]) dexElementField.get(hostPathList);
//2.插件
ClassLoader pluginClassLoader = new DexClassLoader(apkpath, context.getCacheDir().getAbsolutePath(), null,
pathClassLoader);
//通过BaseClassLoader的实例化对象获取到pathList的实例化对象
Object pluginPathList = pathListField.get(pluginClassLoader);
//通过pathList的实例得到elements的对象
Object[] pluginDexElements = (Object[]) dexElementField.get(pluginPathList);
//合并
//new Elements[]
Object[] newElements = (Object[]) Array.newInstance(hostDexElements.getClass().getComponentType(), hostDexElements.length+pluginDexElements.length);
System.arraycopy(hostDexElements, 0, newElements, 0, hostDexElements.length);
System.arraycopy(pluginDexElements, 0, newElements, hostDexElements.length, pluginDexElements.length);
//赋值到宿主的dexElements
//hostDexElements = newElemnts
dexElementField.set(hostPathList, newElements);
} catch (ClassNotFoundException | IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
}
Application启动:
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
LoadUtil.loadClass(this);
}
}
启动插件:
try {
Class<?> clazz = Class.forName("com.example.hotfixplugin.Test");
Method add = clazz.getMethod("add");
Object obj = add.invoke(clazz.newInstance(), 1, 2);
Log.d("test", obj.toString());
} catch (ClassNotFoundException | NoSuchMethodException | java.lang.InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
启动插件的Activity流程:
(启动插件的Activity和普通类最主要的区别是,启动Activity的时候,AMS会对Activity是否注册进行
校验,而正常情况下,我们宿主是没有注册插件的Activity的)
启动Activity流程:
这其中关系到两次的进程间通信:
1)应用调用AMS
系统的服务都是实现了Binder的服务端,应用进程要想和它通信需要获取它的代理端。
2)AMS到应用
在创建一个新的应用进程之后,系统首先会启动ActivityThread,ActivityThread是应用进程的主线程,在ActivityThread创建的时候会创建一个ApplicationThread的对象。这个ApplicationThread实现了一个Binder的服务端。新的进程创建完成之后通知AMS服务的之后同时把自己进程的ApplicationThread的代理端送给AMS服务。AMS服务中保存了所有应用进程的ApplicationThread的代理对象。所以AMS要想给应用进程发送消息,只需要得到目标应景进程的ApplicationThread的代理端对象即可。
滴滴插件化方案:https://github.com/didi/VirtualAPK
Activity:假设要启动插件中的Activity1,我们伪装一个Activity2骗过系统,预先注册在AndroidManifest.xml中,占个坑;
1)创建一个VasIinstruentation,通过反射机制和代理模式,替换掉系统中的Instrumentation,所有经过Instrumentation的操作都会到VasInstumentaion替代掉。
2)这时startActivity是在VasInstrumentation中执行,startActivity实际会调用到AMS中执行,因为AMS会对要启动的Activity1是否注册过进行校验。我们先保存Activity1的信息,然后告诉AMS我们要启动的是startActivity2(通过修改intent)。AMS看到启动的是Activity2,就通过校验。
3)AMS的作用:
a:对Activity的注册进行校验
b:栈的调度
c:AMS作为服务端,进行生命周期的管理,Client端的ActivityThread负责响应各个生命周期
4)AMS启动Activity2之后,根据上面流程图可知,最终会回到应用的mInstrumentation.newActivity(),newActivity通过类加载器生成实际上的Activity对象,我们的VasInstrumentation就可以对该方法进行重写,把原来实际要启动的Activity1的信息重新提取出来,替换掉当前的Activity2,生成Activity2对象,就完成了正常的Activiyt1启动。
4:查找Hook点的原则:
1)尽量静态变量或者单例对象:有利于反射和动态代理,反射的时候,如果不是静态的,就需要往前面找,直到可以得到一个类的对象为止。
2)尽量Hook public的对象和方法:谷歌提供给外面使用的,一般不会怎么修改。
5:hook代码:以下选择hookIActivityManager以及Handler callback:
package com.example.hotfixhost;
import android.content.Intent;
import android.os.Handler;
import android.os.Message;
import androidx.annotation.NonNull;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* @author yanjim
* @Date 2023/3/15
*/
public class HookUtil {
private static final String ORIGINAL_INTENT_INFO = "original_intent_info";
public static void hookAMS() {
//动态代理,替换IActivityManager
try {
//获取singleton对象
Class<?> clazz = Class.forName("android.app.ActivityManager");
Field iActivityManagerSingletonField = clazz.getDeclaredField("IActivityManagerSingleton");
Object singleton = iActivityManagerSingletonField.get(null);
//mInstance对象---》IActivityManager对象
Class<?> singletonClass = Class.forName("android.util.Singleton");
Field mInstanceField = singletonClass.getDeclaredField("mInstance");
Object mInstance = mInstanceField.get(singleton);
Class<?> iActivityManagerClass = Class.forName("android.app.IActivityManager");
Object mInstanceProxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[]{iActivityManagerClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("startActivity".equals(method.getName())) {
int index = 0;
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Intent) {
index = i;
break;
}
}
Intent intent = (Intent) args[index];
//启动代理的Intent
Intent intentProxy = new Intent();
//宿主定义的用于欺骗AMS的Activity类
intentProxy.setClassName("packagename", "className");
//将插件的intent信息保存起来,供后续重新拿出来使用
intentProxy.putExtra(ORIGINAL_INTENT_INFO, index);
args[index] = intentProxy;
}
//第一个参数,系统的IActivity对象
return method.invoke(mInstance, args);
}
});
//用代理对象替换掉系统的IActivityManager
mInstanceField.set(singleton, mInstanceProxy);
} catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
public static void hookHandler() {
try {
Class<?> clazz = Class.forName("android.app.ActivityThread");
Field sCurrentActivityThreadField = clazz.getDeclaredField("sCurrentActivityThread");
sCurrentActivityThreadField.setAccessible(true);
Object activityThread = sCurrentActivityThreadField.get(null);
//mh对象
Field mHField = clazz.getDeclaredField("mH");
mHField.setAccessible(true);
Handler mH = (Handler)mHField.get(activityThread);
Class<?> handlerClass = Class.forName("android.os.Handler");
Field mCallbackField = handlerClass.getDeclaredField("mCallback");
mCallbackField.setAccessible(true);
Handler.Callback callback = new Handler.Callback() {
@Override
public boolean handleMessage(@NonNull Message msg) {
switch (msg.what) {
case 100:
//拿到了message
//ActivityClientRecord的对象 msg.obj
try {
Field intentField = msg.obj.getClass().getDeclaredField("intent");
intentField.setAccessible(true);
//启动代理类
Intent intentProxy = (Intent) intentField.get(msg.obj);
Intent intent = intentProxy.getParcelableExtra(ORIGINAL_INTENT_INFO);
if (intent != null) {
intentField.set(msg.obj, intent);
}
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
break;
}
return false;
}
};
//系统的Callback替换成自己创建的callback对象
mCallbackField.set(mH, callback);
} catch (ClassNotFoundException | IllegalAccessException | NoSuchFieldException e) {
e.printStackTrace();
}
}
}
加载插件的资源:
1:rwa文件夹和assets文件夹的区别:
1)Android会自动为这个文件中的所有文件资源生成一个ID,可以容易访问,XML也可以访问,并且速度是最快的。
2)不生成ID,只能通过AsserManager访问,xml不能访问,范文速度慢,但是操作方便。
2:读取Asserts下的文件:
AssetManager assetManager = context.getAssets();
InputStream inputStream= assetManager.open(“filename”);
Resource类也是通过AssertManager来访问那些被编译过的资源文件的:
public String getString(@StringRes int id) throws NotFoundException {
return getText(id).toString();
}
@NonNull public CharSequence getText(@StringRes int id) throws NotFoundException {
CharSequence res = mResourcesImpl.getAssets().getResourceText(id);
if (res != null) {
return res;
}
throw new NotFoundException("String resource ID #0x"
+ Integer.toHexString(id));
}