开发者社区> 黑夜路口> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

14.AndFix热修复的使用和源码分析(客户端修复逻辑)

简介: 使用方法: 1.下载AndFix源码:https://github.com/alibaba/AndFix 2.生成两个apk文件,一个是含有bug的old.
+关注继续查看
使用方法:

1.下载AndFix源码:https://github.com/alibaba/AndFix
2.生成两个apk文件,一个是含有bug的old.apk 一个是修复之后的new.apk
3.进入AndFix目录中的tools文件夹找到apkpatch工具,这个工具负责生成差分包,将它解压
4.在命令行中进入解压后的文件夹,我的这里是这样的C:\Users\renzhenming\Desktop\AndFix-master\tools\apkpatch-1.0.3
5.使用命令生成差分包

apkpatch -f <new> -t <old> -o <output> -k <keystore> -p <***> -a <alias> -e <***>

 -a,--alias <alias>     keystore entry alias.
 -e,--epassword <***>   keystore entry password.
 -f,--from <loc>        new Apk file path.
 -k,--keystore <loc>    keystore path.
 -n,--name <name>       patch name.
 -o,--out <dir>         output dir.
 -p,--kpassword <***>   keystore password.
 -t,--to <loc>          old Apk file path.

我的命令如下,注意每个命令参数代表的含义

apkpatch -f C:\Users\renzhenming\Desktop\after.apk 
-t C:\Users\renzhenming\Desktop\before.apk -o C:\Users\renzhenming\Desktop\patch 
-k C:\Users\renzhenming\Desktop\renzhenming.jks -p renzhenming -a renzhenming 
-e renzhenming

可以看到执行后还打印了这句话,表示发生异常的类

add modified Method:V  bump(Landroid/view/View;)  in Class:Lcom/app/rzm/test/TestFixDexActivity;

6.客户端进行合称,修复问题。运行apk,你会发现bug消失了,操作立即生效,无需重启

        //初始化阿里热修复
        mPatchManger = new PatchManager(this);
        //获取当前应用版本
        mPatchManger.init(AppUtils.getVersionName(this));
        mPatchManger.loadPatch();

        //获取下载到的patch包
        File patchFile = new File(Environment.getExternalStorageDirectory().getAbsolutePath(),"fix.apatch");
        if (patchFile != null){
            try {
                mPatchManger.addPatch(patchFile.getAbsolutePath());
                Toast.makeText(this,"AndFix修复成功",Toast.LENGTH_SHORT).show();
            } catch (IOException e) {
                e.printStackTrace();
                Toast.makeText(this,"AndFix修复失败",Toast.LENGTH_SHORT).show();
            }
        }

7.另外,如果你的项目是多成员开发,可能存在每个小组都修复自己的问题生成了多个差分包,那么你可以通过命令将多个差分包合并成一个

apkpatch -m <apatch_path...> -o <output> -k <keystore> -p <***> -a <alias> -e <***>
 -a,--alias <alias>     keystore entry alias.
 -e,--epassword <***>   keystore entry password.
 -k,--keystore <loc>    keystore path.
 -m,--merge <loc...>    path of .apatch files.
 -n,--name <name>       patch name.
 -o,--out <dir>         output dir.
 -p,--kpassword <***>   keystore password.
AndFix的局限性

AndFix支持ARM和X86平台,支持Dalvik虚拟机和Art虚拟机,但是Android 版本只能支持到7.0,对于目前的8.0版本不提供支持,而且从GitHub上可以看到,已经停止维护将近两年,所以如果要实现热修复,AndFix已经不再是首选方案,目前我们公司在使用腾讯的Tinker,但是我觉得还是有必要对AndFix原理有一个了解

使用时需要注意

1.生成之后一定要测试,确保未知问题存在
2.尽量不要分包,不要分多个dex
3.混淆时,注意native方法和注解不要混淆

-keep class * extends java.lang.annotation.Annotation
-keepclasseswithmembernames class * {
    native <methods>;
}

4.如果生成之后要加固,差分包一定要在加固之前生成

源码分析

AndFix实现热修复的流程如下


img_6252a68052316c2869454460e0875325.png
AndFix热修复实现流程.png

