Tinker源码分析(二):加载补丁

本系列 Tinker 源码解析基于 Tinker v1.9.12

前一篇讲到了利用反射执行的是 TinkerLoader.tryLoad 方法

tryLoad

    @Override
    public Intent tryLoad(TinkerApplication app) {
        Intent resultIntent = new Intent();
    
        long begin = SystemClock.elapsedRealtime();
        tryLoadPatchFilesInternal(app, resultIntent);
        long cost = SystemClock.elapsedRealtime() - begin;
        ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);
        return resultIntent;
    }

加载的流程主要在 tryLoadPatchFilesInternal 里面。tryLoadPatchFilesInternal 方法很长,我们需要分段来看。

tryLoadPatchFilesInternal

一开始是各种校验

检查 tinker 是否开启

    final int tinkerFlag = app.getTinkerFlags();
    // 检查 tinker 是否开启
    if (!ShareTinkerInternals.isTinkerEnabled(tinkerFlag)) {
        Log.w(TAG, "tryLoadPatchFiles: tinker is disable, just return");
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_DISABLE);
        return;
    }

检查当前的进程

    // 检查当前的进程,确保不是 :patch 进程
    if (ShareTinkerInternals.isInPatchProcess(app)) {
        Log.w(TAG, "tryLoadPatchFiles: we don't load patch with :patch process itself, just return");
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_DISABLE);
        return;
    }

获取 tinker 目录,检查目录是否存在

    // tinker 获取 tinker 目录,/data/data/tinker.sample.android/tinker
    File patchDirectoryFile = SharePatchFileUtil.getPatchDirectory(app);
    if (patchDirectoryFile == null) {
        Log.w(TAG, "tryLoadPatchFiles:getPatchDirectory == null");
        //treat as not exist
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_DIRECTORY_NOT_EXIST);
        return;
    }
    String patchDirectoryPath = patchDirectoryFile.getAbsolutePath();
    // 检查 tinker 目录是否存在
    //check patch directory whether exist
    if (!patchDirectoryFile.exists()) {
        Log.w(TAG, "tryLoadPatchFiles:patch dir not exist:" + patchDirectoryPath);
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_DIRECTORY_NOT_EXIST);
        return;
    }

检查 patch.info 文件是否存在, patch.info 是补丁信息文件,里面记录着新旧两个补丁版本的 md5 值

    // 文件目录 /data/data/tinker.sample.android/tinker/patch.info
    File patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectoryPath);
    
    // 检查 patch.info 补丁信息文件是否存在
    //check patch info file whether exist
    if (!patchInfoFile.exists()) {
        Log.w(TAG, "tryLoadPatchFiles:patch info not exist:" + patchInfoFile.getAbsolutePath());
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_NOT_EXIST);
        return;
    }

读取 patch.info 文件,并包装成一个 SharePatchInfo ,并检查 patchInfo 是否为空 (先加锁 file lock 再解锁)

    //old = 641e634c5b8f1649c75caf73794acbdf
    //new = 2c150d8560334966952678930ba67fa8
    // tinker/info.lock
    File patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectoryPath);
    
    // 检查 patch info 文件中的补丁版本信息
    patchInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);
    if (patchInfo == null) {
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_CORRUPTED);
        return;
    }

检查读取出来的 patchInfo 补丁版本信息

    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
        Log.w(TAG, "tryLoadPatchFiles:onPatchInfoCorrupted");
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_CORRUPTED);
        return;
    }

