百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

Android SDK工程编译优化

bigegpt 2024-08-07 17:52 14 浏览

一、背景

子曰:“工欲善其事,必先利其器”。在Android开发中,IDEA就是我们的工具,想要提高开发效率,就必须把我们的工具打磨“锋利”。

SDK工程随着功能日益丰富、项目规模也越来越庞大。这时候由于要编译大量的源代码和资源文件,编译速度也变得越来越慢,甚至有时候发现修改一行代码,demo编译很久甚至卡住了。这个时候基本什么都做不了,只能清除缓存或者重启IDEA。基于以上情况,需要对工程编译进行优化。

二、编译分析

针对工程编译慢和编译卡住问题,我们将通过以下步骤去分析问题:

    • 日志分析
    • 堆栈查看
    • 插件调试
    • 任务耗时分析

2.1 日志分析

编译demo 现在执行的是./gradlew :androidx:assembleAt37GamesDebug 这个命令,为了定位问题查看更多日志可以加上-d或者–-debug参数

执行./gradlew :androidx:assembleAt37GamesDebug -d

看到有8个任务同时在进行,同时一直循环等待锁、获取锁、释放锁。

我们看下这些任务是什么意思

    • prepareGitHookConfig:拷贝pre-commit脚本到hooks目录,处理git提交前的一些代码规范校验
    • packageDebugRenderscript:处理renderscript
    • compileDebugAidl:将.aidl文件通过工具转换成编译器能够处理的Java接口文件
    • processResources :复制生产资源到生产 class 文件目录
    • compileJava :使用 javac 命令编译产生 java源文件

查看以上任务,并没有定位出什么问题。其实目前我们并不知道gradle 执行到什么阶段,我们不妨看看目前是卡到gradle的哪一个阶段。

Gradle生命周期大概分为初始化(Initialization)、配置(Configuration)、执行(Execution)三个阶段。

在setting.gradle配置监听gradle生命周期

//初始化阶段
gradle.settingsEvaluated {
    println "-----初始化阶段->回调方法:settingsEvaluated-----"
}
    //初始化阶段执行完毕
gradle.projectsLoaded {
    println "-----初始化阶段->回调方法:projectsLoaded-----"
}






//配置阶段
    //build.gradle 执行前
gradle.beforeProject {Project project ->
    println "-----配置阶段->回调方法:beforeProject,${project.name}配置前-----"
}
    //build.gradle 执行后
gradle.afterProject {Project project ->
    println "-----配置阶段->回调方法:afterProject,${project.name}配置后-----"
}
gradle.projectsEvaluated {
    println "-----配置阶段->回调方法:projectsEvaluated,所有项目的build.gralde执行完毕-----"
}
//配置阶段完毕
gradle.taskGraph.whenReady {
    println "-----配置阶段->回调方法:whenReady 任务依赖关系建立完毕-----"
}




//执行阶段
gradle.taskGraph.beforeTask { Task task ->
    println "-----执行阶段->回调方法:beforeTask ,${task.name}执行前-----"
}


gradle.taskGraph.afterTask { Task task ->
    println "-----执行阶段->回调方法:afterTask ,${task.name}执行后-----"
}




gradle.buildFinished {
    println "-----buildFinished-----"
}

看到以下这些任务在执行阶段卡住了,只有执行前没有执行后。

2.2 堆栈查看

jstack工具可以分析线程死循环、线程阻塞、死锁等问题。Java 1.7 及更高版本,可以使用 jcmd 命令,功能更为全面。

具体使用:

首先用jps命令查看进程pid

由于目前工程使用的是java1.8,所以使用jcmd命令,查看进程堆栈命令为 jcmd <pid> Thread.print

堆栈日志很多,只截取部分日志截图。

看到main线程,优先级为5,操作系统优先级31,CPU占用时间为883.70毫米,已经运行78.34秒,线程ID为0x00007fc39b80b000,

线程为等待状态,在同步队列,等待的内存地址是0x0000000701100000。看起来似乎是正在等待其他线程资源的释放或者状态改变。


继续往下看日志,看到线程名称为“File lock request listener”,线程处于运行状态。

根据日志,大概的关系如图:

线程执行的是线程执行的方法是 java.net.PlainDatagramSocketImpl.receive0,该方法是 Java 原生方法(Native Method)。

该线程持有了锁 <0x0000000700cbd520>(java.net.PlainDatagramSocketImpl),并且该锁被其他线程所等待。

