加快Android編譯速度的技巧總結
對於Android開發者而言,隨著工程不斷的壯大,Android專案的編譯時間也逐漸變長,即便是有時候新增一行程式碼也需要等待好久才能看見期待的效果。之前加快Android編譯的工具相對較少,其中最具有代表性的開源專案當屬FaceBook的Buck和 mmin18的LayoutCast,除此之外還有JRebel 和 Jimulabs。不過前兩天google宣佈推出Instant Run加快Android 編譯速度,相信對其他的工具來說都是一次衝擊,這也是寫這篇文章的動機。
相對於Buck而言,LayoutCast顯得更輕量一些,對專案的侵入性較弱。今年8月份的時候,花了一個星期左右的時間才完成公司的程式碼的適配,對於一些繁重的專案而言,Buck帶來的好處是顯而易見的,但是適配過程中的坑也是很多的。Instant Run 對專案的侵入性其實也是比較大的,但是這些都不需要使用者去操作、配置,所以看起來和LayoutCast一樣屬於輕量型的。
時間去哪了?
Android程式編譯大致過程如圖所示,詳細的過程可以參考gradle 中的tasks。

那麼為什麼我們每次編譯都需要等待那麼久?事實上我們我們可以gradle中新增TaskExecutionListener來監聽gradle指令碼中每個task的執行時間。
class TimingsListener implements TaskExecutionListener, BuildListener { private Clock clock private timings = [] @Override void beforeExecute(Task task) { clock = new org.gradle.util.Clock() } @Override void afterExecute(Task task, TaskState taskState) { def ms = clock.timeInMs timings.add([ms, task.path]) task.project.logger.warn "${task.path} took ${ms}ms" } @Override void buildFinished(BuildResult result) { println "Task timings:" for (timing in timings) { if (timing[0] >= 50) { printf "%7sms %s\n", timing } } } @Override void buildStarted(Gradle gradle) {} @Override void projectsEvaluated(Gradle gradle) {} @Override void projectsLoaded(Gradle gradle) {} @Override void settingsEvaluated(Settings settings) {} } gradle.addListener new TimingsListener()
執行指令碼可以發現主要的費時在dex(包含preDex)以及install這兩個步驟。BUCK和LayoutCast的主要工作也是集中於這些費時的步驟上面。
如何加快?
開發過程中對專案的改動一般分為Java檔案的修改以及資原始檔的修改,這些修改都會涉及到上述的幾個費時步驟,這也就是為什麼即便我們修改一行程式碼也需要編譯很久。
1、Java檔案修改
通常,修改的.java檔案會先經過javac操作生成.class檔案。而後與其他的.class檔案經過dx生成.dex檔案。經過dx的操作很費時,針對這種情況,BUCK、LayoutCast和Instant Run採用了兩種方法來解決。
BUCK
BUCK建立了一套完善的依賴規則以及細化的快取系統來縮減編譯時間,並通過使用三方的dex merege工具將.dex檔案合併的時間複雜度從O(N^2)降到O(NlgN)。

如圖所示,當修改A.java檔案時,只涉及到相應的dx操作以及dex merge操作(紅色部分),這樣就大大的縮減了dx的操作時間。BUCK在依賴規則上狠下功夫推出了ABI,更是進一步的減少了不必要的操作。
LayoutCast
LayoutCast的實現同很多外掛的實現原理差不多,具體分析如下:
在ClassLoader查詢類的時候會先去呼叫BaseDexClassLoader類中的findClass方法。
//----dalvik/system/BaseDexClassLoader.java protected Class<?> findClass(String name) throws ClassNotFoundException { Class clazz = pathList.findClass(name); if (clazz == null) { throw new ClassNotFoundException(name); } return clazz; }
隨後在DexPathList類中根據dexElements來查詢相應的class。
//----dalvik/system/DexPathList.java public Class findClass(String name) { for (Element element : dexElements) { DexFile dex = element.dexFile; if (dex != null) { Class clazz = dex.loadClassBinaryName(name, definingContext); if (clazz != null) { return clazz; } } } return null; }
其中dexElements代表著不同dex檔案。
/** list of dex/resource (class path) elements */ private final Element[] dexElements;
也就是說,在ClassLoader載入類的時候會去按照dexElements中dex檔案的順序依次查詢,如下圖所示,在1.dex中查詢到了A類,那麼就不會再從後面的dex檔案中繼續查詢了。

