腾讯热修复框架tinker

Tinker分析:

什么是tinker?

Tinker是腾讯出的一款热修复框架,可以修复代码,资源文件,so库,但不能新增四大组件。

热修复与增量更新的本质区别:增量更新是根据new.apk和old.apk按照bsdiff算法,生成一个patch,然后将patch通过服务端推送,推送给客户端,客户端下载patch,再使用bsdiff算法,将patch和old.apk生成新的apk,完成升级。需要重新安装。

热修复,是不需要进行重新安装,所以这就导致了热修复是不能新增四大组件的。

Tinker使用:

目前是2种,一种是直接使用tencent提供的gradleproject依赖的方式,直接项目依赖;另一种是使用命令行手动生成patch.下面就说明本地测试的使用命令行的方式进行的demo:

…………后面再说

Tinker源码分析:分2步,首先是生成patch的过程。克隆tencenttinker github源码,目录下的module:tinker-patch-cli就是patch工具的代码。

使用该jar工具的方式,命令行输入:

java -jar tinker-patch-cli-1.7.7.jar -oldold.apk -new new.apk -config tinker_config.xml -out output

private void run(String[] args) {

     …………

       try {

           ReadArgs readArgs = new ReadArgs(args).invoke();//就是生成patch时输入的命令:java-jar tinker-patch-cli-1.7.7.jar -old old.apk -new new.apk -configtinker_config.xml -out output

           File configFile = readArgs.getConfigFile();// 配置文件,tinker_config.xml

           File outputFile = readArgs.getOutputFile();//

           File oldApkFile = readArgs.getOldApkFile();

           File newApkFile = readArgs.getNewApkFile();

           if (oldApkFile == null || newApkFile == null) {

               Logger.e("Missing old apk or new apk file argument");

               goToError();

           } else if (!oldApkFile.exists() || !newApkFile.exists()) {

               Logger.e("Old apk or new apk file does not exist");

               goToError();

           }

           if (outputFile == null) {

               outputFile = new File(mRunningLocation, TypedValue.PATH_DEFAULT_OUTPUT);

           }

这3个方法是关键,下面进行一一说明。

           loadConfigFromXml(configFile, outputFile,oldApkFile, newApkFile);

            Logger.initLogger(config);

            tinkerPatch();

        }catch (IOException e) {

           e.printStackTrace();

           goToError();

        }finally {

           Logger.closeLogger();

        }

}

loadConfigFromXml(configFile,outputFile, oldApkFile, newApkFile);

整个方法就是生成一个config对象,就相当于把tinker_config.xml转化成一个对象。

 

下面开始具体的patch生成:tinkerPatch();

protected void tinkerPatch() {

       Logger.d("-----------------------Tinker patchbegin-----------------------");

       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 patchend-------------------------");

}

ApkDecoder:

 publicApkDecoder(Configuration config) throws IOException {

       super(config);

       this.mNewApkDir = config.mTempUnzipNewDir;

       this.mOldApkDir = config.mTempUnzipOldDir;

       this.manifestDecoder = new ManifestDecoder(config);

       //put meta files in assets

       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 = newArrayList<>();

}

会发现,针对dex文件,so文件和res文件会生成相应的decoder,这些decoder都继承自BaseDecoder,相当于文件解码器。这些decoder的主要工作都在抽象方法patch()中实现。

decoder.onAllPatchesStart()和decoder.onAllPatchesEnd()都是空实现,不用分析,下面重点分析:

decoder.patch(config.mOldApkFile, config.mNewApkFile);

public boolean patch(File oldFile, File newFile)throws Exception {

       writeToLogFile(oldFile, newFile);//写入log文件,忽略。

       //check manifest change first

//主要分析1

        manifestDecoder.patch(oldFile, newFile);

//主要分析2

        unzipApkFiles(oldFile, newFile);

//主要分析3

       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 librarypattern, "

               + "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;

    }


主要分析1:先看ManifestDecoder的patch():

@Override

    publicboolean patch(File oldFile, File newFile) throws IOException,TinkerPatchException {

       try {

 //这2个方法涉及到解析编译后的AndroidManifest.xml和resource.arsc文件,这是一个非常复杂的工程。就不详细分析了。

           AndroidParser oldAndroidManifest = AndroidParser.getAndroidManifest(oldFile);

           AndroidParser newAndroidManifest =AndroidParser.getAndroidManifest(newFile);

           //check minSdkVersion

           int minSdkVersion =Integer.parseInt(oldAndroidManifest.apkMeta.getMinSdkVersion());

           if (minSdkVersion < TypedValue.ANDROID_40_API_LEVEL) {

               if (config.mDexRaw) {

                    final StringBuilder sb =new StringBuilder();

                    sb.append("your oldapk's minSdkVersion ")

                      .append(minSdkVersion)

                      .append(" is below14, you should set the dexMode to 'jar', ")

                      .append("otherwise,it will crash at some time");

                   announceWarningOrException(sb.toString());

               }

           }

           final String oldXml = oldAndroidManifest.xml.trim();

           final String newXml = newAndroidManifest.xml.trim();

           final boolean isManifestChanged = !oldXml.equals(newXml);

           if (!isManifestChanged) {

               Logger.d("\nManifest has no changes, skip rest decodeworks.");

               return false;

           }

           // check whether there is any new Android Component and get their names.

            // so far only Activity increment can passchecking.

//不支持新增四大组件。

           final Set<String> incActivities =getIncrementActivities(oldAndroidManifest.activities,newAndroidManifest.activities);

           final Set<String> incServices =getIncrementServices(oldAndroidManifest.services, newAndroidManifest.services);

           final Set<String> incReceivers =getIncrementReceivers(oldAndroidManifest.receivers,newAndroidManifest.receivers);

           final Set<String> incProviders =getIncrementProviders(oldAndroidManifest.providers,newAndroidManifest.providers);

           final boolean hasIncComponent = (!incActivities.isEmpty() ||!incServices.isEmpty()

                    || !incProviders.isEmpty()|| !incReceivers.isEmpty());

           if (!config.mSupportHotplugComponent && hasIncComponent) {

               announceWarningOrException("manifest was changed, while hot plugcomponent support mode is disabled. "

                        + "Such changeswill not take effect.");

           }

           // generate increment manifest.

           if (hasIncComponent) {

               final Document newXmlDoc =DocumentHelper.parseText(newAndroidManifest.xml);

               final Document incXmlDoc = DocumentHelper.createDocument();

               final Element newRootNode = newXmlDoc.getRootElement();

               final String packageName =newRootNode.attributeValue(XML_NODEATTR_PACKAGE);

               if (Utils.isNullOrNil(packageName)) {

                   throw newTinkerPatchException("Unable to find package name from manifest: " +newFile.getAbsolutePath());

               }

               final Element newAppNode =newRootNode.element(XML_NODENAME_APPLICATION);

               final Element incAppNode = incXmlDoc.addElement(newAppNode.getQName());

               copyAttributes(newAppNode, incAppNode);

               if (!incActivities.isEmpty()) {

                    final List<Element>newActivityNodes = newAppNode.elements(XML_NODENAME_ACTIVITY);

                    final List<Element>incActivityNodes = getIncrementActivityNodes(packageName, newActivityNodes,incActivities);

                    for (Element node :incActivityNodes) {

                       incAppNode.add(node.detach());

                    }

               }

               if (!incServices.isEmpty()) {

                    final List<Element>newServiceNodes = newAppNode.elements(XML_NODENAME_SERVICE);

                   final List<Element>incServiceNodes = getIncrementServiceNodes(packageName, newServiceNodes,incServices);

                    for (Element node :incServiceNodes) {

                       incAppNode.add(node.detach());

                    }

                }

               if (!incReceivers.isEmpty()) {

                    final List<Element>newReceiverNodes = newAppNode.elements(XML_NODENAME_RECEIVER);

                    final List<Element>incReceiverNodes = getIncrementReceiverNodes(packageName, newReceiverNodes,incReceivers);

                    for (Element node :incReceiverNodes) {

                       incAppNode.add(node.detach());

                    }

               }

               if (!incProviders.isEmpty()) {

                    final List<Element>newProviderNodes = newAppNode.elements(XML_NODENAME_PROVIDER);

                    final List<Element>incProviderNodes = getIncrementProviderNodes(packageName, newProviderNodes,incProviders);

                    for (Element node :incProviderNodes) {

                       incAppNode.add(node.detach());

                    }

               }

               final File incXmlOutput = new File(config.mTempResultDir,TypedValue.INCCOMPONENT_META_FILE);

               if (!incXmlOutput.exists()) {

                   incXmlOutput.getParentFile().mkdirs();

               }

               OutputStream os = null;

               try {

                    os = newBufferedOutputStream(new FileOutputStream(incXmlOutput));

                    final XMLWriter docWriter =new XMLWriter(os);

                    docWriter.write(incXmlDoc);

                    docWriter.close();

               } finally {

                    Utils.closeQuietly(os);

               }

           }

           if (isManifestChanged && !hasIncComponent) {

               Logger.d("\nManifest was changed, while there's no any newcomponents added."

                       + " Make sure ifsuch changes were all you expected.\n");

            }

        }catch (ParseException e) {

           e.printStackTrace();

           throw new TinkerPatchException("Parse android manifesterror!");

        }catch (DocumentException e) {

           e.printStackTrace();

           throw new TinkerPatchException("Parse android manifest by dom4jerror!");

        }catch (IOException e) {

           e.printStackTrace();

           throw new TinkerPatchException("Failed to generate incrementmanifest.", e);

        }

       return false;

}

