Tinker和Instant Run的并存
Tinker 非代理模式的启动
Tinker的启动流程
加载补丁入口
前面介绍到了加载补丁的地方主要在TinkerLoader的tryLoad方法中,代码如下:
private void loadTinker() {
try {
//reflect tinker loader, because loaderClass may be define by user!
Class<?> tinkerLoadClass = Class.forName(loaderClassName, false, TinkerApplication.class.getClassLoader());
Method loadMethod = tinkerLoadClass.getMethod(TINKER_LOADER_METHOD, TinkerApplication.class);
Constructor<?> constructor = tinkerLoadClass.getConstructor();
tinkerResultIntent = (Intent) loadMethod.invoke(constructor.newInstance(), this);
} catch (Throwable e) {
//has exception, put exception error code
tinkerResultIntent = new Intent();
ShareIntentUtil.setIntentReturnCode(tinkerResultIntent, ShareConstants.ERROR_LOAD_PATCH_UNKNOWN_EXCEPTION);
tinkerResultIntent.putExtra(INTENT_PATCH_EXCEPTION, e);
}
}
loaderClassName这个方法是manifest中的真正的application中的创建时传递的类。一般情况是com.tencent.tinker.loader.TinkerLoader这个类,通过反射创建这个类之后,调用TINKER_LOADER_METHOD即tryLoad这个方法。之前我们直接跳过了tryLoad 这个方法分析应用的启动,现在分析下tryLoad这个加载补丁的流程,tryLoad及之后的流程才是tinker的关键所在。
patch文件的检查
TinkerLoader继承自抽象类AbstractTinkerLoader,AbstractTinkerLoader只有一个抽象方法tryLoad。TinkerLoader实现了tryLoad方法,这个方法中主要调用了tryLoadPatchFilesInternal,这个方法执行主要的逻辑。主要代码如下:
private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
final int tinkerFlag = app.getTinkerFlags();// 在RealApplication中传入的参数
......省略部分异常检查代码,检查包括是否开启tinker,是否非patch进程,patch文件是否存在等。
//tinker/patch.info
//1 获取patch.info.有点疑问,这个patch.info文件是什么时候放到这个目录下的?文件内容是什么?
File patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectoryPath);
//old = 641e634c5b8f1649c75caf73794acbdf
//new = 2c150d8560334966952678930ba67fa8
File patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectoryPath);
//2 eadAndCheckPropertyWithLock有点玄机,lock 应该是和同步相关,利用文件实现的同步?
//读取patch.info文件中的内容
patchInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);
if (patchInfo == null) {
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_CORRUPTED);
return;
}
......
//patch-641e634c
String patchName = SharePatchFileUtil.getPatchVersionDirectory(version);
//tinker/patch.info/patch-641e634c
String patchVersionDirectory = patchDirectoryPath + "/" + patchName;
File patchVersionDirectoryFile = new File(patchVersionDirectory);
//tinker/patch.info/patch-641e634c/patch-641e634c.apk
final String patchVersionFileRelPath = SharePatchFileUtil.getPatchVersionFile(version);
File patchVersionFile = (patchVersionFileRelPath != null ? new File(patchVersionDirectoryFile.getAbsolutePath(), patchVersionFileRelPath) : null);
ShareSecurityCheck securityCheck = new ShareSecurityCheck(app);
// 3 对patch文件进行检查,包括tinkerId,以及patch中的文件。
int returnCode = ShareTinkerInternals.checkTinkerPackage(app, tinkerFlag, patchVersionFile, securityCheck);
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_PACKAGE_CONFIG, securityCheck.getPackagePropertiesIfPresent());
final boolean isEnabledForDex = ShareTinkerInternals.isTinkerEnabledForDex(tinkerFlag);
final boolean isEnabledForNativeLib = ShareTinkerInternals.isTinkerEnabledForNativeLib(tinkerFlag);
//check resource
final boolean isEnabledForResource = ShareTinkerInternals.isTinkerEnabledForResource(tinkerFlag);
//only work for art platform oat,because of interpret, refuse 4.4 art oat
//android o use quicken default, we don't need to use interpret mode
boolean isSystemOTA = ShareTinkerInternals.isVmArt()
&& ShareTinkerInternals.isSystemOTA(patchInfo.fingerPrint)
&& Build.VERSION.SDK_INT >= 21 && !ShareTinkerInternals.isAfterAndroidO();
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_SYSTEM_OTA, isSystemOTA);
.......
}
tryLoadPatchFilesInternal这个方法非常繁琐,主要是能否进行patch的前提进行检查,包括是否开启tinker功能,是否在非patch进程,是否存在patch文件,以及检查patch文件是否符合要求等。另外检查过程中有个疑问,patch.info文件是什么是创建的? 这个答案需要我们来解答。
接下来看tryLoadPatchFilesInternal的剩余一部分,代码如下:
private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
final boolean isEnabledForDex = ShareTinkerInternals.isTinkerEnabledForDex(tinkerFlag);
// 判断逻辑中有“com.huawei.ark.app.ArkApplicationInfo”的类名,貌似是华为的方舟,后续先认为isArkHotRuning结果都是false
final boolean isArkHotRuning = ShareTinkerInternals.isArkHotRuning();
if (!isArkHotRuning && isEnabledForDex) {
//tinker/patch.info/patch-641e634c/dex
boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, oatDex, resultIntent);
if (!dexCheck) {
//file not found, do not load patch
Log.w(TAG, "tryLoadPatchFiles:dex check fail");
return;
}
}
final boolean isEnabledForArkHot = ShareTinkerInternals.isTinkerEnabledForArkHot(tinkerFlag);
if (isArkHotRuning && isEnabledForArkHot) {
boolean arkHotCheck = TinkerArkHotLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
if (!arkHotCheck) {
// file not found, do not load patch
Log.w(TAG, "tryLoadPatchFiles:dex check fail");
return;
}
}
final boolean isEnabledForNativeLib = ShareTinkerInternals.isTinkerEnabledForNativeLib(tinkerFlag);
if (isEnabledForNativeLib) {
//tinker/patch.info/patch-641e634c/lib
boolean libCheck = TinkerSoLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
if (!libCheck) {
//file not found, do not load patch
Log.w(TAG, "tryLoadPatchFiles:native lib check fail");
return;
}
}
//check resource
final boolean isEnabledForResource = ShareTinkerInternals.isTinkerEnabledForResource(tinkerFlag);
Log.w(TAG, "tryLoadPatchFiles:isEnabledForResource:" + isEnabledForResource);
if (isEnabledForResource) {
boolean resourceCheck = TinkerResourceLoader.checkComplete(app, patchVersionDirectory, securityCheck, resultIntent);
if (!resourceCheck) {
//file not found, do not load patch
Log.w(TAG, "tryLoadPatchFiles:resource check fail");
return;
}
}
//only work for art platform oat,because of interpret, refuse 4.4 art oat
//android o use quicken default, we don't need to use interpret mode
boolean isSystemOTA = ShareTinkerInternals.isVmArt()
&& ShareTinkerInternals.isSystemOTA(patchInfo.fingerPrint)
&& Build.VERSION.SDK_INT >= 21 && !ShareTinkerInternals.isAfterAndroidO();
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_SYSTEM_OTA, isSystemOTA);
//we should first try rewrite patch info file, if there is a error, we can't load jar
if (mainProcess) {
if (versionChanged) {
patchInfo.oldVersion = version;
}
if (oatModeChanged) {
patchInfo.oatDir = oatDex;
// delete interpret odex
// for android o, directory change. Fortunately, we don't need to support android o interpret mode any more
Log.i(TAG, "tryLoadPatchFiles:oatModeChanged, try to delete interpret optimize files");
SharePatchFileUtil.deleteDir(patchVersionDirectory + "/" + ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH);
}
}
if (!checkSafeModeCount(app)) {
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, new TinkerRuntimeException("checkSafeModeCount fail"));
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_UNCAUGHT_EXCEPTION);
Log.w(TAG, "tryLoadPatchFiles:checkSafeModeCount fail");
return;
}
//now we can load patch jar
if (!isArkHotRuning && isEnabledForDex) {
boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, patchVersionDirectory, oatDex, resultIntent, isSystemOTA, isProtectedApp);
if (isSystemOTA) {
// update fingerprint after load success
patchInfo.fingerPrint = Build.FINGERPRINT;
patchInfo.oatDir = loadTinkerJars ? ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH : ShareConstants.DEFAULT_DEX_OPTIMIZE_PATH;
// reset to false
oatModeChanged = false;
if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile)) {
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_REWRITE_PATCH_INFO_FAIL);
Log.w(TAG, "tryLoadPatchFiles:onReWritePatchInfoCorrupted");
return;
}
// update oat dir
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OAT_DIR, patchInfo.oatDir);
}
if (!loadTinkerJars) {
Log.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail");
return;
}
}
if (isArkHotRuning && isEnabledForArkHot) {
boolean loadArkHotFixJars = TinkerArkHotLoader.loadTinkerArkHot(app, patchVersionDirectory, resultIntent);
if (!loadArkHotFixJars) {
Log.w(TAG, "tryLoadPatchFiles:onPatchLoadArkApkFail");
return;
}
}
......
}
在tryLoadPatchFilesInternal这个方法的第二部分分别对各项标志位进行赋值,包括是否支持dex进行修复,是否支持对资源文件进行修复,是否支持对native文件进行修复等。其中还有sSystemOTA判断,只要用户是ART环境并且做了OTA升级,则在加载dex补丁的时候,就会先把最近一次的补丁全部DexFile.loadDex一遍。这么做的原因是有些场景做了OTA后,oat的规则可能发生变化,在这种情况下去加载上个系统版本oat过的dex就会出现问题。由此可见,将东西做好的代价有点大。可惟其如此,才有真正的价值。这里先假设所有的检查都是通过,先看主流程。检查之后就是开始加载的过程。
开始加载
private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
......
//now we can load patch jar
if (!isArkHotRuning && isEnabledForDex) {
boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, patchVersionDirectory, oatDex, resultIntent, isSystemOTA, isProtectedApp);
if (isSystemOTA) {
// update fingerprint after load success
patchInfo.fingerPrint = Build.FINGERPRINT;
patchInfo.oatDir = loadTinkerJars ? ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH : ShareConstants.DEFAULT_DEX_OPTIMIZE_PATH;
// reset to false
oatModeChanged = false;
if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile)) {
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_REWRITE_PATCH_INFO_FAIL);
Log.w(TAG, "tryLoadPatchFiles:onReWritePatchInfoCorrupted");
return;
}
// update oat dir
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OAT_DIR, patchInfo.oatDir);
}
if (!loadTinkerJars) {
Log.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail");
return;
}
}
if (isArkHotRuning && isEnabledForArkHot) {
boolean loadArkHotFixJars = TinkerArkHotLoader.loadTinkerArkHot(app, patchVersionDirectory, resultIntent);
if (!loadArkHotFixJars) {
Log.w(TAG, "tryLoadPatchFiles:onPatchLoadArkApkFail");
return;
}
}
//now we can load patch resource
if (isEnabledForResource) {
boolean loadTinkerResources = TinkerResourceLoader.loadTinkerResources(app, patchVersionDirectory, resultIntent);
if (!loadTinkerResources) {
Log.w(TAG, "tryLoadPatchFiles:onPatchLoadResourcesFail");
return;
}
}
// Init component hotplug support.
if ((isEnabledForDex || isEnabledForArkHot) && isEnabledForResource) {
ComponentHotplug.install(app, securityCheck);
}
// Before successfully exit, we should update stored version info and kill other process
// to make them load latest patch when we first applied newer one.
if (mainProcess && versionChanged) {
//update old version to new
if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile)) {
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_REWRITE_PATCH_INFO_FAIL);
Log.w(TAG, "tryLoadPatchFiles:onReWritePatchInfoCorrupted");
return;
}
ShareTinkerInternals.killProcessExceptMain(app);
}
//all is ok!
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_OK);
Log.i(TAG, "tryLoadPatchFiles: load end, ok!");
return;
}
加载补丁的方法因为多个参数而有所区别,这里我们重点看下loadTinkerJars()和loadTinkerResources这两个方法。最后加载成功后将patch的版本更新到文件,然后将其他进程都杀掉,最后将patch成功的信息返回。如果tinker更新成功之后,下次启动是否还需要进行patch的过程呢?按道理是不需要的,此处更新了版本信息应该是出于这个目的。热更的两个点一个是代码更新,一个是资源更新,分别对应于loadTinkerJars和loadTinkerResources这两个方法,接下来我们分析这个两个过程。
代码加载
在loadTinkerJars方法里面调用了一些关于patch文件的信息,这些信息是在checkComplete中准备好的,所以先看下checkComplete这个方法,代码如下:
public static boolean checkComplete(String directory, ShareSecurityCheck securityCheck, String oatDir, Intent intentResult) {
String meta = securityCheck.getMetaContentMap().get(DEX_MEAT_FILE);
//not found dex
if (meta == null) {
return true;
}
LOAD_DEX_LIST.clear();
classNDexInfo.clear();
ArrayList<ShareDexDiffPatchInfo> allDexInfo = new ArrayList<>();
// 解析patch文件中assets/dex_meta.txt的内容
ShareDexDiffPatchInfo.parseDexDiffPatchInfo(meta, allDexInfo);
if (allDexInfo.isEmpty()) {
return true;
}
HashMap<String, String> dexes = new HashMap<>();
ShareDexDiffPatchInfo testInfo = null;
for (ShareDexDiffPatchInfo info : allDexInfo) {
//for dalvik, ignore art support dex
if (isJustArtSupportDex(info)) {
continue;
}
if (!ShareDexDiffPatchInfo.checkDexDiffPatchInfo(info)) {
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_PACKAGE_PATCH_CHECK, ShareConstants.ERROR_PACKAGE_CHECK_DEX_META_CORRUPTED);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_PACKAGE_CHECK_FAIL);
return false;
}
if (isVmArt && info.rawName.startsWith(ShareConstants.TEST_DEX_NAME)) {
testInfo = info;
} else if (isVmArt && ShareConstants.CLASS_N_PATTERN.matcher(info.realName).matches()) {
classNDexInfo.add(info);
} else {
dexes.put(info.realName, getInfoMd5(info));
LOAD_DEX_LIST.add(info);
}
}
if (isVmArt
&& (testInfo != null || !classNDexInfo.isEmpty())) {
if (testInfo != null) {
classNDexInfo.add(ShareTinkerInternals.changeTestDexToClassN(testInfo, classNDexInfo.size() + 1));
}
dexes.put(ShareConstants.CLASS_N_APK_NAME, "");
}
...... 省略部分代码
//if is ok, add to result intent
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_DEXES_PATH, dexes);
return true;
}
checkComplete中主要检查patch中assets/dex_meta.txt文件的内容,这个文件包含了patch中dex的名字,md5,crc校验等信息。这个文件是在patch打包时生成。生成时机是在配置完成后,大概流程如下:
TinkerPatchPlugin.apply->Runner.inkerPatch->ApkDecoder.patch()->ApkFilesVisitor.visitFile()->UniqueDexDiffDecoder.patch()->DexDiffDecoder.onAllPatchesEnd()
patch文件如何生成,此处先按照上面这样理解,后续再详细分析。这里我们先回过来头接着分析TinkerDexLoader的loadTinkerJars方法。
// private static File testOptDexFile;
private static HashSet<ShareDexDiffPatchInfo> classNDexInfo = new HashSet<>();
// classNDexInfo这个容器中的内容是上面checkComplete中添加进入的。
public static boolean loadTinkerJars(final TinkerApplication application, String directory, String oatDir, Intent intentResult, boolean isSystemOTA, boolean isProtectedApp) {
if (LOAD_DEX_LIST.isEmpty() && classNDexInfo.isEmpty()) {
Log.w(TAG, "there is no dex to load");
return true;
}
BaseDexClassLoader classLoader = (BaseDexClassLoader) TinkerDexLoader.class.getClassLoader();
if (classLoader != null) {
Log.i(TAG, "classloader: " + classLoader.toString());
} else {
Log.e(TAG, "classloader is null");
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_CLASSLOADER_NULL);
return false;
}
String dexPath = directory + "/" + DEX_PATH + "/";
ArrayList<File> legalFiles = new ArrayList<>();
for (ShareDexDiffPatchInfo info : LOAD_DEX_LIST) {
//for dalvik, ignore art support dex
if (isJustArtSupportDex(info)) {
continue;
}
String path = dexPath + info.realName;
File file = new File(path);
// isTinkerLoadVerifyFlag这个标志在项目的applictaion中配置,跳过可以加快速度
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));
}
// 将patch文件中的dex信息加入到legalFiles中
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();
// 跳过对dex进行md5校验的步骤,一般设置为跳过,可以加快速度
legalFiles.add(classNFile);
}
File optimizeDir = new File(directory + "/" + oatDir);
// 省略isSystemOTA为真的情况
try {
SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles, isProtectedApp);
} catch (Throwable e) {
Log.e(TAG, "install dexes failed");
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_LOAD_EXCEPTION);
return false;
}
return true;
}
loadTinkerJars这个方法也是将patch中的信息处理加工,最后调用SystemClassLoaderAdder.installDexes方法开始加载patch中的dex文件。代码如下:
public static void installDexes(Application application, BaseDexClassLoader loader, File dexOptDir, List<File> files, boolean isProtectedApp)
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 && !isProtectedApp) {
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);
}
}
}
经过一系列的校验,终于到了加载dex文件的地方了。如果是在24以上的机器,就调用AndroidNClassLoader的inject对classloader进行处理,为什么处理的原因在这里Android N混合编译与对热补丁影响解析。inject的过程有点复杂,我们先忽略。接下来按照系统版本分为了四种方式加载补丁,这么做的原因的加载的过程有变化。各个历史版本的代码可以在这里来看AOSPXRef,非常方便。我们先分析下V23中的install过程。代码如下:
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;
}
}
}
通过反射拿到BaseDexClassLoader中的pathList,然后调用expandFieldArray更改这个成员变量中dexElements,用makePathElements组装好的Elements数组替换原来的dexElements的值。类的加载都是由BaseDexClassLoader中的findclass来查找后加载,而findclass就是从pathList这个数组中查找的。如果扩展了原来BaseDexClassLoader的pathList变量,从其中加入patch中的class,那就能替换原来出问题的class。所以我们想怎样让pathList这个变量中加入我们前面patch文件中dex的class。DexPathList这个类中有个 Element数组dexElements,我们可以通过反射的方式将Element数组更改,按照类加载器的原理,加载到想要的类后就会直接返回,因此如果我们只要将patch中dex插入到Element数组前面。之后加载特定类就会从patch中的dex加载修复后的类,而不会从原有Element数组加载原来的类。插队的操作是在expandFieldArray这个方法中完成,代码如下:
/**
* Replace the value of a field containing a non null array, by a new array containing the
* elements of the original array plus the elements of extraElements.
*
* @param instance the instance whose field is to be modified.
* @param fieldName the field to modify.
* @param extraElements elements to append at the end of the array.
*/
public static void expandFieldArray(Object instance, String fieldName, Object[] extraElements)
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Field jlrField = findField(instance, fieldName);
Object[] original = (Object[]) jlrField.get(instance);
Object[] combined = (Object[]) Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length);
// NOTE: changed to copy extraElements first, for patch load first
System.arraycopy(extraElements, 0, combined, 0, extraElements.length);
System.arraycopy(original, 0, combined, extraElements.length, original.length);
jlrField.set(instance, combined);
}
通过数组拷贝的方式将patch中dex生成的Element插入到数组的前面,而patch中的dex如何转为成Element呢?答案在makePathElements方法中,代码如下:
/**
* A wrapper around
* {@code private static final dalvik.system.DexPathList#makePathElements}.
*/
private static Object[] makePathElements(
Object dexPathList, ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions)
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Method makePathElements;
try {
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);
}
这个方法也是通过反射调用DexPathList中的makePathElements方法,最终将dex转变为Elelments。下面看下makePathElements方法,代码如下:
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
Element[] elements = new Element[files.size()];
int elementsPos = 0;
/*
* Open all files and load the (direct or contained) dex files up front.
*/
for (File file : files) {
if (file.isDirectory()) {
// We support directories for looking up resources. Looking up resources in
// directories is useful for running libcore tests.
elements[elementsPos++] = new Element(file);
} else if (file.isFile()) {
String name = file.getName();
DexFile dex = null;
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
if (dex != null) {
elements[elementsPos++] = new Element(dex, null);
}
} catch (IOException suppressed) {
System.logE("Unable to load dex file: " + file, suppressed);
suppressedExceptions.add(suppressed);
}
} else {
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
/*
* IOException might get thrown "legitimately" by the DexFile constructor if
* the zip file turns out to be resource-only (that is, no classes.dex file
* in it).
* Let dex == null and hang on to the exception to add to the tea-leaves for
* when findClass returns null.
*/
suppressedExceptions.add(suppressed);
}
if (dex == null) {
elements[elementsPos++] = new Element(file);
} else {
elements[elementsPos++] = new Element(dex, file);
}
}
if (dex != null && isTrusted) {
dex.setTrusted();
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
}
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}
return elements;
}
在这个方法中通过loadDexFile 将dex转化为Elelments。然后将Element返回。至此我们分析了dex的校验,加载,以及最后的插入到dexPathList中。其他版本最后的install方法也比较类似,其他流程的讲解参考Android 热修复方案Tinker(三) Dex补丁加载.这篇文章讲的比较详细,可以结合不同版本的代码学习下。
参考链接
感谢微信的开源,让tinker成为了小厂的基础设置,让很多应用开发者摆脱了热更的诸多麻烦。
感谢先行者的详细分析和无私分享。功力尚浅,请不吝指教。
网友评论