LayoutCast就是利用這樣的原理,將修改的Java檔案生成dex檔案,並將此dex檔案利用反射的方式插入到dexElements陣列的前面。當然,從Java到dex的過程需要額外的查詢各種依賴包之類的工作,這部分工作在cast.py中實現。
這種方式的實現在ART下是沒有問題的,但是在Dalvik中就會出現IllegalAccessError的問題
java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation dalvik.system.DexFile.defineClass(Native Method) dalvik.system.DexFile.loadClassBinaryName(DexFile.java:211) dalvik.system.DexPathList.findClass(DexPathList.java:315) dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.j
具體的原因以及解決方案可以參考Bugly的文章
Install Run
Install Run 同樣也是生成新的增量dex,但是新增dex中的類和原來的類名有區別。比如說,在修改Hello.java類之後,會生成包含Hello$overide類的dex檔案。
那麼,這個新增的dex檔案中Hello$Override類是如何被呼叫的?
我們先看看原來的Hello.java檔案經過Instant Run 編譯前後的區別:
編譯前的hello.java檔案
public String name(String str) { return str; }
經過Instant Run之後的
---compiled Hello.java public String name(String str) { IncrementalChange var2 = $change; return var2 != null?(String)var2.access$dispatch("name.(Ljava/lang/String;)Ljava/lang/String;", new Object[]{this, str}):str; }
可以看出,如果$change存在的話,就會呼叫$change中相應的函式,那麼我們只需要通過反射將Hello.java中$change欄位改為修改後的Hello$override的類就Ok了。
這也就是為什麼Instant Run並不存在前面說到的IllegalAccessError的問題,並且支援不重啟就能看見修改效果的原因。具體可以看看寒江不釣的部落格
2、Res修改
Resource檔案的修改會涉及到AAPT、ApkBuilder以及最後的Install操作。其中APPT的操作要求比較高,LayoutCast、Instant Run均沒有在這部分進行優化,他們的主要工作在於後面的兩個操作。其主要的思路在於將修改的後的資源利用aapt打包成新的.ap_檔案,並通過反射的方式將原來的資原始檔改為修改後的。
LayoutCast
LayoutCast主要做了兩件事。
修改LayoutInflater服務
對於下面的用法我們並不陌生:
LayoutInflater layoutInflater = LayoutInflater.from(context); View view = layoutInflater.inflate(resourceId, root);
其中LayoutInflater.from的實現是在Context的實現類ContextImp中獲取LAYOUT_INFLATER_SERVICE
系統服務
//---- android/view/LayoutInflater.java public static LayoutInflater from(Context context) { LayoutInflater LayoutInflater = (LayoutInflater)context.getSystemService(Context. LAYOUT_INFLATER_SERVICE); if (LayoutInflater == null) { throw new AssertionError("LayoutInflater not found."); } return LayoutInflater; }
那麼ContextImpl又是如何獲取相應的服務的,檢視ContextImpl類可以發現,
//---- android/app/ContextImpl.java public Object getSystemService(String name) { ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name); return fetcher == null ? null : fetcher.getService(this); }
可以發現呼叫getSystemService的過程是在SYSTEM_SERVICE_MAP
的表中查詢ServiceFetcher
,並返回ServiceFetcher
中的mCachedInstance
。那麼只需要將mCachedInstance
替換為自定義的BootInflater並在BootInflater中完成Resource的Overrirde就可以了,如下圖所示。