主要分析2:unzipApkFiles(oldFile, newFile),

就是一个解压新旧apk的过程。可以学习到的是,针对apk的解压步骤。下面是解压apk的主要代码:

 publicstatic void unZipAPk(String fileName, String filePath) throws IOException {

       checkDirectory(filePath);//解压前,先判断destinationpath是否为空,为空的话就新建相关目录。

        ZipFile zipFile = newZipFile(fileName);//apk其实也是一种zip压缩格式的文件,下面就是java代码如何解压zip格式的文件。

第一步:zipfile.entries()得到该zip文件中所有的文件enum。

       Enumeration enumeration = zipFile.entries();

       try {

第二步:遍历emum,类似于cursor遍历。

           while (enumeration.hasMoreElements()) {

               ZipEntry entry = (ZipEntry) enumeration.nextElement();

第三步:如果是目录的话,就需要新建一个目录。

               if (entry.isDirectory()) {

                    new File(filePath,entry.getName()).mkdirs();

                    continue;

               }

第四步:如果不是目录,那肯定就是文件,开始进行readwrite过程。无论是inputstream还是outputstream都需要用bufferedstream来装饰下。

               BufferedInputStream bis = newBufferedInputStream(zipFile.getInputStream(entry));

               File file = new File(filePath + File.separator + entry.getName());

               File parentFile = file.getParentFile();

               if (parentFile != null && (!parentFile.exists())) {

                    parentFile.mkdirs();

               }

               FileOutputStream fos = null;

               BufferedOutputStream bos = null;

               try {

                    fos = newFileOutputStream(file);

                    bos = newBufferedOutputStream(fos, TypedValue.BUFFER_SIZE);

                    byte[] buf = newbyte[TypedValue.BUFFER_SIZE];

                    int len;

                    while ((len = bis.read(buf,0, TypedValue.BUFFER_SIZE)) != -1) {

                        fos.write(buf, 0, len);

                    }

                } finally {

                    if (bos != null) {

                        bos.flush();

                        bos.close();

                    }

                    if (bis != null) {

                        bis.close();

                    }

                }

           }

        }finally {

           if (zipFile != null) {

               zipFile.close();

           }

        }

    }

主要分析3:Files.walkFileTree(Path,FileVisitor)

该方法是NIO中的方法,用于对一个目录进行遍历操作,里面的参数1是一个path,参数2是一个接口FileVisitor.该接口有四个抽象方法,具体使用方法可以查询百度。

public interface FileVisitor<T> {

//访问目录前

   FileVisitResult preVisitDirectory(T var1, BasicFileAttributes var2)throws IOException;

//访问文件

   FileVisitResult visitFile(T var1, BasicFileAttributes var2) throwsIOException;

//访问文件失败

   FileVisitResult visitFileFailed(T var1, IOException var2) throwsIOException;

//访问目录后

   FileVisitResult postVisitDirectory(T var1, IOException var2) throwsIOException;

}

在该方法中传入的是ApkFilesVisitor对象。

class ApkFilesVisitor extendsSimpleFileVisitor<Path> {

       BaseDecoder     dexDecoder;

       BaseDecoder     soDecoder;

       BaseDecoder     resDecoder;

       Configuration   config;

       Path            newApkPath;

       Path            oldApkPath;

       ApkFilesVisitor(Configuration config, Path newPath, Path oldPath,BaseDecoder dex, BaseDecoder so, BaseDecoder resDecoder) {

           this.config = config;

           this.dexDecoder = dex;

           this.soDecoder = so;

           this.resDecoder = resDecoder;

           this.newApkPath = newPath;

           this.oldApkPath = oldPath;

        }

       @Override

       public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)throws IOException {

           Path relativePath = newApkPath.relativize(file);//relative方法就是p1到p2的相对路径。这里拿到的是文件到XXXapk这个路径的相对路径。

           Path oldPath = oldApkPath.resolve(relativePath);//如果relativepath是绝对路径,那么直接返回relativepath;否则,将relativepath添加到oldapkpath的后面。

            File oldFile = null;

           //is a new file?!

           if (oldPath.toFile().exists()) {如果这个成立,意味着这是一个新增文件。

               oldFile = oldPath.toFile();

           }

           String patternKey = relativePath.toString().replace("\\","/");

//判断当前访问的文件是不是classesN.dex文件,这个pattern是从tinker_config.xml中读出来的。

Xml文件中的注释

 <!--what dexes in apk are expected to dealwith tinkerPatch-->

        <!--it support * or ? pattern.-->

        <patternvalue="classes*.dex"/>

        <pattern value="assets/secondary-dex-?.jar"/>

           if (Utils.checkFileInPattern(config.mDexFilePattern, patternKey)) {

               //also treat duplicate file as unchanged

               if (Utils.checkFileInPattern(config.mResFilePattern, patternKey)&& oldFile != null) {

                   resDuplicateFiles.add(oldFile);

               }

               try {

                    dexDecoder.patch(oldFile,file.toFile());//这个就是dexdecoder的实际生成dex patch的操作。

               } catch (Exception e) {

//                    e.printStackTrace();

                    throw newRuntimeException(e);

               }

               return FileVisitResult.CONTINUE;

           }

           if (Utils.checkFileInPattern(config.mSoFilePattern, patternKey)) {

               //also treat duplicate file as unchanged

               if (Utils.checkFileInPattern(config.mResFilePattern, patternKey)&& oldFile != null) {

                   resDuplicateFiles.add(oldFile);

               }

               try {

                    soDecoder.patch(oldFile, file.toFile());//.so库生成patch

               } catch (Exception e) {

//                    e.printStackTrace();

                    throw newRuntimeException(e);

               }

               return FileVisitResult.CONTINUE;

           }

           if (Utils.checkFileInPattern(config.mResFilePattern, patternKey)) {

               try {

                    resDecoder.patch(oldFile,file.toFile());//resource文件生成patch

               } catch (Exception e) {

//                    e.printStackTrace();

                    throw newRuntimeException(e);

               }

               return FileVisitResult.CONTINUE;

           }

           return FileVisitResult.CONTINUE;

        }

}

DexDiffDecoder.java:

主要方法patch()
public boolean patch(final File oldFile, final File newFile) throwsIOException, TinkerPatchException {

       final String dexName = getRelativeDexName(oldFile, newFile);

        //first of all, we should check input files if excluded classes were modified.

        Logger.d("Checkfor loader classes in dex: %s", dexName);

       try {

           主要分析1:将classes.dex文件转化成Dex对象,dex对象是根据class.dex文件格式定义的一种数据格式
excludedClassModifiedChecker.checkIfExcludedClassWasModifiedInNewDex(oldFile,newFile);

        }catch (IOException e) {

           throw new TinkerPatchException(e);

        }catch (TinkerPatchException e) {

           if (config.mIgnoreWarning) {

               Logger.e("Warning:ignoreWarning is true, but we found %s",e.getMessage());

           } else {

               Logger.e("Warning:ignoreWarning is false, but we found %s",e.getMessage());

               throw e;

           }

        }catch (Exception e) {

           e.printStackTrace();

        }

        //If corresponding new dex was completely deleted, just return false.

        //don't process 0 length dex

        if(newFile == null || !newFile.exists() || newFile.length() == 0) {

           return false;

        }

       File dexDiffOut = getOutputPath(newFile).toFile();

       final String newMd5 = getRawOrWrappedDexMD5(newFile);

       //new add file

        if(oldFile == null || !oldFile.exists() || oldFile.length() == 0) {

           hasDexChanged = true;

//新增的classes.dex文件集合

            copyNewDexAndLogToDexMeta(newFile,newMd5, dexDiffOut);

           return true;

        }

//获取文件的MD5值。可以学到的是求一个文件的md5的方法。

       final String oldMd5 = getRawOrWrappedDexMD5(oldFile);

        if((oldMd5 != null && !oldMd5.equals(newMd5)) || (oldMd5 == null&& newMd5 != null)) {

           hasDexChanged = true;

           if (oldMd5 != null) {

修改了的dex文件集合

               collectAddedOrDeletedClasses(oldFile, newFile);

           }

        }

       RelatedInfo relatedInfo = new RelatedInfo();

       relatedInfo.oldMd5 = oldMd5;

       relatedInfo.newMd5 = newMd5;

//把相对应的oldfile和newfile做成一个entry

        //collect current old dex file and corresponding new dex file for furtherprocessing.

       oldAndNewDexFilePairList.add(new AbstractMap.SimpleEntry<>(oldFile,newFile));

       dexNameToRelatedInfoMap.put(dexName, relatedInfo);

       return ;

}

