IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Tinker源码解析 -> 正文阅读

[移动开发]Tinker源码解析

Tinker

Git项目地址:https://github.com/Tencent/tinker

本例解析tag为v1.9.14.19

一、概述

Tinker is a hot-fix solution library for Android, it supports dex, library and resources update without reinstalling apk.

在这里插入图片描述
tinker作为Android一款热修复框架,实践应用在微信上,其稳定性,兼容性不言而喻;看下官方说明
在这里插入图片描述
上图来自tinker官方,我截了个图,可以看到tinker相比其他热修复框架优势还是非常明显的,作为Android开发有必要探究下起内部实现机制,今天就来扒一扒tinker皮

tinker基本接入步骤直接忽略,下面以tinker-sample-android为例进行逐步解析,既然Tinker是一款免安装针对Android的热补丁修复框架,热修复 必定绕过不二部分

  1. class修复,在Android中也就是dex修复
  2. 资源修复

二、Tinker在启动做了什么?

先看下SampleApplication

image-20220314113732420

这里的SampleApplication是由tinker-android-anno自动生成的,可以看到它且继承了TinkerApplication,且在构造器中传入了一个参数给父类,里面包含了代理类自定义加载器类名

在这里插入图片描述

我们直接看TinkerApplication.attachBaseContext好了

@Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        final long applicationStartElapsedTime = SystemClock.elapsedRealtime();
        final long applicationStartMillisTime = System.currentTimeMillis();
        Thread.setDefaultUncaughtExceptionHandler(new TinkerUncaughtHandler(this));
        onBaseContextAttached(base, applicationStartElapsedTime, applicationStartMillisTime);
    }
  1. 设置了Tinker异常处理器,当有异常出现时记录堆栈信息并持久化到本地
  2. 调用onBaseContextAttached
    1. loadTinker
    2. 调用delegateClassName对象即SampleApplicationLike.onBaseContextAttached方法
// TinkerApplication
protected void onBaseContextAttached(Context base, 
                                     long applicationStartElapsedTime,
                                     long applicationStartMillisTime) {
        try {
            loadTinker();
            mCurrentClassLoader = base.getClassLoader();
            mInlineFence = createInlineFence(this, tinkerFlags, delegateClassName,
                    tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime,
                    tinkerResultIntent);
            TinkerInlineFenceAction.callOnBaseContextAttached(mInlineFence, base);
            //reset save mode
            if (useSafeMode) {
                ShareTinkerInternals.setSafeModeCount(this, 0);
            }
        } catch (TinkerRuntimeException e) {
            throw e;
        } catch (Throwable thr) {
            throw new TinkerRuntimeException(thr.getMessage(), thr);
        }
    }

loadTinker是其tinker启动加载核心;内部其实就是寻找并解压补丁包,并动态加载

三、loadTinker

image-20220314115707431

我们看到loadTinker代码实现非常简单,是调用loaderClassName即com.tencent.tinker.loader.TinkerLoader.tryLoad方法,当然这里的tinkerLoader对象也是反射搞出来的

tryLoad内部实现了啥?可以大胆猜测是对下发的补丁包进行解压,解压后有二类资源一类是dex文件,一类是资源文件;对于dex文件可以直接使用DexClassLoader实现动态加载,对于资源文件可以通过反射调用addAssetPath将资源告知给系统;接下来看下tinker内部实现是不是这样做的?

@Override
    public Intent tryLoad(TinkerApplication app) {
        ShareTinkerLog.d(TAG, "tryLoad test test");
        Intent resultIntent = new Intent();

        long begin = SystemClock.elapsedRealtime();
      	// 1. 核心代码
        tryLoadPatchFilesInternal(app, resultIntent);
        long cost = SystemClock.elapsedRealtime() - begin;
      
      	// 2. 记录补丁包耗时
        ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);
        return resultIntent;
    }

tryLoadPatchFilesInternal内部实现有330行代码左右,但主流程非常清晰,我们以功能来划分逐部拆解即可,主要分为四个小块

  1. 读取补丁信息
  2. patch版本相关处理
  3. dex,res,so等校验处理
  4. load patch dex

1. 解析补丁信息

private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
		 final int tinkerFlag = app.getTinkerFlags();
		// 一些校验处理
        if (!ShareTinkerInternals.isTinkerEnabled(tinkerFlag)) {
            ShareTinkerLog.w(TAG, "tryLoadPatchFiles: tinker is disable, just return");
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_DISABLE);
            return;
        }
        if (ShareTinkerInternals.isInPatchProcess(app)) {
            ShareTinkerLog.w(TAG, "tryLoadPatchFiles: we don't load patch with :patch process itself, just return");
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_DISABLE);
            return;
        }
  
  			// 1. 获取补丁包位置
        //tinker
        File patchDirectoryFile = SharePatchFileUtil.getPatchDirectory(app);
        if (patchDirectoryFile == null) {
            ShareTinkerLog.w(TAG, "tryLoadPatchFiles:getPatchDirectory == null");
            //treat as not exist
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_DIRECTORY_NOT_EXIST);
            return;
        }
        String patchDirectoryPath = patchDirectoryFile.getAbsolutePath();
				
  			// 2. 补丁目录校验
        //check patch directory whether exist
        if (!patchDirectoryFile.exists()) {
            ShareTinkerLog.w(TAG, "tryLoadPatchFiles:patch dir not exist:" + patchDirectoryPath);
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_DIRECTORY_NOT_EXIST);
            return;
        }
				
        //tinker/patch.info  3. 读取补丁相关信息
        File patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectoryPath);

        //check patch info file whether exist
        if (!patchInfoFile.exists()) {
            ShareTinkerLog.w(TAG, "tryLoadPatchFiles:patch info not exist:" + patchInfoFile.getAbsolutePath());
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_NOT_EXIST);
            return;
        }
        //old = 641e634c5b8f1649c75caf73794acbdf
        //new = 2c150d8560334966952678930ba67fa8
        File patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectoryPath);
				
  			// 4. 解析补丁信息
        patchInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);
        if (patchInfo == null) {
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_CORRUPTED);
            return;
        }	
}

解析补丁信息(readAndCheckPropertyWithLock)字段包括如下

old --> oldVer
new --> newVer
is_protected_app --> 加固app
is_remove_new_version
print --> finger print
dir --> oat dir
is_remove_interpret_oat_dir

2. patch版本处理

