转: 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的执行时间。

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

12345678
//----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。

12345678910111213
//----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文件。

12
/** 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的问题

12345
java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementationdalvik.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文件

123
public String name(String str) { return str;}

经过Instant Run之后的

12345
---compiled  Hello.javapublic 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服务

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

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

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

12345678910
//----  android/view/LayoutInflater.javapublic 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类可以发现,

12345
//---- android/app/ContextImpl.javapublic 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()方法

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

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

Instant Run

Instant Run 对资源文件的处理和LayoutCast基本类似,但是在细节的处理上有所不同,比如Instant Run 通过对ActivityThread类中的mPackagesmResourcePackages的修改来改变LoadedApkmResDir的值。

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

    一.修改运行内存 进入项目,菜单栏-help-Edit Custom VM Option Paste_Image.png 添加或修改为: -Xms2048m -Xmx2048m -XX:MaxPerm ...

  2. 美图Android编译速度优化实践指南

    分享嘉宾:张仙华 美图 资深开发工程师 分享嘉宾:张仙华,美图秀秀android团队资深研发工程师,负责编译加速.性能优化.架构设计等公共基础相关工作 导读:本文的主题是美图秀秀的Android编译速 ...

  3. 利用Injection插件加快Xcode编译速度

    我们在调试iOS原生代码时,每次修改都需要Command+R来重新编译运行.当项目代码量很大,编译时间就会很漫长.因此对于开发中来说,如果能加快编译速度,能大大提高生产效率.如果我们能像Swift P ...

  4. vs怎么更改编译的堆空间_再见吧 buildSrc, 拥抱 Composing builds 提升 Android 编译速度...

    前言 长期以来困扰我们的一个问题就是构建速度,AndroidStudio 的构建速度严重影响 Android 开发者的工作效率,尤其是更新一个版本号,导致整个项目重新构建,在网络慢的情况下,这是无法忍 ...

  5. Android 编译速度优化黑科技 - RocketX

    一.背景描述 二.效果展示 三.思路问题分析与模块搭建 四.问题解决与实现 五.一天一个小惊喜 六.下一步展望 一.背景描述 在项目体量越来越大的情况下,编译速度也随着增长,有时候一个修改需要等待长达 ...

  6. 再见吧 buildSrc, 拥抱 Composing builds 提升 Android 编译速度

    前言 长期以来困扰我们的一个问题就是构建速度,AndroidStudio 的构建速度严重影响 Android 开发者的工作效率,尤其是更新一个版本号,导致整个项目重新构建,在网络慢的情况下,这是无法忍 ...

  7. [CB]加快BCB编译速度

    我玩电脑有3年了,被比尔盖次折磨成了老手. 做为老手最讨厌重装系统尤其是格式化后得化6个小时完全装好. 所以就备份System目录然后备份注册表,每装一个软件备份一次注册表,每各一星期再备份一次注册表 ...

  8. VS加快程序编译速度——以VS2013为例

    目录 前言 方法介绍: 附录 原文: https://blog.csdn.net/amusi1994/article/details/53679140 前言 由于当前项目包含了很大.cpp文件,以至于 ...

  9. 加快linux编译速度,Linux Makefile 编译速度的优化【转】

    前言 如何提高 Makefile 的编译速度呢?既然是提高,那肯定是对于大项目而言,因为小项目本身源文件不多,Makefile 优化与否,对于编译速度而言,影响并不大.当然对于那些追求速度达到极致的人 ...

最新文章

  1. 基于python的证件照_20行代码教你用python给证件照换底色的方法示例
  2. Machine Learning | (9) 回归算法-线性回归
  3. R绘制发散型条形图(Diverging Bars)
  4. wireshark协议解析器原理与插件编写
  5. Android各种各样的Drawable-更新中
  6. VTK:图表之RandomGraphSource
  7. IIS连接数、IIS并发连接数、IIS最大并发工作线程数、应用程序池的队列长度、应用程序池的...
  8. MFC关键技术-动态创建
  9. urllib2.URLError: urlopen error [Errno 111] Connection refused
  10. word 代码_怎样在Word 中插入 C/C++ 代码
  11. Python办公自动化(七)|自动更新不对称表格
  12. 程序员求职之道(《程序员面试笔试宝典》)之程序设计基础(static的使用)?...
  13. python简明教程中文pdf-《python简明教程中文》.pdf
  14. 鼓形齿的计算机械设计标准,鼓形齿设计主要尺寸计算
  15. UltraEdit,无法卸载,无法安装
  16. CCNA题库第一部分
  17. 搭建和配置支撑2000人同时观看的流媒体服务器系统(Linux步骤详解)
  18. Druid加载(load data)HDFS文件数据
  19. 关于通过前端xslx解析excel日期少一天原因
  20. error: ‘stol’ was not declared in this scope

热门文章

  1. 二维burgers方程_二维Burgers方程的RKDG有限元解法
  2. php tp5清空数据表并主键,tp5数据库——更新数据
  3. linux远程监控毕业设计,毕业设计论文:基于嵌入式Linux远程监控系统的设计与实现.doc...
  4. eclipse指定JDK版本启动,解决version XXX of the JVM is not suitable for this product.Version:XXX 问题
  5. Eclipse 答疑:Eclipse 如何设置 Java 代码自动提示和自动补全?
  6. php 复选框全选和取消,基于JavaScript实现复选框的全选和取消全选
  7. fdtd中时间监视器怎么放_利用FDTD软件仿真拓扑光子(六)-单向传播仿真与软件设置...
  8. Ubuntu 16.04卸载火狐浏览器
  9. java中paint方法和paintComponent方法的不同
  10. flume avro java 发送数据_flume将数据发送到kafka、hdfs、hive、http、netcat等模式的使用总结...