excludedClassModifiedChecker.checkIfExcludedClassWasModifiedInNewDex(oldFile,newFile);

public void checkIfExcludedClassWasModifiedInNewDex(FileoldFile, File newFile) throws IOException, TinkerPatchException {

        if(oldFile == null && newFile == null) {

           throw new TinkerPatchException("both oldFile and newFile arenull.");

        }

        oldDex = (oldFile !=null ? new Dex(oldFile) : null);

       newDex = (newFile != null ? new Dex(newFile) : null);

       int stmCode = STMCODE_START;

       while (stmCode != STMCODE_END) {

           switch (stmCode) {

               /**

                * Check rule:

                * Loader classes must only appear in primary dex and each of them inprimary old dex should keep

                * completely consistent in new primary dex.

                *

                * An error is announced when any of these conditions below is fit:

                * 1. Primary old dex is missing.

                * 2. Primary new dex is missing.

                * 3. There are not any loader classes in primary old dex.

                * 4. There are some new loader classes added in new primary dex.

                * 5. Loader classes in old primary dex are modified, deleted in newprimary dex.

                * 6. Loader classes are found in secondary old dexes.

                * 7. Loader classes are found in secondary new dexes.

                */

               case STMCODE_START: {

//dex中的类是大部分不能做任何修改的,包括添加新类,删除已有类。如果对类做了修改,但是该类在ignorechangewarning的名单中,那么是允许的,否则不允许。还有一种错误情况是,在tinker_xml中用loader标签的dex文件,被放在了非主dex中,这样也会报错。

                   boolean isPrimaryDex =isPrimaryDex((oldFile == null ? newFile : oldFile));

                    if (isPrimaryDex) {

                        if (oldFile == null) {

                            stmCode =STMCODE_ERROR_PRIMARY_OLD_DEX_IS_MISSING;

                        } else if (newFile == null) {

                            stmCode =STMCODE_ERROR_PRIMARY_NEW_DEX_IS_MISSING;

                        } else {

                           dexCmptor.startCheck(oldDex, newDex);//就是对new old dex包进行比较。

                            deletedClassInfos =dexCmptor.getDeletedClassInfos();//删除的class

                            addedClassInfos =dexCmptor.getAddedClassInfos();//新增的class

                           changedClassInfosMap = new HashMap<>(dexCmptor.getChangedClassDescToInfosMap());//做了更改的class

                            // All loaderclasses are in new dex, while none of them in old one.

                            if(deletedClassInfos.isEmpty() && changedClassInfosMap.isEmpty()&& !addedClassInfos.isEmpty()) {

                                stmCode =STMCODE_ERROR_LOADER_CLASS_NOT_IN_PRIMARY_OLD_DEX;

                            } else {

                                if(deletedClassInfos.isEmpty() && addedClassInfos.isEmpty()) {

                                    // classdescriptor is completely matches, see if any contents changes.

                                   ArrayList<String> removeClasses = new ArrayList<>();

                                    for (Stringclassname : changedClassInfosMap.keySet()) {

                                        if(Utils.checkFileInPattern(ignoreChangeWarning, classname)) {

                                           Logger.e("loader class pattern: " + classname + " haschanged, but it match ignore change pattern, just ignore!");

                                           removeClasses.add(classname);

                                        }

                                    }

                                   changedClassInfosMap.keySet().removeAll(removeClasses);

                                    if(changedClassInfosMap.isEmpty()) {

                                        stmCode= STMCODE_END;

                                    } else {

                                        stmCode= STMCODE_ERROR_LOADER_CLASS_CHANGED;

                                    }

                                } else {

                                    stmCode =STMCODE_ERROR_LOADER_CLASS_IN_PRIMARY_DEX_MISMATCH;

                                }

                            }

                        }

                    } else {

                        Set<Pattern>patternsOfClassDescToCheck = new HashSet<>();

                        for (String patternStr: config.mDexLoaderPattern) {

                           patternsOfClassDescToCheck.add(

                               Pattern.compile(

                                   PatternUtils.dotClassNamePatternToDescriptorRegEx(patternStr)

                                )

                           );

                        }

                        if (oldDex != null) {

                           oldClassesDescToCheck.clear();

//这里就是判断是否存在使用loader标注的class被放在了非主dex中。

                            for (ClassDefclassDef : oldDex.classDefs()) {

                                String desc =oldDex.typeNames().get(classDef.typeIndex);

                                if(Utils.isStringMatchesPatterns(desc, patternsOfClassDescToCheck)) {

                                   oldClassesDescToCheck.add(desc);

                                }

                            }

                            if(!oldClassesDescToCheck.isEmpty()) {

                                stmCode =STMCODE_ERROR_LOADER_CLASS_FOUND_IN_SECONDARY_OLD_DEX;

                               break;

                            }

                        }

                        if (newDex != null) {

                           newClassesDescToCheck.clear();

                            for (ClassDefclassDef : newDex.classDefs()) {

                                String desc =newDex.typeNames().get(classDef.typeIndex);

                                if(Utils.isStringMatchesPatterns(desc, patternsOfClassDescToCheck)) {

                                   newClassesDescToCheck.add(desc);

                                }

                            }

                            if(!newClassesDescToCheck.isEmpty()) {

                                stmCode =STMCODE_ERROR_LOADER_CLASS_FOUND_IN_SECONDARY_NEW_DEX;

                                break;

                            }

                        }

                        stmCode = STMCODE_END;

                   }

                    break;

               }

               case STMCODE_ERROR_PRIMARY_OLD_DEX_IS_MISSING: {

                    throw newTinkerPatchException("old primary dex is missing.");

               }

               case STMCODE_ERROR_PRIMARY_NEW_DEX_IS_MISSING: {

                    throw newTinkerPatchException("new primary dex is missing.");

               }

               case STMCODE_ERROR_LOADER_CLASS_NOT_IN_PRIMARY_OLD_DEX: {

                    throw newTinkerPatchException("all loader classes don't appear in old primarydex.");

               }

               case STMCODE_ERROR_LOADER_CLASS_IN_PRIMARY_DEX_MISMATCH: {

                    throw newTinkerPatchException(

                        "loader classes inold primary dex are mismatched to those in new primary dex, \n"

                            + "if deletedclasses is not empty, check if your dex division strategy is fine. \n"

                            + "addedclasses: " + Utils.collectionToString(addedClassInfos) + "\n"

                            + "deletedclasses: " + Utils.collectionToString(deletedClassInfos)

                    );

               }

               case STMCODE_ERROR_LOADER_CLASS_FOUND_IN_SECONDARY_OLD_DEX: {

                    throw newTinkerPatchException("loader classes are found in old secondary dex. Foundclasses: " + Utils.collectionToString(oldClassesDescToCheck));

               }

               case STMCODE_ERROR_LOADER_CLASS_FOUND_IN_SECONDARY_NEW_DEX: {

                    throw newTinkerPatchException("loader classes are found in new secondary dex. Foundclasses: " + Utils.collectionToString(newClassesDescToCheck));

               }

               case STMCODE_ERROR_LOADER_CLASS_CHANGED: {

                    String msg =

                        "some loader classhas been changed in new dex."

                            + " Such thesechanges will not take effect!!"

                            + " relatedclasses: "

                            +Utils.collectionToString(changedClassInfosMap.keySet());

                    throw newTinkerPatchException(msg);

               }

               default: {

                   Logger.e("internal-error: unexpected stmCode.");

                    stmCode = STMCODE_END;

                    break;

               }

           }

        }

}

Dex.java

public Dex(File file) throws IOException {

        if(file == null) {

           throw new IllegalArgumentException("file is null.");

        }

        if(FileUtils.hasArchiveSuffix(file.getName())) {

           ZipFile zipFile = null;

           try {

               zipFile = new ZipFile(file);

这种情况是指对dex文件进行了jar打包操作。

               ZipEntry entry = zipFile.getEntry(DexFormat.DEX_IN_JAR_NAME);

               if (entry != null) {

                    InputStream inputStream =null;

                    try {

                        inputStream =zipFile.getInputStream(entry);

                        loadFrom(inputStream,(int) entry.getSize());

                    } finally {

                        if (inputStream !=null) {

                           inputStream.close();

                        }

                    }

               } else {

                    throw newDexException("Expected " + DexFormat.DEX_IN_JAR_NAME + " in" + file);

               }

           } finally {

               if (zipFile != null) {

                    try {

                        zipFile.close();

                    } catch (Exception e) {

                        // ignored.

                    }

               }

           }

这种就是未对.dex文件进行jar打包操作的。

        }else if (file.getName().endsWith(".dex")) {

           InputStream in = null;

           try {

               in = new BufferedInputStream(new FileInputStream(file));

               loadFrom(in, (int) file.length());

           } catch (Exception e) {

               throw new DexException(e);

           } finally {

               if (in != null) {

                    try {

                        in.close();

                    } catch (Exception e) {

                        // ignored.

                    }

               }

           }

        } else {

           throw new DexException("unknown output extension: " + file);

        }

}

