Android 热修复 Tinker Gradle Plugin解析,拥有百万粉丝的大牛讲述学Android的历程

一类是对 文件 即为资源,例如R.layout.xxx,R.drawable.xxx等;另一类为xml文档中以@+(去除@+android:id),其实就是找到我们自定义id节点,然后截取该节点的id值部分作为属性的名称(例如:@+id/tv,tv即为属性的名称)。

如果和设置的old apk中文件中相同name和type的节点不需要特殊处理,直接复用即可;如果不存在则需要生成新的typeId、resourceId等信息。

会将所有生成的资源都存到rTypeResourceMap中,最后写文件。

这样就基本收集到了所有的需要生成资源信息的所有的资源,最后写到public.xml即可。

总结性的语言难免有一些疏漏,实际以源码分析为标准。

开始源码分析

@TaskAction

def applyResourceId() {

// 资源mapping文件

String resourceMappingFile = project.extensions.tinkerPatch.buildConfig.applyResourceMapping

// resDir /build/intermediates/res/merged/debug

String idsXml = resDir + “/values/ids.xml”;

String publicXml = resDir + “/values/public.xml”;

FileOperation.deleteFile(idsXml);

FileOperation.deleteFile(publicXml);

List resourceDirectoryList = new ArrayList();

// /build/intermediates/res/merged/debug

resourceDirectoryList.add(resDir);

project.logger.error(“we build ${project.getName()} apk with apply resource mapping file ${resourceMappingFile}”);

project.extensions.tinkerPatch.buildConfig.usingResourceMapping = true;

// 收集所有的资源,以type->type,name,id,int/int[]存储

Map<RDotTxtEntry.RType, Set> rTypeResourceMap = PatchUtil.readRTxt(resourceMappingFile);

AaptResourceCollector aaptResourceCollector = AaptUtil.collectResource(resourceDirectoryList, rTypeResourceMap);

PatchUtil.generatePublicResourceXml(aaptResourceCollector, idsXml, publicXml);

File publicFile = new File(publicXml);

if (publicFile.exists()) {

FileOperation.copyFileUsingStream(publicFile, project.file(RESOURCE_PUBLIC_XML));

project.logger.error(“tinker gen resource public.xml in ${RESOURCE_PUBLIC_XML}”);

}

File idxFile = new File(idsXml);

if (idxFile.exists()) {

FileOperation.copyFileUsingStream(idxFile, project.file(RESOURCE_IDX_XML));

project.logger.error(“tinker gen resource idx.xml in ${RESOURCE_IDX_XML}”);

}

}

大体浏览下代码,可以看到首先检测是否设置了resource mapping文件,如果没有设置会直接跳过。并且最后的产物是public.xmlids.xml

因为生成patch时,需要保证两次打包已经存在的资源的id一致,需要public.xmlids.xml的参与。

首先清理已经存在的public.xmlids.xml,然后通过PatchUtil.readRTxt读取resourceMappingFile(参数中设置的),该文件记录的格式如下:

int anim abc_slide_in_bottom 0x7f050006

int id useLogo 0x7f0b0012

int[] styleable AppCompatImageView { 0x01010119, 0x7f010027 }

int styleable AppCompatImageView_android_src 0

int styleable AppCompatImageView_srcCompat 1

大概有两类,一类是int型各种资源;一类是int[]数组,代表styleable,其后面紧跟着它的item(熟悉自定义View的一定不陌生)。

PatchUtil.readRTxt的代码就不贴了,简单描述下:

首先正则按行匹配,每行分为四部分,即idType,rType,name,idValue(四个属性为RDotTxtEntry的成员变量)。

  • idType有两种INTINT_ARRAY

  • rType包含各种资源:

ANIM, ANIMATOR, ARRAY, ATTR, BOOL, COLOR, DIMEN, DRAWABLE, FRACTION, ID, INTEGER, INTERPOLATOR, LAYOUT, MENU, MIPMAP, PLURALS, RAW, STRING, STYLE, STYLEABLE, TRANSITION, XML

http://developer.android.com/reference/android/R.html

name和value就是普通的键值对了。

这里并没有对styleable做特殊处理。

最后按rType分类,存在一个Map中,即key为rType,value为一个RDotTxtEntry类型的Set集合。

回顾下剩下的代码:

//…省略前半部分

AaptResourceCollector aaptResourceCollector = AaptUtil.collectResource(resourceDirectoryList, rTypeResourceMap);

PatchUtil.generatePublicResourceXml(aaptResourceCollector, idsXml, publicXml);

File publicFile = new File(publicXml);

if (publicFile.exists()) {

FileOperation.copyFileUsingStream(publicFile, project.file(RESOURCE_PUBLIC_XML));

project.logger.error(“tinker gen resource public.xml in ${RESOURCE_PUBLIC_XML}”);

}

