加快Android编译速度的技巧总结

2024-08-30 15:58

本文主要是介绍加快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 clockprivate timings = []@Overridevoid beforeExecute(Task task) {clock = new org.gradle.util.Clock()}@Overridevoid afterExecute(Task task, TaskState taskState) {def ms = clock.timeInMstimings.add([ms, task.path])task.project.logger.warn "${task.path} took ${ms}ms"}@Overridevoid buildFinished(BuildResult result) {println "Task timings:"for (timing in timings) {if (timing[0] >= 50) {printf "%7sms  %s\n", timing}}}@Overridevoid buildStarted(Gradle gradle) {}@Overridevoid projectsEvaluated(Gradle gradle) {}@Overridevoid projectsLoaded(Gradle gradle) {}@Overridevoid 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的修改暂时好像还不支持,这在后续应该会有提升吧。


转自:http://www.codeceo.com/article/android-fast-compile.html

这篇关于加快Android编译速度的技巧总结的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1121268

相关文章

Qt实现网络数据解析的方法总结

《Qt实现网络数据解析的方法总结》在Qt中解析网络数据通常涉及接收原始字节流,并将其转换为有意义的应用层数据,这篇文章为大家介绍了详细步骤和示例,感兴趣的小伙伴可以了解下... 目录1. 网络数据接收2. 缓冲区管理(处理粘包/拆包)3. 常见数据格式解析3.1 jsON解析3.2 XML解析3.3 自定义

快速修复一个Panic的Linux内核的技巧

《快速修复一个Panic的Linux内核的技巧》Linux系统中运行了不当的mkinitcpio操作导致内核文件不能正常工作,重启的时候,内核启动中止于Panic状态,该怎么解决这个问题呢?下面我们就... 感谢China编程(www.chinasem.cn)网友 鸢一雨音 的投稿写这篇文章是有原因的。为了配置完

Android使用ImageView.ScaleType实现图片的缩放与裁剪功能

《Android使用ImageView.ScaleType实现图片的缩放与裁剪功能》ImageView是最常用的控件之一,它用于展示各种类型的图片,为了能够根据需求调整图片的显示效果,Android提... 目录什么是 ImageView.ScaleType?FIT_XYFIT_STARTFIT_CENTE

Python ZIP文件操作技巧详解

《PythonZIP文件操作技巧详解》在数据处理和系统开发中,ZIP文件操作是开发者必须掌握的核心技能,Python标准库提供的zipfile模块以简洁的API和跨平台特性,成为处理ZIP文件的首选... 目录一、ZIP文件操作基础三板斧1.1 创建压缩包1.2 解压操作1.3 文件遍历与信息获取二、进阶技

Python实现图片分割的多种方法总结

《Python实现图片分割的多种方法总结》图片分割是图像处理中的一个重要任务,它的目标是将图像划分为多个区域或者对象,本文为大家整理了一些常用的分割方法,大家可以根据需求自行选择... 目录1. 基于传统图像处理的分割方法(1) 使用固定阈值分割图片(2) 自适应阈值分割(3) 使用图像边缘检测分割(4)

Android实现在线预览office文档的示例详解

《Android实现在线预览office文档的示例详解》在移动端展示在线Office文档(如Word、Excel、PPT)是一项常见需求,这篇文章为大家重点介绍了两种方案的实现方法,希望对大家有一定的... 目录一、项目概述二、相关技术知识三、实现思路3.1 方案一:WebView + Office Onl

Android实现两台手机屏幕共享和远程控制功能

《Android实现两台手机屏幕共享和远程控制功能》在远程协助、在线教学、技术支持等多种场景下,实时获得另一部移动设备的屏幕画面,并对其进行操作,具有极高的应用价值,本项目旨在实现两台Android手... 目录一、项目概述二、相关知识2.1 MediaProjection API2.2 Socket 网络

Windows Docker端口占用错误及解决方案总结

《WindowsDocker端口占用错误及解决方案总结》在Windows环境下使用Docker容器时,端口占用错误是开发和运维中常见且棘手的问题,本文将深入剖析该问题的成因,介绍如何通过查看端口分配... 目录引言Windows docker 端口占用错误及解决方案汇总端口冲突形成原因解析诊断当前端口情况解

Android实现悬浮按钮功能

《Android实现悬浮按钮功能》在很多场景中,我们希望在应用或系统任意界面上都能看到一个小的“悬浮按钮”(FloatingButton),用来快速启动工具、展示未读信息或快捷操作,所以本文给大家介绍... 目录一、项目概述二、相关技术知识三、实现思路四、整合代码4.1 Java 代码(MainActivi

Java字符串操作技巧之语法、示例与应用场景分析

《Java字符串操作技巧之语法、示例与应用场景分析》在Java算法题和日常开发中,字符串处理是必备的核心技能,本文全面梳理Java中字符串的常用操作语法,结合代码示例、应用场景和避坑指南,可快速掌握字... 目录引言1. 基础操作1.1 创建字符串1.2 获取长度1.3 访问字符2. 字符串处理2.1 子字