出品 | 滴滴技術
作者 | 江義旺
▍前言 近日,滴滴釋出的開源專案 DroidAssist ,提供了一種簡單易用、無侵入、配置化、輕量級的 Java 位元組碼操作方式,只需要在 XML 配置中新增簡單的 Java 程式碼即可實現編譯期對 Class 檔案的動態修改。
DroidAssist 和其他 AOP 方案不同,它提供了一種簡單易用、無侵入、配置化、輕量級的 Java 位元組碼操作方式,你不需要 Java 位元組碼的相關知識,只需要在 XML 配置中新增簡單的 Java 程式碼即可實現編譯期對 class 檔案的動態修改,同時不需要引入其他額外的依賴。
▍起源
作為大型 APP 的代表,滴滴出行乘客端整合了較多的業務線,包含了大量的依賴庫,每個版本都有多個團隊向乘客端整合大量的程式碼,而且這些程式碼都是難以直接追溯到原始碼的,同時乘客端還有使用者量大,日活高,迭代快等特點,這些情況對乘客端的開發和維護形成很大的挑戰,主要體現在:問題防範難度大、問題規模大、後期維護成本高。
2018年5月,乘客端團隊進行卡頓專項優化, 其中有個問題是:由於安卓系統 SharedPreferences自身機制,當頻繁呼叫 SharedPreferences.apply() 方法時,可能會出現由 QueuedWork.waitToFinish() 造成的卡頓和 ANR。主要原因是系統在 Activity 的 onPause、onStop,以及 Service 的 start 和 stop 生命週期時會執行阻塞等待 QueuedWork 清空,推測系統是為了保證持久化成功率,從而確保使用者離開元件之前完成 SharedPreferences 的檔案寫入。
分析原因之後,我們認為,乘客端 APP 相對處於單一的程式環境,去掉這個持久化阻塞也是可以的。為了解決這個問題,我們決定對系統的 SharedPreferences 進行改造,實現我們自己的 SharedPreferences。
但是隨之而來的問題是,我們自定義的 SharedPreferences 怎麼以最小的成本接入到乘客端呢?很容易想到以下兩種方案:
修改所有呼叫 Context.getSharedPreferences() 的程式碼,返回我們自己的 SharedPreferences 物件,缺點:改動太多,工作量太大,修改、還原成本太高。
所有的 Application、Activity、Service 類都從統一的的 Base 基類派生,在基類中重寫 getSharedPreferences 方法返回自定義 SharedPreferences 物件,和方法一相比,此方法程式碼改動較小,但是也存在是無法修改第三方庫,而且工作量也比較大,修改、還原成本也很高的問題。
以上兩種方式都具有較大的侵入性,會涉及到大量的原始碼以及依賴庫的程式碼改動,後期維護和升級成本也比較高,為了尋找更加理想的解決方案,我們希望找到一種無侵入的 Mock 工具,能做到不修改程式碼就能 Mock 所有 getSharedPreferences()方法的呼叫返回結果,初步有如下兩種實現思路:
Hook:Hook 技術需要一直處理各種廠商和機型的相容性問題,有較大的穩定性風險。
AOP:AOP 類框架在編譯期實現位元組碼操作,比較成熟穩定,可以考慮採用,但是經過分析發現,現有的 AOP 框架包括 AspectJ 並不能實現我們需要的 Mock 功能。
類似 SharedPreferences 替換這樣的需求還有很多,於是我們決定自己開發一個Android 平臺 Mock 工具,經過調研之後,我們確定了位元組碼修改的技術方向,通過修改位元組碼實現這樣的需求,由此 DroidAssist 應運而生。
▍示例
下面例子是背景中提到的 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…
同時歡迎加入「DroidAssist 使用者交流群」
請在滴滴技術公眾號後臺回覆「DroidAssist」即可加入
▍END