插件化开发详解
1:替换DexElements流程:
插件化原理:https://www.cnblogs.com/wnpp/p/16053088.html
插件生成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(); }
在创建一个新的应用进程之后,系统首先会启动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)); }