深入浅出 Gradle Sync 优化

发表于 1年以前  | 总阅读数:353 次

本文分析了 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 模型:

  • Android Studio 作为 Client 发起请求,传递参数告知 Server 自己需要哪些数据
  • Gradle 进程作为 Server,通过一次构建过程收集 Client 所需的信息并回传
  • Client 和 Server 两个进程间通过 Socket 进行通信,Tooling API 则是提供给 Client 端调用的对于通信过程的封装

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 获取逻辑一致,二者之间的区别在于:

  • 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#doResolveProjectInfoBuildAction 的实现则位于 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 时 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 优化实战

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

Initialization 和 Configuration 阶段的优化

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

在这两个阶段中,主要在解析与执行工程中的构建脚本与插件,我们可以使用 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 替代 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) {
        ...
    }
}

但在反序列化时,由于:

  • 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<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 优化方案中,会在配置文件中记录生成缓存时的:

  • Gradle 版本
  • Android Gradle Plugin 版本
  • Java 版本
  • Kotlin 版本
  • Android Studio 版本

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

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

  • 一方面是因为这些工具代表了 Sync 时的基础构建信息,会在若干 Tooling Model 中进行记录(如 BuildEnvironmentKotlinGradleModel 等)
  • 另一方面,上述工具中均包含了若干 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<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、编码效率、代码索引等诸多功能的体验仍有提升空间······

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

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

本文由哈喽比特于1年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/KMYSFterJblcRLPGOrPdNQ

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:6月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:6月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:6月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:6月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:6月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:6月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:6月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:6月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:6月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:6月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:6月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:6月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:6月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:6月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:6月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:6月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:6月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:6月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:6月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:6月以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  236811次阅读
vscode超好用的代码书签插件Bookmarks 1年以前  |  6657次阅读
 目录