loadFrom(in, (int)file.length());

这个就是将dex文件以字节流的方式读入内存中。这个需要理解dex文件解析内容,不做深究。

private void loadFrom(InputStream in, intinitSize) throws IOException {

        byte[] rawData = FileUtils.readStream(in,initSize);

       this.data = ByteBuffer.wrap(rawData);

       this.data.order(ByteOrder.LITTLE_ENDIAN);//大小端问题,dex文件中都是以小端方式放置数据的。

       this.tableOfContents.readFrom(this);

}

getRawOrWrappedDexMD5(oldFile)

最终的实现获取md5的代码:

/**

     * Getthe md5 for inputStream.

     *This method cost less memory. It read bufLen bytes from the FileInputStreamonce.

     *

     *@param is

     *@param bufLen bytes number read from the stream once.

    *               The less bufLen isthe more times getMD5() method takes. Also the less bufLen is the less memorycost.

     */

    publicstatic String getMD5(final InputStream is, final int bufLen) {

        if(is == null || bufLen <= 0) {

            return null;

        }

       try {

           MessageDigest md = MessageDigest.getInstance("MD5");

           StringBuilder md5Str = new StringBuilder(32);

           byte[] buf = new byte[bufLen];

           int readCount = 0;

           while ((readCount = is.read(buf)) != -1) {

               md.update(buf, 0, readCount);

           }

           byte[] hashValue = md.digest();

           for (int i = 0; i < hashValue.length; i++) {

               md5Str.append(Integer.toString((hashValue[i] & 0xff) + 0x100,16).substring(1));

           }

           return md5Str.toString();

        }catch (Exception e) {

           return null;

        }

}

soDecoder.patch(oldFile,file.toFile());//.so库生成patch

Sodecoder实际类型是BsDiffDecoder,下面看下它的patch()方法

@Override

    publicboolean patch(File oldFile, File newFile) throws IOException,TinkerPatchException {

       //first of all, we should check input files

        if(newFile == null || !newFile.exists()) {

           return false;

        }

       //new add file

       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;

        }

       //both file length is 0

        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;

        }

       //new add file

        String oldMd5 = MD5.getMD5(oldFile);

        if(oldMd5.equals(newMd5)) {

           return false;

        }

        if(!bsDiffFile.getParentFile().exists()) {

           bsDiffFile.getParentFile().mkdirs();

        }

//直接使用java版的bsdiff算法,生成so库的patch文件

       BSDiff.bsdiff(oldFile, newFile, bsDiffFile);

//如果文件太大,则直接新增一个file,否则还是按照patch文件制作。

        if(Utils.checkBsDiffFileSize(bsDiffFile, newFile)) {

           writeLogFiles(newFile, oldFile, bsDiffFile, newMd5);

        }else {

           FileOperation.copyFileUsingStream(newFile, bsDiffFile);

           writeLogFiles(newFile, null, null, newMd5);

        }

       return true;

    }

 

resDecoder.patch(oldFile,file.toFile());//resource文件生成patch

@Override

    publicboolean patch(File oldFile, File newFile) throws IOException,TinkerPatchException {

       String name = getRelativePathStringToNewFile(newFile);

       //actually, it won't go below

        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;

        }

        FileoutputFile = getOutputPath(newFile).toFile();

        if(oldFile == null || !oldFile.exists()) {

//该文件刚好在ignorechangepattern匹配格式中,so 忽略它。

           if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, name)) {

               Logger.e("found add resource: " + name + " ,but it matchignore change pattern, just ignore!");

               return false;

           }

           FileOperation.copyFileUsingStream(newFile, outputFile);

           addedSet.add(name);

           writeResLog(newFile, oldFile, TypedValue.ADD);

           return true;

        }

       //both file length is 0

        if(oldFile.length() == 0 && newFile.length() == 0) {

           return false;

        }

       //new add file

        StringnewMd5 = MD5.getMD5(newFile);

       String oldMd5 = MD5.getMD5(oldFile);

       //oldFile or newFile may be 0b length

        if(oldMd5 != null && oldMd5.equals(newMd5)) {

           return false;

        }

//该文件刚好在ignorechangepattern匹配格式中,so 忽略它。

        if(Utils.checkFileInPattern(config.mResIgnoreChangePattern, name)) {

           Logger.d("found modify resource: " + name + ", but itmatch ignore change pattern, just ignore!");

           return false;

        }

//如果该文件是manifest文件,也需要忽略,因为manifest有专门的decoder.

        if(name.equals(TypedValue.RES_MANIFEST)) {

           Logger.d("found modify resource: " + name + ", but it isAndroidManifest.xml, just ignore!");

           return false;

        }

//arsc文件

        if(name.equals(TypedValue.RES_ARSC)) {

           if (AndroidParser.resourceTableLogicalChange(config)) {//这里面又涉及到arsc文件格式的解析,忽略。

               Logger.d("found modify resource: " + name + ", but it islogically the same as original new resources.arsc, just ignore!");

               return false;

           }

        }

//处理被修改的文件。

       dealWithModifyFile(name, newMd5, oldFile, newFile, outputFile);

       return true;

}

dexPatchDecoder.onAllPatchesEnd()
该方法就是根据前面比对的new apk和old apk结果,把新增的或者修改的.class文件(就是dex文件描述的class文件,对应的数据对象的名称是Dexclassinfo),写到一个changed_classes.dex中,这里面涉及到dex文件格式,dex文件写入,还有虚拟机指令,非常深入,下面只简单地过一下流程。

@Override

    publicvoid onAllPatchesEnd() throws Exception {

        if(!hasDexChanged) {

           Logger.d("No dexes were changed, nothing needs to be donenext.");

           return;

        }

        if(config.mIsProtectedApp) {

           generateChangedClassesDexFile();

        }else {

           generatePatchInfoFile();//主要分析这个方法

        }

       addTestDex();

}

 generatePatchInfoFile();
 @SuppressWarnings("NewApi")

   private void generatePatchInfoFile() throws IOException {

       generatePatchedDexInfoFile();

        //generateSmallPatchedDexInfoFile is blocked by issue we found in ART environment

        // which indicates that if inline optimizationis done on patched class, some error

        //such as crash, ClassCastException, mistaken string fetching, etc. would happen.

        //

        //Instead, we will log all classN dexes as 'copy directly' in dex-meta, so that

        //tinker patch applying procedure will copy them out and load them in ARTenvironment.

       //generateSmallPatchedDexInfoFile();

       logDexesToDexMeta();

       checkCrossDexMovingClasses();

}

generatePatchedDexInfoFile();

@SuppressWarnings("NewApi")

   private void generatePatchedDexInfoFile() {

        //Generate dex diff out and full patched dex if a pair of dex is different.

这个oldAndNewDexFilePairList就是patch()方法中得到的有变化的classesN.dex新旧文件。

       for (AbstractMap.SimpleEntry<File, File> oldAndNewDexFilePair :oldAndNewDexFilePairList) {

           File oldFile = oldAndNewDexFilePair.getKey();

           File newFile = oldAndNewDexFilePair.getValue();

           final String dexName = getRelativeDexName(oldFile, newFile);

           RelatedInfo relatedInfo = dexNameToRelatedInfoMap.get(dexName);

           if (!relatedInfo.oldMd5.equals(relatedInfo.newMd5)) {

               diffDexPairAndFillRelatedInfo(oldFile,newFile, relatedInfo);

           } else {

               // In this case newDexFile is the same as oldDexFile, but we still

               // need to treat it as patched dex file so that the SmallPatchGenerator

                // can analyze which class ofthis dex should be kept in small patch.

               relatedInfo.newOrFullPatchedFile = newFile;

               relatedInfo.newOrFullPatchedMd5 = relatedInfo.newMd5;

           }

        }

}

diffDexPairAndFillRelatedInfo(oldFile,newFile, relatedInfo);

