什麼是 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