滴滴開源 DroidAssist : 輕量級 Android 字節碼編輯插件

出品 | 滴滴技術java

做者 | 江義旺android


▍前言  近日,滴滴發佈的開源項目 DroidAssist ,提供了一種簡單易用、無侵入、配置化、輕量級的 Java 字節碼操做方式,只須要在 XML 配置中添加簡單的 Java 代碼便可實現編譯期對 Class 文件的動態修改。git

DroidAssist 和其餘 AOP 方案不一樣,它提供了一種簡單易用、無侵入、配置化、輕量級的 Java 字節碼操做方式,你不須要 Java 字節碼的相關知識,只須要在 XML 配置中添加簡單的 Java 代碼便可實現編譯期對 class 文件的動態修改,同時不須要引入其餘額外的依賴。
github

▍起源

做爲大型 APP 的表明,滴滴出行乘客端集成了較多的業務線,包含了大量的依賴庫,每一個版本都有多個團隊向乘客端集成大量的代碼,並且這些代碼都是難以直接追溯到源碼的,同時乘客端還有用戶量大,日活高,迭代快等特色,這些狀況對乘客端的開發和維護造成很大的挑戰,主要體如今:問題防範難度大、問題規模大、後期維護成本高。
bash

2018年5月,乘客端團隊進行卡頓專項優化, 其中有個問題是:因爲安卓系統 SharedPreferences自身機制,當頻繁調用 SharedPreferences.apply() 方法時,可能會出現由 QueuedWork.waitToFinish() 形成的卡頓和 ANR。主要緣由是系統在 Activity 的 onPause、onStop,以及 Service 的 start 和 stop 生命週期時會執行阻塞等待 QueuedWork 清空,推測系統是爲了保證持久化成功率,從而確保用戶離開組件以前完成 SharedPreferences 的文件寫入。
app

分析緣由以後,咱們認爲,乘客端 APP 相對處於單一的進程環境,去掉這個持久化阻塞也是能夠的。爲了解決這個問題,咱們決定對系統的 SharedPreferences 進行改造,實現咱們本身的 SharedPreferences。
框架

可是隨之而來的問題是,咱們自定義的 SharedPreferences 怎麼以最小的成本接入到乘客端呢?很容易想到如下兩種方案:ide

  • 修改全部調用 Context.getSharedPreferences() 的代碼,返回咱們本身的 SharedPreferences 對象,缺點:改動太多,工做量太大,修改、還原成本過高。工具

  • 全部的 Application、Activity、Service 類都從統一的的 Base 基類派生,在基類中重寫 getSharedPreferences 方法返回自定義 SharedPreferences 對象,和方法一相比,此方法代碼改動較小,可是也存在是沒法修改第三方庫,並且工做量也比較大,修改、還原成本也很高的問題。優化

以上兩種方式都具備較大的侵入性,會涉及到大量的源碼以及依賴庫的代碼改動,後期維護和升級成本也比較高,爲了尋找更加理想的解決方案,咱們但願找到一種無侵入的 Mock 工具,能作到不修改代碼就能 Mock 全部 getSharedPreferences()方法的調用返回結果,初步有以下兩種實現思路:

  • Hook:Hook 技術須要一直處理各類廠商和機型的兼容性問題,有較大的穩定性風險。

  • AOP:AOP 類框架在編譯期實現字節碼操做,比較成熟穩定,能夠考慮採用,可是通過分析發現,現有的 AOP 框架包括 AspectJ 並不能實現咱們須要的 Mock 功能。

相似 SharedPreferences 替換這樣的需求還有不少,因而咱們決定本身開發一個Android 平臺 Mock 工具,通過調研以後,咱們肯定了字節碼修改的技術方向,經過修改字節碼實現這樣的需求,由此 DroidAssist 應運而生。

項目地址:github.com/didi/DroidA…

示例

下面例子是背景中提到的 SharedPreferences 改造,添加以下 DroidAssist 配置,在項目編譯後,全部調用Context.getSharedPreferences() 的代碼,將所有會被修改成返回自定義的 SharedPreferences 實例的代碼:

1 <Replace>
2     <MethodCall>
3DroidAssist
4<Source>android.content.SharedPreferences android.content.Context.getS
5haredPreferences(java.lang.String,int)</Source>
6    <Target>{$_= com.didi.quicksilver.QuicksilverPreferencesHelper.getShar
7edPreferences($0,$$);}</Target>
8  </MethodCall>
9</Replace>
複製代碼

處理前的 class:

1public class MainActivity extends Activity {
2@Override
3    protected void onCreate(Bundle savedInstanceState) {
4        super.onCreate(savedInstanceState);
5        SharedPreferences sp = getSharedPreferences("test", MODE_PRIVATE);
6} }
複製代碼

處理後的 class:

1public class MainActivity extends Activity {
2    protected void onCreate(Bundle savedInstanceState) {
3        super.onCreate(savedInstanceState);
4        SharedPreferences sp = PreferencesHelper.getSharedPreferences(this
5, "test", MODE_PRIVATE); // The target method return custom SharedPreferen
6ces.
7} }
複製代碼

具體的使用方式及原理可參見 DroidAssist WIKI

▍特性

通過不斷的打磨完善,DroidAssist 已經從最開始的 Mock 工具擴展成爲具備完整 AOP 框架功能的工具,有以下特性。

▍簡單易用

採用靈活的配置化方式,使用者只須要依賴一個插件,而後在配置文件中定義字節碼處理方式,DroidAssist 就能夠根據配置文件處理項目中全部的 class 文件。處理過程以及處理後的代碼中都不須要添加額外的依賴,而且不會修改原始代碼行號。

▍豐富的字節碼處理功能

除了解決咱們最初遇到的代碼替換問題外,還擴展了其餘的 AOP 功能,目前有 4 類 28 種代碼修改方式。


  • 替換:把指定位置代碼替換爲指定代碼

  • 插入:在指定位置的先後插入指定代碼

  • 環繞:在指定位置環繞插入指定代碼

  • 加強

    • TryCatch 對指定代碼添加 try catch 代碼

    • Timing 對指定代碼添加耗時統計代碼

簡單易用

支持增量構建,處理速度快,只佔用不多的構建時間。

▍Q&A

1. DroidAssist 能夠實現什麼功能?

DroidAssist 能夠輕易實現諸如代碼替換,代碼插入等功能,滴滴出行 APP 利用 DroidAssist 實現了日誌輸出替換,系統 SharedPreferences 替換,SharedPreferences commit 替換爲 apply,Dialog 展現保護,getDeviceId 接口替換,getPackageInfo 接口替換,getSystemService 接口替換,startActivity 保護,匿名線程重命名,線程池建立監控,主線程卡頓監控,文件夾建立監控,Activity 生命週期耗時統計,APP啓動耗時統計等功能。

2. DroidAssist 和 AspectJ 有什麼區別?

DroidAssist 採用配置化方案,編寫相關配置就能夠實現 AOP 的功能,能夠徹底不用修改 Java 代碼;DroidAssist 在使用上使用比較簡單,不須要複雜的註解配置;DroidAssist 能夠比較方便的實現 AspectJ 不容易實現的代碼替換功能。通常狀況下使用 DroidAssist 能夠完成大部分功能,較複雜狀況能夠和 AspectJ 配合使用。



有關安裝、使用過程以及常見問題解答,請查看如下連接:

GitHub:github.com/didi/DroidA…

Wiki:github.com/didi/DroidA…

同時歡迎加入「DroidAssist 用戶交流羣」

請在滴滴技術公衆號後臺回覆「DroidAssist」便可加入

END





相關文章
相關標籤/搜索