转: 加快Android编译速度
轉(zhuǎn): http://timeszoro.xyz/2015/11/25/%E5%8A%A0%E5%BF%ABandroid%E7%BC%96%E8%AF%91%E9%80%9F%E5%BA%A6/
加快Android編譯速度
發(fā)表于?2015-11-25?? | ?對(duì)于Android開(kāi)發(fā)者而言,隨著工程不斷的壯大,Android項(xiàng)目的編譯時(shí)間也逐漸變長(zhǎng),即便是有時(shí)候添加一行代碼也需要等待好久才能看見(jiàn)期待的效果。之前加快Android編譯的工具相對(duì)較少,其中最具有代表性的開(kāi)源項(xiàng)目當(dāng)屬FaceBook的Buck和 mmin18的LayoutCast,除此之外還有JRebel?和?Jimulabs。不過(guò)前兩天google宣布推出Instant Run加快Android 編譯速度,相信對(duì)其他的工具來(lái)說(shuō)都是一次沖擊,這也是寫(xiě)這篇文章的動(dòng)機(jī)。
相對(duì)于Buck而言,LayoutCast顯得更輕量一些,對(duì)項(xiàng)目的侵入性較弱。今年8月份的時(shí)候,花了一個(gè)星期左右的時(shí)間才完成公司的代碼的適配,對(duì)于一些繁重的項(xiàng)目而言,Buck帶來(lái)的好處是顯而易見(jiàn)的,但是適配過(guò)程中的坑也是很多的。Instant Run 對(duì)項(xiàng)目的侵入性其實(shí)也是比較大的,但是這些都不需要用戶(hù)去操作、配置,所以看起來(lái)和LayoutCast一樣屬于輕量型的。
時(shí)間去哪了?
Android程序編譯大致過(guò)程如圖所示,詳細(xì)的過(guò)程可以參考gradle 中的tasks。
那么為什么我們每次編譯都需要等待那么久?事實(shí)上我們我們可以gradle中添加TaskExecutionListener來(lái)監(jiān)聽(tīng)gradle腳本中每個(gè)task的執(zhí)行時(shí)間。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | 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() |
執(zhí)行腳本可以發(fā)現(xiàn)主要的費(fèi)時(shí)在dex(包含preDex)以及install這兩個(gè)步驟。BUCK和LayoutCast的主要工作也是集中于這些費(fèi)時(shí)的步驟上面。
如何加快?
開(kāi)發(fā)過(guò)程中對(duì)項(xiàng)目的改動(dòng)一般分為Java文件的修改以及資源文件的修改,這些修改都會(huì)涉及到上述的幾個(gè)費(fèi)時(shí)步驟,這也就是為什么即便我們修改一行代碼也需要編譯很久。
1、Java文件修改
通常,修改的.java文件會(huì)先經(jīng)過(guò)javac操作生成.class文件。而后與其他的.class文件經(jīng)過(guò)dx生成.dex文件。經(jīng)過(guò)dx的操作很費(fèi)時(shí),針對(duì)這種情況,BUCK、LayoutCast和Instant Run采用了兩種方法來(lái)解決。
BUCK
BUCK建立了一套完善的依賴(lài)規(guī)則以及細(xì)化的緩存系統(tǒng)來(lái)縮減編譯時(shí)間,并通過(guò)使用三方的dex merege工具將.dex文件合并的時(shí)間復(fù)雜度從O(N^2)降到O(NlgN)。
如圖所示,當(dāng)修改A.java文件時(shí),只涉及到相應(yīng)的dx操作以及dex merge操作(紅色部分),這樣就大大的縮減了dx的操作時(shí)間。BUCK在依賴(lài)規(guī)則上狠下功夫推出了ABI,更是進(jìn)一步的減少了不必要的操作。
LayoutCast
LayoutCast的實(shí)現(xiàn)同很多插件的實(shí)現(xiàn)原理差不多,具體分析如下:
在ClassLoader查找類(lèi)的時(shí)候會(huì)先去調(diào)用BaseDexClassLoader類(lèi)中的findClass方法。
| 1 2 3 4 5 6 7 8 | //----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類(lèi)中根據(jù)dexElements來(lái)查找相應(yīng)的class。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | //----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文件。
| 1 2 | /** list of dex/resource (class path) elements */ private final Element[] dexElements; |
也就是說(shuō),在ClassLoader加載類(lèi)的時(shí)候會(huì)去按照dexElements中dex文件的順序依次查找,如下圖所示,在1.dex中查找到了A類(lèi),那么就不會(huì)再?gòu)暮竺娴膁ex文件中繼續(xù)查找了。
LayoutCast就是利用這樣的原理,將修改的Java文件生成dex文件,并將此dex文件利用反射的方式插入到dexElements數(shù)組的前面。當(dāng)然,從Java到dex的過(guò)程需要額外的查找各種依賴(lài)包之類(lèi)的工作,這部分工作在cast.py中實(shí)現(xiàn)。
這種方式的實(shí)現(xiàn)在ART下是沒(méi)有問(wèn)題的,但是在Dalvik中就會(huì)出現(xiàn)IllegalAccessError的問(wèn)題
| 1 2 3 4 5 | 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中的類(lèi)和原來(lái)的類(lèi)名有區(qū)別。比如說(shuō),在修改Hello.java類(lèi)之后,會(huì)生成包含Hello$overide類(lèi)的dex文件。
那么,這個(gè)新增的dex文件中Hello$Override類(lèi)是如何被調(diào)用的?
我們先看看原來(lái)的Hello.java文件經(jīng)過(guò)Instant Run 編譯前后的區(qū)別:
編譯前的hello.java文件
| 1 2 3 | public String name(String str) { return str; } |
經(jīng)過(guò)Instant Run之后的
| 1 2 3 4 5 | ---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存在的話,就會(huì)調(diào)用$change中相應(yīng)的函數(shù),那么我們只需要通過(guò)反射將Hello.java中$change字段改為修改后的Hello$override的類(lèi)就Ok了。
這也就是為什么Instant Run并不存在前面說(shuō)到的IllegalAccessError的問(wèn)題,并且支持不重啟就能看見(jiàn)修改效果的原因。具體可以看看寒江不釣的博客
2、Res修改
Resource文件的修改會(huì)涉及到AAPT、ApkBuilder以及最后的Install操作。其中APPT的操作要求比較高,LayoutCast、Instant Run均沒(méi)有在這部分進(jìn)行優(yōu)化,他們的主要工作在于后面的兩個(gè)操作。其主要的思路在于將修改的后的資源利用aapt打包成新的.ap_文件,并通過(guò)反射的方式將原來(lái)的資源文件改為修改后的。
LayoutCast
LayoutCast主要做了兩件事。
修改LayoutInflater服務(wù)
對(duì)于下面的用法我們并不陌生:
| 1 2 | LayoutInflater layoutInflater = LayoutInflater.from(context); View view = layoutInflater.inflate(resourceId, root); |
其中LayoutInflater.from的實(shí)現(xiàn)是在Context的實(shí)現(xiàn)類(lèi)ContextImp中獲取LAYOUT_INFLATER_SERVICE系統(tǒng)服務(wù)
| 1 2 3 4 5 6 7 8 9 10 | //---- 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又是如何獲取相應(yīng)的服務(wù)的,查看ContextImpl類(lèi)可以發(fā)現(xiàn),
| 1 2 3 4 5 | //---- android/app/ContextImpl.java public Object getSystemService(String name) { ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name); return fetcher == null ? null : fetcher.getService(this); } |
可以發(fā)現(xiàn)調(diào)用getSystemService的過(guò)程是在SYSTEM_SERVICE_MAP的表中查找ServiceFetcher,并返回ServiceFetcher中的mCachedInstance。那么只需要將mCachedInstance替換為自定義的BootInflater并在BootInflater中完成Resource的Overrirde就可以了,如下圖所示。
修改Resource
我們知道Activity中的通過(guò)調(diào)用getResources()方法來(lái)訪問(wèn)資源,這實(shí)際上是調(diào)用ContextWrapper類(lèi)中的getResource()方法
| 1 2 3 | public Resources getResources(){ return mBase.getResources(); } |
LayoutCast中就采用替換mBase為自定義的OverrideContext,并在其中將Resource返回為修改后的Resource。
Instant Run
Instant Run 對(duì)資源文件的處理和LayoutCast基本類(lèi)似,但是在細(xì)節(jié)的處理上有所不同,比如Instant Run 通過(guò)對(duì)ActivityThread類(lèi)中的mPackages和mResourcePackages的修改來(lái)改變LoadedApk中mResDir的值。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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); } } } } } |
資源文件修改的處理相對(duì)于Java文件的處理較為復(fù)雜,這中間涉及到aapt、attribute唯一性 、ID值一致等問(wèn)題都增加了資源文件處理的難度。
總結(jié)
總的來(lái)說(shuō),每種方法都有自己的特色,BUCK依賴(lài)于自己強(qiáng)大的緩存和依賴(lài)管理系統(tǒng)。而LayoutCast和Instant Run相對(duì)而言采用了更靈巧的方法。相對(duì)而言,Instant Run 憑借著天然的優(yōu)勢(shì)(和升級(jí)后的gradle結(jié)合),可以勝LayoutCast一籌,但是LayoutCast這種想法的提出還是很贊的。目前增量的編譯集中在Java文件的修改,對(duì)于Res的修改暫時(shí)好像還不支持,這在后續(xù)應(yīng)該會(huì)有提升吧。
總結(jié)
以上是生活随笔為你收集整理的转: 加快Android编译速度的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: python写一段脚本代码自动完成输入(
- 下一篇: Android 布局练习