转: 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编译速度

发表于 2015-11-25   |  

对于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的执行时间

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文件的修改以及资源文件的修改,这些修改都会涉及到上述的几个费时步骤,这也就是为什么即便我们修改一行代码也需要编译很久。

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方法。

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文件中继续查找了。

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的问题,并且支持不重启就能看见修改效果的原因。具体可以看看寒江不钓的博客

2、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就可以了,如下图所示。

修改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的修改暂时好像还不支持,这在后续应该会有提升吧。

转: 加快Android编译速度的更多相关文章

  1. 提升Android编译速度

    Android codebase都非常大.编译一次都须要花非常多时间.假设是preloader/lk/bootimage还好,可是Android的话都是非常久. 实际上这个编译时间还是能够进一步缩短! ...

  2. 使用ant优化android项目编译速度,提高工作效率

    1.Android项目编译周期长,编译项目命令取消困难 2.在进行Android项目的编译的同时,Eclipse锁定工作区不能进行修改操作 3.在只进行资源文件的修改时,Eclipse对资源文件的修改 ...

  3. 加快Android Studio的编译速度

    从Eclipse切换到Android Studio后,感觉Android Studio的build速度比Eclipse慢很多,以下几个方法可以提高Android Studio的编译速度 使用Gradl ...

  4. 加快android studio 编译速度

    工程build一次太慢  经过各种搜索 整合以下 仅供参考 1.在下列目录中新建 gradle.properties 文件 /home//.gradle/ (Linux) /Users//.gradl ...

  5. 加速 Android Studio 的编译速度 2.2

    Android studio 2.2 当中有一项新的功能:Dex In Process. 这项功能可以动态的加快编译速度,以及提高Instant Run 的效率. 那么怎么来使用这项新功能呢?你只需要 ...

  6. Android Studio优化编译速度

    随着Android Studio的不断完善,其安卓开发者阵营也基本从Eclipse转移到了Android Studio,毕竟Android Studio是谷歌亲力亲为开发的官方开发软件.不过其最重要的 ...

  7. Android提升Gradle编译速度或减少Gradle编译时间.md

    目录 Android如何提升Gradle编译速度或减少Gradle编译时间 最终优化方案 优化效果比对 将所有项目源码,各种缓存临时目录都移动到高性能SSD磁盘上 gradle.properties ...

  8. 加快XCode的编译链接速度(200%+)—XCode编译速度慢的解决方案

    最近在开发一个大项目的时候遇到一个很头疼的问题,由于项目代码较多,每次都要编译链接1分钟左右,调试的时候很浪费时间,于是研究了一下如何提高编译链接的速度,在这里分享给大家. 提升编译链接的速度主要有以 ...

  9. 加快XCode编译链接速度(200%+)—XCode编译慢液

    最近在一个大型项目的开发的时候遇到一个很头疼的问题,由于该项目的代码更,每次建立联系1纪要.浪费时间调试.因此,一些研究如何提高编译链接速度,这里给大家分享. 为了提高编译和链接的是以下三种方式的速度 ...

随机推荐

  1. 友盟移动开发平台.NET版本SDK

    由于项目需要给安卓.ios提供提送消息服务.找到了umeng这个平台,官方竟然没有提供.net版本的SDK,同时项目需要就拿出来和大家分享一下需要的同学们可以做个参考,建议官方提供.net版本. 这里 ...

  2. 345. Reverse Vowels of a String【Easy】【双指针-反转字符串中的元音字符】

    Write a function that takes a string as input and reverse only the vowels of a string. Example 1: In ...

  3. Nginx反代,后端一个IP绑定多个SSL证书,导致连接失败之解决方法:HTTPS和SNI扩展

    默认:SSL协议进行握手协商进行连接的时候,默认是不会发送主机名的,也就是是以IP的形式来进行https连接握手协商的,这就导致一个问题,当一台服务器上有多个虚拟主机使用同一个IP的时候, Nginx ...

  4. Sqli-labs less 14

    Less-14 本关我们直接进行测试,输入username:admin" Pasword:(随意) 可以看到报错了,那么我们知道了id进行了 " 的操作. 这里和less13一样, ...

  5. Java 生产者消费者 & 例题

    Queue http://m635674608.iteye.com/blog/1739860 http://www.iteye.com/problems/84758 http://blog.csdn. ...

  6. Minimum Height Trees -- LeetCode

    For a undirected graph with tree characteristics, we can choose any node as the root. The result gra ...

  7. 数据库SQL归纳(三)

    数据查询功能 单表查询 选择若干列 1. 指定列 SELECT 列名称 FROM 表名称 2. 全部列 SELECT * FROM 表名称 3. 经过计算的列 SELECT Sname, 2019-S ...

  8. BZOJ 2157 旅游(动态树)

    [题目链接] http://www.lydsy.com/JudgeOnline/problem.php?id=2157 [题目大意] 支持修改边,链上查询最大值最小值总和,以及链上求相反数 [题解] ...

  9. Problem C: 零起点学算法93——矩阵转置

    #include<stdio.h> int main() { ][],b[][]; while(scanf("%d%d",&n,&m)!=EOF) { ...

  10. (Mark)JS中关于闭包

    闭包(Closures) 在ECMAScript中,函数是“第一类”对象.这个名词意味着函数可以作为参数被传递给其他函数使用 (在这种情况下,函数被称为“funargs”——“functional a ...