该线程还持有了两个锁,分别是 <0x00000007240b6be8>(java.net.DatagramPacket)和 <0x0000000700cbd668>(java.net.DatagramSocket)。

线程最后在 org.gradle.cache.internal.locklistener.FileLockCommunicator.receive 方法执行。

根据上述分析,该线程是一个文件锁请求监听器(File lock request listener),它正在运行并且持有一些锁。

搜索了一遍,目前看到的都是Java sdk的堆栈,暂时没有定位到开发层面上代码问题。

2.3 插件调试

调试过程中,有时候打印日志不能满足需求,需要Debug进行调试

可视化界面调试

这种方式比较简单,只需要在IDE找到对应的任务选择debug模式,然后在相应地方打上断点就可以进行调试。

自定义插件调试

自定义插件任务调试的步骤就稍微复杂一些

1.新增一个Remote JVM Debug配置

2.配置插件名字(名字可以任意命名,容易区分就行)

3.gradle 命令执行

IDEA的Configuration切换到上一步新建的配置

然后gradle命令执行任务名称,例如:assembleDebug

./gradlew assembleDebug -Dorg.gradle.debug=true --no-daemon

4.打好断点,开启调试

上一步执行完命令后,gradle处于等待状态

紧接着打好断点,点击5中的debug按钮 就可以愉快地进行插件调试了

2.4 任务耗时分析

利用gradle生命周期的钩子,在buildSrc目录下新增gradle插件BuildTimeStatisticPlugin, 用于统计任务执行耗时插件。在gradle.properties配置BUILD_TASK_TIME=true,可以打开任务统计开关。

class BuildTimeStatisticPlugin : AbstractPlugin() {
    //任务执行情况
    val taskRunTimeMap: MutableMap<String, TaskRunTimeEntity> by lazy { HashMap() }


    // 插件执行开关
    private var buildTimeSwitch: Boolean = true


    override fun applyPlugin(target: Project) {
        buildTimeSwitch = rootProject.properties["BUILD_TASK_TIME"] == "true"


        if (!buildTimeSwitch) {
            return
        }


        saveTaskExecuteTime(target)


        outputTaskExecuteTime(target)
    }


    private fun outputTaskExecuteTime(project: Project) {
        project.gradle.buildFinished {
            println("#########################################")
            println("build finish,print all task execute time")


            val sortList: MutableList<TaskRunTimeEntity> = ArrayList(taskRunTimeMap.values)
            with(sortList){
                //排序,耗时时间大的在前
                sortByDescending { it.totalTime }
                // 打印task执行时间
                forEach { task ->
                    if (task.totalTime > 0) {
                        println("${task.path}  [${task.totalTime} ms]")
                    }
                }
            }


            println("#########################################")
        }
    }


    //保存task构建时间
    private fun saveTaskExecuteTime(project: Project) {
        project.gradle.addListener(object : TaskExecutionListener {
            override fun beforeExecute(task: Task) {
                val taskExecTimeInfo = TaskRunTimeEntity().apply {
                    startTime = System.currentTimeMillis()
                    path = task.path
                }
                taskRunTimeMap[task.path] = taskExecTimeInfo
            }


            override fun afterExecute(task: Task, state: TaskState) {
                taskRunTimeMap[task.path]?.let {
                    it.endTime = System.currentTimeMillis()
                    it.totalTime = it.endTime - it.startTime
                }
            }
        })
    }
}




class TaskRunTimeEntity {
    //task执行总时长
    var totalTime: Long = 0


    //任务路径(工程路径+任务名称)
    var path: String? = null


    //开始时间
    var startTime: Long = 0


    //结束时间
    var endTime: Long = 0
}


具体效果如下,会从耗时最长任务开始打印

以上这种方式只能看到任务执行时候的耗时情况,不过比较方便,无需执行额外的命令,每次编译demo都能看到。如果想要看到初始化、配置阶段的耗时可以用命令行方式去查看。

例如:./gradlew :androidx:assembleAt37GamesDebug --profile

    • Summary:总的构建时间
    • Configuration:配置阶段花费时间
    • Dependency Resolution:依赖解析阶段花费时间
    • Artifact Transforms: 任务transform花费的时间
    • Task Execution:每个任务执行时间

可以看到构建总共耗时8.394s,各个阶段的耗时情况也比较清晰。

查看Configuration这个Tab下