File idxFile = new File(idsXml);

if (idxFile.exists()) {

FileOperation.copyFileUsingStream(idxFile, project.file(RESOURCE_IDX_XML));

project.logger.error(“tinker gen resource idx.xml in ${RESOURCE_IDX_XML}”);

}

那么到了AaptUtil.collectResource方法,传入了resDir目录和我们刚才收集了资源信息的Map,返回了一个AaptResourceCollector对象,看名称是对aapt相关的资源的收集:

看代码:

public static AaptResourceCollector collectResource(List resourceDirectoryList,

Map<RType, Set> rTypeResourceMap) {

AaptResourceCollector resourceCollector = new AaptResourceCollector(rTypeResourceMap);

List<com.tencent.tinker.build.aapt.RDotTxtEntry> references = new ArrayList<com.tencent.tinker.build.aapt.RDotTxtEntry>();

for (String resourceDirectory : resourceDirectoryList) {

try {

collectResources(resourceDirectory, resourceCollector);

} catch (Exception e) {

throw new RuntimeException(e);

}

}

for (String resourceDirectory : resourceDirectoryList) {

try {

processXmlFilesForIds(resourceDirectory, references, resourceCollector);

} catch (Exception e) {

throw new RuntimeException(e);

}

}

return resourceCollector;

}

首先初始化了一个AaptResourceCollector对象,看其构造方法:

public AaptResourceCollector(Map<RType, Set> rTypeResourceMap) {

this();

if (rTypeResourceMap != null) {

Iterator<Entry<RType, Set>> iterator = rTypeResourceMap.entrySet().iterator();

while (iterator.hasNext()) {

Entry<RType, Set> entry = iterator.next();

RType rType = entry.getKey();

Set set = entry.getValue();

for (RDotTxtEntry rDotTxtEntry : set) {

originalResourceMap.put(rDotTxtEntry, rDotTxtEntry);

ResourceIdEnumerator resourceIdEnumerator = null;

// ARRAY主要是styleable

if (!rDotTxtEntry.idType.equals(IdType.INT_ARRAY)) {

// 获得resourceId

int resourceId = Integer.decode(rDotTxtEntry.idValue.trim()).intValue();

// 获得typeId

int typeId = ((resourceId & 0x00FF0000) / 0x00010000);

if (typeId >= currentTypeId) {

currentTypeId = typeId + 1;

}

// type -> id的映射

if (this.rTypeEnumeratorMap.containsKey(rType)) {

resourceIdEnumerator = this.rTypeEnumeratorMap.get(rType);

if (resourceIdEnumerator.currentId < resourceId) {

resourceIdEnumerator.currentId = resourceId;

}

} else {

resourceIdEnumerator = new ResourceIdEnumerator();

resourceIdEnumerator.currentId = resourceId;

this.rTypeEnumeratorMap.put(rType, resourceIdEnumerator);

}

}

}

}

}

}

对rTypeResourceMap根据rType进行遍历,读取每个rType对应的Set集合;然后遍历每个rDotTxtEntry:

  1. 加入到originalResourceMap,key和value都是rDotTxtEntry对象

  2. 如果是int型资源,首先读取其typeId,并持续更新currentTypeId(保证其为遍历完成后的最大值+1)

  3. 初始化rTypeEnumeratorMap,key为rType,value为ResourceIdEnumerator,且ResourceIdEnumerator中的currentId保存着目前同类资源的最大的resouceId,也就是说rTypeEnumeratorMap中存储了各个rType对应的最大的资源Id。

结束完成构造方法,执行了

  1. 遍历了resourceDirectoryList,目前其中只有一个resDir,然后执行了collectResources方法;

  2. 遍历了resourceDirectoryList,执行了processXmlFilesForIds

分别读代码了:

collectResources

