向工程腐化开炮 | manifest 治理
作者:刘天宇(谦风)
工程腐化是 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 命名空间:
然后,根据具体情况,在节点中添加对应 tools:属性。
合并规则标记说明
合并控制的具体规则,请参考官方文档,在此不详述。
manifest 占位符
除了上述合并控制,还可以通过 manifest 占位符,控制清单中节点的属性值。
除此以外,还存在一个默认占位符 ${applicationId},与 android DSL 中 applicationId 配置值绑定。在构建过程中,会将所有占位符替换为对应值。
合并决策日志
最终 AndroidManifest.xml 的每一个节点、属性,来源于哪个清单文件,通过何种策略生成,这些信息都记录在合并决策日志中,对问题的分析和排查,提供重要辅助信息。文件位于 app 工程build/outputs/logs/manifest-merger[-productFlavor]-<buildType>-report.txt
,示例内容如下:
看完本文的基础知识,这里面的内容,应该都能看懂,不再赘述。
几个有意思的配置
至此,我们已经对 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,一旦非预期变更被带到线上,后果不堪设想。
全局配置检测工具,提供基于白名单的全局配置检测能力,包含以下情况:
白名单中配置,在清单中不存在;
白名单中配置,在清单中存在,但配置值不一致。
同时,提供选项,当全局性配置与白名单不一致时,终止构建过程,示例检测结果如下:
优酷全局配置白名单,以及新增防控情况如下:
全局配置治理情况
通过这个检测能力和卡口机制,实现了对关键全局性配置的保护,从而有效避免非预期变化发生。
权限
权限声明,在当下隐私合规监管态势下,需要被严格的管控住。这里的“严格”,体现在既不能多也不能少,必须与 app 隐私协议保持一致。在前文基础知识部分,我们知道 apk 中 AndroidManifest.xml 是通过合并而来的,同时还存在系统权限的隐式带入,这些都增加了权限“严格”管控难度。
对此,开发了两项检测能力:模块包含权限列表、权限检测。
模块包含权限列表,列出了各模块包含的权限使用声明(uses-permission)和权限定义(permission),便于定位权限来源。示例结果:
权限检测,提供基于白名单的双向检测能力:
白名单中权限,在清单中不存在;
清单中权限,不在白名单中。
更近一步,提供选项,当检测结果不通过时,终止构建过程。通过这个检测能力和卡口机制,保障权限声明与 app 隐私协议的连续一致性。优酷的治理 &防控情况如下:
权限治理情况
四大组件
四大组件需要在清单文件中声明,才能在 apk 安装后以及运行时,被系统识别,从而正常发挥作用。同时,四大组件一些关键行为,也需要在清单中进行配置。在优酷实践过程中,主要发现两类问题:组件对应类缺失、非必要组件导出。
组件对应类缺失,是指清单中声明的四大组件,android:name 属性值对应 java 类,在 apk 中不存在。组件类缺失的负面影响如下:
会生成一条 proguard 无用 keep 规则,导致构建耗时增加(一条 keep 虽小,聚沙成塔,也很可观);
运行时一旦组件被调用(启动),会产生 java 异常(crash/功能不可用),或者安全漏洞。即使是无用组件,也要考虑到还有一些黑产组织,会自动化扫描组件并启动(crash 率曲线会有尖刺出现)。
非必要组件导出(定义参见前文),会导致运行时存在安全漏洞的风险增加,优酷收到过多次相关安全漏洞。导出组件处理原则如下:
不必要导出,且为自研代码。关闭导出;
不必要导出,但是为二、三方代码。在 app 工程的清单文件中,通过“合并规则标记”修改 android:exported 属性值为 false;
需要导出,且为自研代码,用于开发期调试。关闭导出,收敛到统一研发调试工具箱中;
需要导出,且为自研代码,用于线上实际业务(外部唤端等)。关闭导出,收敛到统一路由中心;
需要导出,但是为二、三方代码,用于线上实际业务(外部唤端等)。添加白名单。
对此,开发了三项检测能力:
组件归属模块列表,列出所有四大组件,以及包含此组件声明的模块:
缺失组件引用检测,识别缺失引用组件名称,以及哪些模块声明了此组件。同时,提供选项以及白名单,当检测结果不通过时,终止构建过程。示例检测结果如下:
导出组件检测,识别导出组件,以及哪些模块声明了此组件。同时,提供选项以及白名单,当检测结果不通过时,终止构建过程。对于 Target31 更安全导出组件的行为变更,专门提供「禁止隐式导出」配置项,会无视白名单,并在分析结果中增加可识别标记。示例检测结果如下:
在优酷治理实践中,考虑到对各业务研发同学的影响,对存量问题集中添加到白名单,后面择机统一发起清理行动。随着版本迭代,除了对新增问题的有效拦截,存量问题也有一些“自然修复”,整体情况如下:
四大组件治理情况
此外,优酷目前的 targetSdkVersion 是 30,明年会进行 target31 的适配工作,存量隐式导出组件 161 个,占所有导出组件 50%左右,届时这些需要全部解决。在当前工具和卡口体系下,相信这个问题的整改,会变得轻松而可控。
治理全景
至此,对于 manifest 清单,进行了较全面有效的防腐化能力建设和治理。最后,给出一份全景图:
manifest 治理全景
还能做些什么
manifest 包含的内容,比较有限。因此,上述治理应该已经覆盖绝大部分问题,但仍然还有一些低概率的边缘 case,可以通过同样的思路来提前识别 &解决,例如:多个 activity 的 scheme 定义重复,导致通过隐式方式启动 activity 时,出现选择弹窗。
与工程腐化的对抗,依然艰难,任重而道远,与诸君共勉。
【参考文档】
【google】App Manifest Overview:https://developer.android.com/guide/topics/manifest/manifest-intro
【google】Manage manifest files:https://developer.android.com/studio/build/manage-manifests
关注【阿里巴巴移动技术】微信公众号,每周 3 篇移动技术实践 &干货给你思考!
版权声明: 本文为 InfoQ 作者【阿里巴巴移动技术】的原创文章。
原文链接:【http://xie.infoq.cn/article/b97e88104f0b02fd185c3197c】。文章转载请联系作者。
评论