目前执行的是全球平台,发现配置阶段有很多无关的任务在执行,这里可以优化成只配置任务依赖的模块而不是工程所有模块。

更详细的gradle构建信息,可以用扫描命令 ./gradlew :androidx:assembleAt37GamesDebug --scan

三、优化配置

3.1 按需配置

在以上分析过程中,定位到很多无关任务在配置阶段执行了。所以在gradle.properties启用按需配置,配置请求任务相关的,即仅执行当前任务依赖的脚本文件。

# 启用按需配置
org.gradle.configureondemand=true

配置之后,运行的时候报错了。

因为现在是按需配置,编译demo的时候不会去执行merge任务的操作(打包SDK 才会执行的任务)。因为buildSDK插件监听了gradle执行的生命周期,所以仍会回调,

解决方案就是在生命周期的时候判断是否在执行打包或同步配置。

3.2 开启gradle缓存

#尝试为所有构建重用以前构建的输出
org.gradle.caching=true
#开启构建缓存
android.enableBuildCache=true

gradle构建会缓存构建的输出,这样后续构建过程中如果输入内容没有变化可以直接利用这些缓存加快构建速度。在构建日志中,任务复用缓存会有FROM-CACHE日志

3.3 开启kotlin的增量、并行编译

#开启kotlin的增量编译
kotlin.incremental=true
kotlin.incremental.java=true
kotlin.incremental.js=true
kotlin.caching.enabled=true
#开启kotlin并行编译
kotlin.parallel.tasks.in.project=true

在构建日志中,任务增量编译会显示UP-TO-DATE

3.4 优化kapt

#并行运行
kapt.use.worker.api=true
#增量编译
kapt.incremental.apt=true
#如果用kapt依赖的内容没有变化,会完全重用编译内容
kapt.include.compile.classpath=false
kapt.include.compile.classpath控制是否将编译类路径中的类包含在注解处理的输入中。

当此选项为false时,注解处理器只能访问项目的源代码和依赖项中的类,而不能访问编译后的类。

这可以确保注解处理器只依赖于源代码和公共API,并减少了注解处理器对不应公开的类的访问。

3.5 其他的配置

1.Kotlin跨模块增量

#开启Kotlin跨模块增量编译
kotlin.incremental.useClasspathSnapshot=true

Kotlin跨模块增量编译要在Kotlin1.7.0版本以后才能生效,目前我们的Kotlin版本是1.5.32,选择暂不开启。

2.配置缓存

org.gradle.unsafe.configuration-cache=true
# Use this flag carefully, in case some of the plugins are not fully compatible.
org.gradle.unsafe.configuration-cache-problems=warn

配置缓存可让 Gradle 记录有关构建任务图的信息,并在后续 build 中重复使用该任务图,而不必再次重新配置整个 build。


开启后看到booster插件不支持,报ConcurrentModificationException异常,暂时选择不配置。

四、优化效果

优化后编译demo不再出现卡住的问题,同时每个模块也能单独编译生成aar包。

基于设备(MacBook Pro i5四核处理器 16GB内存)进行测试,每次均clean project后再采集。


以上数据分别采样10次,去掉最高和最低求的平均值。看到优化后时间大概减少了41%

五、总结

本文从项目中遇到编译问题出发,讲解笔者从编译分析到优化配置的一个过程。gradle的编译构建受多个因素的影响,比如硬件配置、项目规模、编译配置、依赖关系和构建脚本的复杂性等等。在实际项目中,可以根据项目具体情况,选择优化方式以提高项目编译构建的性能和速率。


引用资料:

1.https://doc.yonyoucloud.com/doc/wiki/project/GradleUserGuide-Wiki/the_java_plugin/java_plugin_tasks.html

2.https://developer.android.com/studio/build/optimize-your-build?hl=zh-cn

3.https://docs.gradle.org/6.7.1/userguide/multi_project_configuration_and_execution.html


作者:林俏耿

来源-微信公众号:三七互娱技术团队

出处:https://mp.weixin.qq.com/s/f3THdElwVwNTFxQubopUkQ

相关推荐

方差分析简介(方差分析通俗理解)

介绍方差分析(ANOVA,AnalysisofVariance)是一种广泛使用的统计方法,用于比较两个或多个组之间的均值。单因素方差分析是方差分析的一种变体,旨在检测三个或更多分类组的均值是否存在...

正如404页面所预示,猴子正成为断网元凶--吧嗒吧嗒真好吃

