作者:劉天宇(謙風)
工程腐化是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 篇移動技術實踐&乾貨給你思考!