如果发现 patchInfo 中的 isRemoveNewVersion 为 true 并且在主进程中运行的话,就代表需要清除补丁了

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

    // So far new version is not loaded in main process and other processes.
    // We can remove new version directory safely.
    if (mainProcess && isRemoveNewVersion) {
        Log.w(TAG, "found clean patch mark and we are in main process, delete patch file now.");
        // 获取新的补丁文件夹,例如 patch-2c150d85
        String patchName = SharePatchFileUtil.getPatchVersionDirectory(newVersion);
        if (patchName != null) {
             // 删除新的补丁文件夹   
            String patchVersionDirFullPath = patchDirectoryPath + "/" + patchName;
            SharePatchFileUtil.deleteDir(patchVersionDirFullPath);
            // 如果旧版本和新版本一致,就把 oldVersion 和 newVersion 设置为空来清除补丁
            if (oldVersion.equals(newVersion)) {
                // !oldVersion.equals(newVersion) means new patch is applied, just fall back to old one in that case.
                // Or we will set oldVersion and newVersion to empty string to clean patch.
                oldVersion = "";
            }
            // 如果 !oldVersion.equals(newVersion) 意味着新补丁已经应用了,需要回退到原来的旧版本
            newVersion = oldVersion;
            patchInfo.oldVersion = oldVersion;
            patchInfo.newVersion = newVersion;
            // 把数据重新写入 patchInfo 文件中
            SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile);
            // 杀掉主进程以外的所有进程
            ShareTinkerInternals.killProcessExceptMain(app);
        }
    }
    
    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)) {
        Log.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) {
        Log.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()) {
        Log.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)) {
        Log.w(TAG, "tryLoadPatchFiles:onPatchVersionFileNotFound");
        //we may delete patch info file
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_VERSION_FILE_NOT_EXIST);
        return;
    }

检查补丁文件签名以及补丁文件中的 tinker id 和基准包的 tinker id 是否一致。

注意,这里补丁文件的 tinker id 是当前补丁基准包的 tinker id ,也就是有 bug 的基准包 tinker id 。另外一个 new tinker id 是补丁加载完成后的 tinker id ,就是 bug 修复后的 tinker id 。

    ShareSecurityCheck securityCheck = new ShareSecurityCheck(app);
    // 1. 检查补丁包 apk 的签名
    // 2. 检查基准包的 tinker id 与补丁包中是否一致
    // 3. 检查 tinker 设置与补丁包中的类型是否符合
    int returnCode = ShareTinkerInternals.checkTinkerPackage(app, tinkerFlag, patchVersionFile, securityCheck);
    if (returnCode != ShareConstants.ERROR_PACKAGE_CHECK_OK) {
        Log.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());

下面是 checkTinkerPackage 方法的具体代码

    public static int checkTinkerPackage(Context context, int tinkerFlag, File patchFile, ShareSecurityCheck securityCheck) {
        // 检查补丁文件签名和 tinker id 是否一致
        // 这里为了快速校验,就只检验补丁包内部以meta.txt结尾的文件的签名,而其他的文件的合法性则通过meta.txt文件内部记录的补丁文件Md5值来校验
        int returnCode = checkSignatureAndTinkerID(context, patchFile, securityCheck);
        if (returnCode == ShareConstants.ERROR_PACKAGE_CHECK_OK) {
            // 检查配置的 tinker flag 和 meta.txt 是否匹配
            // 如果不匹配的话,中断接下来的流程
            returnCode = checkPackageAndTinkerFlag(securityCheck, tinkerFlag);
        }
        return returnCode;
    }

根据不同的情况,最多有四个文件是以meta.txt结尾的:

  • package_meta.txt 补丁包的基本信息
  • dex_meta.txt dex补丁的信息
  • so_meta.txt so补丁的信息
  • res_meta.txt 资源补丁的信息

如果开启了支持 dex 热修复,检查 dex_meta.txt 文件中记录的dex文件信息对应的dex文件是否存在

    final boolean isEnabledForDex = ShareTinkerInternals.isTinkerEnabledForDex(tinkerFlag);
    
    if (isEnabledForDex) {
        //tinker/patch.info/patch-641e634c/dex
        // 检查下发的meta文件中记录的dex信息中对应的dex文件是否存在
        boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, oatDex, resultIntent);
        if (!dexCheck) {
            //file not found, do not load patch
            Log.w(TAG, "tryLoadPatchFiles:dex check fail");
            return;
        }
    }

