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
final Set
final Set
final Set
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
final List
for (Element node :incActivityNodes) {
incAppNode.add(node.detach());
}
}
if (!incServices.isEmpty()) {
final List
final List
for (Element node :incServiceNodes) {
incAppNode.add(node.detach());
}
}
if (!incReceivers.isEmpty()) {
final List
final List
for (Element node :incReceiverNodes) {
incAppNode.add(node.detach());
}
}
if (!incProviders.isEmpty()) {
final List
final List
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;
}
第四步:如果不是目录,那肯定就是文件,开始进行read和write过程。无论是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
//访问目录前
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
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文件中的注释
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
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
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()) {
//该文件刚好在ignorechange的pattern匹配格式中,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;
}
//该文件刚好在ignorechange的pattern匹配格式中,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 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.java的executeAndSaveTo()方法,下面重点分析该方法。
}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
List
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
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;
}