private void diffDexPairAndFillRelatedInfo(FileoldDexFile, File newDexFile, RelatedInfo relatedInfo) {

       File tempFullPatchDexPath = new File(config.mOutFolder + File.separator+ TypedValue.DEX_TEMP_PATCH_DIR);

       final String dexName = getRelativeDexName(oldDexFile, newDexFile);

       File dexDiffOut = getOutputPath(newDexFile).toFile();

       ensureDirectoryExist(dexDiffOut.getParentFile());

       try {

           DexPatchGenerator dexPatchGen = new DexPatchGenerator(oldDexFile,newDexFile);

           dexPatchGen.setAdditionalRemovingClassPatterns(config.mDexLoaderPattern);

           logWriter.writeLineToInfoFile(

                    String.format(

                            "Start diffbetween [%s] as old and [%s] as new:",

                           getRelativeStringBy(oldDexFile, config.mTempUnzipOldDir),

                           getRelativeStringBy(newDexFile, config.mTempUnzipNewDir)

                    )

           );

           dexPatchGen.executeAndSaveTo(dexDiffOut);最终会调用DexPatchGenerator.javaexecuteAndSaveTo()方法,下面重点分析该方法。

        }catch (Exception e) {

           throw new TinkerPatchException(e);

        }

        if(!dexDiffOut.exists()) {

           throw new TinkerPatchException("can not find the diff file:" +dexDiffOut.getAbsolutePath());

        }

       relatedInfo.dexDiffFile = dexDiffOut;

       relatedInfo.dexDiffMd5 = MD5.getMD5(dexDiffOut);

       Logger.d("\nGen %s patch file:%s, size:%d, md5:%s", dexName,relatedInfo.dexDiffFile.getAbsolutePath(), relatedInfo.dexDiffFile.length(),relatedInfo.dexDiffMd5);

       File tempFullPatchedDexFile = new File(tempFullPatchDexPath, dexName);

        if(!tempFullPatchedDexFile.exists()) {

           ensureDirectoryExist(tempFullPatchedDexFile.getParentFile());

        }

       try {

           new DexPatchApplier(oldDexFile,dexDiffOut).executeAndSaveTo(tempFullPatchedDexFile);

           Logger.d(

                    String.format("Verifyingif patched new dex is logically the same as original new dex: %s ...",getRelativeStringBy(newDexFile, config.mTempUnzipNewDir))

           );

           Dex origNewDex = new Dex(newDexFile);

           Dex patchedNewDex = new Dex(tempFullPatchedDexFile);

           checkDexChange(origNewDex, patchedNewDex);

           relatedInfo.newOrFullPatchedFile = tempFullPatchedDexFile;

           relatedInfo.newOrFullPatchedMd5 = MD5.getMD5(tempFullPatchedDexFile);

        }catch (Exception e) {

           e.printStackTrace();

           throw new TinkerPatchException(

                    "Failed to generatetemporary patched dex, which makes MD5 generating procedure of new dex failed,either.", e

           );

        }

        if(!tempFullPatchedDexFile.exists()) {

           throw new TinkerPatchException("can not find the temporary fullpatched dex file:" + tempFullPatchedDexFile.getAbsolutePath());

        }

       Logger.d("\nGen %s for dalvik full dex file:%s, size:%d,md5:%s", dexName, tempFullPatchedDexFile.getAbsolutePath(),tempFullPatchedDexFile.length(), relatedInfo.newOrFullPatchedMd5);

}

executeAndSaveTo(OutputStream out):

public void executeAndSaveTo(OutputStream out)throws IOException {

        //Firstly, collect information of items we want to remove additionally

        //in new dex and set them to corresponding diff algorithm implementations.

// 哪些文件的变化应该忽略,不应该添加到patch中。

       Pattern[] classNamePatterns = new Pattern[this.additionalRemovingClassPatternSet.size()];

       int classNamePatternCount = 0;

       for (String regExStr : this.additionalRemovingClassPatternSet) {

           classNamePatterns[classNamePatternCount++] = Pattern.compile(regExStr);

        }

       List<Integer> typeIdOfClassDefsToRemove = newArrayList<>(classNamePatternCount);

       List<Integer> offsetOfClassDatasToRemove = newArrayList<>(classNamePatternCount);

       for (ClassDef classDef : this.newDex.classDefs()) {

//拿到dex文件中的所有类定义,判断是否在上面得到的忽略list

           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;

               }

           }

        }

       ((ClassDefSectionDiffAlgorithm) this.classDefSectionDiffAlg)

                .setTypeIdOfClassDefsToRemove(typeIdOfClassDefsToRemove);

       ((ClassDataSectionDiffAlgorithm) this.classDataSectionDiffAlg)

               .setOffsetOfClassDatasToRemove(offsetOfClassDatasToRemove);

