当执行完后,将会看到执行过程包含以下流程:
:app:processDebugManifest
:app:tinkerProcessDebugManifest(tinker)
:app:tinkerProcessDebugResourceId (tinker)
:app:processDebugResources
:app:tinkerProguardConfigTask(tinker)
:app:transformClassesAndResourcesWithProguard
:app:tinkerProcessDebugMultidexKeep (tinker)
:app:transformClassesWidthMultidexlistForDebug
:app:assembleDebug
:app:tinkerPatchDebug(tinker)
注:包含(tinker)的都是tinker plugin 所添加的task
可以看到部分task加入到了build的流程中,那么这些task是如何加入到build过程中的呢?
在我们接入tinker之后,build.gradle中有如下代码:
if (buildWithTinker()) {
apply plugin: ‘com.tencent.tinker.patch’
tinkerPatch {} // 各种参数
}
如果开启了tinker,会apply一个plugincom.tencent.tinker.patch
:
名称实际上就是properties文件的名字,该文件会对应具体的插件类。
对于gradle plugin不了解的,可以参考http://www.cnblogs.com/davenkin/p/gradle-learning-10.html,后面写会抽空单独写一篇详细讲gradle的文章。
下面看TinkerPatchPlugin,在apply方法中,里面大致有类似的代码:
// … 省略了一堆代码
TinkerPatchSchemaTask tinkerPatchBuildTask
= project.tasks.create(“tinkerPatch${variantName}”, TinkerPatchSchemaTask)
tinkerPatchBuildTask.dependsOn variant.assemble
TinkerManifestTask manifestTask
= project.tasks.create(“tinkerProcess${variantName}Manifest”, TinkerManifestTask)
manifestTask.mustRunAfter variantOutput.processManifest
variantOutput.processResources.dependsOn manifestTask
TinkerResourceIdTask applyResourceTask
= project.tasks.create(“tinkerProcess${variantName}ResourceId”, TinkerResourceIdTask)
applyResourceTask.mustRunAfter manifestTask
variantOutput.processResources.dependsOn applyResourceTask
if (proguardEnable) {
TinkerProguardConfigTask proguardConfigTask
= project.tasks.create(“tinkerProcess${variantName}Proguard”, TinkerProguardConfigTask)
proguardConfigTask.mustRunAfter manifestTask
def proguardTask = getProguardTask(project, variantName)
if (proguardTask != null) {
proguardTask.dependsOn proguardConfigTask
}
}
if (multiDexEnabled) {
TinkerMultidexConfigTask multidexConfigTask
= project.tasks.create(“tinkerProcess${variantName}MultidexKeep”, TinkerMultidexConfigTask)
multidexConfigTask.mustRunAfter manifestTask
def multidexTask = getMultiDexTask(project, variantName)
if (multidexTask != null) {
multidexTask.dependsOn multidexConfigTask
}
}
可以看到它通过gradle Project API创建了5个task,通过dependsOn,mustRunAfter插入到了原本的流程中。
例如:
TinkerManifestTask manifestTask = …
manifestTask.mustRunAfter variantOutput.processManifest
variantOutput.processResources.dependsOn manifestTask
TinkerManifestTask必须在processManifest之后执行,processResources在manifestTask后执行。
所以流程变为:
processManifest-> manifestTask-> processResources
其他同理。
ok,大致了解了这些task是如何注入的之后,接下来就看看每个task的具体作用吧。
注:如果我们有需求在build过程中搞事,可以参考上述task编写以及依赖方式的设置。
三、每个Task的具体行为
我们按照上述的流程来看,依次为:
TinkerManifestTask
TinkerResourceIdTask
TinkerProguardConfigTask
TinkerMultidexConfigTask
TinkerPatchSchemaTask
丢个图,对应下:
四、TinkerManifestTask
#TinkerManifestTask
@TaskAction
def updateManifest() {
// Parse the AndroidManifest.xml
String tinkerValue = project.extensions.tinkerPatch.buildConfig.tinkerId
tinkerValue = TINKER_ID_PREFIX + tinkerValue;//“tinker_id_”
// /build/intermediates/manifests/full/debug/AndroidManifest.xml
writeManifestMeta(manifestPath, TINKER_ID, tinkerValue)
addApplicationToLoaderPattern()
File manifestFile = new File(manifestPath)
if (manifestFile.exists()) {
FileOperation.copyFileUsingStream(manifestFile, project.file(MANIFEST_XML))
}
}
这里主要做了两件事:
- writeManifestMeta主要就是解析AndroidManifest.xml,在
<application>
内部添加一个meta标签,value为tinkerValue。
例如:
<meta-data
android:name=“TINKER_ID”
android:value=“tinker_id_com.zhy.abc” />
这里不详细展开了,话说groovy解析XML真方便。
- addApplicationToLoaderPattern主要是记录自己的application类名和tinker相关的一些load class
com.tencent.tinker.loader.*
,记录在project.extensions.tinkerPatch.dex.loader
中。
最后copy修改后的AndroidManifest.xml
至build/intermediates/tinker_intermediates/AndroidManifest.xml
。
这里我们需要想一下,在文初的分析中,并没有想到需要tinkerId这个东西,那么它到底是干嘛的呢?
看一下微信提供的参数说明,就明白了:
在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。这个是决定补丁包能运行在哪些基准包上面,一般来说我们可以使用git版本号、versionName等等。
想一下,在非强制升级的情况下,线上一般分布着各个版本的app。但是。你打patch肯定是对应某个版本,所以你要保证这个patch下发下去只影响对应的版本,不会对其他版本造成影响,所以你需要tinkerId与具体的版本相对应。
ok,下一个TinkerResourceIdTask。
五、TinkerResourceIdTask
文初提到,打patch的过程实际上要控制已有的资源id不能发生变化,这个task所做的事就是为此。
如果保证已有资源的id保持不变呢?
实际上需要public.xml
和ids.xml
的参与,即预先在public.xml
中的如下定义,在第二次打包之后可保持该资源对应的id值不变。
注:对xml文件的名称应该没有强要求。
很多时候我们在搜索固化资源,一般都能看到通过public.xml去固化资源id,但是这里有个ids.xml是干嘛的呢?
下面这篇文章有个很好的解释~
首先需要生成public.xml,public.xml的生成通过aapt编译时添加-P参数生成。相关代码通过gradle插件去hook Task无缝加入该参数,有一点需要注意,通过appt生成的public.xml并不是可以直接用的,该文件中存在id类型的资源,生成patch时应用进去编译的时候会报resource is not defined,解决方法是将id类型型的资源单独记录到ids.xml文件中,相当于一个声明过程,编译的时候和public.xml一样,将ids.xml也参与编译即可。
ok,知道了public.xml和ids.xml的作用之后,需要再思考一下如何保证id不变?
首先我们在配置old apk的时候,会配置tinkerApplyResourcePath参数,该参数对应一个R.txt,里面的内容涵盖了所有old apk中资源对应的int值。
那么我们可以这么做,根据这个R.txt,把里面的数据写成public.xml不就能保证原本的资源对应的int值不变了么。
的确是这样的,不过tinker做了更多,不仅将old apk的中的资源信息写到public.xml,而且还干涉了新的资源,对新的资源按照资源id的生成规则,也分配的对应的int值,写到了public.xml,可以说该task包办了资源id的生成。
分析前的总结
好了,由于代码非常长,我决定在这个地方先用总结性的语言总结下,如果没有耐心看代码的可以直接跳过源码分析阶段:
首先将设置的old R.txt读取到内存中,转为:
-
一个Map,key-value都代表一个具体资源信息;直接复用,不会生成新的资源信息。
-
一个Map,key为资源类型,value为该类资源当前的最大int值;参与新的资源id的生成。
接下来遍历当前app中的资源,资源分为:
- values文件夹下文件
对所有values相关文件夹下的文件已经处理完毕,大致的处理为:遍历文件中的节点,大致有item,dimen,color,drawable,bool,integer,array,style,declare-styleable,attr,fraction这些节点,将所有的节点按类型分类存储到rTypeResourceMap(key为资源类型,value为对应类型资源集合Set)中。
其中declare-styleable这个标签,主要读取其内部的attr标签,对attr标签对应的资源按上述处理。
- res下非values文件夹
打开自己的项目有看一眼,除了values相关还有layout,anim,color等文件夹,主要分为两类:
一类是对 文件 即为资源,例如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.xml
和ids.xml
。
因为生成patch时,需要保证两次打包已经存在的资源的id一致,需要
public.xml
和ids.xml
的参与。
首先清理已经存在的public.xml
和ids.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有两种
INT
和INT_ARRAY
。 -
rType包含各种资源:
ANIM, ANIMATOR, ARRAY, ATTR, BOOL, COLOR, DIMEN, DRAWABLE, FRACTION, ID, INTEGER, INTERPOLATOR, LAYOUT, MENU, MIPMAP, PLURALS, RAW, STRING, STYLE, STYLEABLE, TRANSITION, XML
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:
-
加入到originalResourceMap,key和value都是rDotTxtEntry对象
-
如果是int型资源,首先读取其typeId,并持续更新currentTypeId(保证其为遍历完成后的最大值+1)
-
初始化rTypeEnumeratorMap,key为rType,value为ResourceIdEnumerator,且ResourceIdEnumerator中的currentId保存着目前同类资源的最大的resouceId,也就是说rTypeEnumeratorMap中存储了各个rType对应的最大的资源Id。
结束完成构造方法,执行了
-
遍历了resourceDirectoryList,目前其中只有一个resDir,然后执行了collectResources方法;
-
遍历了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);
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
如何做好面试突击,规划学习方向?
面试题集可以帮助你查漏补缺,有方向有针对性的学习,为之后进大厂做准备。但是如果你仅仅是看一遍,而不去学习和深究。那么这份面试题对你的帮助会很有限。最终还是要靠资深技术水平说话。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。建议先制定学习计划,根据学习计划把知识点关联起来,形成一个系统化的知识体系。
学习方向很容易规划,但是如果只通过碎片化的学习,对自己的提升是很慢的。
同时我还搜集整理2020年字节跳动,以及腾讯,阿里,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节。
在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!
AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算
516)]
如何做好面试突击,规划学习方向?
面试题集可以帮助你查漏补缺,有方向有针对性的学习,为之后进大厂做准备。但是如果你仅仅是看一遍,而不去学习和深究。那么这份面试题对你的帮助会很有限。最终还是要靠资深技术水平说话。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。建议先制定学习计划,根据学习计划把知识点关联起来,形成一个系统化的知识体系。
学习方向很容易规划,但是如果只通过碎片化的学习,对自己的提升是很慢的。
同时我还搜集整理2020年字节跳动,以及腾讯,阿里,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节。
[外链图片转存中…(img-SdTksU5x-1712302242516)]
在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。
[外链图片转存中…(img-M60SVX93-1712302242532)]
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!
AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算