private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {				
				// ...
				final boolean isProtectedApp = patchInfo.isProtectedApp;
        resultIntent.putExtra(ShareIntentUtil.INTENT_IS_PROTECTED_APP, isProtectedApp);

        String oldVersion = patchInfo.oldVersion;
        String newVersion = patchInfo.newVersion;
        String oatDex = patchInfo.oatDir;

        if (oldVersion == null || newVersion == null || oatDex == null) {
            //it is nice to clean patch
            ShareTinkerLog.w(TAG, "tryLoadPatchFiles:onPatchInfoCorrupted");
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_CORRUPTED);
            return;
        }

        boolean mainProcess = ShareTinkerInternals.isInMainProcess(app);
        boolean isRemoveNewVersion = patchInfo.isRemoveNewVersion;

        if (mainProcess) {
            final String patchName = SharePatchFileUtil.getPatchVersionDirectory(newVersion);
            // So far new version is not loaded in main process and other processes.
            // We can remove new version directory safely.
            if (isRemoveNewVersion) {
                ShareTinkerLog.w(TAG, "found clean patch mark and we are in main process, delete patch file now.");
                if (patchName != null) {
                    // oldVersion.equals(newVersion) means the new version has been loaded at least once
                    // after it was applied.
                    final boolean isNewVersionLoadedBefore = oldVersion.equals(newVersion);
                    if (isNewVersionLoadedBefore) {
                        // Set oldVersion and newVersion to empty string to clean patch
                        // if current patch has been loaded before.
                        oldVersion = "";
                    }
                    newVersion = oldVersion;
                    patchInfo.oldVersion = oldVersion;
                    patchInfo.newVersion = newVersion;
                    patchInfo.isRemoveNewVersion = false;
                    SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile);

                    String patchVersionDirFullPath = patchDirectoryPath + "/" + patchName;
                    SharePatchFileUtil.deleteDir(patchVersionDirFullPath);

                    if (isNewVersionLoadedBefore) {
                        ShareTinkerInternals.killProcessExceptMain(app);
                        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_DIRECTORY_NOT_EXIST);
                        return;
                    }
                }
            }
            if (patchInfo.isRemoveInterpretOATDir) {
                // delete interpret odex
                // for android o, directory change. Fortunately, we don't need to support android o interpret mode any more
                ShareTinkerLog.i(TAG, "tryLoadPatchFiles: isRemoveInterpretOATDir is true, try to delete interpret optimize files");

                patchInfo.isRemoveInterpretOATDir = false;
                SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile);
                ShareTinkerInternals.killProcessExceptMain(app);
                String patchVersionDirFullPath = patchDirectoryPath + "/" + patchName;
                SharePatchFileUtil.deleteDir(patchVersionDirFullPath + "/" + ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH);
            }
        }

        resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OLD_VERSION, oldVersion);
        resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_NEW_VERSION, newVersion);

        boolean versionChanged = !(oldVersion.equals(newVersion));
        boolean oatModeChanged = oatDex.equals(ShareConstants.CHANING_DEX_OPTIMIZE_PATH);
        oatDex = ShareTinkerInternals.getCurrentOatMode(app, oatDex);
        resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OAT_DIR, oatDex);

        String version = oldVersion;
        if (versionChanged && mainProcess) {
            version = newVersion;
        }
        if (ShareTinkerInternals.isNullOrNil(version)) {
            ShareTinkerLog.w(TAG, "tryLoadPatchFiles:version is blank, wait main process to restart");
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_BLANK);
            return;
        }

        //patch-641e634c
        String patchName = SharePatchFileUtil.getPatchVersionDirectory(version);
        if (patchName == null) {
            ShareTinkerLog.w(TAG, "tryLoadPatchFiles:patchName is null");
            //we may delete patch info file
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_VERSION_DIRECTORY_NOT_EXIST);
            return;
        }
        //tinker/patch.info/patch-641e634c
        String patchVersionDirectory = patchDirectoryPath + "/" + patchName;

        File patchVersionDirectoryFile = new File(patchVersionDirectory);

        if (!patchVersionDirectoryFile.exists()) {
            ShareTinkerLog.w(TAG, "tryLoadPatchFiles:onPatchVersionDirectoryNotFound");
            //we may delete patch info file
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_VERSION_DIRECTORY_NOT_EXIST);
            return;
        }

        //tinker/patch.info/patch-641e634c/patch-641e634c.apk
        final String patchVersionFileRelPath = SharePatchFileUtil.getPatchVersionFile(version);
        File patchVersionFile = (patchVersionFileRelPath != null ? new File(patchVersionDirectoryFile.getAbsolutePath(), patchVersionFileRelPath) : null);

        if (!SharePatchFileUtil.isLegalFile(patchVersionFile)) {
            ShareTinkerLog.w(TAG, "tryLoadPatchFiles:onPatchVersionFileNotFound");
            //we may delete patch info file
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_VERSION_FILE_NOT_EXIST);
            return;
        }

        ShareSecurityCheck securityCheck = new ShareSecurityCheck(app);

        int returnCode = ShareTinkerInternals.checkTinkerPackage(app, tinkerFlag, patchVersionFile, securityCheck);
        if (returnCode != ShareConstants.ERROR_PACKAGE_CHECK_OK) {
            ShareTinkerLog.w(TAG, "tryLoadPatchFiles:checkTinkerPackage");
            resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_PACKAGE_PATCH_CHECK, returnCode);
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_PACKAGE_CHECK_FAIL);
            return;
        }

        resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_PACKAGE_CONFIG, securityCheck.getPackagePropertiesIfPresent());
 				// ... 
}

3. 校验

private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
  		// ..
        //tinker/patch.info/patch-641e634c/patch-641e634c.apk
        final String patchVersionFileRelPath = SharePatchFileUtil.getPatchVersionFile(version);
        File patchVersionFile = (patchVersionFileRelPath != null ? new File(patchVersionDirectoryFile.getAbsolutePath(), patchVersionFileRelPath) : null);
				
				// 1. 校验apk文件是否可读
        if (!SharePatchFileUtil.isLegalFile(patchVersionFile)) {
            ShareTinkerLog.w(TAG, "tryLoadPatchFiles:onPatchVersionFileNotFound");
            //we may delete patch info file
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_VERSION_FILE_NOT_EXIST);
            return;
        }
        ShareSecurityCheck securityCheck = new ShareSecurityCheck(app);
				// 2. 校验补丁包apk签名校验,以及TinkerID
        int returnCode = ShareTinkerInternals.checkTinkerPackage(app, tinkerFlag, patchVersionFile, securityCheck);
        if (returnCode != ShareConstants.ERROR_PACKAGE_CHECK_OK) {
          ShareTinkerLog.w(TAG, "tryLoadPatchFiles:checkTinkerPackage");
          resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_PACKAGE_PATCH_CHECK, returnCode);
          ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_PACKAGE_CHECK_FAIL);
          return;
        }
  
  			resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_PACKAGE_CONFIG, securityCheck.getPackagePropertiesIfPresent());
}

上述一大坨代码,都是一些清理,和补丁包apk的校验操作

assets/so_meta.txt格式

$name,$path,$md5,$rawCrc,$pathMd5

对应ShareBsDiffPatchInfo

assets/res_meta.txt格式 ==> ShareResPatchInfo

$arscBasseCrc,$resArscMd5 //firstLine

Dex校验

final boolean isEnabledForDex = ShareTinkerInternals.isTinkerEnabledForDex(tinkerFlag);
  
// 方舟编译器
final boolean isArkHotRuning = ShareTinkerInternals.isArkHotRuning();

if (!isArkHotRuning && isEnabledForDex) {
  // 3. 解析dex_meta.txt以校验dex是否有丢失(所有dex是否都在assets/dex_meta.txt中)
  //tinker/patch.info/patch-641e634c/dex
  boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, oatDex, resultIntent);
  if (!dexCheck) {
    //file not found, do not load patch
    ShareTinkerLog.w(TAG, "tryLoadPatchFiles:dex check fail");
    return;
  }
}

