深入浅出 Gradle Sync 优化

532次阅读  |  发布于2年以前

本文分析了 Android Studio Sync 在 Gradle 层面的底层逻辑,并且从原理出发介绍了 DevOps - Build 团队 Gradle Sync 优化框架的实现细节以及在飞书项目中进行 Sync 优化的实战经验。

高频却不高效的 Sync

作为 Android 开发者,我们在使用 Android Studio 时,离不开名为 Sync 的操作:代码索引、自动补全等功能均需通过成功的 Sync 过程方可使用。以飞书工程 2021 年 10 月为例,一个月内共触发 Sync 3805 次,日均触发 172.95 次。

然而这样一个日常开发中的高频操作其平均耗时近 3min,远超同期飞书增量编译的平均耗时(1.77min)。如果能够将 Sync 操作的耗时缩短,显然能够大幅提升工程师的开发效率。

Sync 过程探秘

若要优化 Sync 过程,首先要理解在 Android Studio 中点击 Gradle Sync 按钮后,都发生了什么。

从用户视角看,项目的全部信息均存储于工程的代码中,而 Android Studio 需要做的事情便是将这些信息转化为 GUI 层面可视元素。由于涉及到获取工程源代码和依赖等信息,最直接的想法便是通过构建系统来获得上述信息,而目前 Android 工程普遍使用的构建系统 Gradle 恰巧提供了该能力,那便是 Tooling API。

Tooling API:IDE 与 Gradle 沟通的桥梁

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 模型:

IDE 与 Gradle 的通信协议:Tooling Model

既然涉及到两个进程之间的通信,便需要一套能够使双方能够相互理解的协议,在 Gradle 中该协议称之为 Tooling Model。

为了方便大家理解,这里使用一个示例工程来展示上述进程间通信。该示例工程是一个通过 Tooling API 获取目标 Gradle 项目中 Gradle Plugin 插件名称并打印的 Java 应用。

约定数据格式

由于 Client 与 Server 均为 Java 应用,我们可以直接通过接口来定义二者之间的数据格式,接口类对于 Client 与 Server 均可见,而接口的实现类则可以仅对实际生产数据的 Server 端可见即可,这也符合面向接口编程的设计理念。

在这里,作为数据格式接口类如下:

public interface GradlePluginModel {
    // 目标工程的 Gradle Plugin 名称列表
    List<String> getPlugins();
}

由于涉及在进程间传递,实际传递的实现类的对象需要进行序列化与反序列化,所以该接口的实现类也实现了 java.io.Serializable 接口:

public class GradlePluginModelImpl implements GradlePluginModel, Serializable {
    public static final long serialVersionUID = 42L;

    private List<String> plugins;

    public GradlePluginModelImpl(List<String> plugins) {
        this.plugins = plugins;
    }

    @Override
    public List<String> 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<String> 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<Project> {

    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 发起请求

在 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 中的 Tooling API 调用

Android Studio 的 Sync 过程需要各种各样的数据,但本质上都与示例工程的 Tooling Model 获取逻辑一致,二者之间的区别在于:

关于 Intellij IDEA Platform SDK 中的扩展概念,可以参考文档:Extensions | Intellij Platform(https://plugins.jetbrains.com/docs/intellij/plugin-extensions.html)

Sync 时的 Gradle 构建

那么 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 构建包含三个阶段:

参考原文: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 优化实战

了解了 Sync 过程中 Gradle 构建做的事情,我们便可以从这三个阶段分别入手,优化 Sync 操作。

Initialization 和 Configuration 阶段的优化

对于这两个阶段的优化思路较为一致,故合并为一节讲解。

在这两个阶段中,主要在解析与执行工程中的构建脚本与插件,我们可以使用 async profiler 查看这两个过程中的火焰图,定位到耗时脚本/插件,而后根据其与 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 替代 alleach

一个常见的会导致 Configuration 阶段性能劣化的实现便是对于 Gradle 中的集合类调用 alleach 方法进行遍历等操作:

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 }
    }
}

Execution 阶段优化:BuildInfra Gradle Sync 优化方案

由 DevOps - Build 团队推出的 Gradle Sync 优化方案 重点在于优化占据 Sync 过程最大比例的 Execution 阶段。该方案在飞书工程上收获了近 50% 的收益,本节将着重讲解该方案底层的优化原理。