private static void collectResources(String resourceDirectory, AaptResourceCollector resourceCollector) throws Exception {

File resourceDirectoryFile = new File(resourceDirectory);

File[] fileArray = resourceDirectoryFile.listFiles();

if (fileArray != null) {

for (File file : fileArray) {

if (file.isDirectory()) {

String directoryName = file.getName();

if (directoryName.startsWith(“values”)) {

if (!isAValuesDirectory(directoryName)) {

throw new AaptUtilException(“'” + directoryName + “’ is not a valid values directory.”);

}

processValues(file.getAbsolutePath(), resourceCollector);

} else {

processFileNamesInDirectory(file.getAbsolutePath(), resourceCollector);

}

}

}

}

}

遍历我们的resDir中的所有文件夹

  • 如果是values相关文件夹,执行processValues

  • 非values相关文件夹则执行processFileNamesInDirectory

processValues处理values相关文件,会遍历每一个合法的values相关文件夹下的文件,执行processValuesFile(file.getAbsolutePath(), resourceCollector);

public static void processValuesFile(String valuesFullFilename,

AaptResourceCollector resourceCollector) throws Exception {

Document document = JavaXmlUtil.parse(valuesFullFilename);

String directoryName = new File(valuesFullFilename).getParentFile().getName();

Element root = document.getDocumentElement();

for (Node node = root.getFirstChild(); node != null; node = node.getNextSibling()) {

if (node.getNodeType() != Node.ELEMENT_NODE) {

continue;

}

String resourceType = node.getNodeName();

if (resourceType.equals(ITEM_TAG)) {

resourceType = node.getAttributes().getNamedItem(“type”).getNodeValue();

if (resourceType.equals(“id”)) {

resourceCollector.addIgnoreId(node.getAttributes().getNamedItem(“name”).getNodeValue());

}

}

if (IGNORED_TAGS.contains(resourceType)) {

continue;

}

if (!RESOURCE_TYPES.containsKey(resourceType)) {

throw new AaptUtilException(“Invalid resource type '<” + resourceType + “>’ in '” + valuesFullFilename + “'.”);

}

RType rType = RESOURCE_TYPES.get(resourceType);

String resourceValue = null;

switch (rType) {

case STRING:

case COLOR:

case DIMEN:

case DRAWABLE:

case BOOL:

case INTEGER:

resourceValue = node.getTextContent().trim();

break;

case ARRAY://has sub item

case PLURALS://has sub item

case STYLE://has sub item

case STYLEABLE://has sub item

resourceValue = subNodeToString(node);

break;

case FRACTION://no sub item

resourceValue = nodeToString(node, true);

break;

case ATTR://no sub item

resourceValue = nodeToString(node, true);

break;

}

try {

addToResourceCollector(resourceCollector,

new ResourceDirectory(directoryName, valuesFullFilename),

node, rType, resourceValue);

} catch (Exception e) {

throw new AaptUtilException(e.getMessage() + “,Process file error:” + valuesFullFilename, e);

}

}

}

values下相关的文件基本都是xml咯,所以遍历xml文件,遍历其内部的节点,(values的xml文件其内部一般为item,dimen,color,drawable,bool,integer,array,style,declare-styleable,attr,fraction等),每种类型的节点对应一个rType,根据不同类型的节点也会去获取节点的值,确定一个都会执行:

addToResourceCollector(resourceCollector,

new ResourceDirectory(directoryName, valuesFullFilename),

node, rType, resourceValue);

注:除此以外,这里在ignoreIdSet记录了声明的id资源,这些id是已经声明过的,所以最终在编写ids.xml时,可以过滤掉这些id。

下面继续看:addToResourceCollector

源码如下:

private static void addToResourceCollector(AaptResourceCollector resourceCollector,

ResourceDirectory resourceDirectory,

Node node, RType rType, String resourceValue) {

String resourceName = sanitizeName(rType, resourceCollector, extractNameAttribute(node));

if (rType.equals(RType.STYLEABLE)) {

int count = 0;

for (Node attrNode = node.getFirstChild(); attrNode != null; attrNode = attrNode.getNextSibling()) {

if (attrNode.getNodeType() != Node.ELEMENT_NODE || !attrNode.getNodeName().equals(“attr”)) {

continue;

}

String rawAttrName = extractNameAttribute(attrNode);

String attrName = sanitizeName(rType, resourceCollector, rawAttrName);

if (!rawAttrName.startsWith(“android:”)) {

resourceCollector.addIntResourceIfNotPresent(RType.ATTR, attrName);

}

}

} else {

resourceCollector.addIntResourceIfNotPresent(rType, resourceName);

}

}

如果不是styleable的资源,则直接获取resourceName,然后调用resourceCollector.addIntResourceIfNotPresent(rType, resourceName)。

如果是styleable类型的资源,则会遍历找到其内部的attr节点,找出非android:开头的(因为android:开头的attr的id不需要我们去确定),设置rType为ATTR,value为attr属性的name,调用addIntResourceIfNotPresent。

public void addIntResourceIfNotPresent(RType rType, String name) { //, ResourceDirectory resourceDirectory) {

if (!rTypeEnumeratorMap.containsKey(rType)) {

if (rType.equals(RType.ATTR)) {

rTypeEnumeratorMap.put(rType, new ResourceIdEnumerator(1));

} else {

rTypeEnumeratorMap.put(rType, new ResourceIdEnumerator(currentTypeId++));

}

}

RDotTxtEntry entry = new FakeRDotTxtEntry(IdType.INT, rType, name);

Set resourceSet = null;

if (this.rTypeResourceMap.containsKey(rType)) {

resourceSet = this.rTypeResourceMap.get(rType);

} else {

resourceSet = new HashSet();

this.rTypeResourceMap.put(rType, resourceSet);

}

if (!resourceSet.contains(entry)) {

String idValue = String.format(“0x%08x”, rTypeEnumeratorMap.get(rType).next());

addResource(rType, IdType.INT, name, idValue); //, resourceDirectory);

}

}

首先构建一个entry,然后判断当前的rTypeResourceMap中是否存在该资源实体,如果存在,则什么都不用做。

如果不存在,则需要构建一个entry,那么主要是id的构建。

关于id的构建:

还记得rTypeEnumeratorMap么,其内部包含了我们设置的”res mapping”文件,存储了每一类资源(rType)的资源的最大resourceId值。

那么首先判断就是是否已经有这种类型了,如果有的话,获取出该类型当前最大的resourceId,然后+1,最为传入资源的resourceId.

如果不存在当前这种类型,那么如果类型为ATTR则固定type为1;否则的话,新增一个typeId,为当前最大的type+1(currentTypeId中也是记录了目前最大的type值),有了类型就可以通过ResourceIdEnumerator.next()来获取id。

经过上述就可以构造出一个idValue了。

最后调用:

addResource(rType, IdType.INT, name, idValue);

查看代码:

public void addResource(RType rType, IdType idType, String name, String idValue) {

Set resourceSet = null;

if (this.rTypeResourceMap.containsKey(rType)) {

resourceSet = this.rTypeResourceMap.get(rType);

} else {

resourceSet = new HashSet();

this.rTypeResourceMap.put(rType, resourceSet);

}

RDotTxtEntry rDotTxtEntry = new RDotTxtEntry(idType, rType, name, idValue);

if (!resourceSet.contains(rDotTxtEntry)) {

if (this.originalResourceMap.containsKey(rDotTxtEntry)) {

this.rTypeEnumeratorMap.get(rType).previous();

rDotTxtEntry = this.originalResourceMap.get(rDotTxtEntry);

}

resourceSet.add(rDotTxtEntry);

}

}

大体意思就是如果该资源不存在就添加到rTypeResourceMap。

首先构建出该资源实体,判断该类型对应的资源集合是否包含该资源实体(这里contains只比对name和type),如果不包含,判断是否在originalResourceMap中,如果存在(这里做了一个previous操作,其实与上面的代码的next操作对应,主要是针对资源存在我们的res map中这种情况)则取出该资源实体,最终将该资源实体加入到rTypeResourceMap中。

ok,到这里需要小节一下,我们刚才对所有values相关文件夹下的文件已经处理完毕,大致的处理为:遍历文件中的节点,大致有item,dimen,color,drawable,bool,integer,array,style,declare-styleable,attr,fraction这些节点,将所有的节点按类型分类存储到rTypeResourceMap中(如果和设置的”res map”文件中相同name和type的节点不需要特殊处理,直接复用即可;如果不存在则需要生成新的typeId、resourceId等信息)。

其中declare-styleable这个标签,主要读取其内部的attr标签,对attr标签对应的资源按上述处理。

处理完成values相关文件夹之后,还需要处理一些res下的其他文件,比如layout、layout、anim等文件夹,该类资源也需要在R中生成对应的id值,这类值也需要固化。

processFileNamesInDirectory

public static void processFileNamesInDirectory(String resourceDirectory,

AaptResourceCollector resourceCollector) throws IOException {

File resourceDirectoryFile = new File(resourceDirectory);

String directoryName = resourceDirectoryFile.getName();

int dashIndex = directoryName.indexOf(‘-’);

if (dashIndex != -1) {

directoryName = directoryName.substring(0, dashIndex);

}

if (!RESOURCE_TYPES.containsKey(directoryName)) {

throw new AaptUtilException(resourceDirectoryFile.getAbsolutePath() + " is not a valid resource sub-directory.");

}

File[] fileArray = resourceDirectoryFile.listFiles();

if (fileArray != null) {

for (File file : fileArray) {

if (file.isHidden()) {

continue;

}

String filename = file.getName();

int dotIndex = filename.indexOf(‘.’);

String resourceName = dotIndex != -1 ? filename.substring(0, dotIndex) : filename;

RType rType = RESOURCE_TYPES.get(directoryName);

resourceCollector.addIntResourceIfNotPresent(rType, resourceName);

System.out.println("rType = " + rType + " , resName = " + resourceName);

ResourceDirectory resourceDirectoryBean = new ResourceDirectory(file.getParentFile().getName(), file.getAbsolutePath());

resourceCollector.addRTypeResourceName(rType, resourceName, null, resourceDirectoryBean);

}

}

}

遍历res下所有文件夹,根据文件夹名称确定其对应的资源类型(例如:drawable-xhpi,则认为其内部的文件类型为drawable类型),然后遍历该文件夹下所有的文件,最终以文件名为资源的name,文件夹确定资源的type,最终调用:

resourceCollector

.addIntResourceIfNotPresent(rType, resourceName);

processXmlFilesForIds

public static void processXmlFilesForIds(String resourceDirectory,

List references, AaptResourceCollector resourceCollector) throws Exception {

List xmlFullFilenameList = FileUtil

.findMatchFile(resourceDirectory, Constant.Symbol.DOT + Constant.File.XML);

if (xmlFullFilenameList != null) {

for (String xmlFullFilename : xmlFullFilenameList) {

File xmlFile = new File(xmlFullFilename);

String parentFullFilename = xmlFile.getParent();

File parentFile = new File(parentFullFilename);

if (isAValuesDirectory(parentFile.getName()) || parentFile.getName().startsWith(“raw”)) {

// Ignore files under values* directories and raw*.

continue;

}

processXmlFile(xmlFullFilename, references, resourceCollector);

}

}

}

遍历除了raw*以及values*相关文件夹下的xml文件,执行processXmlFile。

public static void processXmlFile(String xmlFullFilename, List references, AaptResourceCollector resourceCollector)

throws IOException, XPathExpressionException {

Document document = JavaXmlUtil.parse(xmlFullFilename);

NodeList nodesWithIds = (NodeList) ANDROID_ID_DEFINITION.evaluate(document, XPathConstants.NODESET);

for (int i = 0; i < nodesWithIds.getLength(); i++) {

String resourceName = nodesWithIds.item(i).getNodeValue();

if (!resourceName.startsWith(ID_DEFINITION_PREFIX)) {

throw new AaptUtilException(“Invalid definition of a resource: '” + resourceName + “'”);

}

resourceCollector.addIntResourceIfNotPresent(RType.ID, resourceName.substring(ID_DEFINITION_PREFIX.length()));

}

// 省略了无关代码

}

主要找xml文档中以@+(去除@+android:id),其实就是找到我们自定义id节点,然后截取该节点的id值部分作为属性的名称(例如:@+id/tv,tv即为属性的名称),最终调用:

resourceCollector

.addIntResourceIfNotPresent(RType.ID,

resourceName.substring(ID_DEFINITION_PREFIX.length()));

上述就完成了所有的资源的收集,那么剩下的就是写文件了:

public static void generatePublicResourceXml(AaptResourceCollector aaptResourceCollector,

String outputIdsXmlFullFilename,

String outputPublicXmlFullFilename) {

if (aaptResourceCollector == null) {

return;

}

FileUtil.createFile(outputIdsXmlFullFilename);

FileUtil.createFile(outputPublicXmlFullFilename);

PrintWriter idsWriter = null;

PrintWriter publicWriter = null;

try {

FileUtil.createFile(outputIdsXmlFullFilename);

FileUtil.createFile(outputPublicXmlFullFilename);

idsWriter = new PrintWriter(new File(outputIdsXmlFullFilename), “UTF-8”);

publicWriter = new PrintWriter(new File(outputPublicXmlFullFilename), “UTF-8”);

idsWriter.println(“<?xml version=\"1.0\" encoding=\"utf-8\"?>”);

publicWriter.println(“<?xml version=\"1.0\" encoding=\"utf-8\"?>”);

idsWriter.println(“”);

publicWriter.println(“”);

Map<RType, Set> map = aaptResourceCollector.getRTypeResourceMap();

Iterator<Entry<RType, Set>> iterator = map.entrySet().iterator();

while (iterator.hasNext()) {

Entry<RType, Set> entry = iterator.next();

RType rType = entry.getKey();

if (!rType.equals(RType.STYLEABLE)) {

Set set = entry.getValue();

for (RDotTxtEntry rDotTxtEntry : set) {

String rawName = aaptResourceCollector.getRawName(rType, rDotTxtEntry.name);

if (StringUtil.isBlank(rawName)) {

rawName = rDotTxtEntry.name;

}

publicWriter.println(“<public type=”" + rType + “” name=“” + rawName + “” id=“” + rDotTxtEntry.idValue.trim() + “” />");

}

Set ignoreIdSet = aaptResourceCollector.getIgnoreIdSet();

for (RDotTxtEntry rDotTxtEntry : set) {

if (rType.equals(RType.ID) && !ignoreIdSet.contains(rDotTxtEntry.name)) {

idsWriter.println(“<item type=”" + rType + “” name=“” + rDotTxtEntry.name + “”/>");

}

}

}

idsWriter.flush();

publicWriter.flush();

}

idsWriter.println(“”);

publicWriter.println(“”);

} catch (Exception e) {

throw new PatchUtilException(e);

} finally {

if (idsWriter != null) {

idsWriter.flush();

idsWriter.close();

}

if (publicWriter != null) {

publicWriter.flush();

publicWriter.close();

}

}

}

主要就是遍历rTypeResourceMap,然后每个资源实体对应一条public标签记录写到public.xml中。

此外,如果发现该元素节点的type为Id,并且不在ignoreSet中,会写到ids.xml这个文件中。(这里有个ignoreSet,这里ignoreSet中记录了values下所有的<item type=id的资源,是直接在项目中已经声明过的,所以去除)。

六、TinkerProguardConfigTask


还记得文初说:

  1. 我们在上线app的时候,会做代码混淆,如果没有做特殊的设置,每次混淆后的代码差别应该非常巨大;所以,build过程中理论上需要设置混淆的mapping文件。
  1. 在接入一些库的时候,往往还需要配置混淆,比如第三方库中哪些东西不能被混淆等(当然强制某些类在主dex中,也可能需要配置相对应的混淆规则)。

这个task的作用很明显了。有时候为了确保一些类在main dex中,简单的做法也会对其在混淆配置中进行keep(避免由于混淆造成类名更改,而使main dex的keep失效)。

如果开启了proguard会执行该task。

这个就是主要去设置混淆的mapping文件,和keep一些必要的类了。

@TaskAction

def updateTinkerProguardConfig() {

def file = project.file(PROGUARD_CONFIG_PATH)

project.logger.error(“try update tinker proguard file with ${file}”)

// Create the directory if it doesnt exist already

file.getParentFile().mkdirs()

// Write our recommended proguard settings to this file

FileWriter fr = new FileWriter(file.path)

String applyMappingFile = project.extensions.tinkerPatch.buildConfig.applyMapping

//write applymapping

if (shouldApplyMapping && FileOperation.isLegalFile(applyMappingFile)) {

project.logger.error(“try add applymapping ${applyMappingFile} to build the package”)

fr.write("-applymapping " + applyMappingFile)

fr.write(“\n”)

} else {

project.logger.error(“applymapping file ${applyMappingFile} is illegal, just ignore”)

}

fr.write(PROGUARD_CONFIG_SETTINGS)

fr.write(“#your dex.loader patterns here\n”)

//they will removed when apply

Iterable loader = project.extensions.tinkerPatch.dex.loader

for (String pattern : loader) {

if (pattern.endsWith(“*”) && !pattern.endsWith(“**”)) {

pattern += “*”

}

fr.write("-keep class " + pattern)

fr.write(“\n”)

}

fr.close()

// Add this proguard settings file to the list

applicationVariant.getBuildType().buildType.proguardFiles(file)

def files = applicationVariant.getBuildType().buildType.getProguardFiles()

project.logger.error(“now proguard files is ${files}”)

}

读取我们设置的mappingFile,设置

-applymapping applyMappingFile

然后设置一些默认需要keep的规则:

PROGUARD_CONFIG_SETTINGS =

“-keepattributes Annotation \n” +

“-dontwarn com.tencent.tinker.anno.AnnotationProcessor \n” +

“-keep @com.tencent.tinker.anno.DefaultLifeCycle public class *\n” +

“-keep public class * extends android.app.Application {\n” +

" *;\n" +

“}\n” +

“\n” +

“-keep public class com.tencent.tinker.loader.app.ApplicationLifeCycle {\n” +

" *;\n" +

“}\n” +

“-keep public class * implements com.tencent.tinker.loader.app.ApplicationLifeCycle {\n” +

" *;\n" +

“}\n” +

“\n” +

“-keep public class com.tencent.tinker.loader.TinkerLoader {\n” +

" *;\n" +

“}\n” +

“-keep public class * extends com.tencent.tinker.loader.TinkerLoader {\n” +

" *;\n" +

“}\n” +

“-keep public class com.tencent.tinker.loader.TinkerTestDexLoad {\n” +

" *;\n" +

“}\n” +

“\n”

最后是keep住我们的application、com.tencent.tinker.loader.**以及我们设置的相关类。

TinkerManifestTask中:addApplicationToLoaderPattern主要是记录自己的application类名和tinker相关的一些load class com.tencent.tinker.loader.*,记录在project.extensions.tinkerPatch.dex.loader

七、TinkerMultidexConfigTask


对应文初:

当项目比较大的时候,我们可能会遇到方法数超过65535的问题,我们很多时候会通过分包解决,这样就有主dex和其他dex的概念。集成了tinker之后,在应用的Application启动时会非常早的就去做tinker的load操作,所以就决定了load相关的类必须在主dex中。

如果multiDexEnabled开启。

主要是让相关类必须在main dex。

“-keep public class * implements com.tencent.tinker.loader.app.ApplicationLifeCycle {\n” +

" *;\n" +

“}\n” +

“\n” +

“-keep public class * extends com.tencent.tinker.loader.TinkerLoader {\n” +

" *;\n" +

“}\n” +

“\n” +

“-keep public class * extends android.app.Application {\n” +

" *;\n" +

“}\n”

Iterable loader = project.extensions.tinkerPatch.dex.loader

for (String pattern : loader) {

if (pattern.endsWith(“*”)) {

if (!pattern.endsWith(“**”)) {

pattern += “*”

}

}

lines.append(“-keep class " + pattern + " {\n” +

" *;\n" +

“}\n”)

.append(“\n”)

}

相关类都在loader这个集合中,在TinkerManifestTask中设置的。

八、TinkerPatchSchemaTask


主要执行Runner.tinkerPatch

protected void tinkerPatch() {

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

}

}

主要分为以下环节:

  • 生成patch

  • 生成meta-file和version-file,这里主要就是在assets目录下写一些键值对。(包含tinkerId以及配置中configField相关信息)

  • build patch

(1)生成pacth

顾名思义就是两个apk比较去生成各类patch文件,那么从一个apk的组成来看,大致可以分为:

  • dex文件比对的patch文件

  • res文件比对的patch res文件

  • so文件比对生成的so patch文件

看下代码:

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

//check manifest change first

manifestDecoder.patch(oldFile, newFile);

unzipApkFiles(oldFile, newFile);

Files.walkFileTree(mNewApkDir.toPath(), new ApkFilesVisitor(config, mNewApkDir.toPath(),

mOldApkDir.toPath(), dexPatchDecoder, soPatchDecoder, resPatchDecoder));

soPatchDecoder.onAllPatchesEnd();

dexPatchDecoder.onAllPatchesEnd();

manifestDecoder.onAllPatchesEnd();

resPatchDecoder.onAllPatchesEnd();

//clean resources

dexPatchDecoder.clean();

soPatchDecoder.clean();

resPatchDecoder.clean();

return true;

}

代码内部包含四个Decoder:

  • manifestDecoder

  • dexPatchDecoder

  • soPatchDecoder

  • resPatchDecoder

刚才提到需要对dex、so、res文件做diff,但是为啥会有个manifestDecoder。目前tinker并不支持四大组件,也就是说manifest文件中是不允许出现新增组件的。

所以,manifestDecoder的作用实际上是用于检查的:

  1. minSdkVersion<14时仅允许dexMode使用jar模式(TODO:raw模式的区别是什么?)

  2. 会解析manifest文件,读取出组大组件进行对比,不允许出现新增的任何组件。

代码就不贴了非常好理解,关于manifest的解析是基于该库封装的:

https://github.com/clearthesky/apk-parser

然后就是解压两个apk文件了,old apk(我们设置的),old apk 生成的。

解压的目录为:

  • old apk: build/intermediates/outputs/old apk名称/

  • new apk: build/intermediates/outputs/app-debug/

解压完成后,就是单个文件对比了:

对比的思路是,以newApk解压目录下所有的文件为基准,去oldApk中找同名的文件,那么会有以下几个情况:

  1. 在oldApkDir中没有找到,那么说明该文件是新增的

  2. 在oldApkDir中找到了,那么比对md5,如果不同,则认为改变了(则需要根据情况做diff)

有了大致的了解后,可以看代码:

Files.walkFileTree(

mNewApkDir.toPath(),

new ApkFilesVisitor(

config,

mNewApkDir.toPath(),

mOldApkDir.toPath(),

dexPatchDecoder,

soPatchDecoder,

resPatchDecoder));

Files.walkFileTree会以mNewApkDir.toPath()为基准,遍历其内部所有的文件,ApkFilesVisitor中可以对每个遍历的文件进行操作。

重点看ApkFilesVisitor是如何操作每个文件的:

@Override

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

Path relativePath = newApkPath.relativize(file);

// 在oldApkDir中找到该文件

Path oldPath = oldApkPath.resolve(relativePath);

File oldFile = null;

//is a new file?!

if (oldPath.toFile().exists()) {

oldFile = oldPath.toFile();

}

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

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

dexDecoder.patch(oldFile, file.toFile());

}

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

soDecoder.patch(oldFile, file.toFile());

}

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

resDecoder.patch(oldFile, file.toFile());

}