AndFix热修复包括两个核心点,第一对发生bug的apk和修复后的apk进行分析,通过AndFix内置的一个工具生成差分包,客户端下载到差分包后将差分包和本地包合并实现修复bug的目的。差分包和问题包合并的原理是方法的替换,差分包会将发生bug的方法添加注解,通过这个注解找到问题所在,然后在运行的时候运行正确的方法,从而绕过bug


img_efe0ff398c3d3338f8588a5612e65a6d.png
AndFix 实现原理.png

那么接下来我们具体来看代码中的实现

PatchManager patchManger = new PatchManager(this);
初始化一个AndFixManager,一个目录mPatchDir 和两个集合,你就把它们分别当作Set和Map集合就行,这两个集合的内部原理使用优点不是今天的内容

    public PatchManager(Context context) {
        mContext = context;
        mAndFixManager = new AndFixManager(mContext);
        mPatchDir = new File(mContext.getFilesDir(), DIR);
        mPatchs = new ConcurrentSkipListSet<Patch>();
        mLoaders = new ConcurrentHashMap<String, ClassLoader>();
    }

我们看看AndFixManager初始化的时候做了什么,可以看到主要就是检测当前app是否支持AndFix的时候,如果不支持也就不了了之了,而且也不会抛个异常提醒你,只会打个log,这一点我认为做的并不完善,提醒的不到位

    public AndFixManager(Context context) {
        mContext = context;
        //检测系统,版本等是否支持AndFix
        mSupport = Compat.isSupport();
        if (mSupport) {
            //签名相关的一些检测
            mSecurityChecker = new SecurityChecker(mContext);
            //初始化一个目录,和刚才初始化的目录在同一个位置下 file文件夹
            ,= new File(mContext.getFilesDir(), DIR);
            if (!mOptDir.exists() && !mOptDir.mkdirs()) {// make directory fail
                mSupport = false;
                Log.e(TAG, "opt dir create error.");
            } else if (!mOptDir.isDirectory()) {// not directory
                mOptDir.delete();
                mSupport = false;
            }
        }
    }
    //在这个方法中我们可以看到AndFix所支持的条件
    //1.支持Android系统,不支持阿里云系统
    //2.SDK版本从支持大于等于8小于等于24,也就是Android2.3到7.0
    // setup方法是native方法实现的,我们可以看看C++端的代码,在andfix.cpp中
    public static synchronized boolean isSupport() {
        if (isChecked)
            return isSupport;

        isChecked = true;
        // not support alibaba's YunOs
        if (!isYunOS() && AndFix.setup() && isSupportSDKVersion()) {
            isSupport = true;
        }

        if (inBlackList()) {
            isSupport = false;
        }

        return isSupport;
    }

andfix.cpp
传入的参数isart表示是否是Art虚拟机,apilevel是当前SDK版本,可以看到,AndFix不但支持art虚拟级,也支持dalvik虚拟级,但是art是无条件的支持,dalvik却是在一定限制下才能支持的,具体我们看代码

static jboolean setup(JNIEnv* env, jclass clazz, jboolean isart,
        jint apilevel) {
    isArt = isart;
    LOGD("vm is: %s , apilevel is: %i", (isArt ? "art" : "dalvik"),
            (int )apilevel);
    if (isArt) {
        return art_setup(env, (int) apilevel);
    } else {
        return dalvik_setup(env, (int) apilevel);
    }
}
//在art_method_replace.cpp中找到这个方法,可以看到直接返回的true
extern jboolean __attribute__ ((visibility ("hidden"))) art_setup(JNIEnv* env,
        int level) {
    apilevel = level;
    return JNI_TRUE;
}

//dalvik虚拟机的初始化方法相对复杂,我们在dalvik_method_replace.cpp中找到这个方法
extern jboolean __attribute__ ((visibility ("hidden"))) dalvik_setup(
        JNIEnv* env, int apilevel) {
    //加载系统的libdvm.so库,如果没有加载到,则不支持
    //libdvm.so是Dalvik的库文件之一,位于system/lib/下,如果是art虚拟机
    //则没有libdvm.so而是libart.so,从4.4开始,已经开始使用art虚拟机了
    //dlopen,打开一个库,并为使用该库做些准备,通过dlopen动态
    //的打开动态库,动态库加载完成后,返回一个句柄,然后把句柄传给
    //dlsym定位到你需要执行的函数指针,函数指针拿到了,就可以使用
    //这个函数了。
    
    void* dvm_hand = dlopen("libdvm.so", RTLD_NOW);
    if (dvm_hand) {
        //dlsym,在打开的库中查找符号的值,根据版本不同,查找不同的值
        dvmDecodeIndirectRef_fnPtr = dvm_dlsym(dvm_hand,
                apilevel > 10 ?
                        "_Z20dvmDecodeIndirectRefP6ThreadP8_jobject" :
                        "dvmDecodeIndirectRef");
        //如果没有找到,则返回false
        if (!dvmDecodeIndirectRef_fnPtr) {
            return JNI_FALSE;
        }
        //继续找另一个值
        dvmThreadSelf_fnPtr = dvm_dlsym(dvm_hand,
                apilevel > 10 ? "_Z13dvmThreadSelfv" : "dvmThreadSelf");
        //找不到则返回false
        if (!dvmThreadSelf_fnPtr) {
            return JNI_FALSE;
        }
        jclass clazz = env->FindClass("java/lang/reflect/Method");
        jClassMethod = env->GetMethodID(clazz, "getDeclaringClass",
                        "()Ljava/lang/Class;");

        return JNI_TRUE;
    } else {
        return JNI_FALSE;
    }
}

所以支持dalvik虚拟机的条件如下:
1.系统文件中有libdvm.so这个库
2.apilevel > 10的状态下,可以从libdvm.so中找到_Z20dvmDecodeIndirectRefP6ThreadP8_jobject和_Z13dvmThreadSelfv这两个符号
3.apilevel <= 10的状态下,可以从libdvm.so中找到dvmDecodeIndirectRef和dvmThreadSelf这两个符号

言归正穿,我们继续往下看代码

mPatchManger.init(AppUtils.getVersionName(this));

public void init(String appVersion) {
        //file/apatch文件夹创建失败则返回
        if (!mPatchDir.exists() && !mPatchDir.mkdirs()) {// make directory fail
            Log.e(TAG, "patch dir create error.");
            return;
        } else if (!mPatchDir.isDirectory()) {// not directory
            //不是文件夹同样返回
            mPatchDir.delete();
            return;
        }
        SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,
                Context.MODE_PRIVATE);
        String ver = sp.getString(SP_VERSION, null);
        //判断当前apk的版本和差分包的版本是否相同,如果不同则删除差分包
        //热修复生成的差分包版本要和当前修复的apk一致
        if (ver == null || !ver.equalsIgnoreCase(appVersion)) {
            cleanPatch();
            sp.edit().putString(SP_VERSION, appVersion).commit();
        } else {
            initPatchs();
        }
    }

接下来,如果情况正常,那么遍历file/apatch文件夹下的差分包,将每一个差分包文件封装成Patch对象加入mPatchs集合中

    private void initPatchs() {
        File[] files = mPatchDir.listFiles();
        for (File file : files) {
            addPatch(file);
        }
    }

    private Patch addPatch(File file) {
        Patch patch = null;
        if (file.getName().endsWith(SUFFIX)) {
            try {
                patch = new Patch(file);
                mPatchs.add(patch);
            } catch (IOException e) {
                Log.e(TAG, "addPatch", e);
            }
        }
        return patch;
    }

所以init方法只是将差分包存入集合中,还没有开始修复。然后开始loadPatch

    public void loadPatch() {
        //存储ClassLoader
        mLoaders.put("*", mContext.getClassLoader());// wildcard
        Set<String> patchNames;
        List<String> classes;
        //遍历差分包集合
        for (Patch patch : mPatchs) {
            patchNames = patch.getPatchNames();
            for (String patchName : patchNames) {
                //从patch对象中获取到一个集合然后开始fix
                classes = patch.getClasses(patchName);
                mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),
                        classes);
            }
        }
    }