先说下dex_meta.txt配置文件格式

$name,$path,$destMd5InDvm,$dexMd5InArt,$dexDiffMd5,$oldDexCrc,$newDexCrc,$dexMode

接下来逐步分析如何校验dex文件

/**
     * all the dex files in meta file exist?
     * fast check, only check whether exist
     *
     * @return boolean
     */
public static boolean checkComplete(String directory, ShareSecurityCheck securityCheck, String oatDir, Intent intentResult) {
  		// 1. 获取dex_meta.txt文件内容
        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<>();
  		
  		// 2. 解析dex_meta.txt内容
        ShareDexDiffPatchInfo.parseDexDiffPatchInfo(meta, allDexInfo);

        if (allDexInfo.isEmpty()) {
            return true;
        }

        HashMap<String, String> dexes = new HashMap<>();

        ShareDexDiffPatchInfo testInfo = null;
				
  			// 3. 对解析出的dex相关信息列表进行遍历
        for (ShareDexDiffPatchInfo info : allDexInfo) {
          	
          	// dalvik虚拟机不支持多dex加载,直接忽略
            //for dalvik, ignore art support dex
            if (isJustArtSupportDex(info)) {
                continue;
            }
          	
          	// rawName及md5长度校验
            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;
            }
          	
          	// 过滤test.dex
            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 {
              	// 记录dex
                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, "");
        }
        //tinker/patch.info/patch-641e634c/dex
        String dexDirectory = directory + "/" + DEX_PATH + "/";

        File dexDir = new File(dexDirectory);
				// dexDir相关校验
        if (!dexDir.exists() || !dexDir.isDirectory()) {
            ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_DIRECTORY_NOT_EXIST);
            return false;
        }
        String optimizeDexDirectory = directory + "/" + oatDir + "/";
        File optimizeDexDirectoryFile = new File(optimizeDexDirectory);
			
        //fast check whether there is any dex files missing
        for (String name : dexes.keySet()) {
            File dexFile = new File(dexDirectory + name);
						
          	// 对每个dex文件做合法(存在&可读)判断
            if (!SharePatchFileUtil.isLegalFile(dexFile)) {
                intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_MISSING_DEX_PATH, dexFile.getAbsolutePath());
                ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_FILE_NOT_EXIST);
                return false;
            }
            //check dex opt whether complete also
            File dexOptFile = new File(SharePatchFileUtil.optimizedPathFor(dexFile, optimizeDexDirectoryFile));
          	// 对odex文件进行校验处理
            if (!SharePatchFileUtil.isLegalFile(dexOptFile)) {
                if (SharePatchFileUtil.shouldAcceptEvenIfIllegal(dexOptFile)) {
                    continue;
                }
                intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_MISSING_DEX_PATH, dexOptFile.getAbsolutePath());
                ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_OPT_FILE_NOT_EXIST);
                return false;
            }
            // // find test dex
            // if (dexOptFile.getName().startsWith(ShareConstants.TEST_DEX_NAME)) {
            //     testOptDexFile = dexOptFile;
            // }
        }

        //if is ok, add to result intent
        intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_DEXES_PATH, dexes);
        return true;
    }

这部操作总结如下:

  • 解析dex_meta.txt配置文件
  • 从配置文件解析出的dex进行文件校验及odex文件校验(本次属于快速校验,所以没有对每个dex做完整性校验)

华为方舟编译器校验

// 4. 校验是否运行在华为ark环境下且是否可用
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
    ShareTinkerLog.w(TAG, "tryLoadPatchFiles:dex check fail");
    return;
  }
}

SO校验

// 5. 校验so文件
final boolean isEnabledForNativeLib = ShareTinkerInternals.isTinkerEnabledForNativeLib(tinkerFlag);

if (isEnabledForNativeLib) {
  // 解析so_meta.tx文件以校验so文件是否存在丢失
  //tinker/patch.info/patch-641e634c/lib
  boolean libCheck = TinkerSoLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
  if (!libCheck) {
    //file not found, do not load patch
    ShareTinkerLog.w(TAG, "tryLoadPatchFiles:native lib check fail");
    return;
  }
}

so库校验其实就是解析so_meta.tx文件中信息以校验meta中记载的文件是否存在

so_meta.tx每行格式如下(对应ShareBsDiffPatchInfo类)

$name,$path,$md5,$rawCrc,$pathMd5

资源校验

// 6. 资源校验
//check resource
final boolean isEnabledForResource = ShareTinkerInternals.isTinkerEnabledForResource(tinkerFlag);
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:isEnabledForResource:" + isEnabledForResource);
if (isEnabledForResource) {
  // 校验资源包完整性;dir/res/resources.apk是否存在且可读;资源是否可以patch()
  boolean resourceCheck = TinkerResourceLoader.checkComplete(app, patchVersionDirectory, securityCheck, resultIntent);
  if (!resourceCheck) {
    //file not found, do not load patch
    ShareTinkerLog.w(TAG, "tryLoadPatchFiles:resource check fail");
    return;
  }
}

总结:资源校验其实分为二个步骤

  1. 资源完整性校验
  2. 资源是否可以下发修复校验
1. 资源完整性校验
public static boolean checkComplete(Context context, String directory, ShareSecurityCheck securityCheck, Intent intentResult) {
    String meta = securityCheck.getMetaContentMap().get(RESOURCE_META_FILE);
    //not found resource
    if (meta == null) {
        return true;
    }
    //only parse first line for faster
    ShareResPatchInfo.parseResPatchInfoFirstLine(meta, resPatchInfo);

    if (resPatchInfo.resArscMd5 == null) {
        return true;
    }
  	
  	// 完整性校验
    if (!ShareResPatchInfo.checkResPatchInfo(resPatchInfo)) {
        intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_PACKAGE_PATCH_CHECK, ShareConstants.ERROR_PACKAGE_CHECK_RESOURCE_META_CORRUPTED);
        ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_PACKAGE_CHECK_FAIL);
        return false;
    }
    String resourcePath = directory + "/" + RESOURCE_PATH + "/";

    File resourceDir = new File(resourcePath);

    if (!resourceDir.exists() || !resourceDir.isDirectory()) {
        ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_DIRECTORY_NOT_EXIST);
        return false;
    }

    File resourceFile = new File(resourcePath + RESOURCE_FILE);
  	// resources.apk是否存在可读
    if (!SharePatchFileUtil.isLegalFile(resourceFile)) {
        ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_FILE_NOT_EXIST);
        return false;
    }
    try {
      	// 2. 校验资源是否可以进行修复
        TinkerResourcePatcher.isResourceCanPatch(context);
    } catch (Throwable e) {
        ShareTinkerLog.e(TAG, "resource hook check failed.", e);
        intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
        ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_LOAD_EXCEPTION);
        return false;
    }
    return true;
}

这步主要做了如下:

  • 解析res_meta.txt文件以判断resArscMd5是否合法(长度为32即可)
  • 校验resources.apk是否存在且可读

assets/res_meta.txt格式如下 ==> ShareResPatchInfo

