tinker熱更新原理

什麼是 Tinker?

Tinker 是一個開源項目(Github鏈接),它是微信官方的 Android 熱補丁解決方案,它支持動態下發代碼、So 庫以及資源,讓應用能夠在不需要重新安裝的情況下實現更新。

熱更新方案比較

當前市面的熱補丁方案有很多,其中比較出名的有阿里的 AndFix、美團的 Robust 以及 QZone 的超級補丁方案。
在這裏插入圖片描述

1、AndFix作爲native解決方案,首先面臨的是穩定性與兼容性問題,更重要的是它無法實現類替換,它是需要大量額外的開發成本的;

2、Robust兼容性與成功率較高,但是它與AndFix一樣,無法新增變量與類只能用做的bugFix方案;

3、Qzone方案可以做到發佈產品功能,但是它主要問題是插樁帶來Dalvik的性能問題,以及爲了解決Art下內存地址問題而導致補丁包急速增大的。

特別是在Android N之後,由於混合編譯的inline策略修改,對於市面上的各種方案都不太容易解決而Tinker熱補丁方案不僅支持類、So以及資源的替換,它還是2.X-8.X(1.9.0以上支持8.X)的全平臺支持。利用Tinker我們不僅可以用做bugfix,甚至可以替代功能的發佈。Tinker已運行在微信的數億Android設備上,那麼爲什麼你不使用Tinker呢?

Tinker的已知問題

由於原理與系統限制,Tinker有以下已知問題:

1、Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大組件(1.9.0支持新增非export的Activity)

2、由於Google Play的開發者條款限制,不建議在GP渠道動態更新代碼;
在Android N上,補丁對應用啓動時間有輕微的影響;

3、不支持部分三星android-21機型,加載補丁時會主動出"TinkerRuntimeException:checkDexInstall failed";

4、對於資源替換,不支持修改remoteView。例如transition動畫,notification icon以及桌面圖標。

Android類動態加載機制

要了解tinker熱更新原理就要先了解Android的類加載流程Android中虛擬機類加載流程圖如下:

在這裏插入圖片描述
DexClassLoader 和 PathClassLoader

在Android中,ClassLoader是一個抽象類,實際開發過程中,一般是使用其具體的子類DexClassLoader、PathClassLoader這些類加載器來加載類的,不同之處是:

1、PathClassLoader:支持加載DEX或者已經安裝的APK(因爲存在緩存的DEX)

2、DexClassLoader:支持加載APK、DEX和JAR,也可以從SD卡進行加載。

這2個類都繼承於BaseDexClassLoader, BaseDexClassLoader繼承於ClassLoader。

DexClassLoader的構造方法:

public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }

PathClassLoader的構造方法:

public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }

BaseDexClassLoader的構造方法:

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

DexPathList的loadDexFile方法

private static DexFile loadDexFile(File file, File optimizedDirectory)
            throws IOException {
        if (optimizedDirectory == null) {
            return new DexFile(file);
        } else {
            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
            return DexFile.loadDex(file.getPath(), optimizedPath, 0);
        }
    }

private static String optimizedPathFor(File path,File optimizedDirectory) {
        String fileName = path.getName();
        if (!fileName.endsWith(DEX_SUFFIX)) {
            int lastDot = fileName.lastIndexOf(".");
            if (lastDot < 0) {
                fileName += DEX_SUFFIX;
            } else {
                StringBuilder sb = new StringBuilder(lastDot + 4);
                sb.append(fileName, 0, lastDot);
                sb.append(DEX_SUFFIX);
                fileName = sb.toString();
            }
        }
        File result = new File(optimizedDirectory, fileName);
        return result.getPath();
    }

由上可知:
1、optimizedDirectory是用來緩存需要加載的dex文件的,並創建一個DexFile對象,如果它爲null,那麼會直接使用dex文件原有的路徑來創建DexFile對象。

2、optimizedDirectory必須是一個內部存儲路徑,無論哪種動態加載,加載的可執行文件一定要存放在內部存儲。DexClassLoader可以指定自己的optimizedDirectory,所以它可以加載外部的dex,因爲這個dex會被複制到內部路徑的optimizedDirectory。

3、PathClassLoader沒有optimizedDirectory,所以它只能加載內部的dex,這些大都是存在系統中已經安裝過的apk裏面的。

Tinker熱更新原理

首先給出tinker官方的熱更新原理圖:
在這裏插入圖片描述
由上圖可以看出tinker的主要流程是:

1、通過生成的fix.dex,也就是修復包的dex文件與base.dex也就是已經發布出去的需要修復的dex文件進行一個對比,生成patch.dex文件。

2、然後通過patch.dex文件與classes.dex文件合併生成新的fix_classes.dex文件代替掉原來的classes.dex文件。

3、將合成後的全量dex 插入到dex elements前面,完成修復