从patch中获取到的集合中是什么,为了搞清楚这个问题,我们需要回过头看看封装patch的时候做了什么

    public Patch(File file) throws IOException {
        mFile = file;
        init();
    }

    @SuppressWarnings("deprecation")
    private void init() throws IOException {
        JarFile jarFile = null;
        InputStream inputStream = null;
        try {
            jarFile = new JarFile(mFile);
            JarEntry entry = jarFile.getJarEntry(ENTRY_NAME);
            inputStream = jarFile.getInputStream(entry);
            Manifest manifest = new Manifest(inputStream);
            Attributes main = manifest.getMainAttributes();
            mName = main.getValue(PATCH_NAME);
            mTime = new Date(main.getValue(CREATED_TIME));

            mClassesMap = new HashMap<String, List<String>>();
            Attributes.Name attrName;
            String name;
            List<String> strings;
            //Manifest是以键值对的形式存储了差分包的信息
            for (Iterator<?> it = main.keySet().iterator(); it.hasNext();) {
                attrName = (Attributes.Name) it.next();
                name = attrName.toString();
                if (name.endsWith(CLASSES)) {
                    strings = Arrays.asList(main.getValue(attrName).split(","));
                    if (name.equalsIgnoreCase(PATCH_CLASSES)) {
                        mClassesMap.put(mName, strings);
                    } else {
                        mClassesMap.put(
                                name.trim().substring(0, name.length() - 8),// remove
                                                                            // "-Classes"
                                strings);
                    }
                }
            }
        } finally {
            if (jarFile != null) {
                jarFile.close();
            }
            if (inputStream != null) {
                inputStream.close();
            }
        }

    }

fix方法从加载到的差分包dex文件中获取到加了注解的class,差分包生成时会给发生bug的类生成一个xxx_CF类名的类,在这个类中给发生bug的方法添加了注解,比如@MethodReplace(clazz="com.app.rzm.test.TestFixDexActivity", method="bump"),生成差分包的逻辑后边再说。这里获取到这个类后在执行fixClass方法

    public synchronized void fix(File file, ClassLoader classLoader,
            List<String> classes) {
        if (!mSupport) {
            return;
        }
        //签名校验之类的
        if (!mSecurityChecker.verifyApk(file)) {// security check fail
            return;
        }

        try {
            File optfile = new File(mOptDir, file.getName());
            boolean saveFingerprint = true;
            if (optfile.exists()) {
                //如果文件夹下已经存在这个文件,那么进行校验,防止被攻击
                // need to verify fingerprint when the optimize file exist,
                // prevent someone attack on jailbreak device with
                // Vulnerability-Parasyte.
                // btw:exaggerated android Vulnerability-Parasyte
                // http://secauo.com/Exaggerated-Android-Vulnerability-Parasyte.html
                if (mSecurityChecker.verifyOpt(optfile)) {
                    saveFingerprint = false;
                } else if (!optfile.delete()) {
                    return;
                }
            }
            //打开file这个dex文件,并把它写入到optfile这个文件中
            final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
                    optfile.getAbsolutePath(), Context.MODE_PRIVATE);

            if (saveFingerprint) {
                mSecurityChecker.saveOptSig(optfile);
            }

            ClassLoader patchClassLoader = new ClassLoader(classLoader) {
                @Override
                protected Class<?> findClass(String className)
                        throws ClassNotFoundException {
                    Class<?> clazz = dexFile.loadClass(className, this);
                    if (clazz == null
                            && className.startsWith("com.alipay.euler.andfix")) {
                        return Class.forName(className);// annotation’s class
                                                        // not found
                    }
                    if (clazz == null) {
                        throw new ClassNotFoundException(className);
                    }
                    return clazz;
                }
            };
            Enumeration<String> entrys = dexFile.entries();
            Class<?> clazz = null;
            while (entrys.hasMoreElements()) {
                String entry = entrys.nextElement();
                if (classes != null && !classes.contains(entry)) {
                    continue;// skip, not need fix
                }
                clazz = dexFile.loadClass(entry, patchClassLoader);
                if (clazz != null) {
                    fixClass(clazz, classLoader);
                }
            }
        } catch (IOException e) {
            Log.e(TAG, "pacth", e);
        }
    }