$arscBasseCrc,$resArscMd5 //firstLine
2. 校验资源是否可以修复
TinkerResourcePatcher.isResourceCanPatch(context)
  • 4.4-7.0)获取取android.app.ResourcesManage.getInstance()中的mActiveResources属性,

  • 7.0及以后开始获取mActiveResources属性

  • 4.4以前获取android.app.ActivityThread中的mActiveResources

如果获取属性失败,则会抛出异常,说明资源不能patch下发
在这里插入图片描述

编译模式

之所以要说这个是,在android不同版本情况下,app安装时针对apk会启用不同编译模式处理,这就导致单纯地通过将补丁包中的dex动态插入到dexElements到前面不再那么好使,所以要分为治之

ART vs Dalvik

Android Runtime (ART) 是 Android 上的应用和部分系统服务使用的托管式运行时。ART 及其前身 Dalvik 最初是专为 Android 项目打造的。作为运行时的 ART 可执行 Dalvik 可执行文件并遵循 Dex 字节码规范。
ART 和 Dalvik 是运行 Dex 字节码的兼容运行时,因此针对 Dalvik 开发的应用也能在 ART 环境中运作。不过,Dalvik 采用的一些技术并不适用于 ART

JIT:just-in-time 即时编译,边运行边编译(Android2.2版本被引入,4.4版本之后被AOT替代),JIT只对热点函数,热点trace进行编译,非热点函授还是走解释器;JIT编译生成的机器码存储在内存中,app下起启动时需要重新编译热点代码

参考:https://source.android.com/devices/tech/dalvik/jit-compiler?hl=zh-cn

JIT架构

在这里插入图片描述

JIT 编译

在这里插入图片描述

ART虚拟机引入了AOT预编译模式,旨在提高应用性能;在APK安装时使用设备自带的dex2oat工具编译应用
AOT:ahead of time 提前编译,即安装app时编译成机器码存储到硬盘。这样app运行时直接从本地取到机器码然后执行,提高代码执行效率

? .dex --> dex2oat —> .oat文件

Android不同版本启用模式差异如下

  • ~ 至4.4 JIT编译模式

  • 5.x、6.x默认 AOT模式

  • 7.0(Android-N)混合编译模式,开始结合使用AOT、JIT编译和配置文件引导型编译

private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
   		// ...
   		// 判断是否系统进行OTA升级 【5.0,8.0)
		//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;
               patchInfo.isRemoveInterpretOATDir = true;
           }
       }
	   // 判断是否是安全模式(加载补丁是否失败超过了三次,超过三次后直接删除对应补丁回退)
       if (!checkSafeModeCount(app)) {
           if (mainProcess) {
             	// 主进程杀死其他同应用的进程,同时删除补丁包
               // Mark current patch as deleted so that other process will not load patch after reboot.
               patchInfo.oldVersion = "";
               patchInfo.newVersion = "";
               patchInfo.isRemoveNewVersion = false;
               SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile);
               ShareTinkerInternals.killProcessExceptMain(app);

               // Actually delete patch files.
               String patchVersionDirFullPath = patchDirectoryPath + "/" + patchName;
               SharePatchFileUtil.deleteDir(patchVersionDirFullPath);

               resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, new TinkerRuntimeException("checkSafeModeCount fail"));
               ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_UNCAUGHT_EXCEPTION);
               ShareTinkerLog.w(TAG, "tryLoadPatchFiles:checkSafeModeCount fail, patch was deleted.");
               return;
           } else {
             	// isRemoveNewVersion设置为true,以便下次启动时清除patch
               ShareTinkerLog.w(TAG, "tryLoadPatchFiles:checkSafeModeCount fail, but we are not in main process, mark the patch to be deleted and continue load patch.");
               ShareTinkerInternals.cleanPatch(app);
           }
       }
  			// ... 
  }

4. load patch dex

		//now we can load patch jar
        if (!isArkHotRuning && isEnabledForDex) {
        	// load tinker jars
            boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, patchVersionDirectory, oatDex, resultIntent, isSystemOTA, isProtectedApp);

            if (isSystemOTA) {
                // update fingerprint after load success
                ...
                // 针对OTA做的一些处理
            }
            if (!loadTinkerJars) {
                ShareTinkerLog.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail");
                return;
            }
        }

        if (isArkHotRuning && isEnabledForArkHot) {
            boolean loadArkHotFixJars = TinkerArkHotLoader.loadTinkerArkHot(app, patchVersionDirectory, resultIntent);
            if (!loadArkHotFixJars) {
                ShareTinkerLog.w(TAG, "tryLoadPatchFiles:onPatchLoadArkApkFail");
                return;
            }
        }

这部分主要做了如下操作

  • 非方舟编译器调用TinkerDexLoader.loadTinkerJars加载jar
  • jar加载成功,针对ota的系统,更新patch.fingerPrint、otaDir等
  • 如果是运行在方舟编译器中,单独使用TinkerArkHotLoader.loadTinkerArkHot做处理

我们先看看loadTinkerJars吧