//下面开始就是根据dex数据格式,比较新的dex文件和旧的dex文件,根据区别生成dex patch,算法复杂,需要对dex文件格式非常精通。

        //Then, run diff algorithms according to sections' dependencies.

        //Use size calculated by algorithms above or from dex file definition to

        //calculate sections' offset and patched dex size.

        //Calculate header and id sections size, so that we can work out

        //the base offset of typeLists Section.

       int patchedheaderSize = SizeOf.HEADER_ITEM;

       int patchedStringIdsSize = newDex.getTableOfContents().stringIds.size *SizeOf.STRING_ID_ITEM;

       int patchedTypeIdsSize = newDex.getTableOfContents().typeIds.size *SizeOf.TYPE_ID_ITEM;

        //Although simulatePatchOperation can calculate this value, since protoIdssection

        //depends on typeLists section, we can't run protoIds Section'ssimulatePatchOperation

        //method so far. Instead we calculate protoIds section's size using informationin newDex

        //directly.

       int patchedProtoIdsSize = newDex.getTableOfContents().protoIds.size *SizeOf.PROTO_ID_ITEM;

       int patchedFieldIdsSize = newDex.getTableOfContents().fieldIds.size *SizeOf.MEMBER_ID_ITEM;

       int patchedMethodIdsSize = newDex.getTableOfContents().methodIds.size *SizeOf.MEMBER_ID_ITEM;

       int patchedClassDefsSize = newDex.getTableOfContents().classDefs.size *SizeOf.CLASS_DEF_ITEM;

       int patchedIdSectionSize =

               patchedStringIdsSize

                        + patchedTypeIdsSize

                        + patchedProtoIdsSize

                        + patchedFieldIdsSize

                        + patchedMethodIdsSize

                        + patchedClassDefsSize;

       this.patchedHeaderOffset = 0;

        //The diff works on each sections obey such procedure:

       //  1. Execute diff algorithms tocalculate indices of items we need to add, del and replace.

       //  2. Execute patch algorithmsimulation to calculate indices and offsets mappings that is

       //  necessary to next section'sdiff works.

        //Immediately do the patch simulation so that we can know:

       //  1. Indices and offsets mappingbetween old dex and patched dex.

       //  2. Indices and offsets mappingbetween new dex and patched dex.

        //These information will be used to do next diff works.

       this.patchedStringIdsOffset = patchedHeaderOffset + patchedheaderSize;

        if(this.oldDex.getTableOfContents().stringIds.isElementFourByteAligned) {

           this.patchedStringIdsOffset

                    = SizeOf.roundToTimesOfFour(this.patchedStringIdsOffset);

        }

       this.stringDataSectionDiffAlg.execute();

       this.patchedStringDataItemsOffset = patchedheaderSize +patchedIdSectionSize;

        if(this.oldDex.getTableOfContents().stringDatas.isElementFourByteAligned) {

           this.patchedStringDataItemsOffset

                    =SizeOf.roundToTimesOfFour(this.patchedStringDataItemsOffset);

        }

       this.stringDataSectionDiffAlg.simulatePatchOperation(this.patchedStringDataItemsOffset);

       this.typeIdSectionDiffAlg.execute();

       this.patchedTypeIdsOffset = this.patchedStringIdsOffset +patchedStringIdsSize;

        if(this.oldDex.getTableOfContents().typeIds.isElementFourByteAligned) {

           this.patchedTypeIdsOffset

                    =SizeOf.roundToTimesOfFour(this.patchedTypeIdsOffset);

        }

       this.typeIdSectionDiffAlg.simulatePatchOperation(this.patchedTypeIdsOffset);

       this.typeListSectionDiffAlg.execute();

       this.patchedTypeListsOffset

               = patchedheaderSize

               + patchedIdSectionSize

               + this.stringDataSectionDiffAlg.getPatchedSectionSize();

        if(this.oldDex.getTableOfContents().typeLists.isElementFourByteAligned) {

           this.patchedTypeListsOffset

                    =SizeOf.roundToTimesOfFour(this.patchedTypeListsOffset);

        }

       this.typeListSectionDiffAlg.simulatePatchOperation(this.patchedTypeListsOffset);

       this.protoIdSectionDiffAlg.execute();

       this.patchedProtoIdsOffset = this.patchedTypeIdsOffset +patchedTypeIdsSize;

        if(this.oldDex.getTableOfContents().protoIds.isElementFourByteAligned) {

           this.patchedProtoIdsOffset = SizeOf.roundToTimesOfFour(this.patchedProtoIdsOffset);

        }

       this.protoIdSectionDiffAlg.simulatePatchOperation(this.patchedProtoIdsOffset);

       this.fieldIdSectionDiffAlg.execute();

       this.patchedFieldIdsOffset = this.patchedProtoIdsOffset +patchedProtoIdsSize;

        if(this.oldDex.getTableOfContents().fieldIds.isElementFourByteAligned) {

           this.patchedFieldIdsOffset =SizeOf.roundToTimesOfFour(this.patchedFieldIdsOffset);

        }

       this.fieldIdSectionDiffAlg.simulatePatchOperation(this.patchedFieldIdsOffset);

       this.methodIdSectionDiffAlg.execute();

       this.patchedMethodIdsOffset = this.patchedFieldIdsOffset +patchedFieldIdsSize;

        if(this.oldDex.getTableOfContents().methodIds.isElementFourByteAligned) {

           this.patchedMethodIdsOffset =SizeOf.roundToTimesOfFour(this.patchedMethodIdsOffset);

        }

       this.methodIdSectionDiffAlg.simulatePatchOperation(this.patchedMethodIdsOffset);

       this.annotationSectionDiffAlg.execute();

       this.patchedAnnotationItemsOffset

               = this.patchedTypeListsOffset

               + this.typeListSectionDiffAlg.getPatchedSectionSize();

        if(this.oldDex.getTableOfContents().annotations.isElementFourByteAligned) {

           this.patchedAnnotationItemsOffset

                    =SizeOf.roundToTimesOfFour(this.patchedAnnotationItemsOffset);

        }

       this.annotationSectionDiffAlg.simulatePatchOperation(this.patchedAnnotationItemsOffset);

       this.annotationSetSectionDiffAlg.execute();

       this.patchedAnnotationSetItemsOffset

               = this.patchedAnnotationItemsOffset

               + this.annotationSectionDiffAlg.getPatchedSectionSize();

        if(this.oldDex.getTableOfContents().annotationSets.isElementFourByteAligned) {

           this.patchedAnnotationSetItemsOffset

                    =SizeOf.roundToTimesOfFour(this.patchedAnnotationSetItemsOffset);

        }

       this.annotationSetSectionDiffAlg.simulatePatchOperation(

               this.patchedAnnotationSetItemsOffset

        );

       this.annotationSetRefListSectionDiffAlg.execute();

       this.patchedAnnotationSetRefListItemsOffset

               = this.patchedAnnotationSetItemsOffset

               + this.annotationSetSectionDiffAlg.getPatchedSectionSize();

        if(this.oldDex.getTableOfContents().annotationSetRefLists.isElementFourByteAligned){

           this.patchedAnnotationSetRefListItemsOffset

                    =SizeOf.roundToTimesOfFour(this.patchedAnnotationSetRefListItemsOffset);

        }

       this.annotationSetRefListSectionDiffAlg.simulatePatchOperation(

               this.patchedAnnotationSetRefListItemsOffset

        );

        this.annotationsDirectorySectionDiffAlg.execute();

       this.patchedAnnotationsDirectoryItemsOffset

               = this.patchedAnnotationSetRefListItemsOffset

               + this.annotationSetRefListSectionDiffAlg.getPatchedSectionSize();

        if(this.oldDex.getTableOfContents().annotationsDirectories.isElementFourByteAligned){

           this.patchedAnnotationsDirectoryItemsOffset

                    =SizeOf.roundToTimesOfFour(this.patchedAnnotationsDirectoryItemsOffset);

        }

        this.annotationsDirectorySectionDiffAlg.simulatePatchOperation(

               this.patchedAnnotationsDirectoryItemsOffset

        );

       this.debugInfoSectionDiffAlg.execute();

       this.patchedDebugInfoItemsOffset

               = this.patchedAnnotationsDirectoryItemsOffset

               + this.annotationsDirectorySectionDiffAlg.getPatchedSectionSize();

        if(this.oldDex.getTableOfContents().debugInfos.isElementFourByteAligned) {

           this.patchedDebugInfoItemsOffset

                    =SizeOf.roundToTimesOfFour(this.patchedDebugInfoItemsOffset);

        }

       this.debugInfoSectionDiffAlg.simulatePatchOperation(this.patchedDebugInfoItemsOffset);

       this.codeSectionDiffAlg.execute();

       this.patchedCodeItemsOffset

               = this.patchedDebugInfoItemsOffset

               + this.debugInfoSectionDiffAlg.getPatchedSectionSize();

        if(this.oldDex.getTableOfContents().codes.isElementFourByteAligned) {

           this.patchedCodeItemsOffset =SizeOf.roundToTimesOfFour(this.patchedCodeItemsOffset);

        }

       this.codeSectionDiffAlg.simulatePatchOperation(this.patchedCodeItemsOffset);

       this.classDataSectionDiffAlg.execute();

       this.patchedClassDataItemsOffset

               = this.patchedCodeItemsOffset

               + this.codeSectionDiffAlg.getPatchedSectionSize();

        if(this.oldDex.getTableOfContents().classDatas.isElementFourByteAligned) {

           this.patchedClassDataItemsOffset

                    =SizeOf.roundToTimesOfFour(this.patchedClassDataItemsOffset);

        }

       this.classDataSectionDiffAlg.simulatePatchOperation(this.patchedClassDataItemsOffset);

       this.encodedArraySectionDiffAlg.execute();

       this.patchedEncodedArrayItemsOffset

               = this.patchedClassDataItemsOffset

               + this.classDataSectionDiffAlg.getPatchedSectionSize();

        if(this.oldDex.getTableOfContents().encodedArrays.isElementFourByteAligned) {

           this.patchedEncodedArrayItemsOffset

                    =SizeOf.roundToTimesOfFour(this.patchedEncodedArrayItemsOffset);

        }

       this.encodedArraySectionDiffAlg.simulatePatchOperation(this.patchedEncodedArrayItemsOffset);

       this.classDefSectionDiffAlg.execute();

       this.patchedClassDefsOffset = this.patchedMethodIdsOffset +patchedMethodIdsSize;

        if(this.oldDex.getTableOfContents().classDefs.isElementFourByteAligned) {

           this.patchedClassDefsOffset =SizeOf.roundToTimesOfFour(this.patchedClassDefsOffset);

        }

        //Calculate any values we still know nothing about them.

       this.patchedMapListOffset

               = this.patchedEncodedArrayItemsOffset

               + this.encodedArraySectionDiffAlg.getPatchedSectionSize();

        if(this.oldDex.getTableOfContents().mapList.isElementFourByteAligned) {

           this.patchedMapListOffset =SizeOf.roundToTimesOfFour(this.patchedMapListOffset);

        }

       int patchedMapListSize = newDex.getTableOfContents().mapList.byteCount;

       this.patchedDexSize

               = this.patchedMapListOffset

               + patchedMapListSize;

        //Finally, write results to patch file.

       writeResultToStream(out);

}

云里雾里总算粗略地过了一遍,其实还有resdecoder.onpatchEnd()没有分析,实在头大,等把patch合成过程分析完后,再回过头来看看patch的生成过程,或许会好一些。

上面就是patch的生成过程,接下来需要分析patch的合成过程。。。。。。

客户端用法,就不详述了,需要重写application,使用tinker提供的applicationlike模板,好吧这又涉及到了一个知识点,gradle模板动态生成代码,有时间再看。。。。然后在合适的时机调用启动合成patch的方法:

参数就传patch文件所在路径

TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),

               Environment.getExternalStorageDirectory().getAbsolutePath() +"/patch_signed_7zip.apk");

初始化一些基本参数:

public Builder(Context context) {

           if (context == null) {

               throw new TinkerRuntimeException("Context must not be null.");

           }

           this.context = context;

           this.mainProcess = TinkerServiceInternals.isInMainProcess(context);

           this.patchProcess = TinkerServiceInternals.isInTinkerPatchServiceProcess(context);

           this.patchDirectory = SharePatchFileUtil.getPatchDirectory(context);

           if (this.patchDirectory == null) {

               TinkerLog.e(TAG, "patchDirectory is null!");

               return;

           }

           this.patchInfoFile =SharePatchFileUtil.getPatchInfoFile(patchDirectory.getAbsolutePath());

           this.patchInfoLockFile =SharePatchFileUtil.getPatchInfoLockFile(patchDirectory.getAbsolutePath());

           TinkerLog.w(TAG, "tinker patch directory: %s",patchDirectory);

        }

接下来调用DefaultPatchListener.java的onPatchReceived(Stringpath):

@Override

    publicint onPatchReceived(String path) {

需要分析1

        int returnCode = patchCheck(path);

        if(returnCode == ShareConstants.ERROR_PATCH_OK) {

需要分析2

           TinkerPatchService.runPatchService(context, path);

        }else {

           Tinker.with(context).getLoadReporter().onLoadPatchListenerReceiveFail(newFile(path), returnCode);

        }

       return returnCode;

    }

patchCheck():主要做配置检查。代码略。

