大厂面试经验分享怎么写,Android 热修复 Tinker Gradle Plugin解析,顺利通过阿里Android岗面试

当执行完后,将会看到执行过程包含以下流程:

: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.xmlbuild/intermediates/tinker_intermediates/AndroidManifest.xml

这里我们需要想一下,在文初的分析中,并没有想到需要tinkerId这个东西,那么它到底是干嘛的呢?

看一下微信提供的参数说明,就明白了:

在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。这个是决定补丁包能运行在哪些基准包上面,一般来说我们可以使用git版本号、versionName等等。

想一下,在非强制升级的情况下,线上一般分布着各个版本的app。但是。你打patch肯定是对应某个版本,所以你要保证这个patch下发下去只影响对应的版本,不会对其他版本造成影响,所以你需要tinkerId与具体的版本相对应。

ok,下一个TinkerResourceIdTask。

五、TinkerResourceIdTask


文初提到,打patch的过程实际上要控制已有的资源id不能发生变化,这个task所做的事就是为此。

如果保证已有资源的id保持不变呢?

实际上需要public.xmlids.xml的参与,即预先在public.xml中的如下定义,在第二次打包之后可保持该资源对应的id值不变。

注:对xml文件的名称应该没有强要求。

很多时候我们在搜索固化资源,一般都能看到通过public.xml去固化资源id,但是这里有个ids.xml是干嘛的呢?

下面这篇文章有个很好的解释~

http://blog.csdn.net/sbsujjbcy/article/details/52541803

首先需要生成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.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);

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

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

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

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

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

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

如何做好面试突击,规划学习方向?

面试题集可以帮助你查漏补缺,有方向有针对性的学习,为之后进大厂做准备。但是如果你仅仅是看一遍,而不去学习和深究。那么这份面试题对你的帮助会很有限。最终还是要靠资深技术水平说话。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。建议先制定学习计划,根据学习计划把知识点关联起来,形成一个系统化的知识体系。

学习方向很容易规划,但是如果只通过碎片化的学习,对自己的提升是很慢的。

同时我还搜集整理2020年字节跳动,以及腾讯,阿里,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节

image

在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。

image

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

本文已被CODING开源项目:《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的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

本文已被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、付费专栏及课程。

余额充值