/**
     * Load tinker JARs and add them to
     * the Application ClassLoader.
     *
     * @param application The application.
     */
    public static boolean loadTinkerJars(final TinkerApplication application, String directory, String oatDir, Intent intentResult, boolean isSystemOTA, boolean isProtectedApp) {
      	// 1. check下是否存有dex(校验dex步骤回解析出相关的dex文件)
        if (LOAD_DEX_LIST.isEmpty() && classNDexInfo.isEmpty()) {
            ShareTinkerLog.w(TAG, "there is no dex to load");
            return true;
        }
				
      	// 2. classLoader check
        ClassLoader classLoader = TinkerDexLoader.class.getClassLoader();
        if (classLoader != null) {
            ShareTinkerLog.i(TAG, "classloader: " + classLoader.toString());
        } else {
            ShareTinkerLog.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);

            if (application.isTinkerLoadVerifyFlag()) {
                long start = System.currentTimeMillis();
                String checkMd5 = getInfoMd5(info);
              	// 3. dex完整性校验
                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;
                }
                ShareTinkerLog.i(TAG, "verify dex file:" + file.getPath() + " md5, use time: " + (System.currentTimeMillis() - start));
            }
          	
          	// 4. 记录合法dex文件
            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()) {
                for (ShareDexDiffPatchInfo info : classNDexInfo) {
                  	// 5. 从tinker_classN.apk中提取dex进行完整性校验
                    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;
                    }
                }
            }
            ShareTinkerLog.i(TAG, "verify dex file:" + classNFile.getPath() + " md5, use time: " + (System.currentTimeMillis() - start));

            legalFiles.add(classNFile);
        }
      
      	// 6. oat逻辑处理
        File optimizeDir = new File(directory + "/" + oatDir);

        if (isSystemOTA) {
            final boolean[] parallelOTAResult = {true};
            final Throwable[] parallelOTAThrowable = new Throwable[1];
            String targetISA;
            try {
                targetISA = ShareTinkerInternals.getCurrentInstructionSet();
            } catch (Throwable throwable) {
                ShareTinkerLog.i(TAG, "getCurrentInstructionSet fail:" + throwable);
                // try {
                //     targetISA = ShareOatUtil.getOatFileInstructionSet(testOptDexFile);
                // } catch (Throwable throwable) {
                // don't ota on the front
                deleteOutOfDateOATFile(directory);

                intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_INTERPRET_EXCEPTION, throwable);
                ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_GET_OTA_INSTRUCTION_SET_EXCEPTION);
                return false;
                // }
            }
			// 清理oat文件
            deleteOutOfDateOATFile(directory);

            ShareTinkerLog.w(TAG, "systemOTA, try parallel oat dexes, targetISA:" + targetISA);
            // change dir
            optimizeDir = new File(directory + "/" + INTERPRET_DEX_OPTIMIZE_PATH);
			
			// 解释模式 dex2oat优化
            TinkerDexOptimizer.optimizeAll(
                  application, legalFiles, optimizeDir, true,
                  application.isUseDelegateLastClassLoader(), targetISA,
                  new TinkerDexOptimizer.ResultCallback() {
                      long start;

                      @Override
                      public void onStart(File dexFile, File optimizedDir) {
                          start = System.currentTimeMillis();
                          ShareTinkerLog.i(TAG, "start to optimize dex:" + dexFile.getPath());
                      }

                      @Override
                      public void onSuccess(File dexFile, File optimizedDir, File optimizedFile) {
                          // Do nothing.
                          ShareTinkerLog.i(TAG, "success to optimize dex " + dexFile.getPath() + ", use time " + (System.currentTimeMillis() - start));
                      }

                      @Override
                      public void onFailed(File dexFile, File optimizedDir, Throwable thr) {
                          parallelOTAResult[0] = false;
                          parallelOTAThrowable[0] = thr;
                          ShareTinkerLog.i(TAG, "fail to optimize dex " + dexFile.getPath() + ", use time " + (System.currentTimeMillis() - start));
                      }
                  }
            );


            if (!parallelOTAResult[0]) {
                ShareTinkerLog.e(TAG, "parallel oat dexes failed");
                intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_INTERPRET_EXCEPTION, parallelOTAThrowable[0]);
                ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_OTA_INTERPRET_ONLY_EXCEPTION);
                return false;
            }
        }
        try {
            final boolean useDLC = application.isUseDelegateLastClassLoader();
          	// 安装dex
            SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles, isProtectedApp, useDLC);
        } catch (Throwable e) {
            ShareTinkerLog.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;
    }
  • dex文件的完整性校验
  • 如果系统是ota升级,则清理oat相关文件,并以解释模式,进行dex2oat优化
  • 安装dex(7.0开始使用NewClassLoader来规避混合编译带来的问题,之前版本使用常规方法,将dex插入原ClassLoader.pathList.dexElements列表最前面)
public static void installDexes(Application application, ClassLoader loader, File dexOptDir, List<File> files,
                                    boolean isProtectedApp, boolean useDLC) throws Throwable {
        ShareTinkerLog.i(TAG, "installDexes dexOptDir: " + dexOptDir.getAbsolutePath() + ", dex size:" + files.size());

        if (!files.isEmpty()) {
            files = createSortedAdditionalPathEntries(files);
            ClassLoader classLoader = loader;
          	//[7.0之后,使用NewClassLoader规避混合编译带来的问题
            if (Build.VERSION.SDK_INT >= 24 && !isProtectedApp) {
                classLoader = NewClassLoaderInjector.inject(application, loader, dexOptDir, useDLC, files);
            } else {
              	// dex插到classLoader.pathList.dexElements前面
                injectDexesInternal(classLoader, files, dexOptDir);
            }
            //install done
            sPatchDexCount = files.size();
            ShareTinkerLog.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的加载

7.0之前dex加载

7.0之前加载dex比较简单,这里以6.0为例

image-20220318090856987

从上图可以看到读取loader中的pathList(DexPathList)中dexElements,热修复的dex会插入到dexElements数组前面,这样就达到热修复目的,为什么?这涉及到类加载机制了

/**
 * Base class for common functionality between various dex-based
 * {@link ClassLoader} implementations.
 */
public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

    /**
     * Constructs an instance.
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @param optimizedDirectory directory where optimized dex files
     * should be written; may be {@code null}
     * @param libraryPath the list of directories containing native
     * libraries, delimited by {@code File.pathSeparator}; may be
     * {@code null}
     * @param parent the parent class loader
     */
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }
    /**
     * @hide
     */
    public String getLdLibraryPath() {
        StringBuilder result = new StringBuilder();
        for (File directory : pathList.getNativeLibraryDirectories()) {
            if (result.length() > 0) {
                result.append(':');
            }
            result.append(directory);
        }

        return result.toString();
    }
}

在这里插入图片描述

为什么要插到pathList中?这里需要了解下java类加载机制了,java采用双亲代理机制模型,说白了就是类加载都是先交给父classLoader去加载,如果父类搞不定则由自己来解决
在这里插入图片描述

看到BaseDexClassLoader.findClass方法就明白了,而加载类是依次从pathList中寻找的;
在这里插入图片描述

顺便说下getLdLibraryPath方法应该获取so库的路径,猜想下tinker对so的处理也会涉及到pathList中的nativeLibraryDirectories

7.0及以后dex加载

先了解下ART运行方式

ART 的运作方式

ART 使用预先 (AOT) 编译,并且从 Android 7.0(代号 Nougat,简称 N)开始结合使用 AOT、即时 (JIT) 编译和配置文件引导型编译。所有这些编译模式的组合均可配置,我们将在本部分中对此进行介绍。例如,Pixel 设备配置了以下编译流程:

  1. 最初安装应用时不进行任何 AOT 编译。应用前几次运行时,系统会对其进行解译,并对经常执行的方法进行 JIT 编译。
  2. 当设备闲置和充电时,编译守护程序会运行,以便根据在应用前几次运行期间生成的配置文件对常用代码进行 AOT 编译。
  3. 下一次重新启动应用时将会使用配置文件引导型代码,并避免在运行时对已经过编译的方法进行 JIT 编译。在应用后续运行期间经过 JIT 编译的方法将会添加到配置文件中,然后编译守护程序将会对这些方法进行 AOT 编译。

ART 包括一个编译器(dex2oat 工具)和一个为启动 Zygote 而加载的运行时 (libart.so)。dex2oat 工具接受一个 APK 文件,并生成一个或多个编译工件文件,然后运行时将会加载这些文件。文件的个数、扩展名和名称因版本而异,但在 Android O 版本中,将会生成以下文件:

  • .vdex:其中包含 APK 的未压缩 DEX 代码,以及一些旨在加快验证速度的元数据。
  • .odex:其中包含 APK 中已经过 AOT 编译的方法代码。
  • .art (optional):其中包含 APK 中列出的某些字符串和类的 ART 内部表示,用于加快应用启动速度。

微信对于N上混合编译的解决方案是使用新的ClassLoader来加载后续的所有类,这样尽管牺牲了App Image带来的优化性能

无论是使用插入pathlist还是parent classloader的方式,若补丁修改的class已经存在与app image,它们都是无法通过热补丁更新的。它们在启动app时已经加入到PathClassloader的ClassTable中,系统在查找类时会直接使用base.apk中的class**。**

口说无凭,我们看下7.0中classLoader类loadClass和之前版本到底有什么差异性?
image-20220324142608007

protected final Class<?> findLoadedClass(String name) {
        ClassLoader loader;
        if (this == BootClassLoader.getInstance())
            loader = null;
        else
            loader = this;
        return VMClassLoader.findLoadedClass(loader, name);
    }

