插件化工程R文件瘦身技術方案 | 京東雲技術團隊

来源:https://www.cnblogs.com/Jcloud/archive/2023/06/13/17477072.html
-Advertisement-
Play Games

隨著業務的發展及版本迭代,客戶端工程中不斷增加新的業務邏輯、引入新的資源,隨之而來的問題就是安裝包體積變大,前期各個業務模塊通過無用資源刪減、大圖壓縮或轉上雲、AB實驗業務邏輯下線或其他手段在降低包體積上取得了一定的成果。 ...


隨著業務的發展及版本迭代,客戶端工程中不斷增加新的業務邏輯、引入新的資源,隨之而來的問題就是安裝包體積變大,前期各個業務模塊通過無用資源刪減、大圖壓縮或轉上雲、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 = 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 = 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

作者:耿蕾 田創新

來源:京東雲開發者社區


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • # region Region是HBase數據管理的基本單位,region有一點像關係型數據的分區。 Region中存儲這用戶的真實數據,而為了管理這些數據,HBase使用了RegionSever來管理region。 ## region的分配 一個表中可以包含一個或多個Region。 每個Regio ...
  • 事務隔離級別遺留問題: 在讀已提交的級別下,事務B可以讀到事務A持有寫鎖的的記錄,且讀到的是未更新前的,為何寫讀沒有衝突? 可重覆讀級別,事務B可以更新事務A理論上應該已經獲取讀鎖的記錄,且更新後,事務A依然可以讀到數據,為何讀-寫-讀沒有衝突? 在可重覆讀級別,幻讀沒有產生 其中,前兩個問題就是因 ...
  • 企業數字化轉型以數據為中心,通過數據驅動業務發展、管理協同和運營。因此,數字化轉型關鍵在於數據,數據治理則需先行。從而更好激發數據生產要素潛能,實現業務數據化、數據價值化,助力企業數字化轉型。 ## 那麼何為數據治理? 國際數據管理協會(DAMA)在其《DAMA數據管理知識體系指南(第2版)》一書中 ...
  • # 資料庫模式設計如果不好會導致的問題: 1.冗餘 2.導致數據一致性出現問題 3.插入異常 4.更新異常 5.刪除異常 # 函數依賴 函數依賴是指一個或多個屬性的取值可以確定另一個屬性的取值。具體地說,如果一個關係模式R中屬性集合X的取值能唯一地確定屬性集合Y的取值,那麼我們稱屬性集合Y對於屬性集 ...
  • 這種只含map的操作,如果文件大小控制在合適的情況下,都將只有本地操作,其執行非常高效,運行效率完全不輸於在計算引擎Tez和Spark上運行。 ...
  • 摘要:併發的事務在運行過程中會出現一些可能引發一致性問題的現象,本篇將詳細分析一下。 本文分享自華為雲社區《MySQL讀取的記錄和我想象的不一致——事物隔離級別和MVCC》,作者:磚業洋__。 事務的特性簡介 1.1 原子性(Atomicity) 要麼全做,要麼全不做,一系列操作都是不可分割的,如果 ...
  • 之前寫過一篇文章“SQL Server如何找出視圖依賴的對象和視圖嵌套層數”,這裡我介紹一下Oracle資料庫中如何找出視圖的依賴對象以及視圖嵌套層數關係。主要通過DBA_DEPENDENCIES這個系統視圖(這個系統視圖中包含有對象的依賴關係數據)。另外,我們使用了Oracle的樹形查詢(層級查詢 ...
  • > ViewModel做為架構組件的三元老之一,是實現MVVM的有力武器。 ### ViewModel的設計目標 ViewModel的基本功能就是管理UI的數據。其實,從職責上來說,這又是對Activity和Fragment的一次功能拆分。以前存儲在它們內部的數據,需要它們自己處理創建,更新,存儲, ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...