热修复和插件化

1 . 热修复

1.1 ClassLoader(双亲委派模机制)

某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。保证了只加载一次

1.2 分类

  • ClassLoader 顶层的 classloader
  • BootClassLoader
    由java代码实现而不是c++实现,是Android平台上所有ClassLoader的最终parent,这个内部类是包内可见,所以我们没法使用。
  • BaseDexClassLoader
    PathClassLoader和DexClassLoader都继承自BaseDexClassLoader,其主要逻辑都是在BaseDexClassLoader完成
  • PathClassLoader
    PathClassLoader是用来加载Android系统类和应用的类,并且不建议开发者使用
  • DexClassLoader
    支持加载APK、DEX和JAR,也可以从SD卡进行加载。 上面说dalvik不能直接识别jar,DexClassLoader却可以加载jar文件,这难道不矛盾吗?其实在BaseDexClassLoader里对".jar",".zip",".apk",".dex"后缀的文件最后都会生成一个对应的dex文件,所以最终处理的还是dex文件,而URLClassLoader并没有做类似的处理。 一般我们都是用这个DexClassLoader来作为动态加载的加载器

1.3 热修复开始(分dex包,多包)

  1. 一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找

  2. 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 写代码(摘于Android热补丁动态修复技术(二):实战)

利用反射把咱们的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 ;

@Override
protected 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 任玉刚想法
  1. 找到需要Hook方法的系统类

  2. 利用代理模式来代理系统类的运行拦截我们需要拦截的方法
    也就是代理acitivty 代理activity中去反射apk中activity的所有生命周期的方法,然后将activity的生命周期和代理activity的生命周期进行同步

  3. 使用反射的方法把这个系统类替换成你的代理类

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)

  1. 找到需要Hook方法的系统类
  2. DexClassLoader
  3. 提前占坑(欺上瞒下) VirtualAPK库中的清单文件中有很多activity,1个service 一个广播
  4. 广播是 动态转静态
2.3 开始简单撸代码地代表一下
  1. 写一个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);
     	}
     }
    复制代码
  2. 写一个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方法写,改改我们自己的要改的代码

  1. 在application中 attachBaseContext 方法中调起hook ,attachBaseContext比onCreate更早

     try {
     		Hooker.hookInstrumentation();
     	} catch (Exception e) {
     		e.printStackTrace();
     	}
    复制代码
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值