從大團隊并肩作戰到小團隊帶頭沖鋒,蘇寧App插件化應用實踐
原創【51CTO.com原創稿件】從大團隊并肩作戰到小團隊帶頭沖鋒,高效的研發模式使得 App 本身的整體崩潰率始終維持在 0.02% 以下。
簡介
從大團隊并肩作戰到小團隊帶頭沖鋒,高效的研發模式使得 App 本身的整體崩潰率始終維持在 0.02% 以下。
本著以用戶為中心、以開發者為出發點,根據現有開源方案取長補短,蘇寧易購移動開發部于 2017 年初自主研發出了新型插件化技術——APNP(Android Plugin And Play),旨在讓研發更敏捷,讓發布更靈活,最終滿足用戶對產品的極速體驗、按需下載、動態更新。
需求分析
技術的引入來自于實際業務場景對技術的需求,插件化亦是如此,那么到底是什么原因推動了蘇寧易購 App 的插件化,又是什么原因讓蘇寧易購開發者走上自研插件化的道路?
為什么蘇寧易購 App 需要插件化?
發布周期長,產品迭代跟不上市場需求
對于一個電商 App,不同的時間、地點,伴隨著用戶千變萬化、稍縱即逝的消費需求,誰能在***時間滿足這些需求,誰就能把握住需求帶來的銷量。
而傳統的 App 開發模式周期過長,我們需要更敏捷的發布方案,所以我們做了插件化,這也是蘇寧易購對插件化最原始的需求。
單線研發,管理、協作成本過高
隨著蘇寧易購業務的不斷拓展、項目參與人員數量的增多,單線 App 開發模式所隱藏的問題日益凸顯:一面從需求分析到研發測試,需要監管的內容越來越多;另一方面從方案決策到流程審批,協作溝通越來越頻繁,成本越來越高。
因此我們需要多線、小團隊的研發模式,這讓我們進一步確認了插件化。
安裝***大,運營推廣效率走低
需求在日益增長的同時,安裝包體積也在同步膨脹。面對耗時、耗流量的安裝包下載,新用戶體驗、老用戶升級的阻力也越來越大。
為了解決這個問題,我們需要拆包、需要動態下載、需要局部更新,因此我們正式引入了插件化。
什么選擇自研而不是使用開源方案?
沒有***無暇的開源方案,卻有層出不窮的接入問題
移動團隊在一開始選擇過幾種開源方案,個別方案的可用性也比較高,但是在接入之后,測試環節總會出現些疑難雜癥,修復起來相對困難,一方面是源碼本身的掌握成本較高,另一方面就是開源方案本身存在的缺陷。
要么另起爐灶,要么藕斷絲連
現有的開源方案,要么就是對現有工程的改造較大,開發有心無力;要么就是插件工程和宿主工程相互依賴,牽一發而動全身,成本、風險都很高。
插件方案選型
基于上述種種原因,我們決定自研插件化技術,于是就有了 APNP。APNP采用的插件方案是直接加載 APK 文件(APK 格式不變,內容有所修改),原因有如下幾點:
兼容性高
APK 文件的格式非常穩定,它包含一個 App 正常運行的必要資源;任何版本的 Android 系統,都應該正常解析并運行任意版本匹配的 APK 文件,無論這個 APK 文件是何時產生的;我們只要正確模仿系統加載 APK 的行為,就可以加載任意一個插件 APK 文件。
研發隔離
既然是直接加載 APK 文件,所以在 APNP 的設計方案中,每個插件都是一個單獨工程,這就意味著除了***的集成測試階段,插件對應的整個軟件生命周期都是獨立的,既降低了管理、協作成本,又促使插件產品的發布更加靈活。
接入簡單
既然是獨立工程,無論現有的工程是如何運轉的,開發者唯一要做的就是把插件工程從現有工程中抽離出來。
核心手段及原理
如果直接加載一個原始插件的 APK 文件,絕大多數情況下是無法運行的,如 Class pre-verified 異常、ResourceNotFound 等。
因此 APNP 在保持 APK 格式不變的情況下,對APK里面的內容做了針對性修改,核心手段如下:
共同 Dependency 剔除
在研發過程中,經常需要第三方依賴 Library(Dependency),而當插件工程(以下簡稱 Plugin)和宿主工程(以下簡稱 Host)包含相同的 Dependency 時,就需要剔除 Plugin 中的 Dependency。
這樣一方面可以避免 Class pre-verified 異常,另一方面也可以減少插件包的大小,提升插件包的下載 & 加載速度。
實現方案(以下都默認 IDE 為 Android Studio):通過 Gradle 插件,在 Plugin 的 Transform 過程中,剔除與 Host 相同 Dependency 的所有資源。
Package ID 修改
在 Android 系統中,App 對應的 Package ID 是固定的,也就是說如果我們不人為干預,Plugin 和 Host 打包生產的 APK 文件中,所有 Resource ID 中的 Package ID 都是一樣的,即(0x7f)。
這時如果直接加載 Plugin APK,必然會出現 Resource 類型不匹配、顯示錯亂等異常,所以我們修改 Plugin 的 APK 的 Package ID。(Package ID 滿足 0x01 < PID < 0x7f 即可)
實現方案:業內關于修改 Package ID 的方案有很多(如修改 aapt 源碼等),APNP 采用的方案是直接修改***生成的 APK 文件。
先看下一個簡單 APK 的文件結構,如下圖:
涉及 Package ID 修改的地方有 3 處:
- R.class(classes.dex)
- resources.arsc
- xml 文件
修改 R.class 中的 Package ID
在 Gradle assemble 之后,會在 build/generated/source/r 文件下生成相應的 R.java 文件,如下圖:
此時我們將 R 文件中的 0x7f 直接替換成目標 Package ID ,然后繼續交給 Gradle 做后續操作。
這樣在最終的 classes.dex 中,里面的 R.class 文件對應的 Package ID 就是我們想要設置的 Package ID。
修改 arsc + xml 中的 Package ID
不同于上面通過 Hook Gradle 過程修改中間生成的 R 文件,arsc + xml 則是直接解壓最終的 APK 并修改目標文件,然后重新簽名。
首先我們需要對 APK 文件中的 arsc + xml 的文件格式有一定的認識,Android 為了充分減少 APK 自身的大小,在編譯的過程中會對所有的資源進行重組 + 壓縮。
例如 values 文件下的內容會被統一收集到 arsc 文件中,而不再以文件的形式存在;而 xml 也不再是原始的 xml 文件,xml 中內容會被進一步整合 + 復用。
而無論是 arsc 文件,還是 xml 文件,在它們內部都是通過一個個固定數據結構的 ResChunk 以嵌套 + 組合的形式各自存儲著。
資源信息:每段 ResChunk 的內容都以 8 字節的 ResChunk_header 開頭,用于描述 ResChunk 的類型 + 長度,如下圖:
也就是說 arsc + xml 中的所有內容都可以被反向解析出來,當充分了解每個 ResChunk 的數據結構以及掌握 ResChunk 中哪些內容是需要修改的 Package ID,就可以相對輕松的完成對二進制文件的修改。
Library Chunk 插入
修改完 Package ID 后,還需要在 resources.arsc 文件中插入一段 Library Chunk ,如下圖:
Library Chunk 在 Android 5.0 之后才出現,對應的類型如下圖:
那么為什么需要插入 Library Chunk?原因是在 Android 5.0 之后,ResTable在獲取資源信息時,如果資源含有 Parent(ResTable_map_entry,如 style),會驗證 Parent 的 Package ID 是否合法,也就是 Package ID 是不是已經注冊過了。
如果 lookupResourceId 沒有返回 NO_ERROR,則報錯,繼續跟進,如下圖:
由于在上一環節中,我們已經把 Plugin 中的 Package ID 修改了,上圖中的局部變量 packageId 肯定不會是 APP_PACKAGE_ID 。
***會在 mLookupTable 數組中尋找 packageId 是否有對應的值,如果沒有(值為 0)則拋出異常。
所以我們需要把自定義的 Package ID 注冊到 mLookupTable 數組中,那么注冊的動作是在什么時候發生的?
在 App 啟動并資源***加載時,會調用下面的方法去解析 arsc 文件,如下圖:
如果發現是 Library Chunk 會調用 addMapping,如下圖:
在 addMapping 中,我們會把 Library Chunk 中的 packageId 在 mLookupTable 數組中進行注冊(就是簡單的賦值),注冊之后我們自定義的 Package ID 才會被系統認可,從而確保含有 Parent 資源的正常解析。
Attr ID 替換
完成上面 3 步后,APK 就已經可以加載了,但是插件涉及到自定義屬性的 View ,自定義屬性總是無法正常賦值,這又是什么原因呢?
以 ConstraintLayout 為例,假如 Plugin 和 Host 的 Layout 布局中都包含 ConstraintLayout,并同時都設置了 ConstraintLayout 的自定義屬性如下:
那么在 App 運行的過程中,ConstraintLayout 會通過 android.support.constraint.R.class 獲取解析自定義屬性,如下圖:
而 android.support.constraint.R.class 在內存中只有一份,并且會優先加載 Host,因此在實際的運行內存中,R.class 來自于 Host。
由于 Host 中 R.class 里面的 Package ID 都是 0x7f ,而在上面第二節的修改 Package ID 環節中,Plugin 里面的 xml 的 Package ID 已經被我們修改了(包括 xml 中所引用的 Attr ID)。
因此當 Plugin 通過 Host 的 R.class 去解析 ConstrainLayout 時,必然會出現自定義屬性無法正常解析的異常。
所在 APNP 在修改 Plugin xml 文件 Package ID 的同時,如果發現 Attr 同樣存在于 Host 中,會把 Plugin xml 文件中的 Attr ID 替換成 Host 中的 Attr ID,從而保證 Plugin 中所有的自定義屬性能夠正常解析(紅框:0x7f01000f,0x7f010022,0x7f010026,0x7f01002b),如下圖:
否則直接修改 Package ID(藍框:0x7e010000,0x7e010001,0x7e010002)。
實踐成果
在 APNP 實際應用之后,通過蘇寧云跡平臺的實時數據監控,并沒有發現因 APNP 本身引起的明顯異常。
從大團隊并肩作戰到小團隊帶頭沖鋒,高效的研發模式也使得 App 本身的整體崩潰率始終維持在 0.02% 以下。
相比之前的研發流程,APNP 帶來的影響主要有下面幾個點:
產品更精細、定位更明確
APNP 是產品精細劃分和明確定位的技術支撐,小團隊的研發模式,加快了產品的落實與升級。
研發周期短、發布更靈活
插件可單獨升級、可獨立發布的特征,配合獨立團隊的專人專職,單一產品可以達到一周一版本。
管理更輕松、研發更高效
同步產品線的精細劃分,研發團隊也趨于更小、更獨立,管理也顯得更加輕松;同時明確的責任分工,也促進了研發人員的工作效率。
發展前景
Google 在今年的 IO 大會上推出了一種動態加載方案——App Bundles,可以讓用戶通過 Google Play 動態下載應用功能,而且 App Bundles 采用的方案也是直接加載 APK 文件。
通過分析最終的 Feature Master APK 文件,你會發現 App Bundles 對 APK 做了和 APNP 相同的操作:剔除 Dependency、修改 Package ID、插入 Library Chunk、替換 Attr ID。
這看似巧合實則必然,因為 App Bundles 和 APNP 都是直接加載 APK 文件,所以面對的問題都是一樣的。
在方案上 APNP 可以說有了官方保障,而且 APNP 在后面還可以借鑒 App Bundles 的實現細節,讓插件加載更加高效、安全。
而由于 Android P 對隱藏 API 調用的限制,很多人認為插件化方案將不再可行。
但是 Google 也同時提供了 light greylist,并且國內手機廠商肯定不允許讓主流 App 無法使用的情況發生,所以我覺得在很長一段時間內,插件化依然可以正常運轉。
作者:李呈武
簡介:蘇寧易購前端技術專家,資深Android 開發者,深度掌握 Android 虛擬機、插件化、Weex 等技術,熟悉移動網絡的特質,對移動端的架構設計有獨特的見解,一直致力于通過優秀的架構設計,減少開發成本,提升開發質量。
【51CTO原創稿件,合作站點轉載請注明原文作者和出處為51CTO.com】