转: 加快Android编译速度

转: http://timeszoro.xyz/2015/11/25/%E5%8A%A0%E5%BF%ABandroid%E7%BC%96%E8%AF%91%E9%80%9F%E5%BA%A6/java

加快Android编译速度

对于Android开发者而言,随着工程不断的壮大,Android项目的编译时间也逐渐变长,即使是有时候添加一行代码也须要等待很久才能看见期待的效果。以前加快Android编译的工具相对较少,其中最具备表明性的开源项目当属FaceBook的Buck和 mmin18的LayoutCast,除此以外还有JRebel 和 Jimulabs。不过前两天google宣布推出Instant Run加快Android 编译速度,相信对其余的工具来讲都是一次冲击,这也是写这篇文章的动机。android

相对于Buck而言,LayoutCast显得更轻量一些,对项目的侵入性较弱。今年8月份的时候,花了一个星期左右的时间才完成公司的代码的适配,对于一些繁重的项目而言,Buck带来的好处是显而易见的,可是适配过程当中的坑也是不少的。Instant Run 对项目的侵入性其实也是比较大的,可是这些都不须要用户去操做、配置,因此看起来和LayoutCast同样属于轻量型的。git

时间去哪了?

Android程序编译大体过程如图所示,详细的过程能够参考gradle 中的tasks。github


编译过程

那么为何咱们每次编译都须要等待那么久?事实上咱们咱们能够gradle中添加TaskExecutionListener来监听gradle脚本中每一个task的执行时间bootstrap

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()

执行脚本能够发现主要的费时在dex(包含preDex)以及install这两个步骤。BUCK和LayoutCast的主要工做也是集中于这些费时的步骤上面。数组

如何加快?

开发过程当中对项目的改动通常分为Java文件的修改以及资源文件的修改,这些修改都会涉及到上述的几个费时步骤,这也就是为何即使咱们修改一行代码也须要编译好久。缓存

一、Java文件修改

一般,修改的.java文件会先通过javac操做生成.class文件。然后与其余的.class文件通过dx生成.dex文件。通过dx的操做很费时,针对这种状况,BUCK、LayoutCast和Instant Run采用了两种方法来解决。app

BUCK

BUCK创建了一套完善的依赖规则以及细化的缓存系统来缩减编译时间,并经过使用三方的dex merege工具将.dex文件合并的时间复杂度从O(N^2)降到O(NlgN)。ide


Buck Dex 过程

如图所示,当修改A.java文件时,只涉及到相应的dx操做以及dex merge操做(红色部分),这样就大大的缩减了dx的操做时间。BUCK在依赖规则上狠下功夫推出了ABI,更是进一步的减小了没必要要的操做。函数

LayoutCast

LayoutCast的实现同不少插件的实现原理差很少,具体分析以下:

在ClassLoader查找类的时候会先去调用BaseDexClassLoader类中的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类中根据dexElements来查找相应的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;

也就是说,在ClassLoader加载类的时候会去按照dexElements中dex文件的顺序依次查找,以下图所示,在1.dex中查找到了A类,那么就不会再从后面的dex文件中继续查找了。


插入dex原理

LayoutCast就是利用这样的原理,将修改的Java文件生成dex文件,并将此dex文件利用反射的方式插入到dexElements数组的前面。固然,从Java到dex的过程须要额外的查找各类依赖包之类的工做,这部分工做在cast.py中实现。

这种方式的实如今ART下是没有问题的,可是在Dalvik中就会出现IllegalAccessError的问题

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中的类和原来的类名有区别。好比说,在修改Hello.java类以后,会生成包含Hello$overide类的dex文件。

那么,这个新增的dex文件中Hello$Override类是如何被调用的?

咱们先看看原来的Hello.java文件通过Instant Run 编译先后的区别:

编译前的hello.java文件

1
2
3
public String name(String str) {
return str;
}

通过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存在的话,就会调用$change中相应的函数,那么咱们只须要经过反射将Hello.java中$change字段改成修改后的Hello$override的类就Ok了。
这也就是为何Instant Run并不存在前面说到的IllegalAccessError的问题,而且支持不重启就能看见修改效果的缘由。具体能够看看寒江不钓的博客

二、Res修改

Resource文件的修改会涉及到AAPT、ApkBuilder以及最后的Install操做。其中APPT的操做要求比较高,LayoutCast、Instant Run均没有在这部分进行优化,他们的主要工做在于后面的两个操做。其主要的思路在于将修改的后的资源利用aapt打包成新的.ap_文件,并经过反射的方式将原来的资源文件改成修改后的。

LayoutCast

LayoutCast主要作了两件事。

修改LayoutInflater服务

对于下面的用法咱们并不陌生:

1
2
LayoutInflater layoutInflater = LayoutInflater.from(context);
View view = layoutInflater.inflate(resourceId, root);

其中LayoutInflater.from的实现是在Context的实现类ContextImp中获取LAYOUT_INFLATER_SERVICE系统服务

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又是如何获取相应的服务的,查看ContextImpl类能够发现,

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);
}

能够发现调用getSystemService的过程是在SYSTEM_SERVICE_MAP的表中查找ServiceFetcher,并返回ServiceFetcher中的mCachedInstance。那么只须要将mCachedInstance替换为自定义的BootInflater并在BootInflater中完成Resource的Overrirde就能够了,以下图所示。


插入dex原理

修改Resource

咱们知道Activity中的经过调用getResources()方法来访问资源,这其实是调用ContextWrapper类中的getResource()方法

1
2
3
public Resources getResources(){
return mBase.getResources();
}

LayoutCast中就采用替换mBase为自定义的OverrideContext,并在其中将Resource返回为修改后的Resource。

Instant Run

Instant Run 对资源文件的处理和LayoutCast基本相似,可是在细节的处理上有所不一样,好比Instant Run 经过对ActivityThread类中的mPackagesmResourcePackages的修改来改变LoadedApkmResDir的值。

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);
}
}
}
}
}

资源文件修改的处理相对于Java文件的处理较为复杂,这中间涉及到aapt、attribute惟一性 、ID值一致等问题都增长了资源文件处理的难度。

总结

总的来讲,每种方法都有本身的特点,BUCK依赖于本身强大的缓存和依赖管理系统。而LayoutCast和Instant Run相对而言采用了更灵巧的方法。相对而言,Instant Run 凭借着自然的优点(和升级后的gradle结合),能够胜LayoutCast一筹,可是LayoutCast这种想法的提出仍是很赞的。目前增量的编译集中在Java文件的修改,对于Res的修改暂时好像还不支持,这在后续应该会有提高吧。