AndFix Alibaba開源項目使用及基本原理

熱修復

隨着移動互聯網的快速發展,用戶對app的品質要求也越來越高,對於app來說如果有bug影響到用戶體驗,那對於用戶和產品的傷害就比較大,所以必須快速的解決bug,但是移動app版本升級又是一個繞不過去的坎,你必須在應用市場上重新發布,用戶更新後纔行,這過程耗費時間很久成本比較大,而且頻繁的升級對於用戶是很大的干擾,因此越來越多的app開始使用熱更新技術,這樣就不需要下載全部app,只需要下載補丁文件即可。而目前比較成熟的熱修復框架有1. 阿里AndFix 2.以QQ空間超級補丁技術爲基礎的(https://zhuanlan.zhihu.com/p/20308548?columnSlug=magilu)但是其本身的方案目前並沒有開源出來,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指針的方式,實現方法的替換,達到即時生效無需重啓,對應用無性能消耗的目的。
接下來通過例子來說明下其原理和使用。
其大概原理如官方提供原理圖如下:

這裏寫圖片描述

上面大致意思就是
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);
            }
        }
    }
    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補丁文件

解壓後
這裏寫圖片描述
就是這些文件,通過cmd命令定位到該目錄下,並且把存在bug的apk文件和修復bug的文件apk放在該文件根目錄下,並且把對應的key文件也放進來
如下:這裏寫圖片描述
調用 下面命令:
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的文件信息
生成文件如下:
這裏寫圖片描述

最後說幾點實際使用中的問題
1.如果之前版本存在bug,並且添加過對應的bug補丁的,再次升級的需要把bug修復,此時需要將init中對應的版本號也需要升級,不然會加載之前存放在data中的補丁文件。
2.一個補丁文件可以修復多個bug。
3.如果一個版本第一次加載了補丁,第二次修改另外一個bug,補丁任然是相同的名字,那麼此次新添加的補丁將無效就需要刪除之前的補丁。但是可以同時存在多個不一樣名字的補丁,並且加載按照時間順序來加載。

總的來說AndFix 使用比較簡單, BUG修復的即時性 ,不用重啓,補丁包同樣採用diff技術,生成的PATCH體積小 ,對應用無侵入,幾乎無性能損耗。但是AndFix 不支持新增字段,也不支持對資源的替換。 由於廠商的自定義ROM,對少數機型暫不支持。但是總的來說滿足了目前大部分需求。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章