1 . 热修复
1.1 ClassLoader(双亲委派模机制)
某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。保证了只加载一次
1.2 分类
- ClassLoader 顶层的 classloader
- BootClassLoader 由java代码实现而不是c++实现,是Android平台上所有ClassLoader的最终parent,这个内部类是包内可见,所以我们没法使用。
- BaseDexClassLoader PathClassLoader和DexClassLoader都继承自BaseDexClassLoader,其主要逻辑都是在BaseDexClassLoader完成
- PathClassLoaderPathClassLoader是用来加载Android系统类和应用的类,并且不建议开发者使用
- DexClassLoader支持加载APK、DEX和JAR,也可以从SD卡进行加载。 上面说dalvik不能直接识别jar,DexClassLoader却可以加载jar文件,这难道不矛盾吗?其实在BaseDexClassLoader里对".jar",".zip",".apk",".dex"后缀的文件最后都会生成一个对应的dex文件,所以最终处理的还是dex文件,而URLClassLoader并没有做类似的处理。 一般我们都是用这个DexClassLoader来作为动态加载的加载器
1.3 热修复开始(分dex包,多包)
-
一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找
-
BaseDexClassLoader 中有个 pathList 对象,pathList 中包含一个 DexFile 的集合 dexElements,而对于类加载呢, 就是遍历这个集合,通过DexFile去寻找,(其实寻找类无非就是根据name全限定名来加载的),我们可以利用反射 看下BaseDexClassLoader源码
//所有的 private final DexPathList pathList; @Override protected Class findClass(String name) throws ClassNotFoundException { Class clazz = pathList.findClass(name); if (clazz == null) { throw new ClassNotFoundException(name); } return clazz; } #DexPathList public Class findClass(String name) { for (Element element : dexElements) { DexFile dex = element.dexFile; if (dex != null) { Class clazz = dex.loadClassBinaryName(name, definingContext); if (clazz != null) { return clazz; } } } return null; }复制代码
1.4 写代码(摘于)
利用反射把咱们的dex或者是jar或者是apk里面的 dexElements 插入到原有的之前,就是利用反射 把 插件的dexElements+以前的dexElements 重新赋值给 pathList
private void inject(String path) { try { // 1 . 拿到现有的dexElements // 通过反射获取classes的dexElements Class cl = Class.forName("dalvik.system.BaseDexClassLoader"); // 拿到BaseDexClassLoader中的pathList属性 Object pathList = getField(cl, "pathList", getClassLoader()); // 拿到BaseDexClassLoader中的pathList中dexElements集合 属性 Object baseElements = getField(pathList.getClass(), "dexElements", pathList); // 2 拿补丁包的 dexElements // 获取patch_dex的dexElements(需要先加载dex) String dexopt = getDir("path").getAbsolutePath(); // 把path给 DexClassLoader 让他生成 dexElements DexClassLoader dexClassLoader = new DexClassLoader(path, dexopt, dexopt, getClassLoader()); //拿到 补丁包中的 pathList 属性 Object obj = getField(cl, "pathList", dexClassLoader); //拿到 补丁包中的 dexElements 属性 Object dexElements = getField(obj.getClass(), "dexElements", obj); // 3. 合并 // 合并两个Elements Object combineElements = combineArray(dexElements, baseElements); //4. 再重新复制给classLoader // 将合并后的Element数组重新赋值给app的classLoader setField(pathList.getClass(), "dexElements", pathList, combineElements); //======== 以下是测试是否成功注入 ================= Object object = getField(pathList.getClass(), "dexElements", pathList); int length = Array.getLength(object); //如果length == 2, 证明已经把咱们的放进去了 Log.e("BugFixApplication", "length = " + length); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); }}复制代码
1.5 调用
再 Application中的attachBaseContext中调用 attachBaseContext优先于onCreate public class MyApplication extends Application { private static MyApplication INSTANCE ;
@Overrideprotected void attachBaseContext(Context base) { super.attachBaseContext(base); inject("插件jar的目录")}复制代码
1.6 其次还是不行
- 原因: 在apk安装的时候,虚拟机会将dex优化成odex后才拿去执行。在这个过程中会对所有class一个校验。 校验方式:假设A该类在它的static方法,private方法,构造函数,override方法中直接引用到B类。如果A类和B类在同一个dex中,那么A类就会被打上CLASS_ISPREVERIFIED标记 被打上这个标记的类不能引用其他dex中的类,否则就会报图中的错误
- 解决办法(ASM和javaassist,在每一个构造方法中引入别的dex文件中的类) A类如果还引用了一个C类,而C类在其他dex中,那么A类并不会被打上标记。换句话说,只要在static方法,构造方法,private方法,override方法中直接引用了其他dex中的类,那么这个类就不会被打上CLASS_ISPREVERIFIED标记。
2 插件化
2.1 在一个大的项目里面,为了明确的分工,往往不同的团队负责不同的插件APP,这样分工更加明确
2.2 技术各不同大致的意思
2.2.1 任玉刚想法
-
找到需要Hook方法的系统类
-
利用代理模式来代理系统类的运行拦截我们需要拦截的方法
也就是代理acitivty 代理activity中去反射apk中activity的所有生命周期的方法,然后将activity的生命周期和代理activity的生命周期进行同步 -
使用反射的方法把这个系统类替换成你的代理类
2.2.2 VirtualAPK
先看startActivity的源码,Instrumentation(ɪnstrəmenˈteɪʃn)的execStartActivity方法,然后再通过ActivityManagerProxy与AMS进行交互,之后ams通过binder技术ApplicationThread的scheduleLaunchActivity方法 ,其内部会调用mH类的sendMessage方法,传递的标识为H.LAUNCH_ACTIVITY,进入调用到ActivityThread的handleLaunchActivity方法->ActivityThread#handleLaunchActivity->mInstrumentation.newActivity(最后还是调用到咱们的Instrumentation中) 其实在newActivity中 就是 把传过来的 intent中的acitivty ,或者说是class文件, 从咱们插件中 拿出来(DexClassLoader)
- 找到需要Hook方法的系统类
- DexClassLoader
- 提前占坑(欺上瞒下) VirtualAPK库中的清单文件中有很多activity,1个service 一个广播
- 广播是 动态转静态
2.3 开始简单撸代码地代表一下
-
写一个hooker类 让他去hook Instrumentation类 或者是 ActivityManagerProxy 下面hook的是 Instrumentation类
public class Hooker { private static final String TAG = "Hooker"; public static void hookInstrumentation() throws Exception { // 反射拿到ActivityThread类 Class activityThread = Class.forName("android.app.ActivityThread"); //获取ActivityThread 对象 有两种方法 //拿的过程是首先拿到ActivityThread,由于ActivityThread可以通过静态变量sCurrentActivityThread //或者静态方法currentActivityThread()获取,所以拿到其对象相当轻松 Method sCurrentActivityThread = activityThread.getDeclaredMethod("currentActivityThread"); // 下面这句话是 让它可以通过反射拿到(有的属性时private的话 是不允许拿到的) sCurrentActivityThread.setAccessible(true); //获取ActivityThread 对象 Object activityThreadObject = sCurrentActivityThread.invoke(activityThread); //获取 Instrumentation 对象 Field mInstrumentation = activityThread.getDeclaredField("mInstrumentation"); mInstrumentation.setAccessible(true); // 强转成Instrumentation Instrumentation instrumentation = (Instrumentation) mInstrumentation.get(activityThreadObject); // 把自己的Instrumentation类给设置进去 HookInstrumentation hookInstrumentation = new HookInstrumentation(instrumentation); //将我们的 hookInstrumentation 设置进去 mInstrumentation.set(activityThreadObject, hookInstrumentation); } }复制代码
-
写一个HookInstrumentation 继承 Instrumentation 这个只是简单的把打开com.nzy.myeventbus.MainActivity的activity 换成了com.nzy.myeventbus.ThreeActivity public class MyInstrumentation extends Instrumentation {
private MyInstrumentation base; public MyInstrumentation(Instrumentation base) { this.base = base; } @Override public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException { Log.e("TAG", "invoked MyInstrumentation#newActivity, " + "class name =" + className + ", intent = " + intent); if ("com.nzy.myeventbus.MainActivity".equals(className)) { className= "com.nzy.myeventbus.ThreeActivity"; } return super.newActivity(cl,className , intent); } }复制代码
注意 : 这只是hook了newActivity方法 这个方法可以重写,但是Instrumentation中像execStartActivity是隐藏的不能被重写, 可以在MyInstrumentation 完全按照 Instrumentation 中 execStartActivity方法写,改改我们自己的要改的代码
-
在application中 attachBaseContext 方法中调起hook ,attachBaseContext比onCreate更早
try { Hooker.hookInstrumentation(); } catch (Exception e) { e.printStackTrace(); }复制代码