最终的调用会在UpgradePatch.java的tryPatch()
@Override

    publicboolean tryPatch(Context context, String tempPatchPath, PatchResultpatchResult) {

       Tinker manager = Tinker.with(context);

        finalFile patchFile = new File(tempPatchPath);

        if(!manager.isTinkerEnabled() ||!ShareTinkerInternals.isTinkerEnableWithSharedPreferences(context)) {

           TinkerLog.e(TAG, "UpgradePatch tryPatch:patch is disabled, justreturn");

           return false;

        }

        if(!SharePatchFileUtil.isLegalFile(patchFile)) {

           TinkerLog.e(TAG, "UpgradePatch tryPatch:patch file is not found,just return");

           return false;

        }

签名检查,可以学习下

       //check the signature, we should create a new checker

        ShareSecurityChecksignatureCheck = new ShareSecurityCheck(context);

//签名验证,还必须有tinkerId,否则会直接报错。

       int returnCode = ShareTinkerInternals.checkTinkerPackage(context,manager.getTinkerFlags(), patchFile, signatureCheck);

        if(returnCode != ShareConstants.ERROR_PACKAGE_CHECK_OK) {

           TinkerLog.e(TAG, "UpgradePatch tryPatch:onPatchPackageCheckFail");

           manager.getPatchReporter().onPatchPackageCheckFail(patchFile,returnCode);

           return false;

        }

       String patchMd5 = SharePatchFileUtil.getMD5(patchFile);

        if(patchMd5 == null) {

           TinkerLog.e(TAG, "UpgradePatch tryPatch:patch md5 is null, justreturn");

           return false;

        }

       //use md5 as version

       patchResult.patchVersion = patchMd5;

       TinkerLog.i(TAG, "UpgradePatch tryPatch:patchMd5:%s",patchMd5);

        //check ok, we can real recover a newpatch

       final String patchDirectory =manager.getPatchDirectory().getAbsolutePath();

//file

       File patchInfoLockFile =SharePatchFileUtil.getPatchInfoLockFile(patchDirectory);

       File patchInfoFile =SharePatchFileUtil.getPatchInfoFile(patchDirectory);

       SharePatchInfo oldInfo =SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);

       //it is a new patch, so we should not find a exist

       SharePatchInfo newInfo;

        //already have patch

        if(oldInfo != null) {

           if (oldInfo.oldVersion == null || oldInfo.newVersion == null ||oldInfo.oatDir == null) {

               TinkerLog.e(TAG, "UpgradePatch tryPatch:onPatchInfoCorrupted");

               manager.getPatchReporter().onPatchInfoCorrupted(patchFile,oldInfo.oldVersion, oldInfo.newVersion);

               return false;

           }

           if (!SharePatchFileUtil.checkIfMd5Valid(patchMd5)) {

               TinkerLog.e(TAG,"UpgradePatch tryPatch:onPatchVersionCheckFail md5 %s is valid",patchMd5);

               manager.getPatchReporter().onPatchVersionCheckFail(patchFile, oldInfo,patchMd5);

               return false;

           }

           // if it is interpret now, use changing flag to wait main process

           final String finalOatDir =oldInfo.oatDir.equals(ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH)

               ? ShareConstants.CHANING_DEX_OPTIMIZE_PATH : oldInfo.oatDir;

           newInfo = new SharePatchInfo(oldInfo.oldVersion, patchMd5,Build.FINGERPRINT, finalOatDir);

        }else {

           newInfo = new SharePatchInfo("", patchMd5, Build.FINGERPRINT,ShareConstants.DEFAULT_DEX_OPTIMIZE_PATH);

        }

        //it is a new patch, we first delete if thereis any files

       //don't delete dir for faster retry

//       SharePatchFileUtil.deleteDir(patchVersionDirectory);

       final String patchName =SharePatchFileUtil.getPatchVersionDirectory(patchMd5);

       final String patchVersionDirectory = patchDirectory + "/" +patchName;

       TinkerLog.i(TAG, "UpgradePatchtryPatch:patchVersionDirectory:%s", patchVersionDirectory);

       //copy file

       File destPatchFile = new File(patchVersionDirectory + "/" +SharePatchFileUtil.getPatchVersionFile(patchMd5));

       try {

           // check md5 first

           if (!patchMd5.equals(SharePatchFileUtil.getMD5(destPatchFile))) {

               SharePatchFileUtil.copyFileUsingStream(patchFile,destPatchFile);

               TinkerLog.w(TAG, "UpgradePatch copy patch file, src file: %s size:%d, dest file: %s size:%d", patchFile.getAbsolutePath(),patchFile.length(),

                    destPatchFile.getAbsolutePath(),destPatchFile.length());

           }

        }catch (IOException e) {

//           e.printStackTrace();

           TinkerLog.e(TAG, "UpgradePatch tryPatch:copy patch file fail from%s to %s", patchFile.getPath(), destPatchFile.getPath());

           manager.getPatchReporter().onPatchTypeExtractFail(patchFile,destPatchFile, patchFile.getName(), ShareConstants.TYPE_PATCH_FILE);

           return false;

        }

        //we use destPatchFile instead of patchFile, because patchFile maybe deleted during the patch process

//开始合并修复dex文件。

tryRecoverDexFiles()→patchDexExtractViaDexDiff()→extractDexDiffInternals()→dexOptimizeDexFiles()

        if(!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context,patchVersionDirectory, destPatchFile)) {

           TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, trypatch dex failed");

           return false;

        }

        if(!BsDiffPatchInternal.tryRecoverLibraryFiles(manager, signatureCheck, context,patchVersionDirectory, destPatchFile)) {

           TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, trypatch library failed");

           return false;

        }

        if(!ResDiffPatchInternal.tryRecoverResourceFiles(manager, signatureCheck,context, patchVersionDirectory, destPatchFile)) {

           TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, trypatch resource failed");

           return false;

        }

        //check dex opt file at last, some phone such as VIVO/OPPO like to change dex2oatto interpreted

        if(!DexDiffPatchInternal.waitAndCheckDexOptFile(patchFile, manager)) {

           TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, checkdex opt file failed");

           return false;

        }

        if(!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, newInfo,patchInfoLockFile)) {

           TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, rewritepatch info failed");

           manager.getPatchReporter().onPatchInfoCorrupted(patchFile,newInfo.oldVersion, newInfo.newVersion);

           return false;

        }

       TinkerLog.w(TAG, "UpgradePatch tryPatch: done, it is ok");

       return true;

}

获取安装包的MD5签名:

@SuppressLint("PackageManagerGetSignatures")

   private void init(Context context) {

       ByteArrayInputStream stream = null;

       try {

           PackageManager pm = context.getPackageManager();

           String packageName = context.getPackageName();

           PackageInfo packageInfo = pm.getPackageInfo(packageName,PackageManager.GET_SIGNATURES);

           mPublicKeyMd5 =SharePatchFileUtil.getMD5(packageInfo.signatures[0].toByteArray());

           if (mPublicKeyMd5 == null) {

               throw new TinkerRuntimeException("get public key md5 isnull");

           }

        }catch (Exception e) {

           throw new TinkerRuntimeException("ShareSecurityCheck init publickey fail", e);

        }finally {

           SharePatchFileUtil.closeQuietly(stream);

        }

}

可以使用jdk中的properties类,实现hashtable和xml文件之间的相互转换,非常方便。

