隨著業務的發展及版本迭代,客戶端工程中不斷增加新的業務邏輯、引入新的資源,隨之而來的問題就是安裝包體積變大,前期各個業務模塊通過無用資源刪減、大圖壓碩訓轉上云、AB實驗業務邏輯下線或其他手段在降低包體積上取得了一定的成果,
在瘦身的程序中我們關注到了R檔案瘦身的概念,目前京東APP是支持插件化的,有業務插件工程、宿主工程,對業務插件包檔案進行分析,發現除了常規的資源及代碼外,R類檔案大概占包體積的3%~5%左右,對宿主工程包檔案進行分析,R類檔案占比也有3%左右,我們先后在對R類檔案瘦身的可行性及業界開源專案進行調研后,探索出了一套適用于插件化工程的R檔案瘦身技術方案,
理論基礎—R檔案
R檔案也就是我們日常作業中經常打交道的R.java檔案,在Android開發規范中我們需要將應用中用到的資源分別放入專門命名的資源目錄中,外部化應用資源以便對其進行單獨維護,
外部化應用資源后,我們可在專案中使用R類ID來訪問這些資源,且R類ID具有唯一性,
public class MainActivity extends BaseActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
在android apk打包流程中R類檔案是由aapt(Android Asset Packaing Tool)工具打包生成的,在生成R類檔案的同時對資源檔案進行編譯,生成resource.arsc檔案,resource.arsc檔案相當于一個檔案索引表,應用層代碼通過R類ID 可以訪問到對應的資源,
R檔案瘦身的可行性分析
日常開發階段,在主工程中通過R.xx.xx的方式參考資源,經過編譯后R類參考對應的常量會被編譯進class中,
setContentView(2131427356);
這種變化叫做行內,行內是java的一種機制(如果一個常量被標記為static final,在java編譯的程序中會將常量行內到代碼中,減少一次變數的記憶體尋址),
非主工程中,R類資源ID以參考的方式編譯進class中,不會產生行內,
setContentView(R.layout.activity_main);
產生這種現象的原因是AGP打包工具導致的,具體細節,大家可以去查閱一下android gradle plugin在R檔案上的處理程序,
結論:R類id行內后程式可運行,但并非所有的工程都會自動產生行內現象,我們需要通過技術手段在合適的時機將R類id行內到程式中,行內完成后,由于不再依賴R類檔案,則可以將R類檔案洗掉,在應用正常運行的同時,達到包瘦身目的,
插件化工程R檔案瘦身實戰
制定技術方案
目前京東Android客戶端是支持插件化的,整個插件化工程包含公共庫(是一個aar工程,用來存放組件和宿主共用的類和資源)、業務插件(插件工程是一個獨立的工程,編譯產物可以運行在宿主環境中)、宿主(主工程,提供運行環境),在插件化的程序中為了防止宿主和插件資源沖突,通過修改插件packageId保證了資源的唯一性,由于公共資源庫、宿主是被很多業務依賴,對這兩個專案進行改動評估影響涉及比較多,插件一般都是業務模塊自行維護,不存在被依賴問題,所以先在業務插件模塊進行R類瘦身實踐,
對業務插件工程打出的包進行反編譯以后,發現R類ID無行內現象,且R類檔案具有一定的大小,對包內的R檔案進行分析,發現R檔案中僅包含業務自身的資源,不包含業務依賴的公共資源R類,
public View onCreateView(LayoutInflater paramLayoutInflater, ViewGroup paramViewGroup, Bundle paramBundle) {
this.b = paramLayoutInflater.inflate(R.layout.lib_pd_main_page, paramViewGroup, false);
this.h = (PDBuyStatusView)this.b.findViewById(R.id.pd_buy_status_view);
this.f = (PageRecyclerView)this.b.findViewById(R.id.lib_pd_recycle_view);}
結合對業界開源專案的調研分析,嘗試制定符合京東商城的技術方案并優先在業務插件內完成R類ID行內并洗掉對應的R檔案,
1.通過transformapi 收集要處理的class檔案
Transform 是 Android Gradle 提供的操作位元組碼的一種方式,它在 class 編譯成 dex 之前通過一系列 Transform 處理來實作修改.class檔案,
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation);
// 通過TransformInvocation.getInputs()獲取輸入檔案,有兩種
// DirectoryInpu以原始碼方式參與編譯的目錄結構及目錄下的檔案
// JarInput以jar包方式參與編譯的所有jar包
allDirs = new ArrayList<>(invocation.getInputs().size());
allJars = new ArrayList<>(invocation.getInputs().size());
Collection<TransformInput> inputs = invocation.getInputs();
for (TransformInput input : inputs) {
Collection<DirectoryInput> directoryInputs = input.getDirectoryInputs();
for (DirectoryInput directoryInput : directoryInputs) {
allDirs.add(directoryInput.getFile());
}
Collection<JarInput> jarInputs = input.getJarInputs();
for (JarInput jarInput : jarInputs) {
allJars.add(jarInput.getFile());
}
}
}
2.對收集到的.class檔案結合ASM框架進行分析處理
ASM是一個操作Java位元組碼的類別庫,通過ASM我們可以方便對.class檔案進行修改,
優先識別R類檔案,通過ClassVisitor訪問R.class檔案,讀取檔案中的靜態常量,進行臨時變數存盤:
@Overridepublic FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { //R類中收集 public static final int 對應的變數 if (JDASMUtil.isPublic(access) && JDASMUtil.isStatic(access) &&JDASMUtil.isFinal(access) &&JDASMUtil.isInt(desc)) { jdRstore.addInlineRField(className, name, value); } return super.visitField(access, name, desc, signature, value);}
非R類檔案,通過MethodVisitor識別到代碼中的R類參考,獲取參考對應的值,進行id值替換:
@Override
public void visitFieldInsn(int opcode, String owner, String name, String desc) {
if (opcode == Opcodes.GETSTATIC) {
//owner:包名;name:具體變數名;value:R類變數對應的具體id值
Object value = https://www.cnblogs.com/Jcloud/archive/2023/06/13/jdRstore.getRFieldValue(owner, name);
if (value != null) {
//呼叫該api實作值替換
mv.visitLdcInsn(value);
return;
}
}
super.visitFieldInsn(opcode, owner, name, desc);
}
*注:以上代碼僅為部分示意代碼,非正式插件代碼,
在業務模塊引入R類瘦身插件后,業務模塊功能可正常運行,且插件包大小均有3%~5%不同程度的減少,
公共資源R類ID行內
由于在京東android客戶端代碼中,更多的資源檔案集中在公共資源庫中,相對的公共庫生成的R類檔案也更大,對編譯后的apk包內容進行分析后,公共資源庫的R類檔案占比高達3%,
公共庫跟隨宿主一起打包,在宿主打包程序中引入R類瘦身插件,打包后的apk有明顯的減小,手機安裝apk后啟動首頁正常展示無問題,但在打開某些業務插件時,會有例外閃退現象,崩潰型別為R.x resource not found,對崩潰原因分析如下:業務插件代碼中使用了公共庫中的R類資源、插件打包流程獨立于宿主打包,在插件打包的程序中僅完成了業務模塊R類的行內,并沒有考慮到公共資源R類的行內,基于上述原因當宿主打包程序完成R類檔案洗掉瘦身后,我們在運行某業務插件的程序中,自然就會報公共資源R類找不到的問題從而產生崩潰,
為了解決這個問題一開始的方案設想是增加白名單機制,keep住所有被業務模塊使用的公共資源,但很快這個想法就被推翻,公共資源存在本身就是希望各個業務模塊直接參考這部分資源,而不是自己定義,如果keep住的話,必然有很大一部分的資源無法刪減,瘦身的效果會大打折扣,
既然保留的方案并不合適,那就將公共資源R類id也行內到代碼中去,前面提到京東是支持插件化的,整個插件化方案是基于aura平臺實作的,我們向aura團隊進行了咨詢,然后get到了新的方案切入點,
aura平臺在插件化的程序中已通過aapt2引入了公共資源id固定的能力,在該能力下,已定義的公共資源id會一直固定(各個業務插件中參考的公共資源id一致),且公共資源庫中已有的資源不可被其他模塊重復定義,否則會覆寫之前已定義好的資源,基于上述的結果和規則,我們對之前的R檔案瘦身gralde plugin功能進行完善,將公共資源的R類id 行內到專案中,
利用appt2的-stable-ids和-emit-ids兩個引數實作固化資源id的功能,并將將固化后的ids檔案命名為shared_res_public.xml存盤在公共資源庫中,業務插件依賴公共資源庫,在打包編譯的程序中aura會將shared_res_public.xml復制到業務工程臨時編譯檔案夾intermediates下的指定位置并參與業務模塊的打包程序中,其檔案內容格式如下:
修改R檔案瘦身gradle plugin 代碼,從指定位置讀取并識別這部分公共資源,按照<name,id>的形式進行變數存盤,并在后續程序中對業務模塊中的公共資源部分進行id替換,
public Map<String, String> parse() throws Exception {
if (in == null) {
return null;
}
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(in);
Element rootElement = doc.getDocumentElement();
NodeList list = rootElement.getChildNodes();
......
return resNode;
}
}
R類資源id行內部分代碼如下:
public void visitFieldInsn(int opcode, String owner, String name, String desc) {
if (opcode == Opcodes.GETSTATIC) {
//優先從業務模塊R類資源中查找
Object value = https://www.cnblogs.com/Jcloud/archive/2023/06/13/jdRstore.getRFieldValue(owner, name);
if (value != null) {
mv.visitLdcInsn(value);
return;
}
//從公共R類資源中查找
value = getPublicRFileValue(name);
if (value != null) {
mv.visitLdcInsn(value);
return;
}
}
super.visitFieldInsn(opcode, owner, name, desc);
}
該方案完善后,結合商詳業務插件進行了驗證,在商詳及宿主均完成R檔案行內瘦身后,商詳模塊業務功能可正常使用,無例外現象,
考慮到R檔案行內瘦身gradle plugin是在打包編譯階段引入的,我們也統計了一下引入該插件以后對打包時長的影響,資料如下:
結合資料來看,引入R檔案瘦身插件后對整體打包時長并無顯著影響,
至此,基于京東商城探索的插件化工程R檔案瘦身gradle plugin就開發完成,目前已在部分業務插件模塊進行了線上驗證,在功能上線以后我們也及時的進行了崩潰觀測以及用戶反饋的跟進,暫無例外問題,當然圍繞R檔案瘦身縮減包體積這個目的,開發人員有各種各樣的技術方案,上述方案不一定適用于所有的客戶端開發體系,另外后續也將圍繞包瘦身這一常態事務建設一系列的相關工具,介入作業當中的各個階段,高效、有效的控制包體積的增長,如大家在瘦身方面有相關建議和想法也歡迎大家來一起討論,
參考文章:
Gradle Plugin:
https://docs.gradle.org/current/userguide/custom_plugins.html
Gradle Transform:
https://developer.android.com/reference/tools/gradle-api/7.0/com/android/build/api/transform/Transform
APK 構建流程:
https://developer.android.com/studio/build/index.html?hl=zh-cn#build-process
作者:耿蕾 田創新
來源:京東云開發者社區
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/555149.html
標籤:其他
下一篇:返回列表