• 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    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);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    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;
            }	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    解析补丁信息(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());
     				// ... 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125

    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());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

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

    assets/so_meta.txt格式

    $name,$path,$md5,$rawCrc,$pathMd5
    
    • 1

    对应ShareBsDiffPatchInfo

    assets/res_meta.txt格式 ==> ShareResPatchInfo

    $arscBasseCrc,$resArscMd5 //firstLine
    
    • 1

    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;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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

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

    接下来逐步分析如何校验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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107

    这部操作总结如下:

    • 解析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;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    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;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

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

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

    $name,$path,$md5,$rawCrc,$pathMd5
    
    • 1

    资源校验

    // 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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

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

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    这步主要做了如下:

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

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

    $arscBasseCrc,$resArscMd5 //firstLine
    
    • 1
    2. 校验资源是否可以修复
    TinkerResourcePatcher.isResourceCanPatch(context)
    
    • 1
    • 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);
               }
           }
      			// ... 
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    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;
                }
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    这部分主要做了如下操作

    • 非方舟编译器调用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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 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);
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    四、补丁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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    在这里插入图片描述

    为什么要插到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);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

    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);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109

    六、组件热修复

    入口代码

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

    原理

    目前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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    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);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

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

    在这里插入图片描述

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

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

    包装远程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));
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    这样所有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);
         }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    从上述代码可以看到是拦截了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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    总结如下:

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

    到了这里其实对于组件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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    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的初始化及运行时的实现原理有了更深入的理解

  • 相关阅读:
    测试入门难度是低,那就该不被认可吗?
    Haproxy实现七层负载均衡
    paddle篇---用yolov3训练自己的数据集
    模数转换电路
    C笔记:引用调用,通过指针传递
    Java 24 Design Pattern 之 状态模式
    【xgtcp】Go TCP服务器框架 支持Hook、功能码函数
    【数学】双根号求值域问题
    飞书中板栗看板适合做复杂任务管理吗
    面试官:深度不够,建议回去深挖
  • 原文地址:https://blog.csdn.net/dbs1215/article/details/126126229