您的位置:  首页 > 技术杂谈 > 正文

向工程腐化开炮 | manifest治理

2022-02-25 15:00 https://my.oschina.net/alimobile/blog/5464390 阿里巴巴移动技术 次阅读 条评论

作者:刘天宇(谦风)

工程腐化是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时,出现选择弹窗。

与工程腐化的对抗,依然艰难,任重而道远,与诸君共勉。

【参考文档】

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

展开阅读全文
  • 0
    感动
  • 0
    路过
  • 0
    高兴
  • 0
    难过
  • 0
    搞笑
  • 0
    无聊
  • 0
    愤怒
  • 0
    同情
热度排行
友情链接