Tinker源码分析

来源:互联网 发布:linux grep -ef|ps 编辑:程序博客网 时间:2024/06/06 12:52

Tinker撸码

APK Diff 生成补丁

Tinker接入方式有两种:gradle插件和命令行。
两者只是配置参数输入源不同,前者是读取gradle文件,后者是读取命令行参数。
入口类分别是

//com.tencent.tinker.build.gradle.task.TinkerPatchSchemaTask    @TaskAction    def tinkerPatch() {        ...        InputParam.Builder builder = new InputParam.Builder()        builder.setOldApk(configuration.oldApk)               .setNewApk(buildApkPath)               .setOutBuilder(outputFolder)               .setIgnoreWarning(configuration.ignoreWarning)               .setDexFilePattern(new ArrayList<String>(configuration.dex.pattern))               .setDexLoaderPattern(new ArrayList<String>(configuration.dex.loader))               .setDexMode(configuration.dex.dexMode)               .setSoFilePattern(new ArrayList<String>(configuration.lib.pattern))               .setResourceFilePattern(new ArrayList<String>(configuration.res.pattern))               .setResourceIgnoreChangePattern(new ArrayList<String>(configuration.res.ignoreChange))               .setResourceLargeModSize(configuration.res.largeModSize)               .setUseApplyResource(configuration.buildConfig.usingResourceMapping)               .setConfigFields(new HashMap<String, String>(configuration.packageConfig.getFields()))               .setSevenZipPath(configuration.sevenZip.path)               .setUseSign(configuration.useSign)        InputParam inputParam = builder.create()        //调用Runner的静态方法,InputParam是配置参数        Runner.gradleRun(inputParam);    }    //com.tencent.tinker.build.patch.Runner    public static void gradleRun(InputParam inputParam) {        mBeginTime = System.currentTimeMillis();        Runner m = new Runner();        m.run(inputParam);    }    private void run(InputParam inputParam) {        loadConfigFromGradle(inputParam);        try {            Logger.initLogger(config);            //调用成员方法,开始差分            tinkerPatch();        } catch (IOException e) {            e.printStackTrace();            goToError();        } finally {            Logger.closeLogger();        }    }
//com.tencent.tinker.patch.CliMainpublic class CliMain extends Runner {        public static void main(String[] args) {            mBeginTime = System.currentTimeMillis();            CliMain m = new CliMain();            setRunningLocation(m);            m.run(args);        }         private void run(String[] args) {                ...                ReadArgs readArgs = new ReadArgs(args).invoke();                File configFile = readArgs.getConfigFile();                File outputFile = readArgs.getOutputFile();                File oldApkFile = readArgs.getOldApkFile();                File newApkFile = readArgs.getNewApkFile();                ...                //读取配置到Configuration类对象中                loadConfigFromXml(configFile, outputFile, oldApkFile, newApkFile);                //调用父类Runner对象的方法,开始差分                tinkerPatch();        }    }

Runner对象的成员方法tinkerPatch()定义了整个差分流程,包括Apk差分(Dex差分、Library差分、Res资源差分)、保存差分信息到文件、将差分后的文件打包、签名、压缩。这三步分别交给三个类去实现:ApkDecoder、PatchInfo、PatchBuilder

//com.tencent.tinker.build.patch.Runnerprotected void tinkerPatch() {        Logger.d("-----------------------Tinker patch begin-----------------------");        Logger.d(config.toString());        try {            //gen patch            ApkDecoder decoder = new ApkDecoder(config);            decoder.onAllPatchesStart();            decoder.patch(config.mOldApkFile, config.mNewApkFile);            decoder.onAllPatchesEnd();            //gen meta file and version file            PatchInfo info = new PatchInfo(config);            info.gen();            //build patch            PatchBuilder builder = new PatchBuilder(config);            builder.buildPatch();        } catch (Throwable e) {            e.printStackTrace();            goToError();        }        Logger.d("Tinker patch done, total time cost: %fs", diffTimeFromBegin());        Logger.d("Tinker patch done, you can go to file to find the output %s", config.mOutFolder);        Logger.d("-----------------------Tinker patch end-------------------------");    }

由于PatchInfo和PatchBuilder只是做一些收尾工作,不妨碍整个Apk差分流程分析,所以就不展开代码了。但是其中有很多开发中的小知识点可以学习,比如将一个目录下的所有文件打包成一个zip文件(IO流操作)、签名(ProcessBuilder)、7zip压缩等。

ApkDecoder

ApkDecoder的构造方法:

//com.tencent.tinker.build.decoder.ApkDecoderpublic class ApkDecoder extends BaseDecoder{    ArrayList<File> resDuplicateFiles;        public ApkDecoder(Configuration config) throws IOException {            super(config);            //新旧Apk目录            this.mNewApkDir = config.mTempUnzipNewDir;            this.mOldApkDir = config.mTempUnzipOldDir;            this.manifestDecoder = new ManifestDecoder(config);            //将差分过程的数据信息放到assets目录下            //例如新旧和差分dex的md5值,新增和修改资源文件            String prePath = TypedValue.FILE_ASSETS + File.separator;            dexPatchDecoder = new UniqueDexDiffDecoder(config, prePath + TypedValue.DEX_META_FILE, TypedValue.DEX_LOG_FILE);            soPatchDecoder = new BsDiffDecoder(config, prePath + TypedValue.SO_META_FILE, TypedValue.SO_LOG_FILE);            resPatchDecoder = new ResDiffDecoder(config, prePath + TypedValue.RES_META_TXT, TypedValue.RES_LOG_FILE);            resDuplicateFiles = new ArrayList<>();        }    }

从ApkDecoder的构造函数可以看出来,一个Apk文件的差分又分成了对manifest、dex、so、res文件的分别差分,ApkDecoder将这三部分的差分分别交由ManifestDecoder、UniqueDexDiffDecoder、BsDiffDecoder、ResDiffDecoder三个类去做。由类名称可以看出So文件的差分是使用了BsDiff算法实现的。同时构造了一个List来存储重复的文件,这个List的作用后面再详细说明。

上述各种Decoder均继承自BaseDecoder,BaseDecoder中定义了两个文件比较粗略的差分流程:onAllPatchesStart() -> patch(File oldFile, File newFile) -> onAllPatchesEnd() -> clean(),每个BaseDecoder的子类根据不同的文件类型来实现不同的差分方式,还可以在差分之前和差分之后做一些特殊的处理。例如Dex的差分是在onAllPatchEnd()步骤中实现的,而不是在patch(File oldFile, File newFile)方法中。下面会详细分析各个Decoder中三个方法的具体实现。

ApkDecoder

ApkDecoder虽然也继承自BaseDecoder,但是它不做任何实际的差分工作,而是起到对各个类型文件差分过程进行分发和统一管理的作用。下面是ApkDecoder的具体实现。

//com.tencent.tinker.build.decoder.ApkDecoderpublic class ApkDecoder extends BaseDecoder{    ...    //通知各个类型的Decoder要开始差分了,可以做一些准备工作    @Override    public void onAllPatchesStart() throws IOException, TinkerPatchException {        manifestDecoder.onAllPatchesStart();        dexPatchDecoder.onAllPatchesStart();        soPatchDecoder.onAllPatchesStart();        resPatchDecoder.onAllPatchesStart();    }    //这里的oldFile和newFile俩个参数代表新旧Apk文件    //完整的Apk文件不能直接做差分,而这个patch方法要做的就是:    //1、解压新旧Apk文件    //2、已新Apk文件为基准,遍历所有文件,根据文件类型分到具体的Decoder中去做差分操作    //3、对于那些满足dex或so文件模式的资源,打印错误日志    //4、通知各个Decoder差分结束,做收尾工作并清除占用资源     public boolean patch(File oldFile, File newFile) throws Exception {        writeToLogFile(oldFile, newFile);        //check manifest change first        manifestDecoder.patch(oldFile, newFile);        unzipApkFiles(oldFile, newFile);        Files.walkFileTree(mNewApkDir.toPath(), new ApkFilesVisitor(config, mNewApkDir.toPath(), mOldApkDir.toPath(), dexPatchDecoder, soPatchDecoder, resPatchDecoder));        //get all duplicate resource file        for (File duplicateRes : resDuplicateFiles) {          //resPatchDecoder.patch(duplicateRes, null);            Logger.e("Warning: res file %s is also match at dex or library pattern, "                + "we treat it as unchanged in the new resource_out.zip", getRelativePathStringToOldFile(duplicateRes));        }        soPatchDecoder.onAllPatchesEnd();        dexPatchDecoder.onAllPatchesEnd();        manifestDecoder.onAllPatchesEnd();        resPatchDecoder.onAllPatchesEnd();        //clean resources        dexPatchDecoder.clean();        soPatchDecoder.clean();        resPatchDecoder.clean();        return true;    }        @Override    public void onAllPatchesEnd() throws IOException, TinkerPatchException {    }    ...}

ManifestDecoder

ManifestDecoder的onAllPatchesStart()方法和onAllPatchesEnd()方法都是空实现,所以只需要关注patch方法即可。

//com.tencent.tinker.build.decoder.ManifestDecoderpublic class ManifestDecoder extends BaseDecoder {    ...     @Override    public boolean patch(File oldFile, File newFile) throws IOException, TinkerPatchException {        final boolean ignoreWarning = config.mIgnoreWarning;        try {            //解析新旧AndroidManifest.xml文件,方便属性的读取            //使用的是org.w3c.dom.Document解析的xml文件            AndroidParser oldAndroidManifest = AndroidParser.getAndroidManifest(oldFile);            AndroidParser newAndroidManifest = AndroidParser.getAndroidManifest(newFile);            //检查支持的最低sdk版本,如果低于14,必须设置dexMode为jar模式            int minSdkVersion = Integer.parseInt(oldAndroidManifest.apkMeta.getMinSdkVersion());            if (minSdkVersion < TypedValue.ANDROID_40_API_LEVEL) {                if (config.mDexRaw) {                    if (ignoreWarning) {                        //ignoreWarning, just log                        Logger.e("Warning:ignoreWarning is true, but your old apk's minSdkVersion %d is below 14, you should set the dexMode to 'jar', otherwise, it will crash at some time", minSdkVersion);                    } else {                        Logger.e("Warning:ignoreWarning is false, but your old apk's minSdkVersion %d is below 14, you should set the dexMode to 'jar', otherwise, it will crash at some time", minSdkVersion);                        throw new TinkerPatchException(                            String.format("ignoreWarning is false, but your old apk's minSdkVersion %d is below 14, you should set the dexMode to 'jar', otherwise, it will crash at some time", minSdkVersion)                        );                    }                }            }            //双层for循环,检查是否有新增组件,Tinker不支持新增组件。            List<String> oldAndroidComponent = oldAndroidManifest.getComponents();            List<String> newAndroidComponent = newAndroidManifest.getComponents();            for (String newComponentName : newAndroidComponent) {                boolean found = false;                for (String oldComponentName : oldAndroidComponent) {                    if (newComponentName.equals(oldComponentName)) {                        found = true;                        break;                    }                }                if (!found) {                    if (ignoreWarning) {                        Logger.e("Warning:ignoreWarning is true, but we found a new AndroidComponent %s, it will crash at some time", newComponentName);                    } else {                        Logger.e("Warning:ignoreWarning is false, but we found a new AndroidComponent %s, it will crash at some time", newComponentName);                        throw new TinkerPatchException(                            String.format("ignoreWarning is false, but we found a new AndroidComponent %s, it will crash at some time", newComponentName)                        );                    }                }            }        } catch (ParseException e) {            e.printStackTrace();            throw new TinkerPatchException("parse android manifest error!");        }        return false;    }    ...}

