飞道的博客

9012年都过去了,你确定还不学安卓的热修复?(手写AndFix)

359人阅读  评论(0)

背景介绍

热修复,乍一听,感觉好牛逼的样子,实际上并没有多么神秘,为什么这样说呢?且听我娓娓道来。。。

你发布了一款安卓应用,早上刚发版,结果发完之后发现有个bug没有修复,会直接导致整个应用崩溃,这时候你该怎么办呢?难道再马上重新打包发版吗?显然是不现实的,那么这时候热修复就来了,帮你打上一个补丁(没错,我认为热修复就像给衣服打补丁。。。),然后在你应用启动的时候直接进行修补,这样就可以不用发版了啊。

听上去感觉有点懵,怎么打补丁,应用怎么提前知道我哪里代码出问题了?什么是热修复?这都是啥?我是谁?我在哪???

不着急,咱们慢慢来,先来看一个目前来说整个市场上的热修复方案的特性吧。

预热

上面啰嗦了一大堆,其实最重要的就是上面这张图,这张图也比较老了,现在都Android 10 了。。。我还弄的7的图。。。将就看吧,意思能表达清楚就行。

目前的热修复大致分为两个方案:一种是native层的,代表的是阿里的AndFix(停更好几年)和Sophix(不开源),另外一种就是java层的,代表的是腾讯的Tinker(开源)。今天准备模仿的是阿里的AndFix。

既然要模仿AndFix,那么就来说一下AndFix的优势吧:首先它打出的修复包要比Tinker打出的小很多(精确到方法),其次它的性能消耗代价要小,最重要的是:它及时生效,无需退出应用重新进入即可修复。

我们都知道:Java方法的执行一定有相应的入口(包括普通执行,亦或通过反射执行)。那么可以思考一下AndFix是怎样工作的?安卓中Java文件编译成class后会打成dex包,方法即存在于dex包中。dex包是在虚拟机中执行的,虚拟机是c/c++编写的,虚拟机在执行方法时在安卓源码中存在着成员变量表和方法表,而方法表中存在着一个结构体,我们的方法都是由这个结构体来保存执行的,这个结构体就是ArtMethod。那么我们需要做的就是:在native层进行方法的替换,将错误的方法替换为正确的方法即可。

当然,虚拟机在安卓4.4以下和5.0以上有了翻天覆地的变化,在4.4及以前,虚拟机为Davik,它采用的是JIT(即时编译);5.0以上虚拟机为Art,采用的是AOT(预编译)。两者区别就是Art安装应用时慢,加载快,Davik安装应用快,加载慢。(细心的肯定发现了安卓4.4及以前的安卓版本安装应用要比现在快很多)。但是今天不考虑Davik,因为现在的手机基本没有4.4及以下的版本了,就不做适配了。这里还要说的是,AndFix热修复基于的是安卓源码中的结构体(art_method.h),所以说国内某些厂商对安卓系统进行魔改了,有可能修复失败;还有就是每一个版本的安卓系统中的源码都不同,需要适配来进行解决,否则会修复失败。

开始编码

我也没想到我能写出上面那么多字,好了,终于到了编码的时候了。来新建一个c++的项目:

直接选择这个:

咱们先来模仿一个崩溃,直接抛出异常:


  
  1. /**
  2. * @ProjectName: Andfix
  3. * @Package: com.zj.andfix
  4. * @Author: jiang zhu
  5. * @Date: 2020/1/2 21:25
  6. */
  7. public class Caclutor {
  8. public void test(Context context){
  9. throw new RuntimeException( "报错了");
  10. }
  11. }

在MainActivity中进行调用,模仿现实中的崩溃:


  
  1. public void test(View view) {
  2. Caclutor caclutor = new Caclutor();
  3. caclutor.test( this);
  4. }

再来模仿写一个解决完bug的类:


  
  1. /**
  2. * @ProjectName: Andfix
  3. * @Package: com.zj.andfix
  4. * @Author: jiang zhu
  5. * @Date: 2020/1/2 21:25
  6. */
  7. public class Caclutor {
  8. public void test(Context context){
  9. //throw new RuntimeException("报错了");
  10. Toast.makeText(context, "修复成功了", Toast.LENGTH_SHORT).show();
  11. }
  12. }

接下来要写一个注解,我们要获取到是哪个类和哪个方法出了问题:


  
  1. /**
  2. * @ProjectName: Andfix
  3. * @Package: com.zj.andfix
  4. * @Author: jiang zhu
  5. * @Date: 2020/1/2 21:18
  6. */
  7. @Retention(RetentionPolicy.RUNTIME)
  8. @Target(ElementType.METHOD)
  9. public @interface Replace {
  10. //类的全限定名
  11. String path();
  12. //方法名
  13. String method();
  14. }

写好注解之后在修复类中加上注解:


  
  1. @Replace(path = "com.zj.andfix.Caclutor",method = "test")
  2. public void test(Context context){
  3. //throw new RuntimeException("报错了");
  4. Toast.makeText(context, "修复成功了", Toast.LENGTH_SHORT).show();
  5. }

接下来就到了最重要的一步,打出修复包,咱们先把错误的代码打一个apk包(release),然后再把修复好的代码打一个aok包。咱们需要打的是一个dex文件,需要使用到安卓sdk中的工具,进入你的sdk/build-tools/版本/dx.bat,这个dx.bat就是咱们需要使用的工具。想要全局使用dx.bat需要配置全局变量:

然后在path中也同样配置一下,就可以在cmd中直接进行使用了。打开cmd,命令是:

dx --dex --output 要打包的路径/名字.dex 源文件路径(即你通过build出的class文件)

执行完命令之后生成了修复包,咱们把这个修复包直接放入测试机的根目录,真实开发中肯定放在私密目录。

最最重要的来了

咱们需要一个工具类来加载咱们的修复包,需要用到上下文,所以可以直接传入:


  
  1. /**
  2. * @ProjectName: Andfix
  3. * @Package: com.zj.andfix
  4. * @Author: jiang zhu
  5. * @Date: 2020/1/2 21:39
  6. */
  7. public class DexManager {
  8. private Context context;
  9. static {
  10. System.loadLibrary( "native-lib");
  11. }
  12. public void setContext(Context context) {
  13. this.context = context;
  14. }
  15. }

别忘了加载native-lib。接下来需要一个方法来加载我们的修复包:


  
  1. public void load(File file) {
  2. try {
  3. DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
  4. new File(context.getCacheDir(), "opt").getAbsolutePath(),
  5. Context.MODE_PRIVATE);
  6. Enumeration<String> entry= dexFile.entries();
  7. while (entry.hasMoreElements()) {
  8. // 全类名
  9. String className = entry.nextElement();
  10. Class realClazz=dexFile.loadClass(className, context.getClassLoader());
  11. if (realClazz != null) {
  12. fixClass(realClazz);
  13. }
  14. // Class.forName(className);
  15. }
  16. } catch (Exception e) {
  17. e.printStackTrace();
  18. }
  19. }

下面简单说一下上面方法的意思:先通过传进来的File文件获取到一个DexFile文件,然后遍历里面所有的类,获取到修复包中类的全限定名,通过loadClass获取到修复类,如果类不为空,则进行修复,下面是fixClass方法的代码:


  
  1. private void fixClass(Class realClazz) {
  2. //加载方法 Method
  3. Method[] methods = realClazz.getMethods();
  4. for (Method rightMethod : methods) {
  5. Replace replace = rightMethod.getAnnotation(Replace.class);
  6. if (replace == null) {
  7. continue;
  8. }
  9. String clazzName = replace.path();
  10. String methodName = replace.method();
  11. try {
  12. Class wrongClazz=Class.forName(clazzName);
  13. //Method right wrong
  14. Method wrongMethod=wrongClazz.getDeclaredMethod(methodName, rightMethod.getParameterTypes());
  15. replace(wrongMethod, rightMethod);
  16. } catch (Exception e) {
  17. e.printStackTrace();
  18. }
  19. }
  20. }

上面的代码首先获取到类中所有的方法,然后进行遍历,获取方法上咱们定义的注解,如果有自定义注解的画,获取类的全限定名和方法名,获取到正确的方法和错误的方法。接下来就交给了replace方法:

   public native  void replace(Method wrongMethod, Method rightMethod);

replace方法是一个native方法,需要写c++来实现了,到这里咱们需要引入安卓源码中的ArtMethod.h头文件了(上面讲到过,注意,只需引入结构体的代码,其他删掉即可,全部引用的话代码太多,一层套一层,会把源码都搬过来的。。。),下面是ArtMethod.h头文件的代码,大家可以直接进行复制,或者去最新的安卓源码中去复制:


  
  1. #include <stdint.h>
  2. namespace art{
  3. namespace mirror{
  4. class Object{
  5. uint32_t klass_;
  6. uint32_t monitor_;
  7. };
  8. class ArtMethod: public Object{
  9. public:
  10. uint32_t access_flags_;
  11. uint32_t dex_code_item_offset_;
  12. uint32_t dex_method_index_;
  13. uint32_t method_index_;
  14. uint32_t dex_cache_resolved_methods_;
  15. uint32_t dex_cache_resolved_types_;
  16. uint32_t declaring_class_;
  17. };
  18. }
  19. }

万事俱备,之前东风,最后需要的就是在c++中进行方法的替换了:


  
  1. extern "C"
  2. JNIEXPORT void JNICALL
  3. Java_com_zj_andfix_DexManager_replace (JNIEnv *env, jobject thiz, jobject wrongMethod,
  4. jobject rightMethod) {
  5. art::mirror::ArtMethod *wrong= reinterpret_cast<art::mirror::ArtMethod *>(env->FromReflectedMethod(wrongMethod));
  6. art::mirror::ArtMethod *right= reinterpret_cast<art::mirror::ArtMethod *>(env->FromReflectedMethod(rightMethod));
  7. // wrong=right;
  8. wrong->declaring_class_ = right->declaring_class_;
  9. wrong->dex_cache_resolved_methods_ = right->dex_cache_resolved_methods_;
  10. wrong->access_flags_ = right->access_flags_;
  11. wrong->dex_cache_resolved_types_ = right->dex_cache_resolved_types_;
  12. wrong->dex_code_item_offset_ = right->dex_code_item_offset_;
  13. wrong->dex_method_index_ = right->dex_method_index_;
  14. wrong->method_index_ = right->method_index_;
  15. }

至此,AndFix基本原理已经实现。“别光写不练啊,运行试试啊!”

好嘞,咱们来看一下运行效果吧:

文末

本来只是想简单总结一下,没想到越写越多,本来还打算写一下阿里的正宗的AndFix的使用流程,放到下一篇文章吧,之后再写写腾讯的Tinker。周六的晚上写到了周日,也是没谁了,好了,准备洗漱,睡觉。晚安了陌生人。

 


转载:https://blog.csdn.net/haojiagou/article/details/103838377
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场