tinker熱更新流程圖
在這裏插入圖片描述
下面看下tinker加載的源碼

  public Intent tryLoad(TinkerApplication app, int tinkerFlag, boolean tinkerLoadVerifyFlag) {
        Intent resultIntent = new Intent();
        long begin = SystemClock.elapsedRealtime();
        tryLoadPatchFilesInternal(app, tinkerFlag, tinkerLoadVerifyFlag, resultIntent); //在tryLoadPatchFilesInternal中首先要進行環境校驗,完成校驗流程後再加載補丁,校驗的詳細內容不展開討論
        long cost = SystemClock.elapsedRealtime() - begin;
        ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);
        return resultIntent;
    }

tinker中的類加載器TinkerDexLoader

 @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public static boolean loadTinkerJars(Application application, boolean tinkerLoadVerifyFlag, String directory, Intent intentResult, boolean isSystemOTA) {

        PathClassLoader classLoader = (PathClassLoader) TinkerDexLoader.class.getClassLoader();
        String dexPath = directory + "/" + DEX_PATH + "/";
        File optimizeDir = new File(directory + "/" + DEX_OPTIMIZE_PATH);
        ArrayList<File> legalFiles = new ArrayList<>();
        final boolean isArtPlatForm = ShareTinkerInternals.isVmArt();
        // 獲取合法的文件列表
        for (ShareDexDiffPatchInfo info : dexList) {
            //for dalvik, ignore art support dex
            if (isJustArtSupportDex(info)) {
                continue;
            }
            String path = dexPath + info.realName;
            File file = new File(path);
            }
            legalFiles.add(file);
        }
//isSystemOTA判斷,如果用戶是ART環境並且做了OTA升級,加載dex補丁的時候首先將最近一次的補丁全部DexFile.loadDex一遍.之所以這樣做是因爲有些場景做了OTA後,OTA的規則可能發生變化,在這種情況下去加載上個系統版本oat過的dex就會出現問題.
        if (isSystemOTA) {
            parallelOTAResult = true;
            parallelOTAThrowable = null;
            Log.w(TAG, "systemOTA, try parallel oat dexes!!!!!");

            TinkerParallelDexOptimizer.optimizeAll(
                legalFiles, optimizeDir,
                new TinkerParallelDexOptimizer.ResultCallback() {
                    long start;

                    @Override
                    public void onSuccess(File dexFile, File optimizedDir, File optimizedFile) {
                        // Do nothing.
                        Log.i(TAG, "success to optimize dex " + dexFile.getPath() + "use time " + (System.currentTimeMillis() - start));
                    }
                    @Override
                    public void onFailed(File dexFile, File optimizedDir, Throwable thr) {
                        parallelOTAResult = false;
                        parallelOTAThrowable = thr;
                        Log.i(TAG, "fail to optimize dex " + dexFile.getPath() + "use time " + (System.currentTimeMillis() - start));
                    }
                }
            );          
               intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, parallelOTAThrowable);
                ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_PARALLEL_DEX_OPT_EXCEPTION);
                return false;
            }
        }
        try {
            //接下來就是調用SystemClassLoaderAdder的installDexes方法
            SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles);
        } catch (Throwable e) {
            intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
            ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_LOAD_EXCEPTION);
            return false;
        }
        return true;
    }

加載patch.dex過程,在tinker中針對不同的版本有不同的加載代碼下面是版本23的加載代碼:

private static void install(ClassLoader loader, List<File> additionalClassPathEntries,File optimizedDirectory)
            throws IllegalArgumentException, IllegalAccessException,
            NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
            Field pathListField = ShareReflectUtil.findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader); //通過反射拿到classloader的patchlist變量
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            // 通過反射獲取pathList的dexElements參數,把經過合併後的DexElements設置爲pathList的dexElements。
            ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,suppressedExceptions));
        }


        private static Object[] makePathElements(
            Object dexPathList, ArrayList<File> files, File optimizedDirectory,
            ArrayList<IOException> suppressedExceptions)
            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {

            Method makePathElements;
            try {
            //反射pathList的makeDexElements方法,傳入插件補丁dexList路徑與優化過的opt目錄,通過這個方法生成一個新的DexElements,這個DexElements爲插件的DexElements。
                makePathElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", List.class, File.class,
                    List.class);
            } catch (NoSuchMethodException e) {
                Log.e(TAG, "NoSuchMethodException: makePathElements(List,File,List) failure");
                try {
                    makePathElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", ArrayList.class, File.class, ArrayList.class);
                } catch (NoSuchMethodException e1) {
                    Log.e(TAG, "NoSuchMethodException: makeDexElements(ArrayList,File,ArrayList) failure");
                    try {
                        Log.e(TAG, "NoSuchMethodException: try use v19 instead");
                        return V19.makeDexElements(dexPathList, files, optimizedDirectory, suppressedExceptions);
                    } catch (NoSuchMethodException e2) {
                        Log.e(TAG, "NoSuchMethodException: makeDexElements(List,File,List) failure");
                        throw e2;
                    }
                }
            }

            return (Object[]) makePathElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions);
        }
    }

上面代碼主要是:

通過反射獲取到patchlist的dexElements字段,然後將合成補丁後的dex文件插入到dexelements數組的前面,這樣打了補丁後的dex就可以先記載到從而完成修復工作。

參考文獻

1、http://www.tinkerpatch.com/Docs/intro
2、https://github.com/Tencent/tinker
3、https://www.jianshu.com/p/2216554d3291

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