Tencent Tinker Hotfix開源方案https://github.com/Tencent/tinker
0x10 應(yīng)用接入Tinker
請參考Tinker官方指導(dǎo)或者Tinker Github
0x20 Tinker工作原理
0x21 生成patch dex
- 運行
assembleRelease
生成base apk - 修改base apk的代碼
public class MainActivity extends AppCompatActivity {
private static final String TAG = "Tinker.MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.e(TAG, "i am on onCreate classloader:" + MainActivity.class.getClassLoader().toString());
//test resource change
// base apk代碼
//Log.e(TAG, "i am on onCreate string:" + getResources().getString(R.string.test_resource));
// patch apk代碼
Log.e(TAG, "i am on patch onCreate");
...
}
...
}
- 修改Gradle腳本聲明base apk
//old apk file to build patch apk
tinkerOldApkPath = "${bakPath}/app-release-1123-20-23-05.apk"
//proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath}/app-release-1123-20-23-05-mapping.txt"
//resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath}/app-release-1123-20-23-05-R.txt"
-
運行
tinkerPatchRelease
生成補丁
對于生成patch dex文件的原理努溃,由于水平所限,暫不討論
0x22 tinker在應(yīng)用內(nèi)的安裝
從SampleApplication
開始分析捻爷。
首先看TinkerLoader
的tryLoad()
方法姻僧,tryLoad()
方法用于加載patch后的classes.dex文件规丽。
public Intent tryLoad(TinkerApplication app) {
Intent resultIntent = new Intent();
long begin = SystemClock.elapsedRealtime();
// 實際調(diào)用tryLoadPatchFilesInternal
tryLoadPatchFilesInternal(app, resultIntent);
long cost = SystemClock.elapsedRealtime() - begin;
ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);
return resultIntent;
}
private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
final int tinkerFlag = app.getTinkerFlags();
// 首先檢查是否可以加載,不滿足直接返回
if (!ShareTinkerInternals.isTinkerEnabled(tinkerFlag)) {
Log.w(TAG, "tryLoadPatchFiles: tinker is disable, just return");
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_DISABLE);
return;
}
if (ShareTinkerInternals.isInPatchProcess(app)) {
Log.w(TAG, "tryLoadPatchFiles: we don't load patch with :patch process itself, just return");
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_DISABLE);
return;
}
//tinker
File patchDirectoryFile = SharePatchFileUtil.getPatchDirectory(app);
if (patchDirectoryFile == null) {
Log.w(TAG, "tryLoadPatchFiles:getPatchDirectory == null");
//treat as not exist
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_DIRECTORY_NOT_EXIST);
return;
}
......
}
tinker的安裝初始化階段撇贺,我們假定patch dex還沒有生成赌莺,這樣加載失敗直接返回。后面分析patch dex生成后显熏,加載patch后的classes.dex的過程雄嚣,也就是hotfix如何生效的過程。
接下來是tinker的安裝過程喘蟆,暫不討論缓升。
0x22 合并patch dex生成oat文件
以tinker-sample-android Demo為例
我們從點擊load patch開始分析。
我們從點擊loadPatchButton
開始分析蕴轨。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.e(TAG, "i am on onCreate classloader:" + MainActivity.class.getClassLoader().toString());
//test resource change
Log.e(TAG, "i am on onCreate string:" + getResources().getString(R.string.test_resource));
// Log.e(TAG, "i am on patch onCreate");
Button loadPatchButton = (Button) findViewById(R.id.loadPatch);
loadPatchButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 加載Patch Dex文件/sdcard/patch_signed_7zip.apk
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");
}
});
......
}
繼續(xù)看onReceiveUpgradePatch()
的實現(xiàn)港谊。
public static void onReceiveUpgradePatch(Context context, String patchLocation) {
Tinker.with(context).getPatchListener().onPatchReceived(patchLocation);
}
getPatchListener()
返回SamplePatchListener
對象,SamplePatchListener
繼承自DefaultPatchListener
橙弱,下面看onPatchReceived()
的實現(xiàn)歧寺。
public int onPatchReceived(String path) {
File patchFile = new File(path);
// Patch檢查
int returnCode = patchCheck(path, SharePatchFileUtil.getMD5(patchFile));
if (returnCode == ShareConstants.ERROR_PATCH_OK) {
// 運行PatchService
TinkerPatchService.runPatchService(context, path);
} else {
Tinker.with(context).getLoadReporter().onLoadPatchListenerReceiveFail(new File(path), returnCode);
}
return returnCode;
}
TinkerPatchService
負責合并patch dex與原始apk的classes.dex以及dex優(yōu)化燥狰,運行在獨立的進程中。TinkerPatchService
繼承自IntentService
斜筐,這樣最終在onHandleIntent()
中處理服務(wù)請求龙致。
protected void onHandleIntent(Intent intent) {
final Context context = getApplicationContext();
Tinker tinker = Tinker.with(context);
tinker.getPatchReporter().onPatchServiceStart(intent);
......
String path = getPatchPathExtra(intent);
if (path == null) {
TinkerLog.e(TAG, "TinkerPatchService can't get the path extra, ignoring.");
return;
}
// patch文件
File patchFile = new File(path);
......
// 前臺服務(wù)
increasingPriority();
PatchResult patchResult = new PatchResult();
try {
if (upgradePatchProcessor == null) {
throw new TinkerRuntimeException("upgradePatchProcessor is null.");
}
// 嘗試安裝patch
result = upgradePatchProcessor.tryPatch(context, path, patchResult);
} catch (Throwable throwable) {
e = throwable;
result = false;
tinker.getPatchReporter().onPatchException(patchFile, e);
}
......
}
upgradePatchProcessor
是UpgradePatch
類的對象,下面看trypatch()
的實現(xiàn)顷链。
public boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult) {
Tinker manager = Tinker.with(context);
final File patchFile = new File(tempPatchPath);
......
if (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed");
return false;
}
......
}
tryRecoverDexFiles()
實現(xiàn)了dex文件的合并與oat文件的生成目代,本文只討論oat文件的生成過程。下面直接看dexOptimizeDexFiles()
方法嗤练。
private static boolean dexOptimizeDexFiles(Context context, List<File> dexFiles, String optimizeDexDirectory, final File patchFile) {
// 參數(shù)dexFiles表示需要優(yōu)化的dex文件(patch合成后)列表
// optimizeDexDirectory為oat文件的存放路徑
final Tinker manager = Tinker.with(context);
optFiles.clear();
if (dexFiles != null) {
File optimizeDexDirectoryFile = new File(optimizeDexDirectory);
......
// add opt files
for (File file : dexFiles) {
String outputPathName = SharePatchFileUtil.optimizedPathFor(file, optimizeDexDirectoryFile);
optFiles.add(new File(outputPathName));
}
......
// try parallel dex optimizer
// 串行優(yōu)化多個dex文件
TinkerDexOptimizer.optimizeAll(
dexFiles, optimizeDexDirectoryFile,
new TinkerDexOptimizer.ResultCallback() {
long startTime;
@Override
public void onStart(File dexFile, File optimizedDir) {
startTime = System.currentTimeMillis();
TinkerLog.i(TAG, "start to parallel optimize dex %s, size: %d", dexFile.getPath(), dexFile.length());
}
@Override
public void onSuccess(File dexFile, File optimizedDir, File optimizedFile) {
// Do nothing.
TinkerLog.i(TAG, "success to parallel optimize dex %s, opt file:%s, opt file size: %d, use time %d",
dexFile.getPath(), optimizedFile.getPath(), optimizedFile.length(), (System.currentTimeMillis() - startTime));
}
@Override
public void onFailed(File dexFile, File optimizedDir, Throwable thr) {
TinkerLog.i(TAG, "fail to parallel optimize dex %s use time %d",
dexFile.getPath(), (System.currentTimeMillis() - startTime));
failOptDexFile.add(dexFile);
throwable[0] = thr;
}
}
);
.......
}
}
optimizeAll()
方法對多個dex文件排序后榛了,調(diào)用OptimizeWorker
的run()
方法優(yōu)化dex。下面看run()
方法的實現(xiàn)煞抬。
public boolean run() {
try {
if (!SharePatchFileUtil.isLegalFile(dexFile)) {
if (callback != null) {
callback.onFailed(dexFile, optimizedDir,
new IOException("dex file " + dexFile.getAbsolutePath() + " is not exist!"));
return false;
}
}
if (callback != null) {
callback.onStart(dexFile, optimizedDir);
}
String optimizedPath = SharePatchFileUtil.optimizedPathFor(this.dexFile, this.optimizedDir);
if (useInterpretMode) {
interpretDex2Oat(dexFile.getAbsolutePath(), optimizedPath);
} else {
DexFile.loadDex(dexFile.getAbsolutePath(), optimizedPath, 0);
}
if (callback != null) {
callback.onSuccess(dexFile, optimizedDir, new File(optimizedPath));
}
} catch (final Throwable e) {
Log.e(TAG, "Failed to optimize dex: " + dexFile.getAbsolutePath(), e);
if (callback != null) {
callback.onFailed(dexFile, optimizedDir, e);
return false;
}
}
return true;
}
在run()
方法中霜大,如果采用interpret-only編譯dex,直接使用Android提供的dex2oat命令生成oat文件革答;否則使用DexFile.loadDex()
方法生成oat文件战坤。interpertDex2Oat()
以及DexFile.loadDex()
均會等待dex2oat進程結(jié)束才會返回。
Tinker之前采用并行優(yōu)化dex文件蝗碎,多個dex2oat進程同時編譯湖笨,系統(tǒng)負載太高,可能會引發(fā)ANR等問題蹦骑,現(xiàn)在代碼中已是串行。另外臀防,對于不緊急的patch眠菇,是否可以使用
JobScheduler
當系統(tǒng)空閑時來優(yōu)化dex文件。DexFile.loadDex()
最終也是通過dex2oat來生成oat文件袱衷,但默認的compiler-filter是speed捎废。
0x23 Hotfix如何生效
在tinker安裝時,TinkerLoader
的tryLoad()
會調(diào)用tryLoadPatchFilesInternal()
嘗試加載oat文件致燥,我們從這里開始分析登疗。
private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
final int tinkerFlag = app.getTinkerFlags();
// 一系列檢查
if (!ShareTinkerInternals.isTinkerEnabled(tinkerFlag)) {
Log.w(TAG, "tryLoadPatchFiles: tinker is disable, just return");
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_DISABLE);
return;
}
if (ShareTinkerInternals.isInPatchProcess(app)) {
Log.w(TAG, "tryLoadPatchFiles: we don't load patch with :patch process itself, just return");
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_DISABLE);
return;
}
//tinker
File patchDirectoryFile = SharePatchFileUtil.getPatchDirectory(app);
if (patchDirectoryFile == null) {
Log.w(TAG, "tryLoadPatchFiles:getPatchDirectory == null");
//treat as not exist
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_DIRECTORY_NOT_EXIST);
return;
}
......
//now we can load patch jar
// 滿足條件加載oat文件
if (isEnabledForDex) {
boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, patchVersionDirectory, oatDex, resultIntent, isSystemOTA);
......
}
......
}
下面分析loadTinkerJars()
.
public static boolean loadTinkerJars(final TinkerApplication application, String directory, String oatDir, Intent intentResult, boolean isSystemOTA) {
if (loadDexList.isEmpty() && classNDexInfo.isEmpty()) {
Log.w(TAG, "there is no dex to load");
return true;
}
......
ArrayList<File> legalFiles = new ArrayList<>();
// 非classesN.dex的dex文件
for (ShareDexDiffPatchInfo info : loadDexList) {
//for dalvik, ignore art support dex
if (isJustArtSupportDex(info)) {
continue;
}
String path = dexPath + info.realName;
File file = new File(path);
if (application.isTinkerLoadVerifyFlag()) {
long start = System.currentTimeMillis();
String checkMd5 = getInfoMd5(info);
if (!SharePatchFileUtil.verifyDexFileMd5(file, checkMd5)) {
//it is good to delete the mismatch file
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_MD5_MISMATCH);
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_MISMATCH_DEX_PATH,
file.getAbsolutePath());
return false;
}
Log.i(TAG, "verify dex file:" + file.getPath() + " md5, use time: " + (System.currentTimeMillis() - start));
}
legalFiles.add(file);
}
// verify merge classN.apk
if (isVmArt && !classNDexInfo.isEmpty()) {
File classNFile = new File(dexPath + ShareConstants.CLASS_N_APK_NAME);
long start = System.currentTimeMillis();
if (application.isTinkerLoadVerifyFlag()) {
// classesN.dex文件,請參考multidex
for (ShareDexDiffPatchInfo info : classNDexInfo) {
if (!SharePatchFileUtil.verifyDexFileMd5(classNFile, info.rawName, info.destMd5InArt)) {
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_MD5_MISMATCH);
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_MISMATCH_DEX_PATH,
classNFile.getAbsolutePath());
return false;
}
}
}
Log.i(TAG, "verify dex file:" + classNFile.getPath() + " md5, use time: " + (System.currentTimeMillis() - start));
legalFiles.add(classNFile);
}
......
try {
// 安裝dex文件嫌蚤,合法的dex文件保存在參數(shù)legalFiles辐益,optimizeDir為dex文件對應(yīng)的oat文件路徑
SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles);
} catch (Throwable e) {
Log.e(TAG, "install dexes failed");
// e.printStackTrace();
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_LOAD_EXCEPTION);
return false;
}
return true;
}
下面看installDexes()
,它是patch dex生效的關(guān)鍵所在脱吱,這里只分析SDK >= 24
的情況智政。
public static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List<File> files)
throws Throwable {
Log.i(TAG, "installDexes dexOptDir: " + dexOptDir.getAbsolutePath() + ", dex size:" + files.size());
if (!files.isEmpty()) {
files = createSortedAdditionalPathEntries(files);
ClassLoader classLoader = loader;
if (Build.VERSION.SDK_INT >= 24 && !checkIsProtectedApp(files)) {
// 創(chuàng)建新的應(yīng)用類加載器
classLoader = AndroidNClassLoader.inject(loader, application);
}
//because in dalvik, if inner class is not the same classloader with it wrapper class.
//it won't fail at dex2opt
if (Build.VERSION.SDK_INT >= 23) {
// 修改類加載器的DexPathList
V23.install(classLoader, files, dexOptDir);
} else if (Build.VERSION.SDK_INT >= 19) {
V19.install(classLoader, files, dexOptDir);
} else if (Build.VERSION.SDK_INT >= 14) {
V14.install(classLoader, files, dexOptDir);
} else {
V4.install(classLoader, files, dexOptDir);
}
//install done
sPatchDexCount = files.size();
Log.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" + sPatchDexCount);
if (!checkDexInstall(classLoader)) {
//reset patch dex
SystemClassLoaderAdder.uninstallPatchDex(classLoader);
throw new TinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL);
}
}
}
下面首先看AndroidNClassLoader
的inject()
方法。
public static AndroidNClassLoader inject(PathClassLoader originClassLoader, Application application) throws Exception {
// originClassLoader為應(yīng)用默認的類加載器
// application為應(yīng)用的Application對象
AndroidNClassLoader classLoader = createAndroidNClassLoader(originClassLoader, application);
// 把新的應(yīng)用類加載器設(shè)置到LoadedApk以及Thread中
reflectPackageInfoClassloader(application, classLoader);
return classLoader;
}
下面看createAndroidNClassLoader()
的實現(xiàn)箱蝠。
private static AndroidNClassLoader createAndroidNClassLoader(PathClassLoader originalClassLoader, Application application) throws Exception {
//let all element ""
// 創(chuàng)建繼承自PathClassLoader的應(yīng)用類加載器
final AndroidNClassLoader androidNClassLoader = new AndroidNClassLoader("", originalClassLoader, application);
final Field pathListField = ShareReflectUtil.findField(originalClassLoader, "pathList");
final Object originPathList = pathListField.get(originalClassLoader);
// To avoid 'dex file register with multiple classloader' exception on Android O, we must keep old
// dexPathList in original classloader so that after the newly loaded base dex was bound to
// AndroidNClassLoader we can still load class in base dex from original classloader.
// 用原類加載器的pathList創(chuàng)建新的類加載器的pathList
Object newPathList = recreateDexPathList(originPathList, androidNClassLoader);
// Update new classloader's pathList.
pathListField.set(androidNClassLoader, newPathList);
return androidNClassLoader;
}
下面看V23.install()
的實現(xiàn)续捂。
private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
/* The patched class loader is expected to be a descendant of
* dalvik.system.BaseDexClassLoader. We modify its
* dalvik.system.DexPathList pathList field to append additional DEX
* file entries.
*/
Field pathListField = ShareReflectUtil.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,
new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
if (suppressedExceptions.size() > 0) {
for (IOException e : suppressedExceptions) {
Log.w(TAG, "Exception in makePathElement", e);
throw e;
}
}
}
install()
主要負責加載新增的dex文件垦垂,以及將新增的dex文件與原來的dex文件列表合并起來。此后牙瓢,應(yīng)用的類加載工作由新類加載器接管劫拗。
如果dex文件沒有優(yōu)化安裝,
makePathElements
會觸發(fā)dex2oat.