代码如下:

 Properties properties = new Properties();

           FileInputStream inputStream = null;

           try {

               inputStream = new FileInputStream(pathInfoFile);

               properties.load(inputStream);//加载文件

               oldVer = properties.getProperty(OLD_VERSION);//根据key获取相应的值。

               newVer = properties.getProperty(NEW_VERSION);

               lastFingerPrint = properties.getProperty(FINGER_PRINT);

               oatDIr = properties.getProperty(OAT_DIR);

parseDexDiffPatchInfo():把生成的patch文件中,与dex文件修改相关的信息提取出来。

public static void parseDexDiffPatchInfo(Stringmeta, ArrayList<ShareDexDiffPatchInfo> dexList) {

        if(meta == null || meta.length() == 0) {

           return;

        }

       String[] lines = meta.split("\n");

       for (final String line : lines) {

           if (line == null || line.length() <= 0) {

               continue;

           }

           final String[] kv = line.split(",", 8);

           if (kv == null || kv.length < 8) {

               continue;

           }

           // key

            final String name = kv[0].trim();

            final String path = kv[1].trim();

            final String destMd5InDvm =kv[2].trim();

            final String destMd5InArt =kv[3].trim();

            final String dexDiffMd5 =kv[4].trim();

            final String oldDexCrc = kv[5].trim();

            final String newDexCrc =kv[6].trim();

 

            final StringdexMode = kv[7].trim();

           ShareDexDiffPatchInfo dexInfo = new ShareDexDiffPatchInfo(name, path,destMd5InDvm, destMd5InArt,

               dexDiffMd5, oldDexCrc, newDexCrc, dexMode);

           dexList.add(dexInfo);

        }

    }

extractDexDiffInternals()
private static boolean extractDexDiffInternals(Context context, String dir,String meta, File patchFile, int type) {

       //parse

       patchList.clear();

//根据patch生成时的记录将每一个改动对应的封装对象加入到patchList中,封装对象字段包括:

this.rawName= name;

        this.path = path;

        this.destMd5InDvm = destMd5InDvm;

        this.destMd5InArt = destMd5InArt;

        this.dexDiffMd5 = dexDiffMd5;

        this.oldDexCrC = oldDexCrc;

        this.newDexCrC = newDexCrC;

        this.dexMode = dexMode;

        if(dexMode.equals(ShareConstants.DEXMODE_JAR)) {

            this.isJarMode = true;

            if(SharePatchFileUtil.isRawDexFile(name)) {

                realName = name +ShareConstants.JAR_SUFFIX;

            } else {

                realName = name;

            }

        } else if(dexMode.equals(ShareConstants.DEXMODE_RAW)) {

            this.isJarMode = false;

            this.realName = name;

        } else {

            throw newTinkerRuntimeException("can't recognize dex mode:" + dexMode);

        }

       ShareDexDiffPatchInfo.parseDexDiffPatchInfo(meta, patchList);

        if(patchList.isEmpty()) {

            TinkerLog.w(TAG, "extract patchlist is empty! type:%s:", ShareTinkerInternals.getTypeString(type));

           return true;

        }

       File directory = new File(dir);

        if(!directory.exists()) {

           directory.mkdirs();

        }

       //I think it is better to extract the raw files from apk

       Tinker manager = Tinker.with(context);

       ZipFile apk = null;

       ZipFile patch = null;

       try {

           ApplicationInfo applicationInfo = context.getApplicationInfo();

           if (applicationInfo == null) {

               // Looks like running on a test Context, so just return withoutpatching.

               TinkerLog.w(TAG, "applicationInfo == null!!!!");

               return false;

           }

           String apkPath = applicationInfo.sourceDir;

           apk = new ZipFile(apkPath);

           patch = new ZipFile(patchFile);

           //dir:patch/dex/

           if (checkClassNDexFiles(dir)) {

               TinkerLog.w(TAG, "class n dex file %s is already exist, and md5match, just continue", ShareConstants.CLASS_N_APK_NAME);

               return true;

           }

           for (ShareDexDiffPatchInfo info : patchList) {

               long start = System.currentTimeMillis();

                final String infoPath =info.path;

               String patchRealPath;

               if (infoPath.equals("")) {

                    patchRealPath =info.rawName;

               } else {

                    patchRealPath = info.path +"/" + info.rawName;

               }

               String dexDiffMd5 = info.dexDiffMd5;

               String oldDexCrc = info.oldDexCrC;

               if (!isVmArt && info.destMd5InDvm.equals("0")) {

                    TinkerLog.w(TAG,"patch dex %s is only for art, just continue", patchRealPath);

                    continue;

               }

               String extractedFileMd5 = isVmArt ? info.destMd5InArt :info.destMd5InDvm;

               if (!SharePatchFileUtil.checkIfMd5Valid(extractedFileMd5)) {

                    TinkerLog.w(TAG, "metafile md5 invalid, type:%s, name: %s, md5: %s",ShareTinkerInternals.getTypeString(type), info.rawName, extractedFileMd5);

                   manager.getPatchReporter().onPatchPackageCheckFail(patchFile,BasePatchInternal.getMetaCorruptedCode(type));

                    return false;

               }

               File extractedFile = new File(dir + info.realName);

               //check file whether already exist

               if (extractedFile.exists()) {

                    if(SharePatchFileUtil.verifyDexFileMd5(extractedFile, extractedFileMd5)) {

                        //it is ok, justcontinue

                        TinkerLog.w(TAG,"dex file %s is already exist, and md5 match, just continue",extractedFile.getPath());

                        continue;

                    } else {

                        TinkerLog.w(TAG,"have a mismatch corrupted dex " + extractedFile.getPath());

                        extractedFile.delete();

                    }

               } else {

                   extractedFile.getParentFile().mkdirs();

               }

               ZipEntry patchFileEntry = patch.getEntry(patchRealPath);

                ZipEntry rawApkFileEntry =apk.getEntry(patchRealPath);

               if (oldDexCrc.equals("0")) {

                    if (patchFileEntry == null){

                        TinkerLog.w(TAG,"patch entry is null. path:" + patchRealPath);

                       manager.getPatchReporter().onPatchTypeExtractFail(patchFile,extractedFile, info.rawName, type);

                        return false;

                    }

                    //it is a new file, butmaybe we need to repack the dex file

                    if (!extractDexFile(patch,patchFileEntry, extractedFile, info)) {

                        TinkerLog.w(TAG,"Failed to extract raw patch file " + extractedFile.getPath());

                       manager.getPatchReporter().onPatchTypeExtractFail(patchFile,extractedFile, info.rawName, type);

                        return false;

                    }

               } else if (dexDiffMd5.equals("0")) {

                    // skip process old dex forreal dalvik vm

                   if (!isVmArt) {

                        continue;

                    }

                    if (rawApkFileEntry ==null) {

                        TinkerLog.w(TAG,"apk entry is null. path:" + patchRealPath);

                        manager.getPatchReporter().onPatchTypeExtractFail(patchFile,extractedFile, info.rawName, type);

                        return false;

                    }

                    //check source crc insteadof md5 for faster

                    String rawEntryCrc =String.valueOf(rawApkFileEntry.getCrc());

                    if(!rawEntryCrc.equals(oldDexCrc)) {

                        TinkerLog.e(TAG,"apk entry %s crc is not equal, expect crc: %s, got crc: %s",patchRealPath, oldDexCrc, rawEntryCrc);

                       manager.getPatchReporter().onPatchTypeExtractFail(patchFile,extractedFile, info.rawName, type);

                        return false;

                    }

                    // Small patched dexgenerating strategy was disabled, we copy full original dex directly now.

                    //patchDexFile(apk, patch,rawApkFileEntry, null, info, smallPatchInfoFile, extractedFile);

                    extractDexFile(apk,rawApkFileEntry, extractedFile, info);

                    if(!SharePatchFileUtil.verifyDexFileMd5(extractedFile, extractedFileMd5)) {

                        TinkerLog.w(TAG,"Failed to recover dex file when verify patched dex: " + extractedFile.getPath());

                       manager.getPatchReporter().onPatchTypeExtractFail(patchFile,extractedFile, info.rawName, type);

                       SharePatchFileUtil.safeDeleteFile(extractedFile);

                        return false;

                    }

               } else {

                    if (patchFileEntry == null){

                        TinkerLog.w(TAG,"patch entry is null. path:" + patchRealPath);

                       manager.getPatchReporter().onPatchTypeExtractFail(patchFile,extractedFile, info.rawName, type);

                        return false;

                    }

                    if(!SharePatchFileUtil.checkIfMd5Valid(dexDiffMd5)) {

                        TinkerLog.w(TAG,"meta file md5 invalid, type:%s, name: %s, md5: %s",ShareTinkerInternals.getTypeString(type), info.rawName, dexDiffMd5);

                       manager.getPatchReporter().onPatchPackageCheckFail(patchFile,BasePatchInternal.getMetaCorruptedCode(type));

                        return false;

                    }

                    if (rawApkFileEntry ==null) {

                        TinkerLog.w(TAG,"apk entry is null. path:" + patchRealPath);

                       manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile,info.rawName, type);

                        return false;

                    }

                    //check source crc insteadof md5 for faster

                    String rawEntryCrc =String.valueOf(rawApkFileEntry.getCrc());

                    if(!rawEntryCrc.equals(oldDexCrc)) {

                        TinkerLog.e(TAG,"apk entry %s crc is not equal, expect crc: %s, got crc: %s",patchRealPath, oldDexCrc, rawEntryCrc);

                       manager.getPatchReporter().onPatchTypeExtractFail(patchFile,extractedFile, info.rawName, type);

                        return false;

                    }

                    patchDexFile(apk, patch,rawApkFileEntry, patchFileEntry, info, extractedFile);

                    if (!SharePatchFileUtil.verifyDexFileMd5(extractedFile,extractedFileMd5)) {

                        TinkerLog.w(TAG,"Failed to recover dex file when verify patched dex: " +extractedFile.getPath());

                       manager.getPatchReporter().onPatchTypeExtractFail(patchFile,extractedFile, info.rawName, type);

                       SharePatchFileUtil.safeDeleteFile(extractedFile);

                        return false;

                    }

                    TinkerLog.w(TAG,"success recover dex file: %s, size: %d, use time: %d",

                           extractedFile.getPath(), extractedFile.length(),(System.currentTimeMillis() - start));

               }

           }

           if (!mergeClassNDexFiles(context, patchFile, dir)) {

               return false;

           }

        }catch (Throwable e) {

           throw new TinkerRuntimeException("patch " +ShareTinkerInternals.getTypeString(type) + " extract failed (" +e.getMessage() + ").", e);

        }finally {

           SharePatchFileUtil.closeZip(apk);

           SharePatchFileUtil.closeZip(patch);

        }

       return true;

    }


猜你喜欢

转载自blog.csdn.net/HelloMagina/article/details/79534158