这里会从bug类中获取到添加的注解,从这个注解上可以得到当前类的完整类名和崩溃发生的方法的方法名,然后开始执行replaceMethod

     /**
     * fix class
     * 
     * @param clazz
     *            class
     */
    private void fixClass(Class<?> clazz, ClassLoader classLoader) {
        Method[] methods = clazz.getDeclaredMethods();
        MethodReplace methodReplace;
        String clz;
        String meth;
        for (Method method : methods) {
            methodReplace = method.getAnnotation(MethodReplace.class);
            if (methodReplace == null)
                continue;
            clz = methodReplace.clazz();
            meth = methodReplace.method();
            if (!isEmpty(clz) && !isEmpty(meth)) {
                replaceMethod(classLoader, clz, meth, method);
            }
        }
    }
    private void replaceMethod(ClassLoader classLoader, String clz,
            String meth, Method method) {
        try {
            String key = clz + "@" + classLoader.toString();
            Class<?> clazz = mFixedClass.get(key);
            if (clazz == null) {// class not load
                Class<?> clzz = classLoader.loadClass(clz);
                // initialize target class
                //对class进行的预处理,下边会去看jni的实现
                clazz = AndFix.initTargetClass(clzz);
            }
            if (clazz != null) {// initialize class OK
                //以ClassLoader的处理后字符串为key,以class为value存储
                mFixedClass.put(key, clazz);
                Method src = clazz.getDeclaredMethod(meth,
                        method.getParameterTypes());
                //jni层开始替换错误的方法
                AndFix.addReplaceMethod(src, method);
            }
        } catch (Exception e) {
            Log.e(TAG, "replaceMethod", e);
        }
    }

接下来我们看两个涉及到jni层的处理,一个是AndFix.initTargetClass(clzz),看它对class做了什么,一个是AndFix.addReplaceMethod(src, method)看它是如何修正方法的.可以看到initTargetClass是对当前class中的Field进行的处理

    public static Class<?> initTargetClass(Class<?> clazz) {
        try {
            Class<?> targetClazz = Class.forName(clazz.getName(), true,
                    clazz.getClassLoader());

            initFields(targetClazz);
            return targetClazz;
        } catch (Exception e) {
            Log.e(TAG, "initTargetClass", e);
        }
        return null;
    }
    private static void initFields(Class<?> clazz) {
        Field[] srcFields = clazz.getDeclaredFields();
        for (Field srcField : srcFields) {
            Log.d(TAG, "modify " + clazz.getName() + "." + srcField.getName()
                    + " flag:");
            setFieldFlag(srcField);
        }
    }
    private static native void setFieldFlag(Field field);

在andfix.cpp中找到setFieldFlag对应的方法,可以看到这里也分了art和dalvik虚拟机的两种不同处理方式

static void setFieldFlag(JNIEnv* env, jclass clazz, jobject field) {
    if (isArt) {
        art_setFieldFlag(env, field);
    } else {
        dalvik_setFieldFlag(env, field);
    }
}

先看art的处理,可以看到这里又区分了不同的sdk版本,我们以>23为例看一下

extern void __attribute__ ((visibility ("hidden"))) art_setFieldFlag(
        JNIEnv* env, jobject field) {
    if (apilevel > 23) {
        setFieldFlag_7_0(env, field);
    } else if (apilevel > 22) {
        setFieldFlag_6_0(env, field);
    } else if (apilevel > 21) {
        setFieldFlag_5_1(env, field);
    } else  if (apilevel > 19) {
        setFieldFlag_5_0(env, field);
    }else{
        setFieldFlag_4_4(env, field);
    }
}

对field的access_flags_ 变量进行了处理,这个操作不影响我们看原理,所以扫一眼即可,重点关注addReplaceMethod这个方法,这个方法中两个参数,一个是修复后的正确的方法Method对象,一个是有bug的Method对象

void setFieldFlag_7_0(JNIEnv* env, jobject field) {
    art::mirror::ArtField* artField =
            (art::mirror::ArtField*) env->FromReflectedField(field);
    artField->access_flags_ = artField->access_flags_ & (~0x0002) | 0x0001;
    LOGD("setFieldFlag_7_0: %d ", artField->access_flags_);
}

还是直接以7.0为例来看

