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

背景介绍

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

你发布了一款安卓应用,早上刚发版,结果发完之后发现有个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++的项目:

直接选择这个:

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

/**
 * @ProjectName: Andfix
 * @Package: com.zj.andfix
 * @Author: jiang zhu
 * @Date: 2020/1/2 21:25
 */
public class Caclutor {

    public void test(Context context){
        throw new RuntimeException("报错了");
    }

}

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

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

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

/**
 * @ProjectName: Andfix
 * @Package: com.zj.andfix
 * @Author: jiang zhu
 * @Date: 2020/1/2 21:25
 */
public class Caclutor {

    public void test(Context context){
        //throw new RuntimeException("报错了");
        Toast.makeText(context, "修复成功了", Toast.LENGTH_SHORT).show();
    }

}

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

/**
 * @ProjectName: Andfix
 * @Package: com.zj.andfix
 * @Author: jiang zhu
 * @Date: 2020/1/2 21:18
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Replace {

    //类的全限定名
    String path();
    
    //方法名
    String method();

}

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

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

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

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

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

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

最最重要的来了

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

/**
 * @ProjectName: Andfix
 * @Package: com.zj.andfix
 * @Author: jiang zhu
 * @Date: 2020/1/2 21:39
 */
public class DexManager {
    private Context context;
    static {
        System.loadLibrary("native-lib");
    }
    public void setContext(Context context) {

        this.context = context;
    }

}

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

public void load(File file) {
        try {
            DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
                    new File(context.getCacheDir(), "opt").getAbsolutePath(),
                    Context.MODE_PRIVATE);
            Enumeration<String> entry= dexFile.entries();
            while (entry.hasMoreElements()) {
//                全类名
                String className = entry.nextElement();
                Class realClazz=dexFile.loadClass(className, context.getClassLoader());
                if (realClazz != null) {
                    fixClass(realClazz);
                }
//                Class.forName(className);

            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

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

    private void fixClass(Class realClazz) {
        //加载方法 Method
        Method[] methods = realClazz.getMethods();
        for (Method rightMethod : methods) {
            Replace replace = rightMethod.getAnnotation(Replace.class);

            if (replace == null) {
                continue;
            }

            String clazzName = replace.path();
            String methodName = replace.method();

            try {
                Class wrongClazz=Class.forName(clazzName);
                //Method     right       wrong
                Method wrongMethod=wrongClazz.getDeclaredMethod(methodName, rightMethod.getParameterTypes());
                replace(wrongMethod, rightMethod);

            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }

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

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

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

#include <stdint.h>

namespace art{
    namespace mirror{
        class Object{
            uint32_t klass_;
            uint32_t monitor_;

        };
        class ArtMethod:public Object{
        public:
            uint32_t access_flags_;
            uint32_t dex_code_item_offset_;
            uint32_t dex_method_index_;
            uint32_t method_index_;
            uint32_t dex_cache_resolved_methods_;
            uint32_t dex_cache_resolved_types_;
            uint32_t declaring_class_;
        };
    }
}

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

extern "C"
JNIEXPORT void JNICALL
Java_com_zj_andfix_DexManager_replace(JNIEnv *env, jobject thiz, jobject wrongMethod,
                                      jobject rightMethod) {
    art::mirror::ArtMethod *wrong= reinterpret_cast<art::mirror::ArtMethod *>(env->FromReflectedMethod(wrongMethod));
    art::mirror::ArtMethod *right= reinterpret_cast<art::mirror::ArtMethod *>(env->FromReflectedMethod(rightMethod));

//    wrong=right;
    wrong->declaring_class_ = right->declaring_class_;
    wrong->dex_cache_resolved_methods_ = right->dex_cache_resolved_methods_;
    wrong->access_flags_ = right->access_flags_;
    wrong->dex_cache_resolved_types_ = right->dex_cache_resolved_types_;
    wrong->dex_code_item_offset_ = right->dex_code_item_offset_;
    wrong->dex_method_index_ = right->dex_method_index_;
    wrong->method_index_ = right->method_index_;
}

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

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

文末

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

发布了87 篇原创文章 · 获赞 248 · 访问量 11万+

猜你喜欢

转载自blog.csdn.net/haojiagou/article/details/103838377