作者:刘天宇(谦风)
工程腐化是app迭代过程中,一个非常棘手的问题,涉及到广泛而细碎的具体细节,对研发效能&体验、工程&产物质量、稳定性、包大小、性能,都有相对“隐蔽”而间接的影响。一般不会造成不可承受的障碍,却时常蹦出来导致“阵痛”,有点像蛀牙或智齿,到了一定程度不拔不行,但不同的是,工程的腐化很难通过一次性“拔除”来根治,任何一次“拔除”之后,需要有效的可持续治理方案,形成常态化的防腐体系。
工程腐化拆解来看,是组成app的代码工程中,工程结构本身,以及各类“元素”(manifest、代码、资源、so、配置)的腐化。优酷架构团队近年来,持续在进行思考、实践与治理,并沉淀了一些技术、工具、方案。现逐一分类汇总,辅以相关领域知识讲解,整理成为《向工程腐化开炮》系列技术文章,分享给大家。希望更多同学,一起加入到与工程腐化的这场持久战中。
系列文章第一篇《向工程腐化开炮 | proguard治理》。本文为系列文章第二篇,将聚焦于manifest这一细分领域。对工程腐化,直接开炮!
背景
manifest是指apk中AndroidManifest.xml文件,作为apk整体信息清单,包含很多重要信息,对app构建期处理、运行时行为、应用商店过滤等,均有至关重要影响。
清单内容&影响
当AndroidManifest.xml文件中内容,发生非预期改变时,会带来意想不到的后果。例如:minSdkVersion变小,上线后低版本os用户升级到最新apk,导致严重的使用体验问题;targetSdkVersion升高,os对app运行时的特定处理发生变化,未适配代码crash/功能异常;新权限被引入,隐私协议未声明,被监管机构发现。上述这些问题,都只是清单文件中一个“微小”的配置值变化引发,清单的腐化导致这类非预期变化,发生的可能性越来越高。manifest治理正是围绕AndroidManifest.xml的内容整理与防控,逐步展开的。
基础知识
本章先简要介绍一些基础知识,方便大家对manifest有一个“框架性”的清晰认知。首先,看一下AndroidManifest.xml文件的生成(合并)过程。
合并流程
app工程、aar类型的subproject工程、外部依赖的aar模块,均包含AndroidManifest.xml文件。在apk构建过程中,这些AndroidManifest.xml文件经过合并后(+一些额外处理),生成唯一的AndroidManifest.xml文件,经过编译后最终放置到apk根目录。
合并是从低优先级,逐步向高优先级进行。横向是不同来源的优先级;模块间优先级从高到低,为在app工程中的声明顺序;build variant、build type、product flavor之间的优先级逐渐降低;product flavor如果包含多个dimension,优先级从高到低为flavorDimensions中指定的顺序。
AndroidManifest.xml优先级&合并顺序
在合并过程中,相同xml元素(一般是android:name属性值,或者元素标签)属性会有合并冲突情况,基本原则是:高优先级和低优先级属性值,如果都存在且不一致,则视为冲突。由于清单文件中元素/属性的多样性,实际规则要复杂很多,具体可以参考google官方文档。合并冲突的解决,除了修改对应AndroidManifest.xml文件之外,还可以通过在app工程AndroidManifest.xml中,增加“合并规则标记”实现。此外,即使未发生冲突,当需要控制清单内容时,也可以通过同样方式实现,接下来对此进行介绍。
合并控制
前文提到的“合并规则标记”,通过对xml节点和属性这两个不同颗粒度,指定合并规则,来实现合并结果控制。首先,需要在manifest根节点,增加tools命名空间:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapplication"
xmlns:tools="http://schemas.android.com/tools">
然后,根据具体情况,在节点中添加对应tools:属性。
合并规则标记说明
合并控制的具体规则,请参考官方文档,在此不详述。
manifest占位符
除了上述合并控制,还可以通过manifest占位符,控制清单中节点的属性值。
# build.gradle文件中定义变量和值
android {
defaultConfig {
manifestPlaceholders = [customKey:"customValue", ...]
}
...
}
# AndroidManifest.xml文件中使用占位符
<intent-filter ... >
<data android:scheme="https" android:host="${customKey}" ... />
...
</intent-filter>
<meta-data android:name="sampleMeta" android:value="${customKey}"/>
除此以外,还存在一个默认占位符${applicationId},与android DSL中applicationId配置值绑定。在构建过程中,会将所有占位符替换为对应值。
合并决策日志
最终AndroidManifest.xml的每一个节点、属性,来源于哪个清单文件,通过何种策略生成,这些信息都记录在合并决策日志中,对问题的分析和排查,提供重要辅助信息。文件位于app工程build/outputs/logs/manifest-merger[-productFlavor]-<buildType>-report.txt
,示例内容如下:
activity#com.example.myapplication.MainActivity
ADDED from /Users/flyeek/workspace/code-lab/android/MyApplication/app/src/main/AndroidManifest.xml:18:9-24:20
android:name
ADDED from /Users/flyeek/workspace/code-lab/android/MyApplication/app/src/main/AndroidManifest.xml:18:19-47
intent-filter#action:name:android.intent.action.MAIN+category:name:android.intent.category.LAUNCHER
ADDED from /Users/flyeek/workspace/code-lab/android/MyApplication/app/src/main/AndroidManifest.xml:19:13-23:29
action#android.intent.action.MAIN
ADDED from /Users/flyeek/workspace/code-lab/android/MyApplication/app/src/main/AndroidManifest.xml:20:17-69
android:name
ADDED from /Users/flyeek/workspace/code-lab/android/MyApplication/app/src/main/AndroidManifest.xml:20:25-66
category#android.intent.category.LAUNCHER
ADDED from /Users/flyeek/workspace/code-lab/android/MyApplication/app/src/main/AndroidManifest.xml:22:17-77
android:name
ADDED from /Users/flyeek/workspace/code-lab/android/MyApplication/app/src/main/AndroidManifest.xml:22:27-74
...
uses-permission#android.permission.READ_EXTERNAL_STORAGE
IMPLIED from /Users/flyeek/workspace/code-lab/android/MyApplication/app/src/main/AndroidManifest.xml:2:1-28:12 reason: com.example.libraryaar1 requested WRITE_EXTERNAL_STORAGE
MERGED from [com.youku.arch:Hound:2.8.15] /Users/flyeek/.gradle/caches/transforms-2/files-2.1/d42ba59a47f7160082879236533c4582/AndroidManifest.xml:11:5-80
MERGED from [com.youku.arch:Hound:2.8.15] /Users/flyeek/.gradle/caches/transforms-2/files-2.1/d42ba59a47f7160082879236533c4582/AndroidManifest.xml:11:5-80
uses-permission#android.permission.WRITE_CALL_LOG
IMPLIED from /Users/flyeek/workspace/code-lab/android/MyApplication/app/src/main/AndroidManifest.xml:2:1-28:12 reason: com.example.libraryaar1 has targetSdkVersion < 16 and requested WRITE_CONTACTS
看完本文的基础知识,这里面的内容,应该都能看懂,不再赘述。
几个有意思的配置
至此,我们已经对manifest文件有了一个“框架性”的整体认知。最后,来看几个比较有意思的配置。
package vs applicationId
这两个概念比较容易混淆,从最终apk文件的视角来看,唯一标识apk的,就是AndroidManifest.xml中manifest节点的package属性值,也就是经常说的“appId”、“app包名”。直接上图:
package vs applicationId
app工程中的package值,仅影响构建过程。而android DSL中的applicationId值,最后会替换AndroidManifest.xml中的package属性值,成为最终apk唯一标识。
隐式系统权限
在某些条件下,清单文件的合并过程,会额外自动添加系统权限声明,如果不加以处理,同时app隐私协议未加以声明,会引发合规风险。自动添加权限声明的情况如下表(直接摘自官方文档):
合并过程添加的权限声明列表
例如,app的targetSdkVersion是28,以外部依赖形式,引入一个模块,其中包含的AndroidManifest.xml(低优先级清单)中targetSdkVersion是14,并且声明了READ_CONTACTS权限,那么最终apk清单文件,将包含READ_CALL_LOG权限声明。
组件导出控制
组件导出,是指android:exported属性为true(显式/隐式),组件可被其它app调用。如果在清单中显式设置了android:exported值,那以此为准;如果未设置,则隐式规则为:如果设置了intent-filter,则exported值为true,否则为false。很多app都会使用组件(尤其是activity)的app内路由机制,因此会设置一些intent-filter,这会导致组件被非预期导出,带来安全风险。这一点需要特别关注,后面也会再讲到。
值得注意的是,当targetSdkVersion设置为31(Android12)及以上时,如果组件设置了intent-filter,那么必须同时显式设置android:exported值。如果未显式设置exported值,对于高版本Android Studio,IDE的build会失败,对于低版本Android Studio,build可以成功,但是安装到Android12及以上设备时会失败。
治理实践
前面对manifest基础知识,以及工程应用,进行了相关讲解,相信大家已经形成初步的整体认知。随着工程模块/代码增加,清单文件可控性逐步降低:无论是关键配置值意外变化,还是非预期权限引入,甚至是无用/冗余/风险节点及属性积累。优酷在与manifest“腐化”斗争中,从上层实际需求(例如隐私合规、安全漏洞、线上问题)出发,通过相关工具建立有效的检测能力,并基于此形成日常研发卡口机制。在确保问题零新增前提下,逐步消化已有存量问题。
全局配置
manifest中一些全局性配置,对apk安装和运行时行为,具有重要影响,最为典型的就是minSdkVersion和targetSdkVersion,一旦非预期变更被带到线上,后果不堪设想。
全局配置检测工具,提供基于白名单的全局配置检测能力,包含以下情况:
- 白名单中配置,在清单中不存在;
- 白名单中配置,在清单中存在,但配置值不一致。
同时,提供选项,当全局性配置与白名单不一致时,终止构建过程,示例检测结果如下:
[absent] [uses-feature] android.hardware.camera # 白名单中的这个uses-feature在清单中不存在
[conflict] [uses-sdk] # 白名单中的uses-sdk节点,属性值与清单中不一致
|-- com.youku.arch:testlib:0.1-SNAPSHOT # 包含uses-sdk节点的模块
|-- project:library-aar-1:1.0 # 包含uses-sdk节点的模块
|-- com.youku.arch:testlib2:0.1-SNAPSHOT # 包含uses-sdk节点的模块
|-- [attr] targetSdkVersion # targetSdkVersion属性值不一致
| |-- [whitelist] 29
| |-- [current] 28
|-- [attr] minSdkVersion # minSdkVersion属性值不一致
| |-- [whitelist] 14
| |-- [current] 21
优酷全局配置白名单,以及新增防控情况如下:
全局配置治理情况
通过这个检测能力和卡口机制,实现了对关键全局性配置的保护,从而有效避免非预期变化发生。
权限
权限声明,在当下隐私合规监管态势下,需要被严格的管控住。这里的“严格”,体现在既不能多也不能少,必须与app隐私协议保持一致。在前文基础知识部分,我们知道apk中AndroidManifest.xml是通过合并而来的,同时还存在系统权限的隐式带入,这些都增加了权限“严格”管控难度。
对此,开发了两项检测能力:模块包含权限列表、权限检测。
模块包含权限列表,列出了各模块包含的权限使用声明(uses-permission)和权限定义(permission),便于定位权限来源。示例结果:
com.youku.android:YPx:1.20.10.19
|-- [uses-permission] android.permission.ACCESS_NETWORK_STATE
|-- [uses-permission] android.permission.BLUETOOTH
|-- [uses-permission] android.permission.VIBRATE
com.taobao.android:ls:4.10.6.6
|-- [uses-permission] android.permission.READ_PHONE_STATE
|-- [uses-permission] android.permission.ACCESS_WIFI_STATE
权限检测,提供基于白名单的双向检测能力:
- 白名单中权限,在清单中不存在;
- 清单中权限,不在白名单中。
[excess] [uses-permission] android.permission.CALL_PHONE # 清单中CALL_PHONE权限声明,不在白名单中
|-- project:app:1.0 # 权限声明,来自app工程
[absent] [uses-permission] android.permission.ACCESS_NETWORK_STATE # 白名单中ACCESS_NETWORK_STATE,在清单中不存在
|-- com.youku.arch:testlib:1.0 # com.youku.arch:testlib模块,包含此权限声明
|-- com.youku.arch:testlib2:1.0 # com.youku.arch:testlib2模块,包含此权限声明
更近一步,提供选项,当检测结果不通过时,终止构建过程。通过这个检测能力和卡口机制,保障权限声明与app隐私协议的连续一致性。优酷的治理&防控情况如下:
权限治理情况
四大组件
四大组件需要在清单文件中声明,才能在apk安装后以及运行时,被系统识别,从而正常发挥作用。同时,四大组件一些关键行为,也需要在清单中进行配置。在优酷实践过程中,主要发现两类问题:组件对应类缺失、非必要组件导出。
组件对应类缺失,是指清单中声明的四大组件,android:name属性值对应java类,在apk中不存在。组件类缺失的负面影响如下:
- 会生成一条proguard无用keep规则,导致构建耗时增加(一条keep虽小,聚沙成塔,也很可观);
- 运行时一旦组件被调用(启动),会产生java异常(crash/功能不可用),或者安全漏洞。即使是无用组件,也要考虑到还有一些黑产组织,会自动化扫描组件并启动(crash率曲线会有尖刺出现)。
非必要组件导出(定义参见前文),会导致运行时存在安全漏洞的风险增加,优酷收到过多次相关安全漏洞。导出组件处理原则如下:
- 不必要导出,且为自研代码。关闭导出;
- 不必要导出,但是为二、三方代码。在app工程的清单文件中,通过“合并规则标记”修改android:exported属性值为false;
- 需要导出,且为自研代码,用于开发期调试。关闭导出,收敛到统一研发调试工具箱中;
- 需要导出,且为自研代码,用于线上实际业务(外部唤端等)。关闭导出,收敛到统一路由中心;
- 需要导出,但是为二、三方代码,用于线上实际业务(外部唤端等)。添加白名单。
对此,开发了三项检测能力:
- 组件归属模块列表,列出所有四大组件,以及包含此组件声明的模块:
# 在manifest合并后不存在的组件,前面会加上[delete]
# 被超过两个以上模块包含的组件,前面会加上[duplicate]
[duplicate] [activity] com.example.myapplication.MainActivity
|-- project:app:1.0
|-- project:library-aar-1:1.0
[deleted] [service] com.example.myapplication.FirstService
|-- project:app:1.0
[receiver] com.example.myapplication.FirstReceiver
|-- project:library-aar-1:1.0
[provider] com.example.myapplication.FirstProvider
|-- com.youku.arch:testlib:1.0
- 缺失组件引用检测,识别缺失引用组件名称,以及哪些模块声明了此组件。同时,提供选项以及白名单,当检测结果不通过时,终止构建过程。示例检测结果如下:
[activity] org.cocos2dx.javascript.AActivity
|-- com.youku.android:interactive-engine:0.2.9
[activity] com.ali.lv.HLActivity
|-- com.ali.phone.wt:n-build:10.2.3.592
[activity] com.youku.pc.debug.DActivity
|-- com.youku.android:YKPChannel:2.14.1.28
[service] com.youku.feed.utils.FAService
|-- com.youku.android:FBase:1.5.20.8
- 导出组件检测,识别导出组件,以及哪些模块声明了此组件。同时,提供选项以及白名单,当检测结果不通过时,终止构建过程。对于Target31更安全导出组件的行为变更,专门提供「禁止隐式导出」配置项,会无视白名单,并在分析结果中增加可识别标记。示例检测结果如下:
# 对于白名单中组件,会在名称前加上[ignored]标识;如果开启「禁止隐式导出」配置项,对于隐式导出组件,会在名称前加上[implicit]标识
[activity] com.youku.app.NPageActivity
|-- com.youku.android:YoukuHPage:1.9.43.8
[ignored][activity] com.ali.MIPreviewActivity
|-- com.ali:m-image-selector:10.1.6.190
[implicit] [activity] com.youku.fbiz.RPageActivity
|-- com.youku.android:fbizSDK:1.0.2.48
在优酷治理实践中,考虑到对各业务研发同学的影响,对存量问题集中添加到白名单,后面择机统一发起清理行动。随着版本迭代,除了对新增问题的有效拦截,存量问题也有一些“自然修复”,整体情况如下:
四大组件治理情况
此外,优酷目前的targetSdkVersion是30,明年会进行target31的适配工作,存量隐式导出组件161个,占所有导出组件50%左右,届时这些需要全部解决。在当前工具和卡口体系下,相信这个问题的整改,会变得轻松而可控。
治理全景
至此,对于manifest清单,进行了较全面有效的防腐化能力建设和治理。最后,给出一份全景图:
manifest治理全景
还能做些什么
manifest包含的内容,比较有限。因此,上述治理应该已经覆盖绝大部分问题,但仍然还有一些低概率的边缘case,可以通过同样的思路来提前识别&解决,例如:多个activity的scheme定义重复,导致通过隐式方式启动activity时,出现选择弹窗。
与工程腐化的对抗,依然艰难,任重而道远,与诸君共勉。
【参考文档】
- 【google】App Manifest Overview:https://developer.android.com...
- 【google】Manage manifest files:https://developer.android.com...
关注【阿里巴巴移动技术】微信公众号,每周 3 篇移动技术实践&干货给你思考!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。