作者:刘天宇(谦风)
系列文章回顾《向工程腐化开炮 | proguard治理》《向工程腐化开炮 | manifest治理》《向工程腐化开炮:Java代码治理》。本文为系列文章第四篇,聚焦于Android 资源,这一细分畛域。对工程腐化,间接开炮!
精确的说,本文配角是Android资源,而java资源归属到java代码治理领域,并在《向工程腐化开炮:Java代码治理》一文中给出了应答计划。
Android资源从定义和应用形式来看,能够分为Resource和Asset两个大类。前者提供受控的结构化拜访形式,每个资源均有惟一id标识,以及多种配置限定符来反对多语言、多设施、多个性等能力;后者提供原始且绝对自在的目录和文件拜访。Resource类型是绝大部分资源应用场景下的最佳抉择,本文次要聚焦的即是这种类型资源,对抵触、无用、缺失类援用、硬编码文本,这几种腐化状况,发展工具研发,以及治理实际。
基础知识
本章先简要介绍一些基础知识,不便大家对Android资源有一个“框架性”的清晰认知,为了解第二章治理实际内容打下基础。此外,也尝试以独特视角,来解说一些乏味的技术点。
1.1 资源分类
对于Resource资源,依照应用场景,官网文档曾经给出了划分和具体阐明。本节以资源编译后,对应R外部类的类型,给出一个分类:
以上24种资源,均能够通过R.<type>.<name>
模式在java代码中援用,其中一些还能够通过@<type>/<name>
模式在manifest和资源中援用。对上述分类中「是否独立文件」、「是否位于resources.arsc」两个维度进行一些解读:
- 是否独立文件。一个资源如果对应一个残缺的独立文件,这种属于File-Base Resource,在最终apk的res目录下也会存在一份对应文件;否则,属于Value-Base Resource,在apk中没有独立文件与之对应,其值(如果有)存储在resources.arsc中。其中color类型比拟非凡,繁多color资源是Value-Base,然而色彩状态列表(ColorStateList)属于File-Base。此外,是否独立文件,是从资源编译后这一视角来看的,在定义资源时,Android提供了一种内嵌xml资源的模式,能够把多个独立文件类型资源,写在一个xml文件中,在此不展开讨论;
- 是否位于resources.arsc。绝大部分资源,在R$<type>类中field的取值,都是0x7fxxxxxx,并且在resources.arsc中都有一条对应记录。对于File-Base资源,记录值是file的相对路径,对于Value-Base资源,记录值就是资源值自身。须要留神的是,styleable类型资源比拟非凡,仅存在于R$styleable类中,其field取值也并不是0x7fxxxxxxx格局,而是整型或整型数组,并且在resources.arsc中并不存在。
通过一个styleable定义示例,来帮忙咱们了解上述常识:
# 资源定义于 res/value/attrs.xml<resources> <declare-styleable name="DeclareStyleable1" > <attr name="attr_enum" format="enum"> <enum name="attrEnum1" value="1"/> <enum name="attrEnum2" value="2"/> </attr> <attr name="attr_integer" format="integer"/> <attr name="android:padding" format="dimension"/> </declare-styleable></resources>
在apk编译过程中,生成以下R.java代码:
# R.java文件中,生成以下代码public static final class id { public static final int attrEnum1=0x7f060000; public static final int attrEnum2=0x7f060001;}public static final class attr { public static final int attr_enum=0x7f020000; public static final int attr_integer=0x7f020001;} public static final class styleable { public static final int[] DeclareStyleable1 = {0x010100d5, 0x7f020000, 0x7f020001}; public static final int DeclareStyleable1_android_padding=0; public static final int DeclareStyleable1_attr_enum=1; public static final int DeclareStyleable1_attr_integer=2;}
最初,在resources.arsc中,生成以下记录:
# resources.arsc中,生成记录type | id | name | valueid 0x7f060000 attrEnum1 Noneid 0x7f060001 attrEnum2 Noneattr 0x7f020000 attr_enum 1,2attr 0x7f020001 attr_integer 0
一个styleable定义,最终会生成一连串产物,由此可见,Android资源的解决逻辑,绝对还是比较复杂的。在这个例子中,还有几个有意思的技术点,值得拿来讲一讲:
- 一个attr,name应用android:xxxx,在R.java和resources.arsc中不会生成对应内容,因而在语意可复用时,应用零碎提供的attr,能够节俭一点包大小空间;
- 如果多个styleable或者style,定义了同名attr,理论只会生成一个attr资源,相当于进步了复用度;
- attrEnum1、attrEnum2这种id类型资源,如果其它类型资源(例如layout)中也有同名定义,那么理论也只会生成一个id资源,同样也进步了复用度。
好了,对于资源分类,就到此为止,如果对于资源编译、R.java、resources.arsc等还不够理解,也没有关系,前面大节或者会给出答案。
1.2 资源援用
资源在定义后,就须要从另外的中央对其进行援用。从援用确定性这个维度来看,能够分为间接和间接(动静)两种;从援用元素为度来看,能够分为java代码、manifest、资源三种:
图注:资源援用形式
其中,间接(动静)援用,提供动态化的资源援用形式,能够在运行时,依据上下文条件,决定援用哪个资源,灵便度很高。然而,这种资源援用形式,绝对于间接援用,须要额定进行由资源名称查找资源id的解决,因而性能略差,审慎应用。
1.3 资源编译
接下来,看看资源的编译过程:
图注:资源编译过程
首先,资源会进行合并,同名资源仅保留一份,同时,manifest也会进行合并。接下来,会以上述二者作为外围输出数据,通过AAPT(2)进行资源编译,具体的编译过程比较复杂,网络上也有比拟全面的解说(能够参考这篇文章:https://www.kancloud.cn/alex_...), 这里重点关注资源编译产物,以及与其它解决逻辑的关系:
- AndroidManifest.xml文件。其中对资源的援用,会替换为对应资源id,并编译为二进制格局,最终会被打包到apk中。
- resources.arsc文件。资源符号(索引)表,记录所有资源id与各配置下的资源值,最终也会被打包到apk中。
- 解决(编译)后的资源文件汇合。所有须要编译的独立资源文件(例如layout),均会编译为二进制格局,和不须要编译的资源文件一起,最终被打包到apk中。
- R.java文件。记录资源类型/名称,与id值的对应关系,用于在java代码中间接援用。每个模块(subproject、flat aar、external aar)都会生成对应的package.R.java文件,最终和其它所有java源文件一起,独特进行javac编译。
- 资源对应keep规定文件。次要包含layout中view节点对应java类,onClick属性值对应java办法,以及manifest中四大组件对应java类。这些keep规定,会与其它自定义keep规定一起,用于后续的proguard解决。
从上述整个过程来看,资源编译与其它几个外围处理过程,都有紧密联系,因而,理解资源编译过程,对把握整个apk构建,具备重要价值。
1.4 资源裁剪
google官网的Android Gradle Plugin,提供了资源裁剪性能。外围原理是,计算资源的间接援用关系,以manifest和java代码中的援用,作为根援用,所有不被援用到的资源,均属于无用资源。看起来是一个十分无效的性能,然而因为java代码中存在间接(动静)援用,为了将这部分援用也笼罩到,采纳了比拟激进的策略:收集java代码中的所有字符串常量,如果资源名称以这些常量结尾,则也认为资源有援用。除此之外,还有几个逻辑,用于解决非凡的援用形式。上述解决逻辑,有以下几个问题:
- 如果通过Resources.getIdentifier动静援用资源时,名称参数齐全是一个变量,那么会导致相干资源被误删;
- 如果java代码常量池中,简直蕴含所有单个字符,例如a-z,1-9,那么所有资源均会被认为有援用,导致不会裁剪任何一个资源(优酷就是如此)。
因而,资源裁剪性能,从技术原理上看,无论如何都是一个非确定性算法,必定会存在误裁、漏裁的可能性。对此,google提供了白名单机制,来解决误裁问题,还有严格模式,用于勾销对间接(动静)援用的保留逻辑。
对于历史包袱不重的app,尽早开启这项性能,有利于加重包大小累赘。对于代码复杂度高,历史包袱重的大型app(优酷就是如此),应该会存在不少间接(动静)援用,不开启严格模式,简直无成果,开启严格模式,存量确认&加白名单的老本又极高。对此,优酷的抉择是,通过建设独立的无用资源检测性能,联合包大小治理,促成从源头间接删除资源,这样既能够升高资源解决耗时,又能够实现升高包大小成果。对于新增无用资源,则通过包大小卡口,实现非实时(可提早)清理。
1.5 几个乏味的问题
最初,来讲几个比拟乏味,并且不容易被留神到的技术点。
被忽视的一员 - id类型资源
id类型资源,作为惟一标识符,在Android资源体系下,承当“穿针引线”作用。例如最罕用的,在layout中定义一个view节点,赋予其一个id名称,这样在java代码中,就能够不便的获取这个view实例,从而进行后续各种操作。再举个例子,在后面styleable示例中,一个enum类型attr蕴含的每一个枚举值,都会生成一个对应id类型资源。
id类型资源,在编译期的一个重要个性是能够全局复用,这一点在后面styleable示例中,曾经讲述过。在app运行时,id类型资源的个性是,部分惟一即可。例如在一个layout中,或者在一个enum类型的attr中,都是如此。讲到这里,有些同学肯定可能想到,咱们是不是能够利用这两个个性,在保障运行时部分唯一性前提下,仅保留一个最小汇合,其它所有定义和援用,均在这个最小汇合内选取即可,而这个最小汇合的数量,取决于所有部分应用场景中,须要id数量的最大值。举个例子:
# styleable类型资源,定义于 res/value/attrs.xml<resources> <declare-styleable name="DeclareStyleable1" > <attr name="attr_enum" format="enum"> <enum name="attrEnum1" value="1"/> <enum name="attrEnum2" value="2"/> </attr> <attr name="attr_integer" format="integer"/> <attr name="android:padding" format="dimension"/> </declare-styleable></resources># layout类型资源,定义于 res/layout/main.xml<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/main_textview" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="@color/purple_200" android:text="Hello World!"/></LinearLayout>
上述一共生成3个id类型资源:attrEnum1、attrEnum2、main_textview,这两个应用场景中,styleable须要2个id,layout须要一个id,所以最小id汇合只须要蕴含2个id。假如咱们能够在资源编译过程中,能够将"@+id/main_textview"批改为"@+id/attrEnum1",就能够缩小1个id类型资源。在优酷这样复杂度的app中,共有1.3万多个id类型资源,而所有部分应用场景中,须要id数量的最大值,置信肯定不会超过两位数。一个id类型资源在apk中占用的大小(Byte),能够简略认为等于id名称长度,激进预计以均匀20Byte来计算,1.3万个id资源,可节俭包大小250KB。因为收益并不显著,并没有理论进行开发,作为一个乏味的思考,留给本文读者。
资源与java代码的桥梁 - R类
通过后面解说,置信读者对R类曾经具备肯定理解。在这里,咱们思考几个状况。
第一个状况,每个模块(subproject、flat aar、external aar)都会生成对应package.R.java文件,然而这些文件蕴含内容,都是<app_package>.R
类的子集。那么,咱们是不是能够,移除所有模块的R类,对立应用app的R类,以此来升高包大小呢?答案是必定的,事实上优酷在apk构建过程中,会删除所有模块R类,并将java代码中对这些R类的援用,转换为对appR类的援用。通过这种形式,升高了MB量级的apk大小,模块数量越多,收益越显著。
第二个状况,R类内容非常简单,就是记录了资源类型/名称,与资源id值的对应关系,manifest和资源这二者对资源的援用,在编译过程中,曾经转换为对应资源id值,那么,如果咱们把所有java代码中R.<type>.<name>
的援用,也全副替换为对应id值,是不是R类就能够删除了呢?答案是必定的,在曾经实现第一种状况的优化后,这个解决的收益比拟无限,因而并没有理论投入研发和应用。然而咱们的确能够这么做!
资源百晓生 - resources.arsc
resources.arsc文件,作为资源符号(索引)表,记录所有资源类型、名称、id值,以及各配置下的值。所有Resource类型资源(运行时视角,排除编译期视角的styleable资源)均记录在案,app运行时,无论java代码还是资源,都是拿着资源id值,到resources.arsc中来获取资源值,称之为“资源百晓生”一点都不夸大。这个查找过程十分高效,相当于给定一个key,获取其在一个hashmap中的值。
实际上,通过Resources.getIdentifier这种间接(动静)形式获取资源id值时,也是以资源类型+名称,在resources.arsc中进行反向查找,找到后,再持续通过id值获取资源值。这个查找过程,相当于给定一个值,获取其在一个hashmap中的key。那么有没有什么形式,能够更高效实现这种运行时灵便的援用资源呢?一个比拟天然的想法,是通过java反射获取R.<type>.<name>
值,那么问题来了,绝对于Resources.getIdentifier形式获取,哪种性能更好一些?答案可能并不是简略的二选一,耗时可能与资源数量,以及是否第一次查问同一种类型资源,都有关系,答案就留给读者来思考和验证吧。
治理实际
随着工程模块&性能减少,资源腐化逐渐积攒:同名资源的抵触状况愈发频繁,导致屡次构建apk,资源值无奈保障一致性;资源援用关系简单,代码删除后往往会遗记,或者不敢轻易删除对应资源,导致无用资源继续积攒;layout中援用自定义view,然而view的java实现类被删除,app运行时layout被“加载”时会引发java异样;资源中的硬编码文本,带来线上隐衷合规危险,或者国家/地区/宗教文化争议问题。上述诸多问题,都是过往优酷与资源“腐化”奋斗中,一直遇到的实在问题,通过相干工具建设无效的检测能力,并基于此造成日常研发卡口机制,在确保问题零新增前提下,逐渐消化已有存量问题。
在问题定位、排查过程中,疾速获取资源来自哪个模块,是一个根本诉求。二、三方模块大量引入,以及app工程模块化水平进步,都使上述信息获取的老本变得越来越高。为此,首先开发了模块蕴含资源列表性能,能够疾速查看,指标资源位于哪个模块(app工程、subproject工程、flat aar、内部依赖模块):
com.youku.android:aln:1.9.49|-- string/m_mode|-- layout/pager_last|-- dimen/h_n_bar_pop_star|-- asset/config/custom_config.jsoncom.youku.android:YHP:1.23.511.1|-- layout/channel_list_footer|-- layout/f_cover_s_feed_item|-- drawable-night-xhdpi-v8/ic_ho
接下来,对各个资源“腐化”项的治理实际,逐个解说。
2.1 抵触资源
抵触资源,是指来自不同模块的同名资源,其对应配置下的内容值不统一。在资源编译过程中,同名资源只会保留一份,抉择哪个资源能够认为是“随机的”(理论和模块申明程序无关),这会导致每次构建进去的apk,对应资源值可能会发生变化。抵触资源,会给运行时带来不确定性危险,轻则文本内容、尺寸大小、UI色彩产生非预期变动,重则导致异样产生。
在优酷历次迭代中,已经产生屡次抵触资源导致的线上解体,为了解决这个顽疾,首先研发了抵触资源检测工具,示例后果如下:
[conflict] drawable/al_down_arrow|-- xhdpi-v4| |-- md5:cc2ef446bf586b03fd08332a5a75b304 (com.ali.user.sdk:au:4.10.6.18)| |-- md5:5f9c59ec3fba027c5783120effa12789 (com.ta.android:lo4android:4.10.6.18)[conflict] string/str_retry|-- en| |-- not calculated (com.ali.android.phone:bee-build:10.2.3.358)|-- default| |-- 重试 (com.ali.android.phone:photo-build:10.2.3.57)| |-- 点击重试 (com.ali.android.phone:bee-build:10.2.3.358)
在上述检测后果中,当同名资源在同一配置下,超过两个模块蕴含此资源值时,才可能发生冲突,因而也才会进行资源特征值计算,否则会显示为not calculated。不同类型资源的特征值计算形式如下:
与此同时,提供资源名称、模块两种不同颗粒度的疏忽名单配置,以长期排除一些二、三方模块之间的抵触资源。更近一步,提供选项,当检测后果不通过时,终止构建过程,造成卡口机制。
优酷在2020年,首先研发了第一版抵触资源检测工具,过后存量抵触资源共计600多个,之后联结QA同学进行两轮清理专项,升高到100个以内,2021年初卡口上线后,截至以后已降至40多个(次要来自二、三方模块之间的抵触):
图注:抵触资源治理状况
抵触资源卡口上线至今,累计拦挡13次,无效避免抵触资源,引发的线上非预期状况,甚至是app解体的重大故障。
2.2 无用资源
后面「资源援用」一节,曾经对资源的援用关系,进行了基础知识解说。总结下,资源可能在如下三个中央进行间接援用:
- java代码。通过R.resourceType.resourceName形式援用,例如R.string.app_name;或者通过资源id形式,间接援用,例如0x7fxxxxxx;
- 清单文件AndroidManifest.xml;
- 其它资源。
以java代码和manifest作为援用根节点,对资源援用关系进行齐全开展,最终未被蕴含到的资源,即为无用资源。对于通过Resources.getIdentifier这种间接(动静)形式援用的资源,不蕴含在此处的资源援用关系计算过程中,因而,无用资源检测后果,须要确认是否存在这种援用形式。基于google官网AndroidGradlePlugin中的无用资源剖析逻辑,全方位加强对工程构造、AndroidGradlePlugin版本、各工具链版本等兼容性,补齐更多类型资源间的援用剖析,增加额定模块归属信息,最终积淀为此无用资源检测性能。
图注:无用资源剖析
无用资源检测,剖析后果示例:
project:app:1.0|-- array/planets_array|-- color/white|-- drawable/fake_drawable|-- layout/layout_miss_view|-- raw/app_resource_raw_chinese_text|-- string/string_resource_chinese_name|-- xml/app_resource_xml_chinese_textproject:library-aar-1:1.0|-- layout/layout_contain_merge|-- string/library_aar_1_name
此外,资源的间接援用关系,也能够输入到剖析后果中:
Resource Reference Graph:array:planets_array:2130771968 is reachable: false. The references =>attr:attr_enum:2130837504 is reachable: true. The references =>referenced by code : [com/example/libraryaar1/CustomImageView (project:library-aar-1:1.0)]referenced by resource : [layout:layout_use_declare_styleable1:2131099652]attr:attr_integer:2130837505 is reachable: true. The references =>referenced by resource : [style:CustomTextStyle:2131361792]
无用资源,思考到存在间接(动静)援用导致误检的问题,因而并没有进一步造成卡口,而是作为包大小剖析后果中,一个可瘦身项来出现。2020年6月性能上线时,共有1.7万个无用资源,目前曾经降至0.9万个,存量清理效果显著。
无用资源治理状况
2.3 缺失类援用
layout中能够申明自定义view节点,如果这个自定义view对应类,最终不在apk的dex文件中,因为资源编译的个性,上述情况并不会引发apk构建过程失败,然而在app运行时,一旦“加载“此layout就会引发异样。上述这种状况,咱们称之为资源的缺失类援用。
资源缺失类援用检测,列出了问题资源,及其所属模块,以及缺失的援用类。示例后果如下:
* [ignored] layout-xxxhdpi/layout_include_layout (project:library-aar-1:1.0)|-- com.example.libraryaar1.NonExistCustomView* layout/layout_miss_view (project:app:1.0, project:library-aar-1:1.0)|-- com.example.myapplication.NonExistView2|-- com.example.myapplication.NonExistView
与此同时,提供资源名称颗粒度的疏忽名单配置,临时排除一些二、三方模块内的问题资源。更近一步,提供选项,当检测后果不通过时,终止构建过程,造成卡口机制。此项性能,近期刚上线对应卡口,尚未有触发卡口拦挡案例呈现,存量30个问题资源,已散发到对应研发团队。
事实上,layout中的每一个自定义view节点,AAPT在进行解决时,都会生成一条keep规定,这会成为一条无用keep规定,在「向工程腐化开炮:proguard治理」一文中,提到了这种状况。在此,把示例再展现下:
# layout中援用不存在的class,在apk编译过程中,并不会引发构建失败,但仍然会生成绝对应的keep规定。# 这个layout一旦在运行时被“加载“,那么会引发Java类找不到的异样。<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <com.example.myapplication.NonExistView android:id="@+id/main_textview" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!"/></LinearLayout># 生成的keep规定为:-keep class com.example.myapplication.NonExistView { <init> ( ... ) ; }
尽管无用keep规定卡口,曾经齐全蕴含资源缺失类援用问题,然而二者管控的维度并不统一,因而依然将资源缺失类援用,作为独立能力提供。
2.4 硬编码文本
硬编码文本,是指间接在资源中编写的字符串文本。隐衷合规检测机构,会检测apk中的一些敏感文本,做为隐衷合规问题的重点狐疑&验证点,例如「发票低头」、「身份证」等,其中一部分就是来自于资源中的硬编码文本(另外可能的起源是java代码、so)。硬编码文本,存在以下毛病:
- 易冗余。多处资源应用同一文本时,会导致存在多份此文本;
- 不灵便。当线上版本呈现问题时(例如各类经营流动),难以动静批改;
- 低平安。一些敏感信息,如果以明文硬编码文本模式存在,非常容易被获取,并用于不正当用处。
对于这类问题,开发了对应检测能力,能够自定义正则表达式,对上述资源中硬编码文本进行匹配。检测后果中,依照模块、资源进行逐级聚合。反对以下类型资源中的字符串文本:
以所有中文字符检测为例:
project:app:1.0|-- array/planets_array| |-- [text] string-array蕴含的中文item|-- raw/app_resource_raw_chinese_text| |-- [text] <files-path name="我是raw类型xml资源文件中,蕴含的中文文本" path="game-bundles/" />|-- string/string_resource_chinese_name| |-- [text] 我是中文string资源|-- xml/app_resource_xml_chinese_text| |-- [text] <files-path name="我是xml资源中的中文文本" path="game-bundles/" />|-- layout/activity_main| |-- [text] android:text="你好,世界!" />project:library-aar-1:1.0|-- asset/library_aar_1_asset_chinese_text| |-- [text] 我是蕴含中文文本的asset资源文件.
目前在优酷,隐衷合规相干的一些敏感文本,是一个正在进行的摸索方向,因为目前没有明确规定,因而还没有理论落地应用。在日常研发过程,对于须要查找特定硬编码文本的场景,曾经可能起到很好的辅助提效作用。
2.5 治理全景
至此,对于Android资源,进行了较全面无效的防腐化能力建设和治理。最初,给出一份全景图:
图注:资源治理全景
还能做些什么
Android资源,并不会像java代码那样多变和简单,后面这些治理项,曾经根本笼罩绝大部分资源腐化场景,然而Android资源在日常研发过程中,非常容易被忽视:一个字符串、一个色彩/尺寸值、一个属性值,一个布局文件,如同每一个都“微不足道”,即便反复定义、即便忘了清理,看起来也没多大影响。而这,正是资源腐化的可怕之处:单个资源过于“渺小”,开发者的业余意识稍有松散,就成了漏网之鱼。
可能进行批量的清理,诚然值得称赞,然而在日常研发的点滴间,可能时刻坚守工匠精力,升高“腐化”代码产生,更难能可贵。“千丈之堤,以蝼蚁之穴溃;百尺之室,以突隙之烟焚”(韩非子·喻老),与诸君共勉。
【参考文档】
- 【google】利用资源:https://developer.android.com...
- 【google】AAPT2:https://developer.android.com...
关注【阿里巴巴挪动技术】微信公众号,每周 3 篇挪动技术实际&干货给你思考!