转: 加快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/

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

时间: 2024-10-13 16:04:48

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

提升Android编译速度

Android codebase都非常大.编译一次都须要花非常多时间.假设是preloader/lk/bootimage还好,可是Android的话都是非常久. 实际上这个编译时间还是能够进一步缩短! 1. 移除modules_to_check (1). Android默认是全部模块都会编译的(无论需不须要),这个有些冗余,能够通过改动编译系统不编译哪些没实用到的模块 (2). 改动方法:alps/build/core/main.mk files: prebuilt \ $(modules_to

加快android studio 编译速度zz

工程build一次太慢  经过各种搜索 整合以下 仅供参考 1.在下列目录中新建 gradle.properties 文件 /home//.gradle/ (Linux) /Users//.gradle/ (Mac) C:\Users\\.gradle (Windows) 在文件中添加 org.gradle.daemon=true 2.在本地工程的gradle.properties 添加如下 org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -X

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

最近在开发一个大项目的时候遇到一个很头疼的问题,由于项目代码较多,每次都要编译链接1分钟左右,调试的时候很浪费时间,于是研究了一下如何提高编译链接的速度,在这里分享给大家. 提升编译链接的速度主要有以下三个方式: 1. 提高XCode编译时使用的线程数 defaults write com.apple.Xcode PBXNumberOfParallelBuildSubtasks 4 XCode默认使用与CPU核数相同的线程来进行编译,但由于编译过程中的IO操作往往比CPU运算要多,因此适当的提升

如何加快C++代码的编译速度 (转)

C++代码一直以其运行时的高性能高调面对世人, 但是说起编译速度,却只有低调的份了.比如我现在工作的源代码,哪怕使用Incredibuild调动近百台机子,一个完整的build也需要四个小时,恐怖!!!虽然平时开发一般不需要在本地做完整的build,但编译几个相关的工程就够你等上好一段时间的了(老外管这个叫monkey around,相当形象).想想若干年在一台单核2.8GHZ上工作时的场景 - 面前放本书,一点build按钮,就低头读一会书~~~往事不堪回首. 可以想象,如果不加以重视,编译速

如何加快C++代码的编译速度 转

http://www.cnblogs.com/baiyanhuang/archive/2010/01/17/1730717.html C++代码一直以其运行时的高性能高调面对世人, 但是说起编译速度,却只有低调的份了.比如我现在工作的源代码,哪怕使用Incredibuild调动近百台机子,一个完整的build也需要四个小时,恐怖!!!虽然平时开发一般不需要在本地做完整的build,但编译几个相关的工程就够你等上好一段时间的了(老外管这个叫monkey around,相当形象).想想若干年在一台单

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

最近在一个大型项目的开发的时候遇到一个很头疼的问题,由于该项目的代码更,每次建立联系1纪要.浪费时间调试.因此,一些研究如何提高编译链接速度,这里给大家分享. 为了提高编译和链接的是以下三种方式的速度: 1. 增加XCode编译时使用的线程数 defaults write com.apple.Xcode PBXNumberOfParallelBuildSubtasks 4 XCode默认使用与CPU核数同样的线程来进行编译,但因为编译过程中的IO操作往往比CPU运算要多,因此适当的提升线程数能够

加快Android SDK Manager的速度

1.明白Android SDK Manager是什么? Android SDK Manager是用来安装Android SDK的,里面可以选择不同版本的Android 2.如何加快Android SDK Manager的速度? 1)修改C:\Windows\System32\drivers\etc\hosts文件,向文件末尾添加如下红色部分内容: 203.208.46.146 www.google.com 74.125.113.121 developer.android.com 203.208.

提高Android和iOS调试编译速度

如果您使用Delphi开发App,就会遇到:Android和iOS的编译实现是太慢了,尤其是debug.这个小技巧使得编译Debug能快不少. 可能你已经知道了这个方法,如果这样设置,那一定为之开心. 您认为Release版本比Debug版本编译需要更长时间,因为它必须执行一堆代码优化阶段,告诉你,实际情况与此相反,在Debug模式下构建适用于iOS或Android的我的应用程序大概需要7.5 分钟,在Release模式下构建相同的应用程序不到1分钟.(当然,与建立相同的Windows应用程序相

【转】实践最有效的提高Android Studio运行、编译速度方案

原文:https://blog.csdn.net/xwh_1230/article/details/60961723 实践最有效的提高Android Studio运行.编译速度方案 最有效提升Android studio编译速度的方式:提升电脑配置!-- 鲁迅 鲁迅所说确实是实情,提升配置是最有效的方式,但对于我等屌丝来说,有时候很难办到,因此我们接着看下面的优化. 一.安装时的问题 1.安装完成后启动卡死 刚刚打开studio就卡在gradle building的界面再也不动了(去连接墙外的网