return FileVisitResult.CONTINUE;

}

首先去除newApkDir中的一个文件,在oldApkDir中寻找同名的apk;然后根据名称判断该文件属于:

  1. dexFile -> dexDecoder.patch 完成dex文件间的比对

  2. soFile -> soDecoder.patch 完成so文件的比对

  3. resFile -> resDecoder.patch 完成res文件的比对

各种文件的规则是可配置的。

(1)dexDecoder.patch

public boolean patch(final File oldFile, final File newFile) {

final String dexName = getRelativeDexName(oldFile, newFile);

// 检查loader class,省略了抛异常的一些代码

excludedClassModifiedChecker.checkIfExcludedClassWasModifiedInNewDex(oldFile, newFile);

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

final String newMd5 = getRawOrWrappedDexMD5(newFile);

//new add file

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

hasDexChanged = true;

copyNewDexAndLogToDexMeta(newFile, newMd5, dexDiffOut);

return true;

}

final String oldMd5 = getRawOrWrappedDexMD5(oldFile);

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

hasDexChanged = true;

if (oldMd5 != null) {

collectAddedOrDeletedClasses(oldFile, newFile);

}

}

RelatedInfo relatedInfo = new RelatedInfo();

relatedInfo.oldMd5 = oldMd5;

relatedInfo.newMd5 = newMd5;