Tooling Model 缓存优化

如前文所述,Gradle Sync 的目的便是通过构建各种各样的 Tooling Model 实现 Android Studio 所需的数据展示。这些 Tooling Model 代表的数据大多数与工程的构建环境相关(例如 Gradle 版本、Kotlin 版本、模块路径、模块的 sourceSet 等),而这些配置在工程中变更的频率较低,但每次 Sync 过程却都要重新执行一次这些 Tooling Model 的构建逻辑,显然是某种程度上的时间浪费。我们可以通过将 Tooling Model 缓存起来,然后在下次 Sync 时直接返回缓存来达到加速的目的。

虽然缓存是性能优化的常规操作,但也往往会带来错误。对于 Tooling Model 也是如此。这里我们要小心处理:

  1. 并非所有 Tooling Model 均可缓存。上文所述的配置相关的、不常变更的 Tooling Model 固然可以缓存,但一些 Tooling Model,如下文提到的用于文件下载的 Tooling Model AdditionalClassifierArtifactsModel,明显是不可缓存的(新增依赖而不进行 sources.jar 下载会导致在 Android Studio 中无法查看这部分依赖的源码)。我们需要仔细确认每个 Tooling Model 是否可缓存
  2. 构建环境虽然不常变更,但为了提升功能的准确性,显然需要有缓存的“淘汰”机制,防止过期的缓存导致错误

内存 - 磁盘二级缓存

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<Object>(model);
}

我们可以通过在该方法中嵌入缓存逻辑,从而实现加速的目的。

Sync 优化框架中,设计了内存 - 磁盘二级缓存,以保证读取缓存效率的同时提升缓存命中率,并通过自定义的 BuildController —— CacheableBuildController 替换默认的 BuildController 来嵌入缓存逻辑。

缓存读取与写入时,需要为每一个 Tooling Model 生成一个 key 值。除了 Tooling Model 的名称(即其接口的完整类名)外,部分 Tooling Model 与 Project 对象关联,生成 key 时需要加入 Projectpath 来进行区别。

内存缓存是使用一个简单的 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) {
        ...
    }
}

但在反序列化时,由于:

直接进行反序列化会直接抛出 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<String> jarPaths) {
        List<URL> 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 优化方案中,会在配置文件中记录生成缓存时的:

当上述任一版本发生变更时,便会将内存、磁盘缓存进行清理并重新生成。

之所以选择上述工具的版本:

当然,上述的缓存淘汰机制仅覆盖了 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 对应构建脚本中各个 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<ExternalDependency> dependencies = new DependencyResolverImpl(project, downloadJavadoc, downloadSources, mySourceSetFinder).resolveDependencies(classpathConfiguration);
  ...
  return buildScriptClasspath;
}

可以看到,该 ModelBuilder 可以下载 Gradle 插件的 javadoc 和 sources.jar 文件,前者默认不下载,后者默认下载。

既然绝大多数场景下我们无需查看这部分源码,那么我们能否直接禁用该行为呢?

答案是肯定的。代码中该 ModelBuilder 读取了 projectIdeaPlugin 扩展中的对应配置并为其前文的标识位赋值:

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<ModuleComponentArtifactMetadata> 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<String, File?> =
      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 的构建效率。

<span style="font-size: 14px;">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<ComponentResult> componentResults = Sets.newHashSet();
    if (!sIsParallelQuery) {
        ...
    } else {
        Set<ComponentResult> resultSet = new ParallelResolutionQueryArtifactSet(
            componentMetaDataResolver,
            artifactResolver,
            componentIds,
            artifactTypes,
            buildOperationExecutor,
            this::createResult
        ).execute();
        componentResults.addAll(resultSet);
    }

    return new DefaultArtifactResolutionResult(componentResults);
}

改造后 AdditionalClassifierArtifactModel 的构建效率显著提升:

优化后的并发文件下载

过滤 test 变体

除了上述进行文件下载的 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<TestedTargetVariant> 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<BuildType> buildTypeData,
        @NonNull List<ProductFlavorData<ProductFlavor>> 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、编码效率、代码索引等诸多功能的体验仍有提升空间······

研发工具优化的世界尚有未知值得探索,开发者体验提升的故事仍旧未完待续。

如果你也对此感兴趣,欢迎加入我们,携手共建一流的开发者体验。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8