修改Resource
我們知道Activity中的通過呼叫getResources()
方法來訪問資源,這實際上是呼叫ContextWrapper類中的getResource()
方法
public Resources getResources(){ return mBase.getResources(); }
LayoutCast中就採用替換mBase為自定義的OverrideContext,並在其中將Resource返回為修改後的Resource。
Instant Run
Instant Run 對資原始檔的處理和LayoutCast基本類似,但是在細節的處理上有所不同,比如Instant Run 通過對ActivityThread
類中的mPackages
和mResourcePackages
的修改來改變LoadedApk
中mResDir
的值。
for (String fieldName : new String[] { "mPackages", "mResourcePackages" }) { Field field = activityThread.getDeclaredField(fieldName); field.setAccessible(true); Object value = field.get(currentActivityThread); for (Map.Entry<String, WeakReference<?>> entry : ((Map)value).entrySet()) { Object loadedApk = ((WeakReference)entry.getValue()).get(); if (loadedApk != null) { if (mApplication.get(loadedApk) == bootstrap) { if (externalResourceFile != null) { mResDir.set(loadedApk, externalResourceFile); } if ((realApplication != null) && (mLoadedApk != null)) { mLoadedApk.set(realApplication, loadedApk); } } } } }
資原始檔修改的處理相對於Java檔案的處理較為複雜,這中間涉及到aapt、attribute唯一性 、ID值一致等問題都增加了資原始檔處理的難度。
總結
總的來說,每種方法都有自己的特色,BUCK依賴於自己強大的快取和依賴管理系統。而LayoutCast和Instant Run相對而言採用了更靈巧的方法。相對而言,Instant Run 憑藉著天然的優勢(和升級後的gradle結合),可以勝LayoutCast一籌,但是LayoutCast這種想法的提出還是很讚的。目前增量的編譯集中在Java檔案的修改,對於Res的修改暫時好像還不支援,這在後續應該會有提升吧。
相關文章
- Android加快編譯速度的另一種方法Android編譯
- android反編譯相關命令總結Android編譯
- Android編譯通過,執行編譯錯誤問題總結Android編譯
- 優化使用kotlin開發Android app的編譯速度優化KotlinAndroidAPP編譯
- 編譯ROCKSDB總結編譯
- Android APK反編譯技巧全講解AndroidAPK編譯
- 加快apk的構建速度,如何把編譯時間從130秒降到17秒(二)APK編譯
- Rust 交叉編譯與條件編譯總結Rust編譯
- Android studio 解決編譯速度慢 Download maven-metadata.xml速度很慢Android編譯MavenXML
- 如何提高 Xcode 的編譯速度XCode編譯
- 編譯器後端總結編譯後端
- [譯]改善 Android Studio 的構建速度Android
- windows10使用DNS優選加快系統執行速度的技巧WindowsDNS
- Android效能最佳化之加快應用啟動速度Android
- JavaScript的工作原理:解析、抽象語法樹(AST)+ 提升編譯速度5個技巧JavaScript抽象語法樹AST編譯
- [譯] 使用自定義檔案模板加快你的應用開發速度
- libusb android ndk編譯--編譯mipsAndroid編譯
- CSS預編譯語言Less的用法總結CSS編譯
- webpack編譯速度提升之DllPluginWeb編譯Plugin
- 使用 ccache 增加 Xcode 編譯速度XCode編譯
- Java動態編譯優化——提升編譯速度(N倍)Java編譯優化
- Assimp Android 編譯Android編譯
- android編譯方法Android編譯
- android 反編譯Android編譯
- 加快Vue專案的開發速度Vue
- 【譯】Web 效能優化:21種優化CSS和加快網站速度的方法Web優化CSS網站
- dll預編譯提高webpack打包速度編譯Web
- flutter 編譯報錯總結(不斷更新)Flutter編譯
- vc-vs2019編譯報錯總結編譯
- Android 增量編譯小解Android編譯
- Android 反編譯指南Android編譯
- Android 編譯優化Android編譯優化
- [20191202]加快scp拷貝速度.txt
- 關於Electron原生模組編譯的一點總結編譯
- 編譯原理第二章學習總結編譯原理
- Java程式碼編寫、程式碼優化技巧總結Java優化
- 新加坡證券交易所利用區塊鏈加快結算速度區塊鏈
- FFmpeg編譯Android使用的so庫編譯Android
- Android 編譯打包的那些疑問Android編譯