Tencent Tinker Hotfix开源方案https://github.com/Tencent/tinker
0x10 应用接入Tinker
请参考Tinker官方指导或者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在应用内的安装
从SampleApplication
开始分析。
首先看TinkerLoader
的tryLoad()
方法,tryLoad()
方法用于加载patch后的classes.dex文件。
public Intent tryLoad(TinkerApplication app) {
Intent resultIntent = new Intent();
long begin = SystemClock.elapsedRealtime();
// 实际调用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");
}
});
......
}
继续看onReceiveUpgradePatch()
的实现。
public static void onReceiveUpgradePatch(Context context, String patchLocation) {
Tinker.with(context).getPatchListener().onPatchReceived(patchLocation);
}
getPatchListener()
返回SamplePatchListener
对象,SamplePatchListener
继承自DefaultPatchListener
,下面看onPatchReceived()
的实现。
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优化,运行在独立的进程中。TinkerPatchService
继承自IntentService
,这样最终在onHandleIntent()
中处理服务请求。
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);
......
// 前台服务
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()
的实现。
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()
实现了dex文件的合并与oat文件的生成,本文只讨论oat文件的生成过程。下面直接看dexOptimizeDexFiles()
方法。
private static boolean dexOptimizeDexFiles(Context context, List dexFiles, String optimizeDexDirectory, final File patchFile) {
// 参数dexFiles表示需要优化的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
// 串行优化多个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文件排序后,调用OptimizeWorker
的run()
方法优化dex。下面看run()
方法的实现。
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进程结束才会返回。
Tinker之前采用并行优化dex文件,多个dex2oat进程同时编译,系统负载太高,可能会引发ANR等问题,现在代码中已是串行。另外,对于不紧急的patch,是否可以使用
JobScheduler
当系统空闲时来优化dex文件。DexFile.loadDex()
最终也是通过dex2oat来生成oat文件,但默认的compiler-filter是speed。
0x23 Hotfix如何生效
在tinker安装时,TinkerLoader
的tryLoad()
会调用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 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文件保存在参数legalFiles,optimizeDir为dex文件对应的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生效的关键所在,这里只分析SDK >= 24
的情况。
public static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List 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)) {
// 创建新的应用类加载器
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为应用默认的类加载器
// application为应用的Application对象
AndroidNClassLoader classLoader = createAndroidNClassLoader(originClassLoader, application);
// 把新的应用类加载器设置到LoadedApk以及Thread中
reflectPackageInfoClassloader(application, classLoader);
return classLoader;
}
下面看createAndroidNClassLoader()
的实现。
private static AndroidNClassLoader createAndroidNClassLoader(PathClassLoader originalClassLoader, Application application) throws Exception {
//let all element ""
// 创建继承自PathClassLoader的应用类加载器
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创建新的类加载器的pathList
Object newPathList = recreateDexPathList(originPathList, androidNClassLoader);
// Update new classloader's pathList.
pathListField.set(androidNClassLoader, newPathList);
return androidNClassLoader;
}
下面看V23.install()
的实现。
private static void install(ClassLoader loader, List 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 suppressedExceptions = new ArrayList();
ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,
new ArrayList(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
if (suppressedExceptions.size() > 0) {
for (IOException e : suppressedExceptions) {
Log.w(TAG, "Exception in makePathElement", e);
throw e;
}
}
}
install()
主要负责加载新增的dex文件,以及将新增的dex文件与原来的dex文件列表合并起来。此后,应用的类加载工作由新类加载器接管。
如果dex文件没有优化安装,
makePathElements
会触发dex2oat.