// collect current old dex file and corresponding new dex file for further processing.

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

dexNameToRelatedInfoMap.put(dexName, relatedInfo);

return true;

}

首先执行:

checkIfExcludedClassWasModifiedInNewDex(oldFile, newFile);

该方法主要用处是检查 tinker loader相关classes**必须存在primary dex中**,且不允许新增、修改和删除。

所有首先将两个dex读取到内存中,按照config.mDexLoaderPattern进行过滤,找出deletedClassInfosaddedClassInfoschangedClassInfosMap,必须保证deletedClassInfos.isEmpty() && addedClassInfos.isEmpty() && changedClassInfosMap.isEmpty()即不允许新增、删除、修改loader 相关类。

继续,拿到输出目录:

  • build/intermediates/outputs/tinker_result/

然后如果oldFile不存在,则newFile认为是新增文件,直接copy到输出目录,并记录log

copyNewDexAndLogToDexMeta(newFile, newMd5, dexDiffOut);

如果存在,则计算两个文件的md5,如果md5不同,则认为dexChanged(hasDexChanged = true),执行:

collectAddedOrDeletedClasses(oldFile, newFile);

该方法收集了addClasses和deleteClasses的相关信息,记录在:

  • addedClassDescToDexNameMap key为addClassDesc 和 该dex file的path

  • deletedClassDescToDexNameMap key为deletedClassDesc 和 该dex file的path