7.0开始VMClassLoder.findLoaderClass 会先从ClassLinker.LookupClass方法中取

在这里插入图片描述

继续跟踪下去原来ClassLinker.LookupClass方法是先从classtable中寻找类,如果有直接返回

在这里插入图片描述

到这时,我们明白了为什么7.0开始通过动态加载dex这种方式会失效了,就是classLoader本身自带的缓存导致的;为解决这问题,Tinker的解决方案是自定类加载器以规避该问题,但首次会有一定的性能损耗

接下来看Tinker在7.0及后面版本如何自定义DexClassLoader及实现注入的

// NewClassLoaderInjector.java
public static ClassLoader inject(Application app, ClassLoader oldClassLoader, File dexOptDir,
                                     boolean useDLC, List<File> patchedDexes) throws Throwable {
        final String[] patchedDexPaths = new String[patchedDexes.size()];
        for (int i = 0; i < patchedDexPaths.length; ++i) {
            patchedDexPaths[i] = patchedDexes.get(i).getAbsolutePath();
        }
  			
  			// 1. 创建新的classLoader
        final ClassLoader newClassLoader = createNewClassLoader(oldClassLoader,
              dexOptDir, useDLC, true, patchedDexPaths);
  			
  		// 2. 注入自定义的classLoader
        doInject(app, newClassLoader);
        return newClassLoader;
    }
  1. 创建新的classLoader并迁移数据

    • 先找到老的so相关库列表
    • image-20220318161828698

    将需要加载的patchDex路径,及原so库路径传递给新classLoader

  2. 注入自定义classLoader

    • 替换Application.mBase对象(ContextImpl)的mClassLoader
    • 替换mBase对象中的**mPackageInfo(LoaderAPK)**的mClassLoader
    • 8.1以前还需要替换app.getResources()的mClassLoader、mDrawableInflater对象的mClassLoader

通过自定义代理ClassLoader实现了运行时先从代理ClassLoader加载类,后从原始ClassLoader加载,以解决混合编译模式下热修复失效问题

五、补丁资源的加载

代码

				//now we can load patch resource
        if (isEnabledForResource) {
            boolean loadTinkerResources = TinkerResourceLoader.loadTinkerResources(app, patchVersionDirectory, resultIntent);
            if (!loadTinkerResources) {
                ShareTinkerLog.w(TAG, "tryLoadPatchFiles:onPatchLoadResourcesFail");
                return;
            }
        }

资源加载相对来说比较简单

  1. 资源映射表文件resources.arsc md5校验

  2. TinkerResourcePatcher.monkeyPatchExistingResources

    image-20220318174036918
    • 修改ActivityThread中mPackages、mResourcepackages中LoaderApk中的resDir为新的资源包路径
    • 调用新建的newAssetmanager.addAssetPath方法将资源路径告知系统
    • 对于N开始版本处理下分享库(动态添加)
    • 对于resources中的assets字段统一用newAssetmanger对象替换
    • 其他一些兼容性问题

贴下代码

/**
     * @param context
     * @param externalResourceFile
     * @throws Throwable
     */
    public static void monkeyPatchExistingResources(Context context, String externalResourceFile) throws Throwable {
        if (externalResourceFile == null) {
            return;
        }

        final ApplicationInfo appInfo = context.getApplicationInfo();

        final Field[] packagesFields;
        if (Build.VERSION.SDK_INT < 27) {
            packagesFields = new Field[]{packagesFiled, resourcePackagesFiled};
        } else {
            packagesFields = new Field[]{packagesFiled};
        }
      
      	// 1. ActivityThread中mPackages、mResourcepackages中**LoaderApk**中的**resDir**为新的资源包路径
        for (Field field : packagesFields) {
            final Object value = field.get(currentActivityThread);

            for (Map.Entry<String, WeakReference<?>> entry
                    : ((Map<String, WeakReference<?>>) value).entrySet()) {
                final Object loadedApk = entry.getValue().get();
                if (loadedApk == null) {
                    continue;
                }
                final String resDirPath = (String) resDir.get(loadedApk);
                if (appInfo.sourceDir.equals(resDirPath)) {
                    resDir.set(loadedApk, externalResourceFile);
                }
            }
        }

        // Create a new AssetManager instance and point it to the resources installed under
        if (((Integer) addAssetPathMethod.invoke(newAssetManager, externalResourceFile)) == 0) {
            throw new IllegalStateException("Could not create new AssetManager");
        }
				
      	// 2. 补充分享库到newAssetmanager
        // Add SharedLibraries to AssetManager for resolve system resources not found issue
        // This influence SharedLibrary Package ID
        if (shouldAddSharedLibraryAssets(appInfo)) {
            for (String sharedLibrary : appInfo.sharedLibraryFiles) {
                if (!sharedLibrary.endsWith(".apk")) {
                    continue;
                }
                if (((Integer) addAssetPathAsSharedLibraryMethod.invoke(newAssetManager, sharedLibrary)) == 0) {
                    throw new IllegalStateException("AssetManager add SharedLibrary Fail");
                }
                Log.i(TAG, "addAssetPathAsSharedLibrary " + sharedLibrary);
            }
        }

        // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
        // in L, so we do it unconditionally.
        if (stringBlocksField != null && ensureStringBlocksMethod != null) {
            stringBlocksField.set(newAssetManager, null);
            ensureStringBlocksMethod.invoke(newAssetManager);
        }
				
      	// 3. 对ResourcesManager.mActiveResources对象遍历。
      	// 绑定到新的newAssetManger
        for (WeakReference<Resources> wr : references) {
            final Resources resources = wr.get();
            if (resources == null) {
                continue;
            }
            // Set the AssetManager of the Resources instance to our brand new one
            try {
              	// 4. 将Resources.mAssets对象替换成新建的newAssetManger
                //pre-N
                assetsFiled.set(resources, newAssetManager);
            } catch (Throwable ignore) {
              	// N开始替换Resources.mResourcesImpl.mAsset属性
                // N
                final Object resourceImpl = resourcesImplFiled.get(resources);
                // for Huawei HwResourcesImpl
                final Field implAssets = findField(resourceImpl, "mAssets");
                implAssets.set(resourceImpl, newAssetManager);
            }

            clearPreloadTypedArrayIssue(resources);

            resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
        }
				// WebView适配问题
        // Handle issues caused by WebView on Android N.
        // Issue: On Android N, if an activity contains a webview, when screen rotates
        // our resource patch may lost effects.
        // for 5.x/6.x, we found Couldn't expand RemoteView for StatusBarNotification Exception
        if (Build.VERSION.SDK_INT >= 24) {
            try {
                if (publicSourceDirField != null) {
                    publicSourceDirField.set(context.getApplicationInfo(), externalResourceFile);
                }
            } catch (Throwable ignore) {
                // Ignored.
            }
        }
				
      	// 5. 验证资源是否正常加载成功
      	// 根据读取assets目录下资源文件only_use_to_test_tinker_resource.txt
        if (!checkResUpdate(context)) {
            throw new TinkerRuntimeException(ShareConstants.CHECK_RES_INSTALL_FAIL);
        }
    }