void replace_7_0(JNIEnv* env, jobject src, jobject dest) {
    art::mirror::ArtMethod* smeth =
            (art::mirror::ArtMethod*) env->FromReflectedMethod(src);

    art::mirror::ArtMethod* dmeth =
            (art::mirror::ArtMethod*) env->FromReflectedMethod(dest);

//  reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->class_loader_ =
//          reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->class_loader_; //for plugin classloader
    reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->clinit_thread_id_ =
            reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->clinit_thread_id_;
    reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->status_ =
            reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->status_ -1;
    //for reflection invoke
    reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->super_class_ = 0;

    //可以看到这里进行了一系列的指针操作,让修复后的方法和发生
    //bug的类发生了关联,也就是正确的方法替换了错误的方法,当代码
    //执行时就会绕过错误的方法从而实现修复的目的        
    smeth->declaring_class_ = dmeth->declaring_class_;
    smeth->access_flags_ = dmeth->access_flags_  | 0x0001;
    smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;
    smeth->dex_method_index_ = dmeth->dex_method_index_;
    smeth->method_index_ = dmeth->method_index_;
    smeth->hotness_count_ = dmeth->hotness_count_;

    smeth->ptr_sized_fields_.dex_cache_resolved_methods_ =
            dmeth->ptr_sized_fields_.dex_cache_resolved_methods_;
    smeth->ptr_sized_fields_.dex_cache_resolved_types_ =
            dmeth->ptr_sized_fields_.dex_cache_resolved_types_;

    smeth->ptr_sized_fields_.entry_point_from_jni_ =
            dmeth->ptr_sized_fields_.entry_point_from_jni_;
    smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =
            dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;

    LOGD("replace_7_0: %d , %d",
            smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_,
            dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_);

}

看到这里基本上阿里热修复的客户端操作我们看完了

总结一下

1.客户端下载到差分包后会从差分包中加载到发生bug的类
2.从这个类中找到添加了注解的方法,这个方法就是异常的方法
3.将正确的方法通过指针的变换,替换掉发生异常的方法实现修复的目的

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
一步步手动实现热修复(二)-类的加载机制简要介绍
*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 本节课程主要分为3块: 1.一步步手动实现热修复(一)-dex文件的生成与加载 2.一步步手动实现热修复(二)-类的加载机制简要介绍 3.一步步手动实现热修复(三)-Class文件的替换 本节示例所用到的任何资源都已开源,项目中包含工程中所用到代码、示例图片、说明文档。
826 0
自动升级系统的设计与实现(源码)
  (最新OAUS版本请参见:自动升级系统OAUS的设计与实现(续))   对于PC桌面应用程序而言,自动升级功能往往是必不可少的。而自动升级可以作为一个独立的C/S系统来开发,这样,就可以在不同的桌面应用中进行复用。
699 0
自动升级系统OAUS的设计与实现(续) (附最新源码)
  (最新OAUS版本请参见:自动升级系统的设计与实现(续2) -- 增加断点续传功能) 一.缘起       自从 自动升级系统的设计与实现(源码) 发布以后,收到了很多使用者的反馈,其中最多的要求就是希望OAUS服务端增加自动检测文件变更的功能,这样每次部署版本升级时,可以节省很多时间,而且可以避免手动修改带来的错误。
1050 0
08.源码阅读(阿里AndFix热修复原理)
使用阿里热修复需要添加依赖 compile 'com.alipay.euler:andfix:0.5.0@aar' 热修复的关键代码 //初始化阿里热修复 mPatchManger = new PatchManager(this); //获取当前应用版本 mPatchManger.
557 0
软件问题修复跟踪系统实战开发教程(上篇)
软件问题修复跟踪系统实战开发教程(上篇)
0 0
够强!一行代码就修复了我提的Dubbo的Bug。
够强!一行代码就修复了我提的Dubbo的Bug。
0 0
接口测试平台代码实现137: 小bug集中修复
接口测试平台代码实现137: 小bug集中修复
0 0
+关注
黑夜路口
安卓高级工程师,目前任职于Wifi万能钥匙
文章
问答
文章排行榜
最热
最新
相关电子书
更多
荷鲁斯 移动端第三方库安全检查引擎介绍
立即下载
ReactNative启动性能优化
立即下载
Android内存泄露自动化链路分析组件——Probe
立即下载