经过上述对ManifestDecoder的逻辑分析,发现并没有对新旧AndroidManifest.xml文件做差分,原因就是Tinker不支持新增四大组件。后面对补丁包合并的过程分析可以知道,在客户端合成的全量Apk中使用的是旧Apk中的AndroidManifest.xml文件。

BsDiffDecoder

BsDiffDecoder中的onAllPatchesStart()和onAllPatchesEnd()方法同样是空实现,还是主要分析patch方法。

com.tencent.tinker.build.decoder.BsDiffDecoderpublic class BsDiffDecoder extends BaseDecoder {     @Override    public boolean patch(File oldFile, File newFile) throws IOException, TinkerPatchException {        //参数合法性检查        if (newFile == null || !newFile.exists()) {            return false;        }        //获取新文件的md5值,新建差分文件        String newMd5 = MD5.getMD5(newFile);        File bsDiffFile = getOutputPath(newFile).toFile();        //如果对应的旧文件不存在,说明是新增文件,直接将新文件拷贝到差分文件中        if (oldFile == null || !oldFile.exists()) {            FileOperation.copyFileUsingStream(newFile, bsDiffFile);            writeLogFiles(newFile, null, null, newMd5);            return true;        }        //空文件不做差分        if (oldFile.length() == 0 && newFile.length() == 0) {            return false;        }        //如果有一个文件是空的,直接将新文件作为差分文件        if (oldFile.length() == 0 || newFile.length() == 0) {            FileOperation.copyFileUsingStream(newFile, bsDiffFile);            writeLogFiles(newFile, null, null, newMd5);            return true;        }        //获取旧文件的MD5        String oldMd5 = MD5.getMD5(oldFile);        //新旧MD5相同,则文件未更改,不做差分        if (oldMd5.equals(newMd5)) {            return false;        }        if (!bsDiffFile.getParentFile().exists()) {            bsDiffFile.getParentFile().mkdirs();        }        //调用bsdiff方法做差分        BSDiff.bsdiff(oldFile, newFile, bsDiffFile);        //检查差分文件大小,如果大于新文件的80%,则按照新增文件处理        if (Utils.checkBsDiffFileSize(bsDiffFile, newFile)) {            writeLogFiles(newFile, oldFile, bsDiffFile, newMd5);        } else {            FileOperation.copyFileUsingStream(newFile, bsDiffFile);            writeLogFiles(newFile, null, null, newMd5);        }        return true;    }}

ResDiffDecoder

由于ResDiffDecoder和BsDiffDecoder有相似之处,所以在分析完ResDiffDecoder的逻辑之后再总结资源文件和So文件差分思想。

com.tencent.tinker.build.decoder.ResDiffDecoder    //构造函数    public ResDiffDecoder(Configuration config, String metaPath, String logPath) throws IOException {        super(config);        ...        //新增文件集合        addedSet = new ArrayList<>();        //修改文件集合        modifiedSet = new ArrayList<>();        //大文件修改集合        largeModifiedSet = new ArrayList<>();        //修改的大文件信息        largeModifiedMap = new HashMap<>();        //删除文件集合        deletedSet = new ArrayList<>();    }    @Override    public boolean patch(File oldFile, File newFile) throws IOException, TinkerPatchException {        String name = getRelativePathStringToNewFile(newFile);        //新文件不存在,检查是否忽略此文件,按照删除文件类型处理        //实际上是不会出现这种情况,因为是按照新Apk为基准做差分的        if (newFile == null || !newFile.exists()) {            String relativeStringByOldDir = getRelativePathStringToOldFile(oldFile);            if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, relativeStringByOldDir)) {                Logger.e("found delete resource: " + relativeStringByOldDir + " ,but it match ignore change pattern, just ignore!");                return false;            }            deletedSet.add(relativeStringByOldDir);            writeResLog(newFile, oldFile, TypedValue.DEL);            return true;        }        File outputFile = getOutputPath(newFile).toFile();        //如果旧文件不存在,则说明是新增的文件        if (oldFile == null || !oldFile.exists()) {            if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, name)) {                Logger.e("found add resource: " + name + " ,but it match ignore change pattern, just ignore!");                return false;            }            FileOperation.copyFileUsingStream(newFile, outputFile);            addedSet.add(name);            writeResLog(newFile, oldFile, TypedValue.ADD);            return true;        }        //空文件不做差分        if (oldFile.length() == 0 && newFile.length() == 0) {            return false;        }        //如果两个文件都不是空文件,应该是文件变更,先获取文件的MD5        String newMd5 = MD5.getMD5(newFile);        String oldMd5 = MD5.getMD5(oldFile);        //如果MD5值相同,则文件未发生变化        if (oldMd5 != null && oldMd5.equals(newMd5)) {            return false;        }        //检查是否忽略此文件        if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, name)) {            Logger.d("found modify resource: " + name + ", but it match ignore change pattern, just ignore!");            return false;        }        //AndroidManifest.xml不做差分        if (name.equals(TypedValue.RES_MANIFEST)) {            Logger.d("found modify resource: " + name + ", but it is AndroidManifest.xml, just ignore!");            return false;        }        //判断resources.arsc文件是否变化        if (name.equals(TypedValue.RES_ARSC)) {            if (AndroidParser.resourceTableLogicalChange(config)) {                Logger.d("found modify resource: " + name + ", but it is logically the same as original new resources.arsc, just ignore!");                return false;            }        }        //处理变化的文件        dealWithModeFile(name, newMd5, oldFile, newFile, outputFile);        return true;    }    //处理变更的文件    private boolean dealWithModeFile(String name, String newMd5, File oldFile, File newFile, File outputFile) throws IOException {        //判断是否是大文件变更,可配置,默认是100KB        //如果文件超出100KB才做文件差分,否则按照新增文件处理。        //并且差分文件不得大于新文件的80%,否则也按照新增文件处理        if (checkLargeModFile(newFile)) {            if (!outputFile.getParentFile().exists()) {                outputFile.getParentFile().mkdirs();            }            BSDiff.bsdiff(oldFile, newFile, outputFile);            //treat it as normal modify            if (Utils.checkBsDiffFileSize(outputFile, newFile)) {                LargeModeInfo largeModeInfo = new LargeModeInfo();                largeModeInfo.path = newFile;                largeModeInfo.crc = FileOperation.getFileCrc32(newFile);                largeModeInfo.md5 = newMd5;                largeModifiedSet.add(name);                largeModifiedMap.put(name, largeModeInfo);                writeResLog(newFile, oldFile, TypedValue.LARGE_MOD);                return true;            }        }        modifiedSet.add(name);        FileOperation.copyFileUsingStream(newFile, outputFile);        writeResLog(newFile, oldFile, TypedValue.MOD);        return false;    }

到此已经将所有的资源文件都按照新增、修改的变更方式做了处理,并存储了文件名称和变更的文件信息,这里并没有处理删除的资源文件,因为获取删除资源集合是在onAllPatchEnd()方法中处理的。接下来就是在onAllPatchesEnd()方法中将所有变更和添加的文件压缩到resources_out.zip文件,并将新增、删除、修改的文件信息写到res_meta.txt文件中。
res_meta.txt文件示例如下

resources_out.zip,1134154093,26a3339220be96e865fc523fe4a162a8pattern:3resources.arscres/*assets/*large modify:1resources.arsc,26a3339220be96e865fc523fe4a162a8,946796371modify:1res/layout/activity_bug.xmladd:199res/drawable-hdpi-v4/abc_ab_share_pack_mtrl_alpha.9.pngres/drawable-hdpi-v4/abc_btn_check_to_on_mtrl_000.pngres/drawable-hdpi-v4/abc_btn_check_to_on_mtrl_015.pngres/drawable-hdpi-v4/abc_btn_radio_to_on_mtrl_000.pngres/drawable-hdpi-v4/abc_btn_radio_to_on_mtrl_015.png
BsDiffDecoder和ResDiffDecoder差分思想

在分析两者的源码时,均发现在做完差分后,有对差分文件检查大小的步骤。如果差分文件超过新文件的80%,则放弃使用差分文件,直接使用新文件,即按照新增文件或变更文件处理。

    com.tencent.tinker.build.util    public static boolean checkBsDiffFileSize(File bsDiffFile, File newFile) {        ···        //计算差分文件相对于新文件的大小        double ratio = bsDiffFile.length() / (double) newFile.length();        //如果这个比例大于预设的值0.8返回false。        if (ratio > TypedValue.BSDIFF_PATCH_MAX_RATIO) {            Logger.e("bsDiff patch file:%s, size:%dk, new file:%s, size:%dk. patch file is too large, treat it as newly file to save patch time!",                bsDiffFile.getName(),                bsDiffFile.length() / 1024,                newFile.getName(),                newFile.length() / 1024            );            return false;        }        return true;    }
BsDiff 算法

快速后缀排序

DexDiffDecoder

检查在新的Dex中,那些已经被排除的类是否被更改。主要是检查Tinker相关的类,因为Loader类只会出现在主Dex中,并且新旧Dex中的Loader类应当保持一致。
当出现下面情况时,会声明异常:

  1. 不存在新主Dex
  2. 不存在旧主Dex
  3. 旧主Dex中不存在Loader类
  4. 新主Dex出现新的Loader类
  5. 旧主Dex中的Loader类出现变化或被删除
  6. Loader类出现在新的二级Dex文件中
  7. Loader类出现在旧的二级Dex文件中

前置条件检查完成后,将Dex差分分为两种情况:

  1. 新增Dex(新Dex文件存在,旧Dex文件不存在)
  2. Dex类变化(新旧Dex文件都存在,但MD5值不同)

情况1很好处理,直接将新Dex文件拷贝到临时目录。
情况2就要对两个Dex文件做分析,找出变化的类。Tinker在对Dex做差分时,分成了两步。第一步在patch(final File oldFile, final File newFile)方法中先把新增和删除的类集合找出来。第二步在onAllPatchesEnd()方法中真正去做差分并生成差分Dex文件。

com.tencent.tinker.build.decoder.DexDiffDecoder    @Override    public void onAllPatchesEnd() throws Exception {        //先判断是否有Dex文件变更        if (!hasDexChanged) {            Logger.d("No dexes were changed, nothing needs to be done next.");            return;        }        //判断是否启用加固        if (config.mIsProtectedApp) {            generateChangedClassesDexFile();        } else {            generatePatchInfoFile();        }    }

处理Dex文件差分的类叫做DexPatchGernerator。

com.tencent.tinker.build.dexpatcher.DexPatchGernerator//构造函数public DexPatchGenerator(Dex oldDex, Dex newDex) {        this.oldDex = oldDex;        this.newDex = newDex;        //新旧Dex、新Dex和差分Dex、旧Dex和差分Dex两两Dex中各个块的索引对应        SparseIndexMap oldToNewIndexMap = new SparseIndexMap();        SparseIndexMap oldToPatchedIndexMap = new SparseIndexMap();        SparseIndexMap newToPatchedIndexMap = new SparseIndexMap();        SparseIndexMap selfIndexMapForSkip = new SparseIndexMap();        //需要额外移除的类正则式集合        additionalRemovingClassPatternSet = new HashSet<>();        //根据Dex文件不同的块,构造对应的差分算法进行差分操作。例如字符串块的差分算法StringDataSectionDiffAlgorithm        this.stringDataSectionDiffAlg = new StringDataSectionDiffAlgorithm(                oldDex, newDex,                oldToNewIndexMap,                oldToPatchedIndexMap,                newToPatchedIndexMap,                selfIndexMapForSkip        );        //其他部分差分算法        ...

接下来就要执行各个块的差分算法了。

com.tencent.tinker.build.dexpatcher.DexPatchGerneratorpublic void executeAndSaveTo(OutputStream out) throws IOException {    //第一步,在新Dex收集需要移除的块信息,并将它们设置到对应的差分算法实现中。一般都是需要移除一部分不需要做差分的类。    ...    List<Integer> typeIdOfClassDefsToRemove = new ArrayList<>(classNamePatternCount);        List<Integer> offsetOfClassDatasToRemove = new ArrayList<>(classNamePatternCount);         //双层for循环,找出移除类的索引和数据块偏移        for (ClassDef classDef : this.newDex.classDefs()) {            String typeName = this.newDex.typeNames().get(classDef.typeIndex);            for (Pattern pattern : classNamePatterns) {                if (pattern.matcher(typeName).matches()) {                    typeIdOfClassDefsToRemove.add(classDef.typeIndex);                    offsetOfClassDatasToRemove.add(classDef.classDataOffset);                    break;                }            }        }    ...    //第二步,执行各个块的差分算法    //1. 执行差分算法,计算需要添加、删除、替换的item索引    //2. 执行补丁模拟算法,计算item索引和偏移量的对应关系。立刻执行补丁模拟算法可以知道新旧Dex文件相对于差分Dex文件item索引和偏移量的对应关系,这些信息在后面差分工作中非常重要。    }

以StringDataSection为例,说明具体差分步骤:

  1. 读取旧Dex文件中的StringData块,存储形式为AbstractMap.SimpleEntry
    private void writeResultToStream(OutputStream os) throws IOException {        DexDataBuffer buffer = new DexDataBuffer();        //写入魔数,代表是Dex差分文件,固定为DXDIFF        buffer.write(DexPatchFile.MAGIC);        //写入Patch文件格式版本        buffer.writeShort(DexPatchFile.CURRENT_VERSION);        //写入文件大小        buffer.writeInt(this.patchedDexSize);        // 这里还不知道第一个数据块的偏移,所以等其他偏移量都写入后再回来写。先写一个0占位        int posOfFirstChunkOffsetField = buffer.position();        buffer.writeInt(0);        //写入各个map list相关的偏移量,这些偏移量应该是新Dex中的偏移,目的是做校验        buffer.writeInt(this.patchedStringIdsOffset);        buffer.writeInt(this.patchedTypeIdsOffset);        buffer.writeInt(this.patchedProtoIdsOffset);        buffer.writeInt(this.patchedFieldIdsOffset);        buffer.writeInt(this.patchedMethodIdsOffset);        buffer.writeInt(this.patchedClassDefsOffset);        buffer.writeInt(this.patchedMapListOffset);        buffer.writeInt(this.patchedTypeListsOffset);        buffer.writeInt(this.patchedAnnotationSetRefListItemsOffset);        buffer.writeInt(this.patchedAnnotationSetItemsOffset);        buffer.writeInt(this.patchedClassDataItemsOffset);        buffer.writeInt(this.patchedCodeItemsOffset);        buffer.writeInt(this.patchedStringDataItemsOffset);        buffer.writeInt(this.patchedDebugInfoItemsOffset);        buffer.writeInt(this.patchedAnnotationItemsOffset);        buffer.writeInt(this.patchedEncodedArrayItemsOffset);        buffer.writeInt(this.patchedAnnotationsDirectoryItemsOffset);        //写入文件签名,计算签名时除去文件开头的32字节        buffer.write(this.oldDex.computeSignature(false));        int firstChunkOffset = buffer.position();        //回到posOfFirstChunkOffsetField位置        buffer.position(posOfFirstChunkOffsetField);        //写入第一个数据块的偏移        buffer.writeInt(firstChunkOffset);        //将buffer写入点移到firstChunkOffset        buffer.position(firstChunkOffset);        //写入各个算法计算出来的差分信息        //1. 先写入删除、添加、替换的Item的个数        //2. 然后按照删除、添加、替换的顺序,写入每一个Item索引与上一个Item索引的差值?        //3. 写入需要添加和替换的Item数据        writePatchOperations(buffer, this.stringDataSectionDiffAlg.getPatchOperationList());        writePatchOperations(buffer, this.typeIdSectionDiffAlg.getPatchOperationList());        writePatchOperations(buffer, this.typeListSectionDiffAlg.getPatchOperationList());        writePatchOperations(buffer, this.protoIdSectionDiffAlg.getPatchOperationList());        writePatchOperations(buffer, this.fieldIdSectionDiffAlg.getPatchOperationList());        writePatchOperations(buffer, this.methodIdSectionDiffAlg.getPatchOperationList());        writePatchOperations(buffer, this.annotationSectionDiffAlg.getPatchOperationList());        writePatchOperations(buffer, this.annotationSetSectionDiffAlg.getPatchOperationList());        writePatchOperations(buffer, this.annotationSetRefListSectionDiffAlg.getPatchOperationList());        writePatchOperations(buffer, this.annotationsDirectorySectionDiffAlg.getPatchOperationList());        writePatchOperations(buffer, this.debugInfoSectionDiffAlg.getPatchOperationList());        writePatchOperations(buffer, this.codeSectionDiffAlg.getPatchOperationList());        writePatchOperations(buffer, this.classDataSectionDiffAlg.getPatchOperationList());        writePatchOperations(buffer, this.encodedArraySectionDiffAlg.getPatchOperationList());        writePatchOperations(buffer, this.classDefSectionDiffAlg.getPatchOperationList());        byte[] bufferData = buffer.array();        //写入文件        os.write(bufferData);        os.flush();    }

至此,一个Apk文件中的所有部分均已差分完成,并写入到临时文件中了。剩下就是做一些清理工作,然后生成补丁补充信息文件如TinkerId、HotPatch版本等,最后是将所有文件打包成Apk包、签名、压缩。

APK Patch Recovery 补丁合成

  • Tinker默认实现了DefaultPatchListener,开发者可自定义收到新补丁的操作,实现PatchListener,并设置给Tinker。
  • DefaultPatchListener会启动:patch进程(TinkerPatchService)来执行补丁合并操作。
  • 在TinkerPatchService的onHandleIntent方法中会使用默认的补丁处理器(UpgradePatch),当然开发者也可以自己实现,在Tinker初始化时调用install方法。补丁处理完成后的操作是由一个IntentService处理的,包括删除原始补丁文件,杀死:patch进程,重启主进程等。
  • 为了防止补丁合成进程被系统杀死,特意提高了:patch进程的优先级。

Tinker的默认补丁升级操作实现类为UpgradePatch(com.tencent.tinker.lib.patch),主要包含了以下几个步骤:补丁验证 -> 准备 -> 合并dex -> 合并so文件 -> 合并res文件 -> 等待并验证opt文件(部分机型) -> 将补丁信息写回path.info文件中 -> 补丁升级结束

//com.tencent.tinker.lib.patch.AbstractPatchpublic abstract class AbstractPatch {    //三个参数    //context  上下文    //tempPatchPath  补丁包路径    //patchResult   补丁合成结果    public abstract boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult);}

补丁验证

验证签名

获取证书公钥
ByteArrayInputStream stream = null;PackageManager pm = context.getPackageManager();String packageName = context.getPackageName();PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);CertificateFactory certFactory = CertificateFactory.getInstance("X.509");stream = new ByteArrayInputStream(packageInfo.signatures[0].toByteArray());X509Certificate cert = (X509Certificate) certFactory.generateCertificate(stream);mPublicKey = cert.getPublicKey();
验证补丁jar包中的各个部分
//com.tencent.tinker.loader.shareutil.ShareSecurityCheckpublic boolean verifyPatchMetaSignature(File path) {        ...        JarFile jarFile = null;        try {            //以JarFile形式读取补丁包            jarFile = new JarFile(path);            //得到压缩包中的条目            final Enumeration<JarEntry> entries = jarFile.entries();            //遍历            while (entries.hasMoreElements()) {                JarEntry jarEntry = entries.nextElement();                // no code                if (jarEntry == null) {                    continue;                }                //META-INF目录下的文件不验证                final String name = jarEntry.getName();                if (name.startsWith("META-INF/")) {                    continue;                }                //为了提高速度,只验证.meta结尾的文件,其他条目会以写在meta文件中的MD5值做校验                if (!name.endsWith(ShareConstants.META_SUFFIX)) {                    continue;                }                //存储meta文件内容到内存中,为了快                metaContentMap.put(name, SharePatchFileUtil.loadDigestes(jarFile, jarEntry));                //获取条目的签名证书                Certificate[] certs = jarEntry.getCertificates();                if (certs == null) {                    return false;                }                //验证条目的证书公钥是否一致                if (!check(path, certs)) {                    return false;                }            }        } catch (Exception e) {            throw new TinkerRuntimeException(                String.format("ShareSecurityCheck file %s, size %d verifyPatchMetaSignature fail", path.getAbsolutePath(), path.length()), e);        } finally {            try {                if (jarFile != null) {                    jarFile.close();                }            } catch (IOException e) {                Log.e(TAG, path.getAbsolutePath(), e);            }        }        return true;    }        private boolean check(File path, Certificate[] certs) {        if (certs.length > 0) {            for (int i = certs.length - 1; i >= 0; i--) {                try {                    //验证所有证书的公钥,一般只有一个                    certs[i].verify(mPublicKey);                    return true;                } catch (Exception e) {                    Log.e(TAG, path.getAbsolutePath(), e);                }            }        }        return false;    }

验证是否启用Tinker及是否支持dex、lib、res模式

准备

检查补丁信息

补丁信息文件patch.info记录了新旧补丁版本、系统指纹、OAT目录。
在读取patch.info文件时,用到了文件锁定,锁定文件为info.lock。每次需要读取或写入patch.info文件时,需要获取info.lock文件的锁定,如果获取不到说明有其他线程正在操作patch.info文件。

//com.tencent.tinker.loader.shareutil.ShareFileLockHelper    //获取锁定次数    public static final int MAX_LOCK_ATTEMPTS   = 3;    //每次获取锁定等待时间    public static final int LOCK_WAIT_EACH_TIME = 10;private ShareFileLockHelper(File lockFile) throws IOException {        outputStream = new FileOutputStream(lockFile);        int numAttempts = 0;        boolean isGetLockSuccess;        FileLock localFileLock = null;        //just wait twice,        Exception saveException = null;        while (numAttempts < MAX_LOCK_ATTEMPTS) {            numAttempts++;            try {                localFileLock = outputStream.getChannel().lock();                isGetLockSuccess = (localFileLock != null);                if (isGetLockSuccess) {                    break;                }                //it can just sleep 0, afraid of cpu scheduling                Thread.sleep(LOCK_WAIT_EACH_TIME);            } catch (Exception e) {                saveException = e;                Log.e(TAG, "getInfoLock Thread failed time:" + LOCK_WAIT_EACH_TIME);            }        }        if (localFileLock == null) {            throw new IOException("Tinker Exception:FileLockHelper lock file failed: " + lockFile.getAbsolutePath(), saveException);        }        fileLock = localFileLock;    }

将补丁拷贝到应用私有目录

    //com.tencent.tinker.loader.shareutil.SharePatchFileUtil    public static void copyFileUsingStream(File source, File dest) throws IOException {        //检查文件合法性        if (!SharePatchFileUtil.isLegalFile(source) || dest == null) {            return;        }        if (source.getAbsolutePath().equals(dest.getAbsolutePath())) {            return;        }        FileInputStream is = null;        FileOutputStream os = null;        //创建目的文件目录        File parent = dest.getParentFile();        if (parent != null && (!parent.exists())) {            parent.mkdirs();        }        try {            //输入输出流互写            is = new FileInputStream(source);            os = new FileOutputStream(dest, false);            byte[] buffer = new byte[ShareConstants.BUFFER_SIZE];            int length;            while ((length = is.read(buffer)) > 0) {                os.write(buffer, 0, length);            }        } finally {            closeQuietly(is);            closeQuietly(os);        }    }

合并

Dex文件合并

从补丁文件中提取出Dex差分文件
  1. 从dex_meta.txt文件中解析出每一个dex差分文件的信息,存储到ShareDexDiffPatchInfo列表中。包含以下字段:

    // com.tencent.tinker.loader.shareutil.ShareDexDiffPatchInfopublic final String rawName;        //dex原始文件名称,例如classes2.dexpublic final String destMd5InDvm;   //在Dvm虚拟机中合成后的Dex的md5值public final String destMd5InArt;   //在Art虚拟机中合成后的Dex的md5值public final String oldDexCrC;      //旧Dex的crc值public final String dexDiffMd5;     //差分Dex文件的MD5值public final String path;           //差分Dex相对于合成后的Dex文件父目录public final String dexMode;        //dex压缩方式raw或jarpublic final boolean isJarMode;     //dex压缩方式是否是jar模式public final String realName;       //差分dex文件的真实文件名称,如果是jar模式,需要在rawName的基础上添加.jar后缀
  2. 从差分包或原始Apk中提取出dex文件到packagename/tinker/patch-basd22fa/dex目录中

    • 新增Dex,从差分包提取dex,放到dex目录下
    • 变化特别大dex,直接从原始apk中提取dex,放到dex目录下
    • 差分dex与原始dex合成后,放到dex目录下
dex补丁合成

如果补丁中的dex文件使用了jar模式,还需要再使用ZipInputStream流包装一次

之后交给DexPatchApplier执行dex文件合并,并存储到dex目录下

// com.tencent.tinker.commons.dexpatcher.DexPatchApplier//构造函数public DexPatchApplier(Dex oldDexIn, DexPatchFile patchFileIn) {   this.oldDex = oldDexIn; //旧dex   this.patchFile = patchFileIn;//补丁dex   this.patchedDex = new Dex(patchFileIn.getPatchedDexSize());//合成后的dex   this.oldToPatchedIndexMap = new SparseIndexMap();}//验证两个dex的签名是否匹配byte[] oldDexSign = this.oldDex.computeSignature(false);byte[] oldDexSignInPatchFile = this.patchFile.getOldDexSignature();if (CompareUtils.uArrCompare(oldDexSign, oldDexSignInPatchFile) != 0) {       throw new IOException(               String.format(                       "old dex signature mismatch! expected: %s, actual: %s",                       Arrays.toString(oldDexSign),                       Arrays.toString(oldDexSignInPatchFile)               )       );   }//1、先将TableOfContents的偏移写入合成后的dex文件中,然后就可以计算出TableOfContents的大小//2、根据各个数据块的依赖关系执行各个数据块的合并算法//3、 写入文件头,mapList。计算并写入合成后的dex文件签名和校验和Dex.Section headerOut = this.patchedDex.openSection(patchedToc.header.off);patchedToc.writeHeader(headerOut);Dex.Section mapListOut=this.patchedDex.openSection(patchedToc.mapList.off);patchedToc.writeMap(mapListOut);this.patchedDex.writeHashes();//4、 将合并后的dex写入文件中。this.patchedDex.writeTo(out);

下面以StringDataSection为例看一下合并过程:

//com.tencent.tinker.commons.dexpatcher.algorithms.patch.DexSectionPatchAlgorithmprivate void doFullPatch(       Dex.Section oldSection,  //旧StringSection       int oldItemCount,        //旧StringSection中数据项个数       int[] deletedIndices,    //删除项的索引或偏移量数组       int[] addedIndices,      //新增项的索引或偏移量数组       int[] replacedIndices    //替换项的索引或偏移量数组) {   int deletedItemCount = deletedIndices.length;   int addedItemCount = addedIndices.length;   int replacedItemCount = replacedIndices.length;   int newItemCount = oldItemCount + addedItemCount - deletedItemCount;   int deletedItemCounter = 0;   int addActionCursor = 0;   int replaceActionCursor = 0;   int oldIndex = 0;   int patchedIndex = 0;   //核心合并算法   while (oldIndex < oldItemCount || patchedIndex < newItemCount) {        //写入新增项       if (addActionCursor < addedItemCount && addedIndices[addActionCursor] == patchedIndex) {           T addedItem = nextItem(patchFile.getBuffer());           int patchedOffset = writePatchedItem(addedItem);           ++addActionCursor;           ++patchedIndex;       } else       if (replaceActionCursor < replacedItemCount && replacedIndices[replaceActionCursor] == patchedIndex) {       //写入补丁中的变更项           T replacedItem = nextItem(patchFile.getBuffer());           int patchedOffset = writePatchedItem(replacedItem);           ++replaceActionCursor;           ++patchedIndex;       } else       if (Arrays.binarySearch(deletedIndices, oldIndex) >= 0) {       //忽略删除项           T skippedOldItem = nextItem(oldSection); // skip old item.           markDeletedIndexOrOffset(                   oldToPatchedIndexMap,                   oldIndex,                   getItemOffsetOrIndex(oldIndex, skippedOldItem)           );           ++oldIndex;           ++deletedItemCounter;       } else       if (Arrays.binarySearch(replacedIndices, oldIndex) >= 0) {       //忽略旧StringSection中的变更项           T skippedOldItem = nextItem(oldSection); // skip old item.           markDeletedIndexOrOffset(                   oldToPatchedIndexMap,                   oldIndex,                   getItemOffsetOrIndex(oldIndex, skippedOldItem)           );           ++oldIndex;       } else       if (oldIndex < oldItemCount) {        //写入未变更项           T oldItem = adjustItem(this.oldToPatchedIndexMap, nextItem(oldSection));           int patchedOffset = writePatchedItem(oldItem);           updateIndexOrOffset(                   this.oldToPatchedIndexMap,                   oldIndex,                   getItemOffsetOrIndex(oldIndex, oldItem),                   patchedIndex,                   patchedOffset           );           ++oldIndex;           ++patchedIndex;       }   }}
odex
  1. 优化后的dex存储目录为odex。
  2. 在art虚拟机下,opt操作是并行的
  3. 在dalvik虚拟机下,机器硬件性能比较低下,串行opt操作
//com.tencent.tinker.lib.patch.DexDiffPatchInternalprivate static boolean patchDexExtractViaDexDiff(Context context, String patchVersionDirectory, String meta, final File patchFile) { ...final Tinker manager = Tinker.with(context);File dexFiles = new File(dir);//需要优化的dex文件数组File[] files = dexFiles.listFiles();//清空旧的opt文件optFiles.clear();if (files != null) {   //opt目录路径 patch-asd42fa/odex/  final String optimizeDexDirectory = patchVersionDirectory + "/" +           DEX_OPTIMIZE_PATH + "/";      //odex目录     File optimizeDexDirectoryFile = new File(optimizeDexDirectory);  // 新建odex文件,例:dex/classes.dex -> odex/classes.dex  for (File file : files) {      String outputPathName = SharePatchFileUtil.optimizedPathFor(file, optimizeDexDirectoryFile);      optFiles.add(new File(outputPathName));  }  // Art虚拟机并行dexopt或dex2oat操作 默认2个线程  if (ShareTinkerInternals.isVmArt()) {      //失败的opt文件集合      final List<File> failOptDexFile = new Vector<>();      final Throwable[] throwable = new Throwable[1];      // TinkerParallelDexOptimizer dex优化类      TinkerParallelDexOptimizer.optimizeAll(          Arrays.asList(files), optimizeDexDirectoryFile,          new TinkerParallelDexOptimizer.ResultCallback() {              long startTime;              @Override              public void onStart(File dexFile, File optimizedDir) {              }              @Override              public void onSuccess(File dexFile, File optimizedDir, File optimizedFile) {              }              @Override              public void onFailed(File dexFile, File optimizedDir, Throwable thr)                    {                  //失败后,存储失败的文件和异常                  failOptDexFile.add(dexFile);                  throwable[0] = thr;              }          }      );      //如果存在失败的文件,通过PatchReporter通知给用户      if (!failOptDexFile.isEmpty()) {          manager.getPatchReporter().onPatchDexOptFail(patchFile, failOptDexFile, throwable[0]);          return false;      }  // dalvik虚拟机,串行操作   } else {      for (File file : files) {          try {              String outputPathName = SharePatchFileUtil.optimizedPathFor(file, optimizeDexDirectoryFile);              //加载dex文件并优化              DexFile.loadDex(file.getAbsolutePath(), outputPathName, 0);          } catch (Throwable e) {               ...               return false;          }      }  }}

Tinker在实现上并没有在合成阶段使用dex2oat,而是在加载dex的时候。

//com.tencent.tinker.loader.TinkerParallelDexOptimizerpublic static boolean optimizeAll(Collection<File> dexFiles, File optimizedDir, ResultCallback cb) {   return optimizeAll(dexFiles, optimizedDir, **false**, null, cb);}//此处useInterpretMode参数一直为falsepublic static boolean optimizeAll(Collection<File> dexFiles, File optimizedDir,             boolean useInterpretMode, String targetISA, ResultCallback cb) {   final AtomicInteger successCount = new AtomicInteger(0);   return optimizeAllLocked(dexFiles, optimizedDir, useInterpretMode, targetISA, successCount, cb, DEFAULT_THREAD_COUNT);    }

Dex2Oat实现:

//com.tencent.tinker.loader.TinkerParallelDexOptimizer.OptimizeWorkerprivate void interpretDex2Oat(String dexFilePath, String oatFilePath) throws IOException {            final File oatFile = new File(oatFilePath);            if (!oatFile.exists()) {                oatFile.getParentFile().mkdirs();            }            //调用dex2oat命令            final List<String> commandAndParams = new ArrayList<>();            commandAndParams.add("dex2oat");            //dex文件路径            commandAndParams.add("--dex-file=" + dexFilePath);            //oat文件目录            commandAndParams.add("--oat-file=" + oatFilePath);            //指定cpu指令集(六种 arm mips x86 32|64位)编译dex文件  此处tartetISA为null              commandAndParams.add("--instruction-set=" + targetISA);            //指定编译选项(verify-none|speed|interpret-only|verify-at-runtime|space|balanced|everything|time)仅编译            commandAndParams.add("--compiler-filter=interpret-only");            final ProcessBuilder pb = new ProcessBuilder(commandAndParams);            pb.redirectErrorStream(true);            final Process dex2oatProcess = pb.start();            StreamConsumer.consumeInputStream(dex2oatProcess.getInputStream());            StreamConsumer.consumeInputStream(dex2oatProcess.getErrorStream());            try {                final int ret = dex2oatProcess.waitFor();                if (ret != 0) {                    throw new IOException("dex2oat works unsuccessfully, exit code: " + ret);                }            } catch (InterruptedException e) {                throw new IOException("dex2oat is interrupted, msg: " + e.getMessage(), e);            }        }

So文件合并

So文件的合并过程和dex文件的合并是一样的,从asset/so_meta.txt文件中读取需要合并的记录,从补丁包或原始APK中提取相应的so文件进程合并,合并后文件的存放目录为lib。

和生成补丁过程类似,so文件的合并也是利用了BsPatch算法。

//com.tencent.tinker.bsdiff.BSPatch/*** 这个补丁算法是快速的,但是需要更多的内存* 占用内存大小 = 旧文件大小 + 差分文件大小 + 新文件大小*/public static byte[] patchFast(byte[] oldBuf, int oldsize, byte[] diffBuf, int diffSize, int extLen) throws IOException {   DataInputStream diffIn = new DataInputStream(new ByteArrayInputStream(diffBuf, 0, diffSize));   diffIn.skip(8); // skip headerMagic at header offset 0 (length 8 bytes)   long ctrlBlockLen = diffIn.readLong(); // ctrlBlockLen after bzip2 compression at heater offset 8 (length 8 bytes)   long diffBlockLen = diffIn.readLong(); // diffBlockLen after bzip2 compression at header offset 16 (length 8 bytes)   int newsize = (int) diffIn.readLong(); // size of new file at header offset 24 (length 8 bytes)   diffIn.close();   InputStream in = new ByteArrayInputStream(diffBuf, 0, diffSize);   in.skip(BSUtil.HEADER_SIZE);   DataInputStream ctrlBlockIn = new DataInputStream(new GZIPInputStream(in));   in = new ByteArrayInputStream(diffBuf, 0, diffSize);   in.skip(ctrlBlockLen + BSUtil.HEADER_SIZE);   InputStream diffBlockIn = new GZIPInputStream(in);   in = new ByteArrayInputStream(diffBuf, 0, diffSize);   in.skip(diffBlockLen + ctrlBlockLen + BSUtil.HEADER_SIZE);   InputStream extraBlockIn = new GZIPInputStream(in);   // byte[] newBuf = new byte[newsize + 1];   byte[] newBuf = new byte[newsize];   int oldpos = 0;   int newpos = 0;   int[] ctrl = new int[3];   // int nbytes;   while (newpos < newsize) {       for (int i = 0; i <= 2; i++) {           ctrl[i] = ctrlBlockIn.readInt();       }       if (newpos + ctrl[0] > newsize) {           throw new IOException("Corrupt by wrong patch file.");       }       // Read ctrl[0] bytes from diffBlock stream       if (!BSUtil.readFromStream(diffBlockIn, newBuf, newpos, ctrl[0])) {           throw new IOException("Corrupt by wrong patch file.");       }       for (int i = 0; i < ctrl[0]; i++) {           if ((oldpos + i >= 0) && (oldpos + i < oldsize)) {               newBuf[newpos + i] += oldBuf[oldpos + i];           }       }       newpos += ctrl[0];       oldpos += ctrl[0];       if (newpos + ctrl[1] > newsize) {           throw new IOException("Corrupt by wrong patch file.");       }       if (!BSUtil.readFromStream(extraBlockIn, newBuf, newpos, ctrl[1])) {           throw new IOException("Corrupt by wrong patch file.");       }       newpos += ctrl[1];       oldpos += ctrl[2];   }   ctrlBlockIn.close();   diffBlockIn.close();   extraBlockIn.close();   return newBuf;}

资源文件合并

  1. 差分信息文件asset/res_meta.txt
  2. 合并后的文件res/resource.apk
  3. 差分记录中的大文件采用BsPatch算法合成
  4. 将没变的文件写到resource.apk文件中
  5. 将AndroidManifest.xml文件写到resource.apk文件中
  6. 将合并后的大文件写入resource.apk中
  7. 将新增的文件写入resource.apk中
  8. 将变更的小文件写入resource.apk中
  9. 写入注释out.setComment(oldApk.getComment());
  10. 验证合并后的resource.apk文件中的resources.asrc文件的MD5值是否与目的文件的MD5值相同

等待odex操作完成

com.tencent.tinker.lib.patch.DexDiffPatchInternalprotected static boolean waitAndCheckDexOptFile(File patchFile, Tinker manager) {        if (optFiles.isEmpty()) {            return true;        }        int size = optFiles.size() * 6;        if (size > MAX_WAIT_COUNT) {            size = MAX_WAIT_COUNT;        }        TinkerLog.i(TAG, "dex count: %d, final wait time: %d", optFiles.size(), size);        for (int i = 0; i < size; i++) {            if (!checkAllDexOptFile(optFiles, i + 1)) {                try {                    Thread.sleep(WAIT_ASYN_OAT_TIME);                } catch (InterruptedException e) {                    TinkerLog.e(TAG, "thread sleep InterruptedException e:" + e);                }            }        }        List<File> failDexFiles = new ArrayList<>();        // check again, if still can be found, just return        for (File file : optFiles) {            TinkerLog.i(TAG, "check dex optimizer file exist: %s, size %d", file.getName(), file.length());            if (!SharePatchFileUtil.isLegalFile(file)) {                TinkerLog.e(TAG, "final parallel dex optimizer file %s is not exist, return false", file.getName());                failDexFiles.add(file);            }        }        if (!failDexFiles.isEmpty()) {            manager.getPatchReporter().onPatchDexOptFail(patchFile, failDexFiles,                        new TinkerRuntimeException(ShareConstants.CHECK_DEX_OAT_EXIST_FAIL));            return false;        }        if (Build.VERSION.SDK_INT >= 21) {            Throwable lastThrowable = null;            for (File file : optFiles) {                TinkerLog.i(TAG, "check dex optimizer file format: %s, size %d", file.getName(), file.length());                int returnType;                try {                    returnType = ShareElfFile.getFileTypeByMagic(file);                } catch (IOException e) {                    // read error just continue                   continue;                }                if (returnType == ShareElfFile.FILE_TYPE_ELF) {                    ShareElfFile elfFile = null;                    try {                        elfFile = new ShareElfFile(file);                    } catch (Throwable e) {                        TinkerLog.e(TAG, "final parallel dex optimizer file %s is not elf format, return false", file.getName());                        failDexFiles.add(file);                        lastThrowable = e;                    } finally {                        if (elfFile != null) {                            try {                                elfFile.close();                            } catch (IOException ignore) {                            }                        }                    }                }            }            if (!failDexFiles.isEmpty()) {                Throwable returnThrowable = lastThrowable == null                    ? new TinkerRuntimeException(ShareConstants.CHECK_DEX_OAT_FORMAT_FAIL)                    : new TinkerRuntimeException(ShareConstants.CHECK_DEX_OAT_FORMAT_FAIL, lastThrowable);                manager.getPatchReporter().onPatchDexOptFail(patchFile, failDexFiles,                    returnThrowable);                return false;            }        }        return true;    }

善后

写入新补丁信息到文件

//com.tencent.tinker.loader.shareutil.SharePatchInfopublic static boolean rewritePatchInfoFileWithLock(File pathInfoFile, SharePatchInfo info, File lockFile) {       ...       File lockParentFile = lockFile.getParentFile();       boolean rewriteSuccess;       ShareFileLockHelper fileLock = null;       //获取文件排他锁       fileLock = ShareFileLockHelper.getFileLock(lockFile);       rewriteSuccess = rewritePatchInfoFile(pathInfoFile, info);       return rewriteSuccess;    }    private static boolean rewritePatchInfoFile(File pathInfoFile, SharePatchInfo info) {        // 写入默认构建指纹        if (ShareTinkerInternals.isNullOrNil(info.fingerPrint)) {            info.fingerPrint = Build.FINGERPRINT;        }        //写入默认oat目录        if (ShareTinkerInternals.isNullOrNil(info.oatDir)) {            info.oatDir = DEFAULT_DIR;        }        boolean isWritePatchSuccessful = false;        int numAttempts = 0;//尝试次数        File parentFile = pathInfoFile.getParentFile();        if (!parentFile.exists()) {            parentFile.mkdirs();        }        //尝试2次        while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isWritePatchSuccessful) {            numAttempts++;            Properties newProperties = new Properties();            newProperties.put(OLD_VERSION, info.oldVersion);//旧补丁版本            newProperties.put(NEW_VERSION, info.newVersion);//新补丁版本            newProperties.put(FINGER_PRINT, info.fingerPrint);//构建指纹            newProperties.put(OAT_DIR, info.oatDir);//oat目录            FileOutputStream outputStream = null;           outputStream = new FileOutputStream(pathInfoFile, false);           String comment = "from old version:" + info.oldVersion + " to new version:" + info.newVersion;           //写入patch.info文件           newProperties.store(outputStream, comment);            //再读取出来,验证是否写入成功            SharePatchInfo tempInfo = readAndCheckProperty(pathInfoFile);            isWritePatchSuccessful = tempInfo != null && tempInfo.oldVersion.equals(info.oldVersion) && tempInfo.newVersion.equals(info.newVersion);            if (!isWritePatchSuccessful) {                pathInfoFile.delete();            }        }        if (isWritePatchSuccessful) {            return true;        }        return false;    }

杀死补丁合成进程:patch

删除旧的合成文件

重启主进程

Patch Load 补丁加载

由于Tinker代理了Application类,所以应用启动的Application类为TinkerApplication。

//com.tencent.tinker.loader.app.TinkerApplication//代理Application类public abstract class TinkerApplication extends Application    //Application初始化调用此方法    @Override    protected void attachBaseContext(Context base) {        super.attachBaseContext(base);        Thread.setDefaultUncaughtExceptionHandler(new TinkerUncaughtHandler(this));        onBaseContextAttached(base);    }//代理attachBaseContext()方法private void onBaseContextAttached(Context base) {        ...        loadTinker();        ...}//加载Tinkerprivate void loadTinker() {   tinkerResultIntent = new Intent();   try {       //反射AbstractTinkerLoader类,因为此类可由开发者自定义。默认为com.tencent.tinker.loader.TinkerLoader       Class<?> tinkerLoadClass = Class.forName(loaderClassName, false, getClassLoader());        //反射调用tryLoad(TinkerApplicaiton app)方法       Method loadMethod = tinkerLoadClass.getMethod(AbstractTinkerLoader, TinkerApplication.class);       Constructor<?> constructor = tinkerLoadClass.getConstructor();       tinkerResultIntent = (Intent) loadMethod.invoke(constructor.newInstance(), this);   } catch (Throwable e) {   }}

准备

验证

验证文件合法性
验证patch-641e634c.apk是否包含对应的dex、so、res文件
验证合成后的补丁文件签名和TinkerId,同时读取出其中包含的文件meta.txt,
将新的补丁信息写回patch.info文件
检查是否超出安全模式次数

加载合成后的dex

//com.tencent.tinker.loader.TinkerDexLoaderpublic static boolean loadTinkerJars(final TinkerApplication application,                             String directory, //补丁版本目录,例patch-641e634c                            String oatDir, //dex优化后文件目录(patch-641e634c/odex                            Intent intentResult, //合并结果                            boolean isSystemOTA  //系统是否支持oat编译) {    //获取默认的PatchClassLoader    PathClassLoader classLoader = (PathClassLoader) TinkerDexLoader.class.getClassLoader();    //dex文件目录  patch-641e634c/dex/    String dexPath = directory + "/" + DEX_PATH + "/";    //如果开启的加载验证,会验证dex文件的MD5值是否与meta.txt文件中的记录是否一致    if (application.isTinkerLoadVerifyFlag()) {        ...    }    //如果系统支持oat,则进行dex2oat操作。前面已经说了dex2oat的实现,这里不再重复。    if (isSystemOTA) {        ...        //这里关键是获取dex的指令集(arm|libs|x86),后面会再详细说明。        targetISA = ShareOatUtil.getOatFileInstructionSet(testOptDexFile);        TinkerParallelDexOptimizer.optimizeAll(                legalFiles, optimizeDir, **true**, targetISA,                new TinkerParallelDexOptimizer.ResultCallback() {            );    }    //然后就是根据不同的系统版本,将新的dex数组插入到dexElements数组前面,实现错误修复。    SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles);}

DexElements操作

类查找原理

在程序运行时,使用到某个类时,虚拟机需要从dex文件中找出响应的类并加载到虚拟机,然后构造其实例对象供开发者使用。那么,虚拟机是如何找到需要的类的呢?答案就是PathClassLoader,PatchClassLoader继承自BaseDexClassLoader,可以加载指定路径的dex文件。BaseDexClassLoader包含一个类型为DexPathList成员变量pathList,顾名思义,DexPathList是包含多个Dex文件的列表,具体类查找过程由DexPathList执行。下面看BaseDexClassLoade的实现:

//dalvik.system.BaseDexClassLoaderpublic class BaseDexClassLoader extends ClassLoader {    private final DexPathList pathList;    //构造函数    public BaseDexClassLoader(String dexPath, File optimizedDirectory,                            String libraryPath, ClassLoader parent) {        super(parent);        this.originalPath = dexPath;        //新建pathList对象        this.pathList =            new DexPathList(this, dexPath, libraryPath, optimizedDirectory);    }    @Override    protected Class<?> findClass(String name) throws ClassNotFoundException {        //类查找操作交给pathList        Class clazz = pathList.findClass(name);        if (clazz == null) {            throw new ClassNotFoundException(name);        }        return clazz;    }}

下面看DexPathList的类查找过程

//dalvik.system.DexPathListfinal class DexPathList {    //dex元素数组 Element是DexPathList的内部静态类,组合了dex原文件、DexFile、ZipFile    private final Element[] dexElements;    //构造函数    public DexPathList(ClassLoader definingContext, String dexPath,            String libraryPath, File optimizedDirectory) {        this.definingContext = definingContext;        //根据dex文件路径和odex目录,初始化dexElements        this.dexElements =            makeDexElements(splitDexPath(dexPath), optimizedDirectory);        this.nativeLibraryDirectories = splitLibraryPath(libraryPath);    }    //类查找    public Class findClass(String name) {    //遍历dexElements数组,如果找到名称一致的类则返回。    for (Element element : dexElements) {        DexFile dex = element.dexFile;        if (dex != null) {            //根据类名从dex加载            Class clazz = dex.loadClassBinaryName(name, definingContext);            if (clazz != null) {               return clazz;            }        }    }    return null;    //由Dex文件创建Element数组,一般通过反射此方法生成补丁dex文件的Element    private static Element[] makeDexElements(ArrayList<File> files,                                            File optimizedDirectory) {    }}
所以类修复的原理就是,加载补丁中的dex,然后将新的dexElements数组插到旧的dexElements数组前面,这样再查找类的时候,就会优先从补丁dex文件寻找,达到动态修复的目的。其中需要反射修改的字段为BaseClassLoader中的pathList字段和DexPathList类中的dexElements字段。不过由于不同系统版本字段名称或方法参数不同,所以需要区分版本进行反射修改。
区分系统版本
//com.tencent.tinker.loader.SystemClassLoaderAdderpublic static void installDexes(Application application, PathClassLoader loader,                                        File dexOptDir, List<File> files){    //对多个classes.dex排序 排序后:[classes.dex classes2.dex classes3.dex test.dex]    files = createSortedAdditionalPathEntries(files);     ClassLoader classLoader = loader;    //在Android7.0以上系统,自定义ClassLoader    if (Build.VERSION.SDK_INT >= 24 && !checkIsProtectedApp(files)) {        classLoader = AndroidNClassLoader.inject(loader, application);    }       //然后根据不同系统版本,有不同的操作实现    if (Build.VERSION.SDK_INT >= 23) {        V23.install(classLoader, files, dexOptDir);    } else if (Build.VERSION.SDK_INT >= 19) {        V19.install(classLoader, files, dexOptDir);    } else if (Build.VERSION.SDK_INT >= 14) {        V14.install(classLoader, files, dexOptDir);    } else {        V4.install(classLoader, files, dexOptDir);    }}
在Android 7.0上,为了避免一个dex文件在多个ClassLoader中注册产生的异常,需要自定义    ClassLoader。也是为了避免AndroidN混合编译带来的影响。1、替换AndroidNClassLoader中的pathList字段为原始PathClassLoader中的 

pathList字段,之后用AndroidNClassLoader做dexElements数组扩展操作。
2、然后还要替换掉 Context.mBase.mPackageInfo中持有的mClassLoader字段和当前线程的classLoader。
这样就保证后面所有类的加载都是用自定义的AndroidNClassLoader。

AndroidNClassLoader实现

//com.tencent.tinker.loader.AndroidNClassLoaderclass AndroidNClassLoader extends PathClassLoader {    //构造方法    private AndroidNClassLoader(String dexPath, PathClassLoader parent,                                         Application application) {        super(dexPath, parent.getParent());        //保存原始ClassLoader,即加载TinkerDexLoader的类加载器        originClassLoader = parent;        String name = application.getClass().getName();        if (name != null && !name.equals("android.app.Application")) {            //保存Application类名称:com.tencent.tinker.loader.app.TinkerApplication            applicationClassName = name;        }    }    //自定义类查找规则    public Class<?> findClass(String name) throws ClassNotFoundException {        // 与tinker.loader相关的类由默认的类加载器加载,包含TinkerApplication类        // 其他的类由此加载器加载        if ((name != null            && name.startsWith("com.tencent.tinker.loader.")            && !name.equals(SystemClassLoaderAdder.CHECK_DEX_CLASS))            || (applicationClassName != null&&applicationClassName.equals(name))){            return originClassLoader.loadClass(name);        }        return super.findClass(name);    }    //新建AndroidNClassLoader并注入到应用上下文中    public static AndroidNClassLoader inject(PathClassLoader originClassLoader,                                         Application application) throws Exception {        //新建AndroidNClassLoader        AndroidNClassLoader classLoader =                           createAndroidNClassLoader(originClassLoader, application);        //修改Context.mBase.mPackageInfo.mClassLoader和Thread持有的ClassLoader        reflectPackageInfoClassloader(application, classLoader);        return classLoader;    }    //反射修改修改Context.mBase.mPackageInfo.mClassLoader字段为AndroidNClassLoader    //设置当前线程的ClassLoader为AndroidNClassLoader    private static void reflectPackageInfoClassloader(Application application,                                       ClassLoader reflectClassLoader) throws Exception {       String defBase = "mBase";       String defPackageInfo = "mPackageInfo";       String defClassLoader = "mClassLoader";       Context baseContext = (Context) ShareReflectUtil.findField(application,                                                              defBase).get(application);       Object basePackageInfo = ShareReflectUtil.findField(baseContext,                                                  defPackageInfo).get(baseContext);       Field classLoaderField = ShareReflectUtil.findField(basePackageInfo,                                                                   defClassLoader);       Thread.currentThread().setContextClassLoader(reflectClassLoader);       classLoaderField.set(basePackageInfo, reflectClassLoader);    }    这里就不展开说明AndroidNClassLoader的新建过程了,主要步骤为从原始的ClassLoader中取出已经加载的DexFile、libFile文件名称列表,再构造出新的DexPathList,然后赋值给AndroidNClassLoader。

获取Dex指令集

dex2oat是将dex指令编译成本地机器指令,所以需要指定编译的指令集,应与当前机器的cpu指令集一致。基本原理为读取dex文件oat之后的ELF文件中的固定块的值,以此判断cpu指令集。代码实现为

//com.tencent.tinker.loader.shareutil.ShareOatUtilpublic static String getOatFileInstructionSet(File oatFile) throws Throwable {    ...    switch (InstructionSet.values()[isaNum]) {                case kArm:                case kThumb2:                    result = "arm";                    break;                case kArm64:                    result = "arm64";                    break;                case kX86:                    result = "x86";                    break;                case kX86_64:                    result = "x86_64";                    break;                case kMips:                    result = "mips";                    break;                case kMips64:                    result = "mips64";                    break;                case kNone:                    result = "none";                    break;                default:                    throw new IOException("Should not reach here.");            }}

判断Cpu指令集无非是分析ELF文件格式,但是这个已经编译好的文件是从哪里来的?之前我们说过,在补丁合并时并没有做dex2oat操作,因为不知道具体机型的指令集。从源码里看是分析的test.dex.dex文件,不过这个文件只是在dex合并时进行了odex操作,留个坑~~~
test.dex测试dex是否插入成功

加载合成后的res

Res资源查找原理

在Android开发中,使用资源的方式分两种: 一种是使用res包下面压缩资源的,通过getResoures()返回的Resources访问。第二种是访问asset文件夹下的原始资源,通过getAsset()返回AssetManager访问。

Resouces资源查找

我们在开发是使用字符串或图片资源的时候,是通过getResources()方法获取到一个Resources对象,然后通过Resources获取各种资源的。无论是在Activity中,还是在Fragment中。在Fragment中获取Resoures时实际上是在基类中调用getActivity.getResources()间接获取到的。

//android.content.res.Resources//访问应用程序资源类。//存在的意义在于只能访问该应用级别的资源//并提供了一组从Assets中获取指定类型数据的高级APIpublic class Resources {    ...    final AssetManager mAssets;    ...    //以获取字符串资源为例    //可以看出Resouces类内部也是通过AssetManager查找的资源    public CharSequence getText(int id) throws NotFoundException {        CharSequence res = mAssets.getResourceText(id);        if (res != null) {            return res;        }    }}
AssetManger资源查找
//android.content.res.AssetManager//该类提供了对应用程序原始资源的访问//该类提供了一个较低级别的Api,通过简单字节流的方式读取与应用绑定的原始文件public final class AssetManager implements AutoCloseable {    //构造函数    //该构造函数通常不会被应用程序使用到,因为新创建的AssetManager仅仅包含基本的系统资源    //应用程序应该通过Resources.getAssets检索对应的资源管理    public AssetManager() {        synchronized (this) {            init(false);            ensureSystemAssets();        }    }    //根据指定的标识符(R.String.id)获取字符串资源    final CharSequence getResourceText(int ident) {         synchronized (this) {            TypedValue tmpValue = mValue;            int block = loadResourceValue(ident, (short) 0, tmpValue, true);            if (block >= 0) {                if (tmpValue.type == TypedValue.TYPE_STRING) {                    return mStringBlocks[block].get(tmpValue.data);                }                return tmpValue.coerceToString();            }        }        return null;    }    //向AssetManager中添加一个新的资源包路径    public final int addAssetPath(String path) {       synchronized (this) {           int res = addAssetPathNative(path);           makeStringBlocks(mStringBlocks);           return res;       }    }}
Resouces和AssetManager前生今世

那Activity中这个Resources是哪来的?通过Activity的继承关系可以得到答案。
Activity extends ContextThemeWrapper extends ContextWrapper extends Context。通过这个继承关系可以知道Activity就是一个Context,Context抽象类中有一个getResources()方法,可以获取到主线程Resources对象。为什么说是主线程的Resources对象,因为Activity是在主线程创建的嘛!
实际上,无论是ContextThemeWrapper和ContextWrapper,从类名可以看出来他们只是一个继承自Context的代理类,并没有具体实现。Context的真正实现是ContextImpl,并且通过ContextWrapper
的attachBaseContext(Context base)方法将代理对象赋值给ContextWrapper。所以Resources对象来自于ContextImpl。

下面看一下ContextImpl中mResoures对象的创建过程。```//android.app.ContextImpl//Context Api的通过实现,为Android四大组件提供了基础的Context对象    class ContextImpl extends Context {    //和res资源查找相关的几个关键属性    final ActivityThread mMainThread;    final LoadedApk mPackageInfo;    private final ResourcesManager mResourcesManager;    private final Resources mResources;    //私有的构造函数    private ContextImpl(ContextImpl container, ActivityThread mainThread,        LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean          restricted,Display display, Configuration overrideConfiguration) {            ...            mOuterContext = this;            mMainThread = mainThread;            mPackageInfo = packageInfo;            mResourcesManager = ResourcesManager.getInstance();            //从主线程中获取Resouces对象,从后面AssetManager的替换情况看,Tinker并没            有直接替换ContextImpl中的mResources属性,而是将ResourcesManager中的所有            Resources对象中的AssetManager替换掉。            Resources resources = packageInfo.getResources(mainThread);            //如果resouces对象不为null,则通过mResoucesManager查找顶级的Resouces            if (resources != null) {                    if (activityToken != null                        || displayId != Display.DEFAULT_DISPLAY                        || overrideConfiguration != null                        || (compatInfo != null && compatInfo.applicationScale                        != resources.getCompatibilityInfo().applicationScale))                        {                resources = mResourcesManager.getTopLevelResources(                                                packageInfo.getResDir(),                                                packageInfo.getSplitResDirs(),                                                      packageInfo.getOverlayDirs(),                        packageInfo.getApplicationInfo().sharedLibraryFiles,                                                    displayId,                            overrideConfiguration, compatInfo, activityToken);                    }             }    mResources = resources;    }    //获取AssetManager    @Override    public AssetManager getAssets() {        return getResources().getAssets();    }    //获取Resources    @Override    public Resources getResources() {        return mResources;    }}```    
//android.app.LoadedApk//当前已经已经加载的Apk的本地状态public final class LoadedApk{    //Apk中资源目录    private final String mResDir;}

看到底,ContextImpl的创建过程:

//android.app.ActivityThread//在应用进程中,管理主线程的执行,调度和执行Activity、广播,还有ActivityManager其他的操作请求public final class ActivityThread {    //ApplicaitonThread是一个Binder接口,用来进程间通信    final ApplicationThread mAppThread = new ApplicationThread();    //    final ArrayMap<String, WeakReference<LoadedApk>> mPackages                            = new ArrayMap<String, WeakReference<LoadedApk>>();    //    final ArrayMap<String, WeakReference<LoadedApk>> mResourcePackages                            = new ArrayMap<String, WeakReference<LoadedApk>>();    //什么鬼                            private final ResourcesManager mResourcesManager;    //该类有两种创建入口    //1、main()方法    //2、systemMain()方法    //新建ActivityThread对象,均调用attach(boolean isSystem)方法,区别就是当前应用是否是      //系统应用    ActivityThread() {        mResourcesManager = ResourcesManager.getInstance();    }    public static ActivityThread systemMain() {        ...        ActivityThread thread = new ActivityThread();        thread.attach(true);        return thread;    }    public static void main(String[] args) {        ...        Looper.prepareMainLooper();        ActivityThread thread = new ActivityThread();        thread.attach(false);        Looper.loop();        ...    }    private void attach(boolean system) {        sCurrentActivityThread = this;        mSystemThread = system;        if (!system) {            ...            RuntimeInit.setApplicationObject(mAppThread.asBinder());            final IActivityManager mgr = ActivityManagerNative.getDefault();            try {                //调用IActivityManager的attachApplication(ApplicationThread at)方法                mgr.attachApplication(mAppThread);            } catch (RemoteException ex) {                // Ignore            }            ...        } else {        }    }    //ActivityThread启动后,会有一系列的binder通信,告知当前进程启动一个Activity    //1、Launcher通过Binder进程间通信机制通知ActivityManagerService,它要启动一个Activity;    //2、ActivityManagerService通过Binder进程间通信机制通知Launcher进入Paused状态;    //3、Launcher通过Binder进程间通信机制通知ActivityManagerService,它已经准备就绪进入Paused状态,于是ActivityManagerService就创建一个新的进程,用来启动一个ActivityThread实例,即将要启动的Activity就是在这个ActivityThread实例中运行;    //4、ActivityThread通过Binder进程间通信机制将一个ApplicationThread类型的Binder对象传递给ActivityManagerService,以便以后ActivityManagerService能够通过这个Binder对象和它进行通信;    //5、ActivityManagerService通过Binder进程间通信机制通知ActivityThread,现在一切准备就绪,它可以真正执行Activity的启动操作了。    //在启动Activity的时候会创建ContextImpl    private Activity performLaunchActivity(ActivityClientRecord r, Intent                                                                          customIntent) {        ...        Activity activity = null;        java.lang.ClassLoader cl = r.packageInfo.getClassLoader();        //新建Activity        activity = mInstrumentation.newActivity(                                        cl, component.getClassName(), r.intent);        if (activity != null) {            //创建ActivityContext            Context appContext = createBaseContextForActivity(r, activity);            ...            //设置给新建的Activity            activity.attach(appContext, this, getInstrumentation(), r.token,                        r.ident, app, r.intent, r.activityInfo, title, r.parent,                        r.embeddedID, r.lastNonConfigurationInstances, config,                        r.referrer, r.voiceInteractor);            ...        }        }    //为Activity创建ContextImpl    private Context createBaseContextForActivity(ActivityClientRecord r,                                                    final Activity activity) {        ContextImpl appContext = ContextImpl.createActivityContext(this,                                                            r.packageInfo, r.token);        appContext.setOuterContext(activity);        Context baseContext = appContext;        ...        return baseContext;    }}

ResourceManager也需要反射的。

//android.app.ResourcesManagerpublic class ResourcesManager {    //保存了应用中所有的Resources对象,如果没有加载外部的apk,则只有一个原始应用apk一个      Resources    final ArrayMap<ResourcesKey, WeakReference<Resources> > mActiveResources            = new ArrayMap<ResourcesKey, WeakReference<Resources> >();}

ApplicationINfo类也需要反射,为了解决WebView翻转的bug

//android.content.pm.ApplicationInfo//基础Apk的全路径类似:/base-1.apkpublic String sourceDir;//sourceDir目录的公共的可用的部分,包含资源文件、manifest//这个目录和sourceDir是不同的,如果应用是向前锁定的//个人理解这个目录是可以被其他进程访问的公开资源目录public String publicSourceDir;

综合上面获取应用资源的流程可以看出,如果想要替换已有的应用资,可以创建一个新的AssetManager对象,加载新的资源包路径。然后通过反射技术替换掉mResouces对象中持有的mAssets变量即可。

通过Resources resources = packageInfo.getResources(mainThread);可知,Resources是存储在LoadedApk类型的packageInfo实例中,所以最好也要把packageInfo中的Resources实例也替换掉。

Tinker加载合并res分析

//com.tencent.tinker.loader.TinkerResourceLoadepublic static boolean loadTinkerResources(TinkerApplication application, String                                                         directory, Intent intentResult) {    //合成文件路径为/tinker/patch-641e634c/res/resources.apk    String resourceString = directory + "/" + RESOURCE_PATH +  "/"+RESOURCE_FILE;    File resourceFile = new File(resourceString);        //如果开启了加载验证,需要验证资源文件的MD5    if (application.isTinkerLoadVerifyFlag()) {    }                //由TinkerResourcePatcher执行资源文件加载    TinkerResourcePatcher.monkeyPatchExistingResources(application,                                                                 resourceString);           }
//com.tencent.tinker.loader.TinkerResourcePatcher//准备工作,各种反射public static void isResourceCanPatch(Context context) throws Throwable {    //反射得到context中的activityThread对象    Class<?> activityThread = Class.forName("android.app.ActivityThread");    currentActivityThread = ShareReflectUtil.getActivityThread(context,                                                                         activityThread);    //加载LoadedApk类,并取到其中的resDir属性                                                                Class<?> loadedApkClass;    try {        loadedApkClass = Class.forName("android.app.LoadedApk");    } catch (ClassNotFoundException e) {        loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo");    }    resDir = loadedApkClass.getDeclaredField("mResDir");    resDir.setAccessible(true);    //取到ActivityThread的packagesFiled和resourcePackagesFiled属性    packagesFiled = activityThread.getDeclaredField("mPackages");    packagesFiled.setAccessible(true);    resourcePackagesFiled = activityThread.getDeclaredField("mResourcePackages");    resourcePackagesFiled.setAccessible(true);    //新建AssetManager,这里区分百度系统自定义的BaiduAssetManager    AssetManager assets = context.getAssets();    // Baidu os    if (assets.getClass().getName().                                equals("android.content.res.BaiduAssetManager")) {        Class baiduAssetManager =                         Class.forName("android.content.res.BaiduAssetManager");        newAssetManager = (AssetManager)                             baiduAssetManager.getConstructor().newInstance();    } else {        newAssetManager = AssetManager.class.getConstructor().newInstance();    }    //获取AssetManager的addAssetPath(String path)方法,方便后面添加补丁路径    addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath",                                                                       String.class);    addAssetPathMethod.setAccessible(true);    //反射获取AssetManager的ensureStringBlocks方法,4.4系统在调用addAssetPath方法后,需    要额外调用此方法    ensureStringBlocksMethod =                      AssetManager.class.getDeclaredMethod("ensureStringBlocks");    ensureStringBlocksMethod.setAccessible(true);    //获取ResourcesManager中持有的所有可用的Resources对象,这些Resoures里面的      AssetManager也需要替换    //4.4系统前后的这个Resrouces列表所处位置不同    //4.4前直接在ActivityThread类中持有,名字是mActivityResoures,没有ResourcesManager    //4.4后在ResourcesManager单例中,名字是mActiveResources(7.0之前)或mResourceReferences(7.0之后)    if (SDK_INT >= KITKAT) {            //4.4之后            //pre-N            // Find the singleton instance of ResourcesManager            Class<?> resourcesManagerClass =                                        Class.forName("android.app.ResourcesManager");            Method mGetInstance =                            resourcesManagerClass.getDeclaredMethod("getInstance");            mGetInstance.setAccessible(true);            Object resourcesManager = mGetInstance.invoke(null);            try {                Field fMActiveResources =                       resourcesManagerClass.getDeclaredField("mActiveResources");                fMActiveResources.setAccessible(true);                ArrayMap<?, WeakReference<Resources>> activeResources19 =                        (ArrayMap<?, WeakReference<Resources>>)                                         fMActiveResources.get(resourcesManager);                references = activeResources19.values();            } catch (NoSuchFieldException ignore) {                // N moved the resources to mResourceReferences                Field mResourceReferences =                    resourcesManagerClass.getDeclaredField("mResourceReferences");                mResourceReferences.setAccessible(true);                references = (Collection<WeakReference<Resources>>)                                        mResourceReferences.get(resourcesManager);            }        } else {            //4.4之前            Field fMActiveResources =                               activityThread.getDeclaredField("mActiveResources");            fMActiveResources.setAccessible(true);            HashMap<?, WeakReference<Resources>> activeResources7 =                         (HashMap<?, WeakReference<Resources>>)                                     fMActiveResources.get(currentActivityThread);            references = activeResources7.values();        }        //最后反射得到Resources的AssetManager字段,为后面替换做准备        //7.0之前该字段名称为mAsset,7.0之后改为mResourcesImpl        if (SDK_INT >= 24) {            try {                // N moved the mAssets inside an mResourcesImpl field                resourcesImplFiled =                         Resources.class.getDeclaredField("mResourcesImpl");                resourcesImplFiled.setAccessible(true);            } catch (Throwable ignore) {                // for safety                assetsFiled = Resources.class.getDeclaredField("mAssets");                assetsFiled.setAccessible(true);            }        } else {            assetsFiled = Resources.class.getDeclaredField("mAssets");            assetsFiled.setAccessible(true);        }}//替换respublic static void monkeyPatchExistingResources(Context context, String                                             externalResourceFile) throws Throwable {    //1、替换LoadedApk中的resDir为合成后的资源文件目录,LoadedApk位于ActivityThread中类型    //为ArrayMap的mPackage和mResourcePackages字段中。    for (Field field : new Field[]{packagesFiled, resourcePackagesFiled}) {            Object value = field.get(currentActivityThread);            for (Map.Entry<String, WeakReference<?>> entry                : ((Map<String, WeakReference<?>>) value).entrySet()) {                Object loadedApk = entry.getValue().get();                if (loadedApk == null) {                    continue;                }                if (externalResourceFile != null) {                    resDir.set(loadedApk, externalResourceFile);                }            }    }    //2、为新建的AssetManager对象,添加新的资源路径    if (((Integer) addAssetPathMethod.invoke(newAssetManager,                                                     externalResourceFile)) == 0) {            throw new IllegalStateException("Could not create new AssetManager");    }    //3、确保安全,调用AssetManager的ensureStingBlocks()方法    ensureStringBlocksMethod.invoke(newAssetManager);    //4、从ResourcesManager中取出所有Resources引用,并替换其中的mAssetManager对象    for (WeakReference<Resources> wr : references) {            Resources resources = wr.get();            if (resources != null) {                try {                    assetsFiled.set(resources, newAssetManager);                } catch (Throwable ignore) {                    // N                    Object resourceImpl = resourcesImplFiled.get(resources);                    // for Huawei HwResourcesImpl                    Field implAssets = ShareReflectUtil.findField(resourceImpl, "mAssets");                    implAssets.setAccessible(true);                    implAssets.set(resourceImpl, newAssetManager);                }                //清空预加载的类型数组问题                //Reource类有一个mTypedArrayPool属性,SynchronizedPool<TypedArray>                             mTypedArrayPool = new SynchronizedPool<TypedArray>(5);                //在miui系统上,把TypedArray改成了MiuiTypesArray,然后从其中获取字符串,                而不是从AssetManager中获取,所以需要把mTypedArrayPool清空。                clearPreloadTypedArrayIssue(resources);                resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());            }        }    //5、问题规避:Android 7.0上,如果Activity包含一个WebView,当屏幕反转后,资源补丁会失效    //在5.x、6.x的机器上,发现了StatusBarNotification无法展开RemoteView异常    if (Build.VERSION.SDK_INT >= 24) {        if (publicSourceDirField != null) {            publicSourceDirField.set(context.getApplicationInfo(),                                                             externalResourceFile);        }    }    //6、检测是否加载Res成功    if (!checkResUpdate(context)) {        throw new TinkerRuntimeException(ShareConstants.CHECK_RES_INSTALL_FAIL);    }}

总结:替换Res的思路是
1、新建AssetManager,并添加新的res文件路径。
2、需要替换的Resource对象是ResourceManager哈希表中存储的Resources列表
3、需要替换LoadedApk中的resDir
4、需要替换ApplicationInfo中publicSourceDir的值新的res目录

手动加载so文件

在加载补丁文件的时候,并不会像预加载Dex那样,直接加载so包,只是缓存下来了so补丁包的路径和对应的MD5值。例:lib/arm-v7/libtest.so : agadsgwee234ft354t

//com.tencent.tinker.loader.TinkerSoLoaderfor (ShareBsDiffPatchInfo info : libraryList) {    String middle = info.path + "/" + info.name;    libs.put(middle, info.md5);}

当需要加载补丁so文件时,可以通过TinkerApplicationHelper类实现。

//com.tencent.tinker.lib.tinker.TinkerApplicationHelperpublic static boolean loadLibraryFromTinker(ApplicationLike applicationLike,                    String relativePath, String libname) throws UnsatisfiedLinkError {    ...    System.load(patchLibraryPath);}

善后

如果是oat模式下,需要杀死其他进程。

if (oatModeChanged) {            ShareTinkerInternals.killAllOtherProcess(app);            Log.i(TAG, "tryLoadPatchFiles:oatModeChanged, try to kill all other process");        }
原创粉丝点击