Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android資訊 >> 加快Android編譯速度的技巧總結

加快Android編譯速度的技巧總結

編輯: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)。

Buck Dex 過程

如圖所示,當修改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文件中繼續查找了。

插入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就可以了,如下圖所示。

插入dex原理

修改Resource

我們知道Activity中的通過調用getResources()方法來訪問資源,這實際上是調用ContextWrapper類中的getResource()方法

public Resources getResources(){
         return mBase.getResources();
}

LayoutCast中就采用替換mBase為自定義的OverrideContext,並在其中將Resource返回為修改後的Resource。

Instant Run

Instant Run 對資源文件的處理和LayoutCast基本類似,但是在細節的處理上有所不同,比如Instant Run 通過對ActivityThread類中的mPackagesmResourcePackages的修改來改變LoadedApkmResDir的值。

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的修改暫時好像還不支持,這在後續應該會有提升吧。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved