AndFix Alibaba开源项目使用及基本原理

热修复

随着移动互联网的快速发展,用户对app的品质要求也越来越高,对于app来说如果有bug影响到用户体验,那对于用户和产品的伤害就比较大,所以必须快速的解决bug,但是移动app版本升级又是一个绕不过去的坎,你必须在应用市场上重新发布,用户更新后才行,这过程耗费时间很久成本比较大,而且频繁的升级对于用户是很大的干扰,因此越来越多的app开始使用热更新技术,这样就不需要下载全部app,只需要下载补丁文件即可。而目前比较成熟的热修复框架有1. 阿里AndFix 2.以QQ空间超级补丁技术为基础的(传送)但是其本身的方案目前并没有开源出来,3.微信Tinker,而结合我们自己项目的需求最后选择了ali的AndFix

AndFix
首先androidFix支持版本从2.3到7.0, ARM and X86 架构都, Dalvik and ART runtime, 32bit and 64bit也都支持.AndFix不同于QQ空间超级补丁技术和微信Tinker通过增加或替换整个DEX的方案,提供了一种运行时在Native修改Filed指针的方式,实现方法的替换,达到即时生效无需重启,对应用无性能消耗的目的。
接下来通过例子来说明下其原理和使用。
其大概原理如官方提供原理图如下:

20170120114439713.png

上面大致意思就是
1.app初始化时加载当前版本下的所有apatch文件。
2.通过注解获取到补丁文件的补丁方法。
3.通过反射获取对应的bugMethd.
4.通过c++层,拿到库文件句柄,并得到对应文件的class对象,得到新旧方法的指针,最后将新方法指针指向目标方。

接下来看看基本的使用方法:

  1. 添加依赖
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:24.2.0'
    compile 'com.alipay.euler:andfix:0.5.0@aar'
}

2.如果要混淆则

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

3.代码中的初始化
一般初始化最好是放在application中:

    public static PatchManager patchManager;
    @Override
    public void onCreate() {
        super.onCreate();
        patchManager = new PatchManager(this);
        PackageManager pm = getPackageManager();
        PackageInfo pi = null;
        try {
            pi = pm.getPackageInfo(getPackageName(), 0);
        } catch (PackageManager.NameNotFoundException e) {
        }
        String versionName = pi.versionName;
        patchManager.init(versionName);//current version
        patchManager.loadPatch();
    }

首先调用patchManager. init()方法初始化
这个方法主要干了些什么可以看下其源码

//首先判断存放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;
        }
        //获取当前补丁的版本 ,如果补丁版本和当前存在的版本一样 就删除当前补丁文件
        //其它的就初始化patch文件,把所有的以.apatch的放入 SortedSet<Patch> mPatchs中,并且会复制到data/你的应用包名下去
        SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,
                Context.MODE_PRIVATE);
        String ver = sp.getString(SP_VERSION, null);
        if (ver == null || !ver.equalsIgnoreCase(appVersion)) {
            cleanPatch();
            sp.edit().putString(SP_VERSION, appVersion).commit();
        } else {
            initPatchs();
        }

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

    /**
     * add patch file
     * 
     * @param file
     * @return patch
     */
    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;
    }

接下来调用 patchManager.loadPatch();
加载补丁的相关信息

public void loadPatch() {
        mLoaders.put("*", mContext.getClassLoader());// wildcard
        Set<String> patchNames;
        List<String> classes;
        //获取补丁的相关信息
        for (Patch patch : mPatchs) {
            patchNames = patch.getPatchNames();
            for (String patchName : patchNames) {
               //获取到存在bug的对象类的名字
                classes = patch.getClasses(patchName);
                mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),
                        classes);
            }
        }
    }

接下来调用fix方法去修复

 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
                //和本地SharedPreferences存的的md5值进行校验
                if (mSecurityChecker.verifyOpt(optfile)) {
                    saveFingerprint = false;
                } else if (!optfile.delete()) {
                    return;
                }
            }
            //加载dev文件
            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);
        }
    }
通过      methodReplace = method.getAnnotation(MethodReplace.class);
注解拿到对应的补丁的方法相关信息,
通过反射拿到待修复的class的信息
    Class<?> clazz = mFixedClass.get(key);
            if (clazz == null) {// class not load
                Class<?> clzz = classLoader.loadClass(clz);
                // initialize target class
                clazz = AndFix.initTargetClass(clzz);
            }
最后通过调用Native 方法去替换对应方法的指针
AndFix.addReplaceMethod(src, method);

而创建补丁文件,官方专门提供了一个工具apkpatch(下载)生成.apatch补丁文件
解压后

20170120145819284.png

就是这些文件,通过cmd命令定位到该目录下,并且把存在bug的apk文件和修复bug的文件apk放在该文件根目录下,并且把对应的key文件也放进来
如下:

20170120150005990.png

调用 下面命令:
apkpatch.bat -f xxxx.apk -t xxxx.apk -o output -k xxxx.jks -p andfix -a andfix -e andfix

apkpatch.bat -f 新apk -t 旧apk -o 输出目录 -k app签名文件 -p 签名文件密码 -a 签名文件别名 -e 别名密码
最后文件生成成功后,命令行会打印出对应修改的class的文件信息
生成文件如下:

20170120150214024.png

最后说几点实际使用中的问题
1.如果之前版本存在bug,并且添加过对应的bug补丁的,再次升级的需要把bug修复,此时需要将init中对应的版本号也需要升级,不然会加载之前存放在data中的补丁文件。
2.一个补丁文件可以修复多个bug。
3.如果一个版本第一次加载了补丁,第二次修改另外一个bug,补丁任然是相同的名字,那么此次新添加的补丁将无效就需要删除之前的补丁。但是可以同时存在多个不一样名字的补丁,并且加载按照时间顺序来加载。

总的来说AndFix 使用比较简单, BUG修复的即时性 ,不用重启,补丁包同样采用diff技术,生成的PATCH体积小 ,对应用无侵入,几乎无性能损耗。但是AndFix 不支持新增字段,也不支持对资源的替换。 由于厂商的自定义ROM,对少数机型暂不支持。但是总的来说满足了目前大部分需求。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容