Tinker源码分析

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.CliMain

public 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.Runner

protected 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.ApkDecoder

public 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.ApkDecoder
public 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.ManifestDecoder

public 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.BsDiffDecoder

public 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文件示例如下

扫描二维码关注公众号,回复: 1732151 查看本文章
resources_out.zip,1134154093,26a3339220be96e865fc523fe4a162a8
pattern:3
resources.arsc
res/*
assets/*
large modify:1
resources.arsc,26a3339220be96e865fc523fe4a162a8,946796371
modify:1
res/layout/activity_bug.xml
add:199
res/drawable-hdpi-v4/abc_ab_share_pack_mtrl_alpha.9.png
res/drawable-hdpi-v4/abc_btn_check_to_on_mtrl_000.png
res/drawable-hdpi-v4/abc_btn_check_to_on_mtrl_015.png
res/drawable-hdpi-v4/abc_btn_radio_to_on_mtrl_000.png
res/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.DexPatchGernerator


public 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.AbstractPatch

public 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.ShareSecurityCheck

public 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.ShareDexDiffPatchInfo
    
    public final String rawName;        //dex原始文件名称,例如classes2.dex
    public 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或jar
    public 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.DexSectionPatchAlgorithm

private 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.DexDiffPatchInternal

private 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.TinkerParallelDexOptimizer

public static boolean optimizeAll(Collection<File> dexFiles, File optimizedDir, ResultCallback cb) {

   return optimizeAll(dexFiles, optimizedDir, **false**, null, cb);
}

//此处useInterpretMode参数一直为false
public 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.OptimizeWorker

private 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.DexDiffPatchInternal

protected 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.SharePatchInfo

public 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();
        ...
}

//加载Tinker
private 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.TinkerDexLoader

public 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.BaseDexClassLoader
public 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.DexPathList

final 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.SystemClassLoaderAdder

public 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.AndroidNClassLoader

class 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.ShareOatUtil
public 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中获取指定类型数据的高级API
public 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.ResourcesManager

public 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.apk
public 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.TinkerResourceLoade

public 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);
        }


}

//替换res
public 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.TinkerSoLoader
for (ShareBsDiffPatchInfo info : libraryList) {
    String middle = info.path + "/" + info.name;
    libs.put(middle, info.md5);
}

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

//com.tencent.tinker.lib.tinker.TinkerApplicationHelper

public 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");
        }

猜你喜欢

转载自blog.csdn.net/joye123/article/details/74760406