六、组件热修复

入口代码

 // Init component hotplug support.
if ((isEnabledForDex || isEnabledForArkHot) && isEnabledForResource) {
  ComponentHotplug.install(app, securityCheck);
}

原理

目前Tinker只支持Android四大组件中的Activity的热修复,Java本身是就具备动态化能力,在Android要启动一个Activity,AMS其实会对这个Activity做校验,AMS是单独的一个进程,我们无法做到hook AMS校验能力;通常的解决方式是偷梁换柱

  1. 预先声明一个占位StubActivity
  2. 在startActivity时,将真实跳转的Activity替换为StubActivity,以绕过AMS的校验机制
  3. hook AMS通知app时启动Activity的事件,将StubActivity替换为真实跳转的Activity

而tinker其实也是这么做的,只是实现方式比网上很多实现的更优雅些

实现

public synchronized static void install(TinkerApplication app, ShareSecurityCheck checker) throws UnsupportedEnvironmentException {
        if (!sInstalled) {
            try {
              	// 1. 解析补丁包中组件activity信息
                if (IncrementComponentManager.init(app, checker)) {
                  	// 2. hook startActivity相关方法
                    sAMSInterceptor = new ServiceBinderInterceptor(app, EnvConsts.ACTIVITY_MANAGER_SRVNAME, new AMSInterceptHandler(app));
                    sPMSInterceptor = new ServiceBinderInterceptor(app, EnvConsts.PACKAGE_MANAGER_SRVNAME, new PMSInterceptHandler());
                    sAMSInterceptor.install();
                    sPMSInterceptor.install();
										
                  	// 3. hook ams通知app启动activity方法
                    if (Build.VERSION.SDK_INT < 27) {
                        final Handler mH = fetchMHInstance(app);
                        sMHMessageInterceptor = new HandlerMessageInterceptor(mH, new MHMessageHandler(app));
                        sMHMessageInterceptor.install();
                    } else {
                        sTinkerHackInstrumentation = TinkerHackInstrumentation.create(app);
                        sTinkerHackInstrumentation.install();
                    }

                    sInstalled = true;

                    ShareTinkerLog.i(TAG, "installed successfully.");
                }
            } catch (Throwable thr) {
                uninstall();
                throw new UnsupportedEnvironmentException(thr);
            }
        }
    }

1. 解析补丁包中的activity组件信息

IncrementComponentManager.init方法就是用来干这事的,它主要从assets/inc_component_meta.txt文件中解析组件Activity信息

在这里插入图片描述
从解析组件代码来看,目前Tinker只支持组件activity,暂不支持service,receiver,provider组件热修复

2. hook AMS的代理对象

activity启动简单回顾(以android9.0-api28为例)

context.startActivity -> startActivityForResult -> mInstrumentation.execStartActivity -> ActivityManager.getService().startActivity

image-20220323155136737

红色圈圈里面其实涉及到跨进程调用,看下ActivityManger.getService()内部实现,如果熟悉binder的同学应该知道返回的是AMS在客户端的一个代理引用,也就是下图中的am

image-20220323155331227
要把跳转的RealActivity替换成StubActivity,则需要hook am,因为Activity的创建,启动等都是AMS来管理,我们hook它在app中的代理对象即可

接下来我们看下tinker是如何hook am的,先看Interceptorinstall方法

public synchronized void install() throws InterceptFailedException {
        try {
            final T_TARGET target = fetchTarget();
            mTarget = target;
            final T_TARGET decorated = decorate(target);
            if (decorated != target) {
                inject(decorated);
            } else {
                ShareTinkerLog.w(TAG, "target: " + target + " was already hooked.");
            }
            mInstalled = true;
        } catch (Throwable thr) {
            mTarget = null;
            throw new InterceptFailedException(thr);
        }
    }

基本流程是先拿到需要hook的目标对象,紧接着对其进行包装,最后在执行注入操作;整个流程很简单,接下来看其实现类ServiceBinderInterceptor

在这里插入图片描述

静态代码块,拿到ServiceMangersCache属性及getService方法,紧接找获取需要hook的对象

@Override
    protected IBinder fetchTarget() throws Throwable {
      	// mServiceName为activity获取的就是binder对象
      	// 如果是package就是pm(PMS在客户端的代理对象)
        return (IBinder) sGetServiceMethod.invoke(null, mServiceName);
    }

包装远程binder引用

@Override
    protected IBinder decorate(IBinder target) throws Throwable {
        if (target == null) {
            throw new IllegalStateException("target is null.");
        }
      
      	// 如果对象是实现了ITinkerHotplugProxy接口(只是一个标志)说明已经被包装过了,直接返回即可
        if (ITinkerHotplugProxy.class.isAssignableFrom(target.getClass())) {
            // Already intercepted, just return the target.
            return target;
        } else {
          	// 通过java动态代理方式创建一个代理对象,并返回
            return createProxy(getAllInterfacesThroughDeriveChain(target.getClass()),
                    new FakeClientBinderHandler(target, mBinderInvocationHandler));
        }
    }

这样所有AMS远程代理对象的方法调用都被转移到FakeClientBinderHandler类中invoke方法了,

@Override
public Object invoke(Object fakeClientBinder, Method method, Object[] args) throws Throwable {
     if ("queryLocalInterface".equals(method.getName())) {
         final String itfName = mOriginalClientBinder.getInterfaceDescriptor();
         String stubClassName = null;
         if (itfName.equals("android.app.IActivityManager")) {
             stubClassName = "android.app.ActivityManagerNative";
         } else {
             stubClassName = itfName + "$Stub";
         }
       
       	// 调用android.app.ActivityManagerNative.asInterface静态方法(返回了一个本地代理对象)
       	// 内部实现也就是IActivityManager.Stub.asInterface(obj);
         final Class<?> stubClazz = Class.forName(stubClassName);
         final Method asInterfaceMethod
                 = ShareReflectUtil.findMethod(stubClazz, "asInterface", IBinder.class);
	
       	// 本地代理对象
         final IInterface originalInterface
                 = (IInterface) asInterfaceMethod.invoke(null, mOriginalClientBinder);

         final InvocationHandler fakeInterfaceHandler
                 = new FakeInterfaceHandler(originalInterface, (IBinder) fakeClientBinder, mBinderInvocationHandler);
		 // 对本地代理对象,再次使用动态代理方式进行包装
         return createProxy(getAllInterfacesThroughDeriveChain(originalInterface.getClass()), fakeInterfaceHandler);
     } else {
         return method.invoke(mOriginalClientBinder, args);
     }
 }

从上述代码可以看到是拦截了Binder.queryLocalInterface并改写内部实现,通过动态代理方式对本地代理对象进行一次包装,这样startActivity等方法都会转移到mBinderInvocationHandler也就是AMSInterceptHandler中了

接下来看看AMSInterceptHandler

3. AMSInterceptHandler

在这里插入图片描述

可以看到tinker对startActivity相关方法进行拦截处理了,内部实现核心就是偷梁换柱

