动手点关注 干货不迷路
本文分析了 Android Studio Sync 在 Gradle 层面的底层逻辑,并且从原理出发介绍了 DevOps - Build 团队 Gradle Sync 优化框架的实现细节以及在飞书项目中进行 Sync 优化的实战经验。
作为 Android 开发者,我们在使用 Android Studio 时,离不开名为 Sync 的操作:代码索引、自动补全等功能均需通过成功的 Sync 过程方可使用。以飞书工程 2021 年 10 月为例,一个月内共触发 Sync 3805 次,日均触发 172.95 次。
然而这样一个日常开发中的高频操作其平均耗时近 3min,远超同期飞书增量编译的平均耗时(1.77min)。如果能够将 Sync 操作的耗时缩短,显然能够大幅提升工程师的开发效率。
若要优化 Sync 过程,首先要理解在 Android Studio 中点击 Gradle Sync 按钮后,都发生了什么。
从用户视角看,项目的全部信息均存储于工程的代码中,而 Android Studio 需要做的事情便是将这些信息转化为 GUI 层面可视元素。由于涉及到获取工程源代码和依赖等信息,最直接的想法便是通过构建系统来获得上述信息,而目前 Android 工程普遍使用的构建系统 Gradle 恰巧提供了该能力,那便是 Tooling API。
Gradle 提供了名为 Tooling API 的编程 API,开发者可以通过它来将 Gradle 嵌入自己的软件中。使用该 API 可以触发并监听构建,还可以查询构建中的运行时信息。
以 Android Studio 为例,用户在 Android Studio 中点击了 Sync 按钮后,Android Studio 便调用 Tooling API 触发了一次 Gradle 构建过程,并通过 Tooling API 获取 Gradle 构建过程中可以拿到的详细信息(例如 Gradle 版本、模块名称、依赖等等),Tooling API 成为了 Android Studio 和 Gradle 两个 Java 进程之间的桥梁。
Android Studio 与 Gradle 之间的通信类似于常见的 Client - Server 模型:
Android Studio 作为 Client 发起请求,传递参数告知 Server 自己需要哪些数据
Gradle 进程作为 Server,通过一次构建过程收集 Client 所需的信息并回传
Client 和 Server 两个进程间通过 Socket 进行通信,Tooling API 则是提供给 Client 端调用的对于通信过程的封装
既然涉及到两个进程之间的通信,便需要一套能够使双方能够相互理解的协议,在 Gradle 中该协议称之为 Tooling Model。
为了方便大家理解,这里使用一个示例工程来展示上述进程间通信。该示例工程是一个通过 Tooling API 获取目标 Gradle 项目中 Gradle Plugin 插件名称并打印的 Java 应用。
由于 Client 与 Server 均为 Java 应用,我们可以直接通过接口来定义二者之间的数据格式,接口类对于 Client 与 Server 均可见,而接口的实现类则可以仅对实际生产数据的 Server 端可见即可,这也符合面向接口编程的设计理念。
在这里,作为数据格式接口类如下:
public interface GradlePluginModel {
// 目标工程的 Gradle Plugin 名称列表
List getPlugins();
}
由于涉及在进程间传递,实际传递的实现类的对象需要进行序列化与反序列化,所以该接口的实现类也实现了 java.io.Serializable
接口:
public class GradlePluginModelImpl implements GradlePluginModel, Serializable {
public static final long serialVersionUID = 42L;
private List plugins;
public GradlePluginModelImpl(List plugins) {
this.plugins = plugins;
}
@Override
public List getPlugins() {
return plugins;
}
}
实际获取数据的逻辑运行于 Gradle 进程中,需要以 Gradle 插件的形式应用于目标 Gradle 工程中。
在示例工程中,为了简化流程,进行数据获取的 Gradle 插件直接在目标工程的buildSrc
中定义并在 build.gradle
中进行应用:
apply plugin: GradlePlugin
...
实际上,Client 端也可以通过 init script 将自定义的 Gradle 插件注入目标构建中来自定义数据获取的逻辑,Android Studio 中代表 sources.jar
的 Tooling Model AdditionalClassifierArtifactsModel
的获取过程便是通过该方式注入的,而 Android 以及 Kotlin 等 Tooling Model 的获取则是通过实际应用于项目的 Android Gradle Plugin 和 Kotlin Gradle Plugin 获取的。
在 Gradle 插件中定义 Tooling Model 获取逻辑,需要实现 ToolingModelBuilder
接口:
public class GradlePluginModelBuilder implements ToolingModelBuilder {
@Override
public boolean canBuild(@Nonnull String modelName) {
System.out.println("[modelName]" + modelName);
return Objects.equals(modelName, GradlePluginModel.class.getName());
}
@Override
@Nullable
public Object buildAll(@Nonnull String modelName, @Nonnull Project project) {
List plugins = new ArrayList<>();
project.getPlugins().forEach(plugin -> plugins.add(project.getPath() + " -> " + plugin.getClass().getName()));
return new GradlePluginModelImpl(plugins);
}
}
其中 canBuild
方法用以判断 Client 端的请求是否应由该 Builder
响应,入参的 modelName
即目标 Tooling Model 接口的完整类名;buildAll
方法为实际获取数据的逻辑,该方法返回前文所述的 Tooling Model 实现类的对象。示例工程中的逻辑较为简单,即获取入参 Project
的所有插件的类名信息。
ToolingModelBuilder
需要通过 ToolingModelBuilderRegistry
在 Gradle 插件中进行注册:
public class GradlePlugin implements Plugin {
private final ToolingModelBuilderRegistry registry;
@Inject
public GradlePlugin(ToolingModelBuilderRegistry registry) {
this.registry = registry;
}
@Override
public void apply(@Nonnull Project project) {
registry.register(new GradlePluginModelBuilder());
}
}
在 Client 端,可以很简单地使用 Tooling API 获取前文定义的 Tooling Model:
public class Main {
public static void main(String[] args) {
GradleConnector connector = GradleConnector.newConnector();
File projectDir = new File("../app");
connector.forProjectDirectory(projectDir);
try (ProjectConnection connection = connector.connect()) {
GradlePluginModel model = connection.getModel(GradlePluginModel.class);
println("***************************************");
println("Fetch model: ");
model.getPlugins().forEach(Main::println);
println("***************************************");
}
}
private static void println(String msg) {
System.out.println("[tooling] " + msg);
}
}
上述逻辑可以简化为下图的流程:
Android Studio 的 Sync 过程需要各种各样的数据,但本质上都与示例工程的 Tooling Model 获取逻辑一致,二者之间的区别在于:
Android Studio 除了会获取直接在目标工程 build.gradle
中应用的 Gradle 插件中注册的 Tooling Model 外,还会通过在调用 Tooling API 时传入 init script 注入自定义的用于获取 Tooling Model 的 Gradle 插件
Android Studio 的 Sync 是通过 BuildAction
接口在一次 Tooling API 调用中获取了多个 Tooling Model 而非示例工程中通过 getModel
方法获取单个 Tooling Model。Android Studio 中的 Tooling API 调用位于 GradleProjectResolver#doResolveProjectInfo
,BuildAction
的实现则位于 ProjectImportAction
,由于只是 API 调用差异而核心逻辑一致,故在此不予赘述,感兴趣的读者可以至上述源码处查看详细实现
Android Studio 底层依托于 IntelliJ IDEA Platform SDK,后者提供了名为 GradleProjectResolverExtension
的扩展点,IDE 插件可以通过该扩展定义自己的 Tooling Model 构建逻辑,这里的 ToolingModelBuilder 最终会通过上文提到的 BuildAction
注入 Sync 过程触发的 Gradle 构建中,所以我们可以通过在自定义插件中定义该扩展来在 Sync 时实现自定义的数据获取与展示逻辑。
关于 Intellij IDEA Platform SDK 中的扩展概念,可以参考文档:Extensions | Intellij Platform(https://plugins.jetbrains.com/docs/intellij/plugin-extensions.html)
那么 Sync 时 Gradle 究竟做了哪些事情呢?为了方便大家理解,我们以飞书工程的一次完整 Sync 过程为例,用 Chrome Trace 的形式可视化其对应 Gradle 构建过程中的所有 BuildOperation
:
在 Gradle 构建中,可以通过传入参数
-Dorg.gradle.internal.operations.trace
获取上图中的 Chrome Trace,对于 Android Studio 的 Gradle Sync,我们可以通过断点 Android Studio 的 Tooling API 调用处,传入该参数获取 Sync 时 BuildOperation 的 Chrome Trace。
一次常规 Gradle 构建包含三个阶段:
Initialization:该阶段中 Gradle 确定哪些 Project 将参与构建并为其创建对应的 Project 实例
Configuration:该阶段中 Gradle 对参与构建的 Project 对象进行配置,执行这些 Project 对象的构建脚本
Execution:Gradle 判断需要执行的 task 子集并最终执行选中的 task
参考原文:Gradle Document: Build LifeCycle(https://docs.gradle.org/current/userguide/build_lifecycle.html#sec:build_phases)
从前文 Chrome Trace 可以看到,由 Sync 触发的 Gradle 构建也基本符合这三个阶段的划分,只是在 Execution 阶段 Sync 触发的构建在串行构建各类 Tooling Model。
了解了 Sync 过程中 Gradle 构建做的事情,我们便可以从这三个阶段分别入手,优化 Sync 操作。
对于这两个阶段的优化思路较为一致,故合并为一节讲解。
在这两个阶段中,主要在解析与执行工程中的构建脚本与插件,我们可以使用 async profiler 查看这两个过程中的火焰图,定位到耗时脚本/插件,而后根据其与 Sync 的关系分别处理:
对于 Sync 过程中无需执行的,直接判断当前是否为 Sync 触发的构建,若是直接跳过该脚本或插件的执行
对于 Sync 过程中需要执行的,则根据火焰图定位其耗时函数,优化代码实现
我们可以通过如下代码判断当前构建是否为 Sync:
ext.isSync = Objects.equals(gradle.startParameter.projectProperties.get("android.injected.build.model.only"), "true");
而后,使用该标志对 Sync 时无需应用的插件和脚本进行跳过:
if (!isSync) {
apply plugin: "org.gradle.android.cache-fix"
}
这里举飞书工程 Sync 优化过程中的典型例子,方便读者理解。
使用 configureEach
替代 all
或 each
一个常见的会导致 Configuration 阶段性能劣化的实现便是对于 Gradle 中的集合类调用 all
或 each
方法进行遍历等操作:
gradle.taskGraph.whenReady {
tasks.each { task ->
if (!task.name.contains("mergeExtDex")) {
return
}
task.outputs.cacheIf { false }
}
}
上述代码会导致添加至 taskGraph
的所有 task
均被创建,从而拖慢配置过程。可以通过将其替换为 configureEach
来规避该劣化行为:
gradle.taskGraph.whenReady {
tasks.configureEach { task ->
if (!task.name.contains("mergeExtDex")) {
return
}
task.outputs.cacheIf { false }
}
}
由 DevOps - Build 团队推出的 Gradle Sync 优化方案 重点在于优化占据 Sync 过程最大比例的 Execution 阶段。该方案在飞书工程上收获了近 50% 的收益,本节将着重讲解该方案底层的优化原理。
如前文所述,Gradle Sync 的目的便是通过构建各种各样的 Tooling Model 实现 Android Studio 所需的数据展示。这些 Tooling Model 代表的数据大多数与工程的构建环境相关(例如 Gradle 版本、Kotlin 版本、模块路径、模块的 sourceSet 等),而这些配置在工程中变更的频率较低,但每次 Sync 过程却都要重新执行一次这些 Tooling Model 的构建逻辑,显然是某种程度上的时间浪费。我们可以通过将 Tooling Model 缓存起来,然后在下次 Sync 时直接返回缓存来达到加速的目的。
虽然缓存是性能优化的常规操作,但也往往会带来错误。对于 Tooling Model 也是如此。这里我们要小心处理:
并非所有 Tooling Model 均可缓存。上文所述的配置相关的、不常变更的 Tooling Model 固然可以缓存,但一些 Tooling Model,如下文提到的用于文件下载的 Tooling Model AdditionalClassifierArtifactsModel
,明显是不可缓存的(新增依赖而不进行 sources.jar
下载会导致在 Android Studio 中无法查看这部分依赖的源码)。我们需要仔细确认每个 Tooling Model 是否可缓存
构建环境虽然不常变更,但为了提升功能的准确性,显然需要有缓存的“淘汰”机制,防止过期的缓存导致错误
内存 - 磁盘二级缓存
Gradle 中构建 Tooling Model 的逻辑位于如下位置:
// DefaultBuildController.java
@Override
public BuildResult> getModel(Object target, ModelIdentifier modelIdentifier, Object parameter)
throws BuildExceptionVersion1, InternalUnsupportedModelException {
...
Object model;
if (parameter == null) {
model = builder.buildAll(modelName, project);
} else if (builder instanceof ParameterizedToolingModelBuilder>) {
model = getParameterizedModel(project, modelName, (ParameterizedToolingModelBuilder>) builder, parameter);
} else {
throw (InternalUnsupportedModelException) (new InternalUnsupportedModelException()).initCause(
new UnknownModelException(String.format("No parameterized builders are available to build a model of type '%s'.", modelName)));
}
return new ProviderBuildResult
我们可以通过在该方法中嵌入缓存逻辑,从而实现加速的目的。
Sync 优化框架中,设计了内存 - 磁盘二级缓存,以保证读取缓存效率的同时提升缓存命中率,并通过自定义的 BuildController
—— CacheableBuildController
替换默认的 BuildController
来嵌入缓存逻辑。
缓存读取与写入时,需要为每一个 Tooling Model 生成一个 key 值。除了 Tooling Model 的名称(即其接口的完整类名)外,部分 Tooling Model 与 Project
对象关联,生成 key 时需要加入 Project
的 path
来进行区别。
内存缓存是使用一个简单的 Map
结构实现的,这里不予赘述。
对于磁盘缓存,Tooling Model 均为实现了 Serializable
接口的 Java Bean,我们可以十分方便的使用 Java Object Stream 将其序列化至磁盘文件:
// DiskBasedBuildModelCache.java
public static boolean put(Project project, String modelName, Object model) {
...
try (FileOutputStream fos = new FileOutputStream(cacheFile)) {
try (ObjectOutputStream oos = new ObjectOutputStream(fos)) {
oos.writeObject(model);
Logger.info("Write model " + project.getDisplayName() + ":" + modelName + " to disk succeeded");
return true;
}
} catch (Throwable e) {
...
}
}
但在反序列化时,由于:
Tooling Model 的实现类位于对应的 Gradle Plugin 中,并由对应 Gradle Plugin 的 ClassLoader
加载
BuildController
以及缓存管理类位于 Gradle 框架层,由框架层 ClassLoader
加载
直接进行反序列化会直接抛出 ClassNotFoundException
。
于是,为了成功反序列化,Sync 优化框架将 Tooling Model 对象写入磁盘的同时,也将该对象的类的 classpath 写入了磁盘配置文件中:
// DiskBasedBuildModelCache.java
private static void addClassJarPath(Class> clazz) throws UnsupportedEncodingException {
String path = clazz.getProtectionDomain().getCodeSource().getLocation().getPath();
String decodePath = URLDecoder.decode(path, "UTF-8");
jarPaths.add(decodePath);
...
}
在反序列化时,则使用自定义的 ObjectInputStream
,使用添加了配置文件中 classpath 路径的自定义 ClassLoader
进行类加载:
// 生成自定义的 ClassLoader
public class SyncModelJarClassLoaderFactory {
public static ClassLoader generate(Set jarPaths) {
List urls = new ArrayList<>();
for (String path : jarPaths) {
try {
urls.add(new File(path).toURI().toURL());
} catch (MalformedURLException e) {
}
}
Logger.info("Generate URLClassLoader using jarPaths: " + AppGson.getInstance().getGson().toJson(jarPaths));
return new URLClassLoader(urls.toArray(new URL[0]), SyncModelJarClassLoaderFactory.class.getClassLoader());
}
}
// 使用自定义的 ClassLoader 进行反序列化的 ObjectInputStream
public class CustomClassLoaderObjectInputStream extends ObjectInputStream {
private final ClassLoader customClassLoader;
public CustomClassLoaderObjectInputStream(InputStream in, ClassLoader classloader) throws IOException {
super(in);
this.customClassLoader = classloader;
}
@Override
protected Class> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
try {
Class> result = Class.forName(desc.getName(), true, customClassLoader);
Logger.info("resolve class " + desc.getName() + " from customClassLoader succeeded");
return result;
} catch (Throwable e) {
// ignore
}
return super.resolveClass(desc);
}
}
缓存清理
如前文所述,为了避免缓存过期导致的错误,需要有合理的缓存淘汰机制。
Sync 优化方案中,会在配置文件中记录生成缓存时的:
Gradle 版本
Android Gradle Plugin 版本
Java 版本
Kotlin 版本
Android Studio 版本
当上述任一版本发生变更时,便会将内存、磁盘缓存进行清理并重新生成。
之所以选择上述工具的版本:
一方面是因为这些工具代表了 Sync 时的基础构建信息,会在若干 Tooling Model 中进行记录(如 BuildEnvironment
、KotlinGradleModel
等)
另一方面,上述工具中均包含了若干 Tooling Model 的构建逻辑,当版本变更时 Tooling Model 的构建逻辑也存在潜在的变更可能,为了保证正确性必须将先前版本的缓存进行清理
当然,上述的缓存淘汰机制仅覆盖了 Android Studio Sync 时的绝大多数场景,由于 Intellij IDEA Platform API 的开放性,任意的 IDE 插件均可注入自己的 Tooling Model 逻辑,所以可能存在遗漏的可能性。
好在本方案在飞书和今日头条项目已上线数周,暂无 Sync 相关的错误反馈。后续,我们将针对 Gradle 以及 Android Studio 版本更新进行持续兼容迭代。
无论从 Android Studio 底部进度条的提示还是前文的 Chrome Trace,我们都可以直观的看到,当我们首次 Sync 或工程依赖发生变更时,依赖对应的文件下载都会占据 Sync 过程中的相当大的比例。在这一节中,我们着重讲解优化方案中对于这一操作的优化原理。
进行文件下载的 Tooling Model
如前文所述,Sync 过程的主要操作都是通过构建各类 Tooling Model 来实现的,而文件下载也不例外。Sync 过程中触发文件下载的主要是如下两个 Tooling Model:
BuildScriptClasspathModel
AdditionalClassifierArtifactsModel
其中 BuildScriptClasspathModel
对应构建脚本中各个 Gradle 插件相关的文件,AdditionalClassifierArtifactsModel
对应模块的编译时和运行时依赖相关的文件。
知晓了文件下载的代码实现所在,我们便可以有的放矢,深入其中进行优化。
禁用插件的 sources.jar
下载
对于绝大多数开发者来说,我们极少查看 Gradle 插件的源码,但前文中的 BuildScriptClasspathModel
却会在 Sync 时下载这部分依赖的 sources.jar
文件,占据了不少耗时:
// ModelBuildScriptClasspathBuilderImpl.java
public Object buildAll(@NotNull final String modelName, @NotNull final Project project, @NotNull ModelBuilderContext context) {
...
boolean downloadJavadoc = false;
boolean downloadSources = true;
...
Collection dependencies = new DependencyResolverImpl(project, downloadJavadoc, downloadSources, mySourceSetFinder).resolveDependencies(classpathConfiguration);
...
return buildScriptClasspath;
}
可以看到,该 ModelBuilder 可以下载 Gradle 插件的 javadoc 和 sources.jar 文件,前者默认不下载,后者默认下载。
既然绝大多数场景下我们无需查看这部分源码,那么我们能否直接禁用该行为呢?
答案是肯定的。代码中该 ModelBuilder 读取了 project
中 IdeaPlugin
扩展中的对应配置并为其前文的标识位赋值:
final IdeaPlugin ideaPlugin = project.getPlugins().findPlugin(IdeaPlugin.class);
if (ideaPlugin != null) {
final IdeaModule ideaModule = ideaPlugin.getModel().getModule();
downloadJavadoc = ideaModule.isDownloadJavadoc();
downloadSources = ideaModule.isDownloadSources();
}
于是在 Sync 优化方案中,我们便直接通过该扩展进行配置,禁用了此处的 sources.jar
的下载:
project.getRootProject().allprojects(p -> {
IdeaPlugin ideaPlugin = p.getPlugins().findPlugin(IdeaPlugin.class);
if (ideaPlugin != null) {
IdeaModule ideaModule = ideaPlugin.getModel().getModule();
ideaModule.setDownloadSources(false);
SyncLogger.i("Disable BuildScriptClasspathModel downloading sources.jar for project " + p.getDisplayName());
}
});
禁用不存在 sources.jar
的依赖的 sources.jar
查询
并非所有依赖都在 maven 仓库中上传了 sources.jar
文件。Gradle 在下载之前会通过一次 HEAD 请求判断目标文件是否存在。
通过对于 Sync 过程的网络请求打点,我们发现返回码为 404 的 HEAD 请求耗时尤为明显。由于公司内 maven 代理仓库的存在,当资源不存在时,会遍历所有的仓库确认所有被代理的仓库中均不存在该资源才会返回 404。
显然,我们可以通过实现收集不存在 sources.jar
的依赖列表,然后在实际 Sync 时跳过这部分资源的查询来节省这部分耗时。
Gradle 中在如下代码处进行外部资源的查询,我们通过 UnresolvedArtifactCollector
记录所有查询失败的组件坐标以及资源类型并将其中 sources.jar
类型不存在的组件写入配置文件中即可:
// ExternalResourceResolver.java
protected Set findOptionalArtifacts(ModuleComponentResolveMetadata module, String type, String classifier) {
ModuleComponentArtifactMetadata artifact = module.artifact(type, "jar", classifier);
if (createArtifactResolver(module.getSources()).artifactExists(artifact, new DefaultResourceAwareResolveResult())) {
return ImmutableSet.of(artifact);
}
UnresolvedArtifactCollector.getInstance().record(module.getId().getDisplayName(), type);
return Collections.emptySet();
}
如前文所述,实际执行 sources.jar
下载的是 Tooling Model AdditionalClassifierArtifactsModel
,在其对应的 ToolingModelBuilder
中通过如下方式触发了文件下载并将最终的本地文件地址返回给 Android Studio:
// AdditionalClassifierArtifactsModelBuilder.kt
override fun buildAll(modelName: String, parameter: AdditionalClassifierArtifactsModelParameter, project: Project): Any {
// Collect the components to download Sources and Javadoc for. DefaultModuleComponentIdentifier is the only supported type.
// See DefaultArtifactResolutionQuery::validateComponentIdentifier.
...
try {
// Create query for Maven Pom File.
val pomQuery = project.dependencies.createArtifactResolutionQuery()
.forComponents(ids)
.withArtifacts(MavenModule::class.java, MavenPomArtifact::class.java)
// Map from component id to Pom File.
val idToPomFile = pomQuery.execute().resolvedComponents.map {
it.id.displayName to getFile(it, MavenPomArtifact::class.java)
}.toMap()
// Create map from component id to location of sample sources file.
val idToSampleLocation: Map =
if (parameter.downloadAndroidxUISamplesSources) {
getSampleSources(parameter, project)
}
else {
emptyMap()
}
// Create query for Javadoc and Sources.`
val docQuery = project.dependencies.createArtifactResolutionQuery()
.forComponents(ids)
.withArtifacts(JvmLibrary::class.java, SourcesArtifact::class.java, JavadocArtifact::class.java)
artifacts = docQuery.execute().resolvedComponents.filter { it.id is ModuleComponentIdentifier }.map {
val id = it.id as ModuleComponentIdentifier
AdditionalClassifierArtifactsImpl(
ArtifactIdentifierImpl(id.group, id.module, id.version),
getFile(it, SourcesArtifact::class.java),
getFile(it, JavadocArtifact::class.java),
idToPomFile[it.id.displayName],
idToSampleLocation[it.id.displayName]
)
}
}
catch (t: Throwable) {
message = "Unable to download sources/javadoc: " + t.message
}
return AdditionalClassifierArtifactsModelImpl(artifacts, message)
}
由于这部分代码位于 Android Studio 代码内部,我们难以更改其实现。但我们可以通过其调用的 Gradle API createArtifactResolutionQuery
在 Gradle 层面对于依赖的文件下载行为进行干预:
// DefaultArtifactResolutionQuery.java
public interface Interceptor {
boolean intercept(ComponentIdentifier componentId, Class extends Artifact> artifact);
}
private static Interceptor sInterceptor;
public static void setInterceptor(Interceptor interceptor) {
sInterceptor = interceptor;
}
private ComponentArtifactsResult buildComponentResult(ComponentIdentifier componentId, ComponentMetaDataResolver componentMetaDataResolver, ArtifactResolver artifactResolver) {
...
for (Class extends Artifact> artifactType : artifactTypes) {
if (sInterceptor != null && sInterceptor.intercept(componentId, artifactType)) {
continue;
}
addArtifacts(componentResult, artifactType, component, artifactResolver);
}
return componentResult;
}
在这里,我们通过外部注入一个拦截器(Interceptor
)的方式对于 Sync 过程中的文件下载行为进行干预。
在框架层面,便是通过前文生成的配置文件对于文件查询进行拦截:
// SyncArtifactResolutionQueryInterceptor.java
public boolean intercept(@NotNull ComponentIdentifier componentIdentifier, @NotNull Class extends Artifact> artifactType) {
if (artifactType == JavadocArtifact.class) {
return true;
}
String gav = componentIdentifier.getDisplayName();
if (CollectionUtils.isNotEmpty(noSourcesJarSet) && artifactType == SourcesArtifact.class) {
String ga = NoSourcesJarConfiguration.gav2ga(gav);
boolean intercept = noSourcesJarSet.contains(ga);
if (intercept) {
SyncLogger.i("Intercept sources download of " + gav);
return true;
}
}
return false;
}
这里为了进一步提升性能,还对 javadoc 文件的下载进行了拦截。
并发文件下载
通过观察前文的 Chrome Trace,我们发现 AdditionalClassifierArtifactsModel
对于文件的下载均为串行执行的。显然,这些资源文件下载是不存在逻辑上的前后依赖关系的,如果能够实现文件的并发下载显然能够显著提升该 Tooling Model 的构建效率。
AdditionalClassifierArtifactsModel
中的串行文件下载
实际上,Sync 过程中对于 aar
文件的下载会通过 ParallelResolveArtifactSet
进行包装,最终通过 BuildOperationExecutor#runAll
方法实现并发下载:
// ParallelResolveArtifactSet.java
public void visit(final ArtifactVisitor visitor) {
// Start preparing the result
StartVisitAction visitAction = new StartVisitAction(visitor);
buildOperationProcessor.runAll(visitAction);
// Now visit the result in order
visitAction.result.visit(visitor);
}
那么,我们便也可以依样画葫芦,将 AdditionalClassifierArtifactsModel
中的文件下载行为改造为并发:
// ByteGradle 中的 DefaultArtifactResolutionQuery.java
private ArtifactResolutionResult createResult(ComponentMetaDataResolver componentMetaDataResolver, ArtifactResolver artifactResolver) {
Set componentResults = Sets.newHashSet();
if (!sIsParallelQuery) {
...
} else {
Set resultSet = new ParallelResolutionQueryArtifactSet(
componentMetaDataResolver,
artifactResolver,
componentIds,
artifactTypes,
buildOperationExecutor,
this::createResult
).execute();
componentResults.addAll(resultSet);
}
return new DefaultArtifactResolutionResult(componentResults);
}
改造后 AdditionalClassifierArtifactModel
的构建效率显著提升:
优化后的并发文件下载
除了上述进行文件下载的 Tooling Model 外,由 Android Gradle Plugin 提供的 Tooling Model Variant
的耗时也较为明显。
查看 Variant
构建的源码,我们会发现其有不少用于处理 test
相关的逻辑:
private VariantImpl createVariant(@NonNull ComponentPropertiesImpl componentProperties) {
...
if (componentProperties instanceof VariantPropertiesImpl) {
VariantPropertiesImpl variantProperties = (VariantPropertiesImpl) componentProperties;
for (VariantType variantType : VariantType.Companion.getTestComponents()) {
ComponentPropertiesImpl testVariant =
variantProperties.getTestComponents().get(variantType);
if (testVariant != null) {
switch ((VariantTypeImpl) variantType) {
case ANDROID_TEST:
extraAndroidArtifacts.add(
createAndroidArtifact(
variantType.getArtifactName(), testVariant));
break;
case UNIT_TEST:
clonedExtraJavaArtifacts.add(
createUnitTestsJavaArtifact(variantType, testVariant));
break;
default:
throw new IllegalArgumentException(
"Unsupported test variant type ${variantType}.");
}
}
}
}
// used for test only modules
Collection testTargetVariants =
getTestTargetVariants(componentProperties);
checkProguardFiles(componentProperties);
return new VariantImpl(
variantName,
componentProperties.getBaseName(),
componentProperties.getBuildType(),
getProductFlavorNames(componentProperties),
new ProductFlavorImpl(
variantDslInfo.getMergedFlavor(), variantDslInfo.getApplicationId()),
mainArtifact,
extraAndroidArtifacts,
clonedExtraJavaArtifacts,
testTargetVariants,
inspectManifestForInstantTag(componentProperties),
getDesugaredMethods(componentProperties));
}
大多数场景下,我们在 Sync 时并不关心 test 变体相关的信息,显然我们可以通过过滤掉 test 变体来节省一部分时间。
基于 Gradle 的类加载机制(在 classpath
中先声明的依赖中的类会在运行时最终生效),我们可以在自己的插件中定义与 Android Gradle Plugin 中同名的类并先于 Android Gradle Plugin 进行声明来达到“类覆盖”的效果。
这里为了过滤 test 相关的变体,我们直接将 VariantManager
中的相关逻辑进行了重写:
// VariantManager.java
public ComponentInfo<
TestComponentImpl extends TestComponentPropertiesImpl>,
TestComponentPropertiesImpl>
createTestComponents(
@NonNull DimensionCombination dimensionCombination,
@NonNull BuildTypeData buildTypeData,
@NonNull List> productFlavorDataList,
@NonNull VariantT testedVariant,
@NonNull VariantPropertiesT testedVariantProperties,
@NonNull VariantType variantType) {
if (VariantUtils.shouldDisableTest(project)) {
SyncLogger.i("Disable TestVariantData for project " + project.getDisplayName());
return null;
}
...
}
这样,我们便从根本上过滤了 test 相关的变体。
通过对 Gradle Sync 过程进行细致深入的分析,我们构思并开发了前文所述的多个 Sync 优化手段,并将数据收集与上报能力整合,形成了一整套数据驱动、多管齐下的解决方案。
目前,该方案已经在公司内的头部应用飞书和今日头条等项目上线,均取得了逾 50% 的收益,Sync 耗时 50 分位值控制在 1min 以内,90 分位值约 3min,优化效果明显。
该方案取得这样的成绩,离不开飞书和今日头条团队相关同学的支持,也离不开抖音、TikTok 等多个团队同学提供的灵感,该方案的成功无疑是站在了巨人的肩膀上方能取得,向上述团队的同学致敬!
虽然已经取得了不错的收益,但我们对于开发者体验提升的探索不会止步于此:从 Gradle 层面来说,除了前文提到的 Tooling Model ,尚有不少 Tooling Model 的构建逻辑值得分析;从 Android Studio 层面来说,除了 Gradle Sync,Indexing、编码效率、代码索引等诸多功能的体验仍有提升空间······
研发工具优化的世界尚有未知值得探索,开发者体验提升的故事仍旧未完待续。
如果你也对此感兴趣,欢迎加入我们,携手共建一流的开发者体验。
我们是字节跳动终端技术团队(Client Infrastructure)下的 DevOps - Build 团队,负责打造公司范围内,面向不同业务场景的研发工具,提升移动应用研发效率。目前急寻 Android 移动研发工程师 / iOS 移动研发工程师 / 服务端研发工程师。
了解更多信息请联系:[email protected],邮件主题 简历-姓名-求职意向-期望城市-电话。