隨著業務的插件程發展及版本迭代,客戶端工程中不斷增加新的化工業務邏輯、引入新的文件資源,隨之而來的瘦身問題就是安裝包體積變大,前期各個業務模塊通過無用資源刪減、技術大圖壓縮或轉上云、插件程AB 實驗業務邏輯下線或其他手段在降低包體積上取得了一定的化工成果。
在瘦身的文件過程中我們關注到了 R 文件瘦身的概念,目前京東 APP 是瘦身支持插件化的,有業務插件工程、技術宿主工程,插件程對業務插件包文件進行分析,化工發現除了常規的文件資源及代碼外,R 類文件大概占包體積的瘦身 3%~5% 左右,對宿主工程包文件進行分析,技術R 類文件占比也有 3% 左右。我們先后在對 R 類文件瘦身的可行性及業界開源項目進行調研后,探索出了一套適用于插件化工程的 R 文件瘦身技術方案。
理論基礎 —R 文件
R 文件也就是我們日常工作中經常打交道的 R.java 文件,在 開發規范中我們需要將應用中用到的資源分別放入專門命名的資源目錄中,外部化應用資源以便對其進行單獨維護。
外部化應用資源后,我們可在項目中使用 R 類 ID 來訪問這些資源,且 R 類 ID 具有唯一性。
class {
@
void (@ ) {
super.();
(R..);
在 apk 打包流程中 R 類文件是由 aapt( Asset Tool)工具打包生成的,在生成 R 類文件的同時對資源文件進行編譯,生成 .arsc 文件,.arsc 文件相當于一個文件索引表,應用層代碼通過 R 類 ID 可以訪問到對應的資源。
R 文件瘦身的可行性分析
日常開發階段,在主工程中通過 R.xx.xx 的方式引用資源,經過編譯后 R 類引用對應的常量會被編譯進 class 中。
();
這種變化叫做內聯,內聯是 java 的一種機制(如果一個常量被標記為 final,在 java 編譯的過程中會將常量內聯到代碼中,減少一次變量的內存尋址)。
非主工程中,R 類資源 ID 以引用的方式編譯進 class 中,不會產生內聯。
(R..);
產生這種現象的原因是 AGP 打包工具導致的。具體細節,大家可以去查閱一下 在 R 文件上的處理過程。
結論:R 類 id 內聯后程序可運行,但并非所有的工程都會自動產生內聯現象,我們需要通過技術手段在合適的時機將 R 類 id 內聯到程序中,內聯完成后,由于不再依賴 R 類文件,則可以將 R 類文件刪除,在應用正常運行的同時,達到包瘦身目的。
插件化工程 R 文件瘦身實戰
制定技術方案
目前京東 客戶端是支持插件化的,整個插件化工程包含公共庫(是一個 aar 工程,用來存放組件和宿主共用的類和資源)、業務插件(插件工程是一個獨立的工程,編譯產物可以運行在宿主環境中)、宿主(主工程,提供運行環境)。在插件化的過程中為了防止宿主和插件資源沖突,通過修改插件 保證了資源的唯一性。由于公共資源庫、宿主是被很多業務依賴,對這兩個項目進行改動評估影響涉及比較多,插件一般都是業務模塊自行維護,不存在被依賴問題,所以先在業務插件模塊進行 R 類瘦身實踐。
對業務插件工程打出的包進行反編譯以后,發現 R 類 ID 無內聯現象,且 R 類文件具有一定的大小,對包內的 R 文件進行分析,發現 R 文件中僅包含業務自身的資源,不包含業務依賴的公共資源 R 類。
View ( , , ) {
this.b = .(R.., , false);
this.h = ()this.b.(R.id.);
this.f = ()this.b.(R.id.);}
結合對業界開源項目的調研分析,嘗試制定符合京東商城的技術方案并優先在業務插件內完成 R 類 ID 內聯并刪除對應的 R 文件。
1. 通過 api 收集要處理的 class 文件
是 提供的操作字節碼的一種方式,它在 class 編譯成 dex 之前通過一系列 處理來實現修改.class 文件。
@
void ( ) , , {
super.();
// 通過.()獲取輸入文件,有兩種
// 以源碼方式參與編譯的目錄結構及目錄下的文件
// 以jar包方式參與編譯的所有jar包
= new (.().size());
= new (.().size());
= .();
for ( input : ) {
= input.();
for ( : ) {
.add(.());
= input.();
for ( : ) {
.add(.());
2. 對收集到的.class 文件結合 ASM 框架進行分析處理
ASM 是一個操作 Java 字節碼的類庫,通過 ASM 我們可以方便對.class 文件進行修改。
優先識別 R 類文件,通過 訪問 R.class 文件,讀取文件中的靜態常量,進行臨時變量存儲:
@ (int , name, desc, , value) { //R類中收集 final int 對應的變量 if (.() && .() &&.() &&.isInt(desc)) { .(, name, value); } super.(, name, desc, , value);}
非 R 類文件,通過 識別到代碼中的 R 類引用,獲取引用對應的值,進行 id 值替換:
@
void (int , owner, name, desc) {
if ( == .) {
//owner:包名;name:具體變量名;value:R類變量對應的具體id值
value = .(owner, name);
if (value != null) {
//調用該api實現值替換
mv.(value);
;
super.(, owner, name, desc);
* 注:以上代碼僅為部分示意代碼,非正式插件代碼。
在業務模塊引入 R 類瘦身插件后,業務模塊功能可正常運行,且插件包大小均有 3%~5% 不同程度的減少。
公共資源 R 類 ID 內聯
由于在京東 客戶端代碼中,更多的資源文件集中在公共資源庫中,相對的公共庫生成的 R 類文件也更大,對編譯后的 apk 包內容進行分析后,公共資源庫的 R 類文件占比高達 3%。
公共庫跟隨宿主一起打包,在宿主打包過程中引入 R 類瘦身插件,打包后的 apk 有明顯的減小,手機安裝 apk 后啟動首頁正常展示無問題,但在打開某些業務插件時,會有異常閃退現象,崩潰類型為 R.x not found。對崩潰原因分析如下:業務插件代碼中使用了公共庫中的 R 類資源、插件打包流程獨立于宿主打包,在插件打包的過程中僅完成了業務模塊 R 類的內聯,并沒有考慮到公共資源 R 類的內聯,基于上述原因當宿主打包過程完成 R 類文件刪除瘦身后,我們在運行某業務插件的過程中,自然就會報公共資源 R 類找不到的問題從而產生崩潰。
為了解決這個問題一開始的方案設想是增加白名單機制,keep 住所有被業務模塊使用的公共資源,但很快這個想法就被推翻,公共資源存在本身就是希望各個業務模塊直接引用這部分資源,而不是自己定義,如果 keep 住的話,必然有很大一部分的資源無法刪減,瘦身的效果會大打折扣。
既然保留的方案并不合適,那就將公共資源 R 類 id 也內聯到代碼中去。前面提到京東是支持插件化的,整個插件化方案是基于 aura 平臺實現的,我們向 aura 團隊進行了咨詢,然后 get 到了新的方案切入點。
aura 平臺在插件化的過程中已通過 aapt2 引入了公共資源 id 固定的能力,在該能力下,已定義的公共資源 id 會一直固定 (各個業務插件中引用的公共資源 id 一致),且公共資源庫中已有的資源不可被其他模塊重復定義,否則會覆蓋之前已定義好的資源,基于上述的結果和規則,我們對之前的 R 文件瘦身 功能進行完善,將公共資源的 R 類 id 內聯到項目中。
利用 appt2 的 - -ids 和 - emit-ids 兩個參數實現固化資源 id 的功能,并將將固化后的 ids 文件命名為 .xml 存儲在公共資源庫中,業務插件依賴公共資源庫,在打包編譯的過程中 aura 會將 .xml 復制到業務工程臨時編譯文件夾 下的指定位置并參與業務模塊的打包過程中,其文件內容格式如下:
修改 R 文件瘦身 代碼,從指定位置讀取并識別這部分公共資源,按照 的形式進行變量存儲,并在后續過程中對業務模塊中的公共資源部分進行 id 替換。
Map parse() {
if (in == null) {
null;
ry = ry.();
= .();
doc = .parse(in);
= doc.();
list = .();
;
R 類資源 id 內聯部分代碼如下:
void (int , owner, name, desc) {
if ( == .) {
//優先從業務模塊R類資源中查找
value = .(owner, name);
if (value != null) {
mv.(value);
;
//從公共R類資源中查找
value = (name);
if (value != null) {
mv.(value);
;
super.(, owner, name, desc);
該方案完善后,結合商詳業務插件進行了驗證,在商詳及宿主均完成 R 文件內聯瘦身后,商詳模塊業務功能可正常使用,無異常現象。
考慮到 R 文件內聯瘦身 是在打包編譯階段引入的,我們也統計了一下引入該插件以后對打包時長的影響,數據如下:
結合數據來看,引入 R 文件瘦身插件后對整體打包時長并無顯著影響。
至此,基于京東商城探索的插件化工程 R 文件瘦身 就開發完成,目前已在部分業務插件模塊進行了線上驗證,在功能上線以后我們也及時的進行了崩潰觀測以及用戶反饋的跟進,暫無異常問題。當然圍繞 R 文件瘦身縮減包體積這個目的,開發人員有各種各樣的技術方案,上述方案不一定適用于所有的客戶端開發體系,另外后續也將圍繞包瘦身這一常態事務建設一系列的相關工具,介入工作當中的各個階段,高效、有效的控制包體積的增長,如大家在瘦身方面有相關建議和想法也歡迎大家來一起討論。
參考文章:
:
:
APK 構建流程:
#build-