如果开启了支持 so 热修复,检查 so_meta.txt 文件中记录的so文件信息对应的so文件是否存在

    final boolean isEnabledForNativeLib = ShareTinkerInternals.isTinkerEnabledForNativeLib(tinkerFlag);
    
    if (isEnabledForNativeLib) {
        //tinker/patch.info/patch-641e634c/lib
        boolean libCheck = TinkerSoLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
        if (!libCheck) {
            //file not found, do not load patch
            Log.w(TAG, "tryLoadPatchFiles:native lib check fail");
            return;
        }
    }

如果开启了支持 res 热修复,检查 res_meta.txt 文件中记录的res文件信息对应的res文件是否存在

    //check resource
    final boolean isEnabledForResource = ShareTinkerInternals.isTinkerEnabledForResource(tinkerFlag);
    Log.w(TAG, "tryLoadPatchFiles:isEnabledForResource:" + isEnabledForResource);
    if (isEnabledForResource) {
        boolean resourceCheck = TinkerResourceLoader.checkComplete(app, patchVersionDirectory, securityCheck, resultIntent);
        if (!resourceCheck) {
            //file not found, do not load patch
            Log.w(TAG, "tryLoadPatchFiles:resource check fail");
            return;
        }
    }

符合条件的话就更新版本信息,并将最新的patch info更新入文件.在v1.7.5的版本开始有了isSystemOTA判断,

只要用户是ART环境并且做了OTA升级,则在加载dex补丁的时候就会先把最近一次的补丁全部DexFile.loadDex一遍重新生成odex,再加载dex补丁。否则会报错

    //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
    // 把patch.info的旧版本的值更新为新版本的值,写入 patch.info 中
    if (mainProcess && (versionChanged || oatModeChanged)) {
        patchInfo.oldVersion = version;
        patchInfo.oatDir = oatDex;
    
        //update old version to new
        if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile)) {
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_REWRITE_PATCH_INFO_FAIL);
            Log.w(TAG, "tryLoadPatchFiles:onReWritePatchInfoCorrupted");
            return;
        }
    
        Log.i(TAG, "tryLoadPatchFiles:success to rewrite patch info, kill other process.");
        ShareTinkerInternals.killProcessExceptMain(app);
    
        if (oatModeChanged) {
            // delete interpret odex
            // for android o, directory change. Fortunately, we don't need to support android o interpret mode any more
            Log.i(TAG, "tryLoadPatchFiles:oatModeChanged, try to delete interpret optimize files");
            SharePatchFileUtil.deleteDir(patchVersionDirectory + "/" + ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH);
        }
    }

加载补丁的安全次数最多三次

    if (!checkSafeModeCount(app)) {
        resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, new TinkerRuntimeException("checkSafeModeCount fail"));
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_UNCAUGHT_EXCEPTION);
        Log.w(TAG, "tryLoadPatchFiles:checkSafeModeCount fail");
        return;
    }

加载补丁 jar ,这一部分的代码我们后面会详细展开

    //now we can load patch jar
    if (isEnabledForDex) {
        boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, patchVersionDirectory, oatDex, resultIntent, isSystemOTA);
        // 如果是 ota 的话,更新 oat 的文件夹路径
        if (isSystemOTA) {
            // update fingerprint after load success
            patchInfo.fingerPrint = Build.FINGERPRINT;
            patchInfo.oatDir = loadTinkerJars ? ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH : ShareConstants.DEFAULT_DEX_OPTIMIZE_PATH;
            // reset to false
            oatModeChanged = false;
    
            if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile)) {
                ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_REWRITE_PATCH_INFO_FAIL);
                Log.w(TAG, "tryLoadPatchFiles:onReWritePatchInfoCorrupted");
                return;
            }
            // update oat dir
            resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OAT_DIR, patchInfo.oatDir);
        }
        if (!loadTinkerJars) {
            Log.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail");
            return;
        }
    }

加载补丁资源,这一部分的代码我们后面会详细展开

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

补丁加载流程结束

    // Init component hotplug support.
    if (isEnabledForDex && isEnabledForResource) {
        ComponentHotplug.install(app, securityCheck);
    }
    
    //all is ok!
    ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_OK);
    Log.i(TAG, "tryLoadPatchFiles: load end, ok!");
    return;
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容