騰訊熱修復框架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;
}
第四步:如果不是目錄,那肯定就是檔案,開始進行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<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);
&nbs