写点什么

向工程腐化开炮 | manifest 治理

  • 2022 年 2 月 25 日
  • 本文字数:7037 字

    阅读完需:约 23 分钟

向工程腐化开炮 | 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 命名空间:


<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.MainActivityADDED 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-47intent-filter#action:name:android.intent.action.MAIN+category:name:android.intent.category.LAUNCHERADDED from /Users/flyeek/workspace/code-lab/android/MyApplication/app/src/main/AndroidManifest.xml:19:13-23:29action#android.intent.action.MAINADDED 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-66category#android.intent.category.LAUNCHERADDED 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_STORAGEIMPLIED from /Users/flyeek/workspace/code-lab/android/MyApplication/app/src/main/AndroidManifest.xml:2:1-28:12 reason: com.example.libraryaar1 requested WRITE_EXTERNAL_STORAGEMERGED from [com.youku.arch:Hound:2.8.15] /Users/flyeek/.gradle/caches/transforms-2/files-2.1/d42ba59a47f7160082879236533c4582/AndroidManifest.xml:11:5-80MERGED from [com.youku.arch:Hound:2.8.15] /Users/flyeek/.gradle/caches/transforms-2/files-2.1/d42ba59a47f7160082879236533c4582/AndroidManifest.xml:11:5-80uses-permission#android.permission.WRITE_CALL_LOGIMPLIED 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/guide/topics/manifest/manifest-intro

  • 【google】Manage manifest files:https://developer.android.com/studio/build/manage-manifests


关注【阿里巴巴移动技术】微信公众号,每周 3 篇移动技术实践 &干货给你思考!

发布于: 刚刚阅读数: 2
用户头像

还未添加个人签名 2018.07.07 加入

阿里巴巴移动&终端技术官方账号。

评论

发布
暂无评论
向工程腐化开炮 | manifest治理