后续会使用这两个数据结构,mark一下。

继续往下走,初始化了一个relatedInfo记录了两个文件的md5,以及在oldAndNewDexFilePairList中记录了两个dex file,在dexNameToRelatedInfoMap中记录了dexName和relatedInfo的映射。

后续会使用该变量,mark一下。

到此,dexDecoder的patch方法就结束了,仅将新增的文件copy到了目标目录。

那么发生改变的文件,理论上应该要做md5看来在后面才会执行。

如果文件是so文件,则会走soDecoder.patch。

(2)soDecoder.patch

soDecoder实际上是BsDiffDecoder

@Override

public boolean patch(File oldFile, File newFile) {

//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;

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

总结

算法知识点繁多,企业考察的题目千变万化,面对越来越近的“金九银十”,我给大家准备好了一套比较完善的学习方法,希望能帮助大家在有限的时间里尽可能系统快速的恶补算法,通过高效的学习来提高大家面试中算法模块的通过率。

这一套学习资料既有文字档也有视频,里面不仅仅有关键知识点的整理,还有案例的算法相关部分的讲解,可以帮助大家更好更全面的进行学习,二者搭配起来学习效果会更好。

部分资料展示:




有了这套学习资料,坚持刷题一周,你就会发现自己的算法知识体系有明显的完善,离大厂Offer的距离更加近。

本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!

AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算

e, File newFile) {

//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;

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-jfCfniuU-1712148346205)]
[外链图片转存中…(img-9TxfFnmk-1712148346206)]
[外链图片转存中…(img-RPd4rwqI-1712148346206)]
[外链图片转存中…(img-St36trYb-1712148346206)]
[外链图片转存中…(img-v0zTG5eB-1712148346207)]
[外链图片转存中…(img-L3AMQ9UE-1712148346207)]
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-L53neWm5-1712148346208)]

总结

算法知识点繁多,企业考察的题目千变万化,面对越来越近的“金九银十”,我给大家准备好了一套比较完善的学习方法,希望能帮助大家在有限的时间里尽可能系统快速的恶补算法,通过高效的学习来提高大家面试中算法模块的通过率。

这一套学习资料既有文字档也有视频,里面不仅仅有关键知识点的整理,还有案例的算法相关部分的讲解,可以帮助大家更好更全面的进行学习,二者搭配起来学习效果会更好。

部分资料展示:

[外链图片转存中…(img-XbD6lWvp-1712148346208)]
[外链图片转存中…(img-NIHDbN1O-1712148346208)]
[外链图片转存中…(img-MrkUUzFU-1712148346208)]
[外链图片转存中…(img-QW6O8oq2-1712148346209)]

有了这套学习资料,坚持刷题一周,你就会发现自己的算法知识体系有明显的完善,离大厂Offer的距离更加近。

本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!

AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值