0. Vorwort
Die Hot-Repair-Technologie ist für das Android-Projektmodul grundsätzlich wichtiger geworden. Hauptsächlich, weil nach der Online-Veröffentlichung eines Projekts unvermeidlich verschiedene Probleme auftreten und es zu teuer ist, sich zur Behebung von Problemen auf Releases zu verlassen.
Die aktuelle Hot-Repair-Technologie umfasst im Wesentlichen Alis AndFix, QZone Plan, den ideologischer Plan vorgeschlagen von Meituan und Tencents Tinker usw.
Unter diesen ist AndFix wahrscheinlich der einfachste (und der Tinker-Befehl). Die Zeilenzugriffsmethode ist ähnlich), aber bei AndFix gibt es bestimmte Kompatibilitätsprobleme und bei der QZone-Lösung eine negative Auswirkung auf die Leistung Es wird eine gewisse Auswirkung geben, und das Problem der Gedächtnisverwirrung tritt im Kunst-Modus auf Der von Meituan vorgeschlagene Ansatz basiert hauptsächlich auf dem Instant Run-Prinzip, das noch nicht Open Source ist und eine gute Kompatibilität aufweist.
Wenn Sie sich für eine Open-Source-Lösung entscheiden, ist Tinker derzeit die beste Wahl Eine grobe Prinzipanalyse von Tinker.
1. Prinzipübersicht
Tinkerwirdold.apk und new.apk machten Diff und bekamen patch.dex und dann mit der apk der lokalen Maschine classes.dex zusammenführen, generiert eine neue classes.dex, führt die zusammengeführten durch Reflektion während der Laufzeit dexFile wird vor dem geladenen dexElements Array platziert. Das Prinzip der Laufzeitsubstitution ähnelt tatsächlich der Lösung von
Qzone, die die Modifikation von dexElements. Der Unterschied zwischen ist: Qzone fügt patch.dex direkt an der Vorderseite des Arrays ein; 🎜>Tinker ist die volle Menge an zusammengeführtem dex, der vor dem Array eingefügt wird. Weil es ein Problem mit der CLASS_ISPREVERIFIED-Lösung gibt, die in der Qzone-Lösung erwähnt wird.
Androids ClassLoader verwendet im Allgemeinen PathClassLoader und DexClassLoader, um Klassen zu laden muss verstehen, dass Android PathClassLoader als Klassenlader , DexClassLoader kann intern aus Dateien des Typs .jar und .apkclasses.dex laden Datei wird in Ordnung sein. Um eine Klasse zu laden, geben Sie einfach einen Klassennamen ein und gehen Sie dann zu Klasse finden, PathClassLoader und DexClassLoader erben beide von BaseDexClassLoader. Es gibt den folgenden Quellcode in BaseDexClassLoader:
#BaseDexClassLoader @Override protected Class<?> findClass(String name) throws ClassNotFoundException { Class clazz = pathList.findClass(name); if (clazz == null) { throw new ClassNotFoundException(name); } return clazz; } #DexPathList public Class findClass(String name) { for (Element element : dexElements) { DexFile dex = element.dexFile; if (dex != null) { Class clazz = dex.loadClassBinaryName(name, definingContext); if (clazz != null) { return clazz; } } } return null; } #DexFile public Class loadClassBinaryName(String name, ClassLoader loader) { return defineClass(name, loader, mCookie); } private native static Class defineClass(String name, ClassLoader loader, int cookie);
wie Sie sehen können BaseDexClassLoader enthält ein pathList-Objekt und pathList enthält ein DexFile Die Sammlung dexElements, und für das Laden der Klasse muss diese Sammlung durchquert und über DexFile gefunden werden.
Laienhaft ausgedrückt kann ein ClassLoader mehrere dex-Dateien enthalten, jede A Die dex-Datei ist ein Element, und mehrere dex-Dateien werden zu einer zusammengefasst Geordnetes Array dexElements. Bei der Suche nach einer Klasse werden die dex-Dateien der Reihe nach durchlaufen und dann mit der aktuell durchlaufenen Datei begonnen dex, um die Klasse zu finden, wird sie zurückgegeben. Wenn sie nicht gefunden wird, fahren Sie mit der Suche ab dem nächsten dex Datei. In diesem Fall können wir unser patch.jar im ersten Element dieses Arrays platzieren, das die reparierte Klasse enthält. In diesem Fall, wenn Beim Durchlaufen von findClass wird die von uns korrigierte Klasse gefunden und ersetzt so die fehlerhafte Klasse .
Zwei Zeilen der Tinker-Quellcode-Analyse in diesem Artikel:(
1) Wenn die App startet, laden Sie die zusammengeführte Datei classes.dex
(2) aus der Standarddatei Verzeichnis Nachdem Patch ausgegeben wurde, synthetisieren Sie classes.dex in das Zielverzeichnis
2. Quellcode-Analyse 2.1 加载patch 加载的代码实际上在生成的Application中调用的,其父类为TinkerApplication,在其attachBaseContext中辗转会调用到loadTinker()方法,在该方法内部,反射调用了TinkerLoader的tryLoad方法。继而调用了tryLoadPatchFilesInternal方法。 其中TinkerDexLoader.checkComplete主要是用于检查下发的meta文件中记录的dex信息(meta文件,可以查看生成patch的产物,在assets/dex-meta.txt),检查meta文件中记录的dex文件信息对应的dex文件是否存在,并把值存在TinkerDexLoader的静态变量dexList中。 TinkerDexLoader.loadTinkerJars传入四个参数,分别为application,tinkerLoadVerifyFlag(注解上声明的值,传入为false),patchVersionDirectory当前version的patch文件夹,intent,当前patch是否仅适用于art。 找出仅支持art的dex,且当前patch是否仅适用于art时,并行去loadDex。关键是最后的installDexes: 这里实际上就是根据不同的系统版本,去反射处理dexElements。我们看一下V19的实现: (1)找到PathClassLoader(BaseDexClassLoader)对象中的pathList对象 (2)根据pathList对象找到其中的makeDexElements方法,传入patch相关的对应的实参,返回Element[]对象 (3)拿到pathList对象中原本的dexElements方法 (4)步骤2与步骤3中的Element[]数组进行合并,将patch相关的dex放在数组的前面 (5)最后将合并后的数组,设置给pathList
2.2 合成patch 入口为onReceiveUpgradePatch()方法: 上述代码会调用DefaultPatchListener中的onPatchReceived方法: 首先对Tinker的相关配置(isEnable)以及patch的合法性进行检测,如果合法,则调用TinkerPatchService.runPatchService(context, path)。 TinkerPatchService是IntentService的子类,这里通过intent设置了两个参数,一个是patch的路径,一个是resultServiceClass,该值是调用Tinker.install的时候设置的,默认为DefaultTinkerResultService.class。由于是IntentService,直接看onHandleIntent即可。 比较清晰,主要关注upgradePatchProcessor.tryPatch方法,调用的是UpgradePatch.tryPatch。这里有个有意思的地方increasingPriority(),其内部实现为: 如果你对“保活”这个话题比较关注,那么对这段代码一定不陌生,主要是利用系统的一个漏洞来启动一个前台Service。下面继续回到tryPatch方法: 拷贝patch文件拷贝至私有目录,然后调用DexDiffPatchInternal.tryRecoverDexFiles: 直接看patchDexExtractViaDexDiff: 核心代码主要在extractDexDiffInternals中: 这里的代码比较关键了,可以看出首先解析了meta里面的信息,meta中包含了patch中每个dex的相关数据。然后通过Application拿到sourceDir,其实就是本机apk的路径以及patch文件;根据mate中的信息开始遍历,其实就是取出对应的dex文件,最后通过patchDexFile对两个dex文件做合并。 erhält den InputStream seiner internen Datei über ZipFile, das tatsächlich von Lokale apk entsprechende dex-Datei und Patch Entspricht der dex-Datei und übergibt die Methode executeAndSaveTo an Merge into patchedDexFile, das private Zielverzeichnis von patch. Was den Zusammenführungsalgorithmus betrifft, ist dies eigentlich der Kern von Tinker Wenn Sie interessiert sind, können Sie sich auf diesen Artikel beziehen! @Override
public Intent tryLoad(TinkerApplication app, int tinkerFlag, boolean tinkerLoadVerifyFlag) {
Intent resultIntent = new Intent();
long begin = SystemClock.elapsedRealtime();
tryLoadPatchFilesInternal(app, tinkerFlag, tinkerLoadVerifyFlag, resultIntent);
long cost = SystemClock.elapsedRealtime() - begin;
ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);
return resultIntent;
}
private void tryLoadPatchFilesInternal(TinkerApplication app, int tinkerFlag, boolean tinkerLoadVerifyFlag, Intent resultIntent) {
// 省略大量安全性校验代码
if (isEnabledForDex) {
//tinker/patch.info/patch-641e634c/dex
boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
if (!dexCheck) {
//file not found, do not load patch
Log.w(TAG, "tryLoadPatchFiles:dex check fail");
return;
}
}
//now we can load patch jar
if (isEnabledForDex) {
boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, tinkerLoadVerifyFlag, patchVersionDirectory, resultIntent, isSystemOTA);
if (!loadTinkerJars) {
Log.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail");
return;
}
}
}
@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);
}
// just for art
if (isSystemOTA) {
parallelOTAResult = true;
parallelOTAThrowable = null;
Log.w(TAG, "systemOTA, try parallel oat dexes!!!!!");
TinkerParallelDexOptimizer.optimizeAll(
legalFiles, optimizeDir,
new TinkerParallelDexOptimizer.ResultCallback() {
}
);
SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles);
return true;
@SuppressLint("NewApi")
public static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List<File> files)
throws Throwable {
if (!files.isEmpty()) {
ClassLoader classLoader = loader;
if (Build.VERSION.SDK_INT >= 24) {
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) {
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);
}
}
}
private static final class V19 {
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);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
if (suppressedExceptions.size() > 0) {
for (IOException e : suppressedExceptions) {
Log.w(TAG, "Exception in makeDexElement", e);
throw e;
}
}
}
}
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),
Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed.apk");
# DefaultPatchListener
@Override
public int onPatchReceived(String path) {
int returnCode = patchCheck(path);
if (returnCode == ShareConstants.ERROR_PATCH_OK) {
TinkerPatchService.runPatchService(context, path);
} else {
Tinker.with(context).getLoadReporter().onLoadPatchListenerReceiveFail(new File(path), returnCode);
}
return returnCode;
}
public static void runPatchService(Context context, String path) {
try {
Intent intent = new Intent(context, TinkerPatchService.class);
intent.putExtra(PATCH_PATH_EXTRA, path);
intent.putExtra(RESULT_CLASS_EXTRA, resultServiceClass.getName());
context.startService(intent);
} catch (Throwable throwable) {
TinkerLog.e(TAG, "start patch service fail, exception:" + throwable);
}
}
@Override
protected void onHandleIntent(Intent intent) {
final Context context = getApplicationContext();
Tinker tinker = Tinker.with(context);
String path = getPatchPathExtra(intent);
File patchFile = new File(path);
boolean result;
increasingPriority();
PatchResult patchResult = new PatchResult();
result = upgradePatchProcessor.tryPatch(context, path, patchResult);
patchResult.isSuccess = result;
patchResult.rawPatchFilePath = path;
patchResult.costTime = cost;
patchResult.e = e;
AbstractResultService.runResultService(context, patchResult, getPatchResultExtra(intent));
}
private void increasingPriority() {
TinkerLog.i(TAG, "try to increase patch process priority");
try {
Notification notification = new Notification();
if (Build.VERSION.SDK_INT < 18) {
startForeground(notificationId, notification);
} else {
startForeground(notificationId, notification);
// start InnerService
startService(new Intent(this, InnerService.class));
}
} catch (Throwable e) {
TinkerLog.i(TAG, "try to increase patch process priority error:" + e);
}
}
# UpgradePatch
@Override
public boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult) {
Tinker manager = Tinker.with(context);
final File patchFile = new File(tempPatchPath);
//it is a new patch, so we should not find a exist
SharePatchInfo oldInfo = manager.getTinkerLoadResultIfPresent().patchInfo;
String patchMd5 = SharePatchFileUtil.getMD5(patchFile);
//use md5 as version
patchResult.patchVersion = patchMd5;
SharePatchInfo newInfo;
//already have patch
if (oldInfo != null) {
newInfo = new SharePatchInfo(oldInfo.oldVersion, patchMd5, Build.FINGERPRINT);
} else {
newInfo = new SharePatchInfo("", patchMd5, Build.FINGERPRINT);
}
//check ok, we can real recover a new patch
final String patchDirectory = manager.getPatchDirectory().getAbsolutePath();
final String patchName = SharePatchFileUtil.getPatchVersionDirectory(patchMd5);
final String patchVersionDirectory = patchDirectory + "/" + patchName;
//copy file
File destPatchFile = new File(patchVersionDirectory + "/" + SharePatchFileUtil.getPatchVersionFile(patchMd5));
// check md5 first
if (!patchMd5.equals(SharePatchFileUtil.getMD5(destPatchFile))) {
SharePatchFileUtil.copyFileUsingStream(patchFile, destPatchFile);
}
//we use destPatchFile instead of patchFile, because patchFile may be deleted during the patch process
if (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory,
destPatchFile)) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed");
return false;
}
return true;
}
protected static boolean tryRecoverDexFiles(Tinker manager, ShareSecurityCheck checker, Context context,
String patchVersionDirectory, File patchFile) {
String dexMeta = checker.getMetaContentMap().get(DEX_META_FILE);
boolean result = patchDexExtractViaDexDiff(context, patchVersionDirectory, dexMeta, patchFile);
return result;
}
private static boolean patchDexExtractViaDexDiff(Context context, String patchVersionDirectory, String meta, final File patchFile) {
String dir = patchVersionDirectory + "/" + DEX_PATH + "/";
if (!extractDexDiffInternals(context, dir, meta, patchFile, TYPE_DEX)) {
TinkerLog.w(TAG, "patch recover, extractDiffInternals fail");
return false;
}
final Tinker manager = Tinker.with(context);
File dexFiles = new File(dir);
File[] files = dexFiles.listFiles();
...files遍历执行:DexFile.loadDex
return true;
}
private static boolean extractDexDiffInternals(Context context, String dir, String meta, File patchFile, int type) {
//parse meta
ArrayList<ShareDexDiffPatchInfo> patchList = new ArrayList<>();
ShareDexDiffPatchInfo.parseDexDiffPatchInfo(meta, patchList);
File directory = new File(dir);
//I think it is better to extract the raw files from apk
Tinker manager = Tinker.with(context);
ZipFile apk = null;
ZipFile patch = null;
ApplicationInfo applicationInfo = context.getApplicationInfo();
String apkPath = applicationInfo.sourceDir; //base.apk
apk = new ZipFile(apkPath);
patch = new ZipFile(patchFile);
for (ShareDexDiffPatchInfo info : patchList) {
final String infoPath = info.path;
String patchRealPath;
if (infoPath.equals("")) {
patchRealPath = info.rawName;
} else {
patchRealPath = info.path + "/" + info.rawName;
}
File extractedFile = new File(dir + info.realName);
ZipEntry patchFileEntry = patch.getEntry(patchRealPath);
ZipEntry rawApkFileEntry = apk.getEntry(patchRealPath);
patchDexFile(apk, patch, rawApkFileEntry, patchFileEntry, info, extractedFile);
}
return true;
}
private static void patchDexFile(
ZipFile baseApk, ZipFile patchPkg, ZipEntry oldDexEntry, ZipEntry patchFileEntry,
ShareDexDiffPatchInfo patchInfo, File patchedDexFile) throws IOException {
InputStream oldDexStream = null;
InputStream patchFileStream = null;
oldDexStream = new BufferedInputStream(baseApk.getInputStream(oldDexEntry));
patchFileStream = (patchFileEntry != null ? new BufferedInputStream(patchPkg.getInputStream(patchFileEntry)) : null);
new DexPatchApplier(oldDexStream, patchFileStream).executeAndSaveTo(patchedDexFile);
}
Das obige ist der detaillierte Inhalt vonAndroid-Entwicklung – eine kurze Einführung in das Hotfixing von Tinker-Quellcode. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!