private Object handleStartActivity(Object target, Method method, Object[] args) throws Throwable {
        int intentIdx = -1;
        for (int i = 0; i < args.length; ++i) {
            if (args[i] instanceof Intent) {
                intentIdx = i;
                break;
            }
        }
        if (intentIdx != -1) {
          	// 构建新的Intent存储旧信息
            final Intent newIntent = new Intent((Intent) args[intentIdx]);
          	
          	// 将跳转的Activity替换为占位Activity
            processActivityIntent(newIntent);
          	// 替换已有的intent
            args[intentIdx] = newIntent;
        }
        return method.invoke(target, args);
    }

private void processActivityIntent(Intent intent) {
        String origPackageName = null;
        String origClassName = null;
  
  			// 1. 解析组件包名、跳转的组件名(activity)
        if (intent.getComponent() != null) {
            origPackageName = intent.getComponent().getPackageName();
            origClassName = intent.getComponent().getClassName();
        } else {
            ResolveInfo rInfo = mContext.getPackageManager().resolveActivity(intent, 0);
            if (rInfo == null) {
                rInfo = IncrementComponentManager.resolveIntent(intent);
            }
            if (rInfo != null && rInfo.filter != null && rInfo.filter.hasCategory(Intent.CATEGORY_DEFAULT)) {
                origPackageName = rInfo.activityInfo.packageName;
                origClassName = rInfo.activityInfo.name;
            }
        }
  			
  			// 2. 如果跳转的组件缺失是来自补丁包中组件,说明需要替换
        if (IncrementComponentManager.isIncrementActivity(origClassName)) {
            final ActivityInfo origInfo = IncrementComponentManager.queryActivityInfo(origClassName);
            final boolean isTransparent = hasTransparentTheme(origInfo);
          	
          	// 找一个合适的占位activity
            final String stubClassName = ActivityStubManager.assignStub(origClassName, origInfo.launchMode, isTransparent);
          	
          	// 在tinker_iek_old_component中保存原始组件名,以便于后面替换回来
          	// 同时跳转的组件名替换为占位组件名
            storeAndReplaceOriginalComponentName(intent, origPackageName, origClassName, stubClassName);
        }
    }

    private void storeAndReplaceOriginalComponentName(Intent intent, String origPackageName, String origClassName, String stubClassName) {
        final ComponentName origComponentName = new ComponentName(origPackageName, origClassName);
        ShareIntentUtil.fixIntentClassLoader(intent, mContext.getClassLoader());
        intent.putExtra(EnvConsts.INTENT_EXTRA_OLD_COMPONENT, origComponentName);
        final ComponentName stubComponentName = new ComponentName(origPackageName, stubClassName);
        intent.setComponent(stubComponentName);
    }

总结如下:

1. 提取出跳转的原始activity组件名称、包名
2. 如果原始组件名称是补丁包中,寻找一个合适StubActivity替换它
3. 在newIntent中存储以key为**tinker_iek_old_component**存储originCompoentName()

到了这里其实对于组件activity的修复目前已经完成了一半工作,接下来就是对于AMS通知app LaunActivity的事件拦截

4. AMS通知app LaunchActivity的事件拦截

对于拦截LaunchActivity事件,tinker其实也是针对不同版本做了兼容处理

入口代码
在这里插入图片描述

8.1之前处理方式

对于8.1之前是hook ActivityThread.mH对象中的callBack
在这里插入图片描述

HandlerMessageInterceptor就是将mH对象的中的mCallBack通过动态代理方式包装一层以达到拦截LaunchActivity目的

MHMessageHandler.handleActivity可以看到对LAUNCH_ACTIVITY事件做拦截,可以猜测内部应该是把上面说的tinker_iek_old_component中存的值取出来设置为真正跳转组件,因为它才是存储我们实际需要跳转的组件信息(RealActivity)

 public boolean handleMessage(Message msg) {
        int what = msg.what;
        if (what == LAUNCH_ACTIVITY) {
            try {
                final Object activityClientRecord = msg.obj;
                if (activityClientRecord == null) {
                    ShareTinkerLog.w(TAG, "msg: [" + msg.what + "] has no 'obj' value.");
                    return false;
                }
                final Field intentField = ShareReflectUtil.findField(activityClientRecord, "intent");
              
              	// 1. 从ActivityClientRecord中获取intent
                final Intent maybeHackedIntent = (Intent) intentField.get(activityClientRecord);
                if (maybeHackedIntent == null) {
                    ShareTinkerLog.w(TAG, "cannot fetch intent from message received by mH.");
                    return false;
                }

                ShareIntentUtil.fixIntentClassLoader(maybeHackedIntent, mContext.getClassLoader());
								
              	// 2. 从intent中找到真正需要跳转的组件信息
                final ComponentName oldComponent = maybeHackedIntent.getParcelableExtra(EnvConsts.INTENT_EXTRA_OLD_COMPONENT);
                if (oldComponent == null) {
                    ShareTinkerLog.w(TAG, "oldComponent was null, start " + maybeHackedIntent.getComponent() + " next.");
                    return false;
                }
                final Field activityInfoField = ShareReflectUtil.findField(activityClientRecord, "activityInfo");
                final ActivityInfo aInfo = (ActivityInfo) activityInfoField.get(activityClientRecord);
                if (aInfo == null) {
                    return false;
                }
                final ActivityInfo targetAInfo = IncrementComponentManager.queryActivityInfo(oldComponent.getClassName());
                if (targetAInfo == null) {
                    ShareTinkerLog.e(TAG, "Failed to query target activity's info,"
                            + " perhaps the target is not hotpluged component. Target: " + oldComponent.getClassName());
                    return false;
                }
              	// 一些兼容性处理
                fixActivityScreenOrientation(activityClientRecord, targetAInfo.screenOrientation);
              
              	// 补充activityInfo信息
                fixStubActivityInfo(aInfo, targetAInfo);
              	
              	// 3. 替换组件
                maybeHackedIntent.setComponent(oldComponent);
              	
              	// 4. 数据清理
                maybeHackedIntent.removeExtra(EnvConsts.INTENT_EXTRA_OLD_COMPONENT);
            } catch (Throwable thr) {
                ShareTinkerLog.e(TAG, "exception in handleMessage.", thr);
            }
        }

        return false;
    }
8.1及之后处理方式

Android8.1 (27)开始tinker则是通过自定义TinkerHackInstrumentation替换ActivityThread.mInstrumentaion对象来实现拦截activity的创建的,这里有一个疑问在27版本中mH中其实是存在LAUNCH_ACTIVITY的消息事件的,28开始mH中没有LAUNCH_ACTIVITY的消息事件,没明白为什么tinker判断条件是系统版本27而不是28

在这里插入图片描述
上图不难猜测processIntent的操作和MHMessageHandler.handleActivity中的操作类似,将占位的组件替换为真正的组件调整从而实现偷梁换柱,代码比较简单,直接上图吧

在这里插入图片描述

至此对Tinker的组件热修复已解析完毕

七、mAppLike.onBaseContextAttached

这里mAppLike就是Sample工程中的SampleApplicationLike这里就是接入Tinker的一些初始化代码了,有兴趣同学可以自行研究下,此处贴下Sample的示例代码
image-20220324162022618

至此我们对Tinker的初始化及运行时的实现原理有了更深入的理解

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-08-06 10:55:38  更:2022-08-06 10:59:02 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 4:50:57-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码