吧嗒吧嗒,绘图:MakiNaro你可以通过加热、冰冻、水淹、模塑、甚至压溃压力来使网络光缆硬化。但用猴子显然是不行的。光缆那新挤压成型的塑料外皮太尼玛诱人了,无法阻挡一场试吃盛宴的举行。印度政府正...

Python数据可视化:箱线图多种库画法

概念箱线图通过数据的四分位数来展示数据的分布情况。例如:数据的中心位置,数据间的离散程度,是否有异常值等。把数据从小到大进行排列并等分成四份,第一分位数(Q1),第二分位数(Q2)和第三分位数(Q3)...

多组独立(完全随机设计)样本秩和检验的SPSS操作教程及结果解读

作者/风仕在上一期,我们已经讲完了两组独立样本秩和检验的SPSS操作教程及结果解读,这期开始讲多组独立样本秩和检验,我们主要从多组独立样本秩和检验介绍、两组独立样本秩和检验使用条件及案例的SPSS操作...

方差分析 in R语言 and Excel(方差分析r语言例题)

今天来写一篇实际中比较实用的分析方法,方差分析。通过方差分析,我们可以确定组别之间的差异是否超出了由于随机因素引起的差异范围。方差分析分为单因素方差分析和多因素方差分析,这一篇先介绍一下单因素方差分析...

可视化:前端数据可视化插件大盘点 图表/图谱/地图/关系图

前端数据可视化插件大盘点图表/图谱/地图/关系图全有在大数据时代,很多时候我们需要在网页中显示数据统计报表,从而能很直观地了解数据的走向,开发人员很多时候需要使用图表来表现一些数据。随着Web技术的...

matplotlib 必知的 15 个图(matplotlib各种图)

施工专题,我已完成20篇,施工系列几乎覆盖Python完整技术栈,目标只总结实践中最实用的东西,直击问题本质,快速帮助读者们入门和进阶:1我的施工计划2数字专题3字符串专题4列表专题5流程控制专题6编...

R ggplot2常用图表绘制指南(ggplot2绘制折线图)

ggplot2是R语言中强大的数据可视化包,基于“图形语法”(GrammarofGraphics),通过分层方式构建图表。以下是常用图表命令的详细指南,涵盖基本语法、常见图表类型及示例,适合...

Python数据可视化:从Pandas基础到Seaborn高级应用

数据可视化是数据分析中不可或缺的一环,它能帮助我们直观理解数据模式和趋势。本文将全面介绍Python中最常用的三种可视化方法。Pandas内置绘图功能Pandas基于Matplotlib提供了简洁的绘...

Python 数据可视化常用命令备忘录

本文提供了一个全面的Python数据可视化备忘单,适用于探索性数据分析(EDA)。该备忘单涵盖了单变量分析、双变量分析、多变量分析、时间序列分析、文本数据分析、可视化定制以及保存与显示等内容。所...

统计图的种类(统计图的种类及特点图片)

统计图是利用几何图形或具体事物的形象和地图等形式来表现社会经济现象数量特征和数量关系的图形。以下是几种常见的统计图类型及其适用场景:1.条形图(BarChart)条形图是用矩形条的高度或长度来表示...

实测,大模型谁更懂数据可视化?(数据可视化和可视化分析的主要模型)

大家好,我是Ai学习的老章看论文时,经常看到漂亮的图表,很多不知道是用什么工具绘制的,或者很想复刻类似图表。实测,大模型LaTeX公式识别,出乎预料前文,我用Kimi、Qwen-3-235B...

通过AI提示词让Deepseek快速生成各种类型的图表制作

在数据分析和可视化领域,图表是传达信息的重要工具。然而,传统图表制作往往需要专业的软件和一定的技术知识。本文将介绍如何通过AI提示词,利用Deepseek快速生成各种类型的图表,包括柱状图、折线图、饼...

数据可视化:解析箱线图(box plot)

箱线图/盒须图(boxplot)是数据分布的图形表示,由五个摘要组成:最小值、第一四分位数(25th百分位数)、中位数、第三四分位数(75th百分位数)和最大值。箱子代表四分位距(IQR)。IQR是...

[seaborn] seaborn学习笔记1-箱形图Boxplot

1箱形图Boxplot(代码下载)Boxplot可能是最常见的图形类型之一。它能够很好表示数据中的分布规律。箱型图方框的末尾显示了上下四分位数。极线显示最高和最低值,不包括异常值。seaborn中...