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

Android SDK工程编译优化

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

一、背景

子曰:“工欲善其事,必先利其器”。在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

相关推荐

php-fpm的配置和优化

目录概述php-fpm配置php-fpm进程优化配置慢日志查询配置php7进阶到架构师相关阅读概述这是关于php进阶到架构之php7核心技术与实战学习的系列课程:php-fpm的配置和优化学习目标:理...

成功安装 Magento2.4.3最新版教程「技术干货」

外贸独立站设计公司xingbell.com经过多次的反复实验,最新版的magento2.4.3在oneinstack的环境下的详细安装教程如下:一.vps系统:LinuxCentOS7.7.19...

十分钟让你学会LNMP架构负载均衡

业务架构、应用架构、数据架构和技术架构一、几个基本概念1、pv值pv值(pageviews):页面的浏览量概念:一个网站的所有页面,在一天内,被浏览的总次数。(大型网站通常是上千万的级别)2、u...

php从远程URL获取(mp4 mp3)音视频的流媒体数据

/***从远程URL中获取媒体(如mp4mp3)的内容*@parammixed$file_url*@parammixed$media_type...

Zabbix5.0安装部署

全盘展示运行状态,减轻运维人员的重复性工作量,提高系统排错速度,加速运维知识学习积累。1.png1、环境安装关闭SELinux并重启系统2.png安装httpd、mariadb、php运行yum-...

php 常见配置详解

以下是PHP常见的配置项及其含义:error_reporting:设置错误报告级别,可以控制PHP显示哪些错误。例如,设置为E_ALL将显示所有错误,而设置为0将禁止显示任何错误。displa...

实践分享|基于基石智算 DeepSeek API + WordPress 插件自动生成访客回复

基石智算举办的DeepSeek案例大赛汇集了不少基于CoresHubDeepSeekAPI服务或模型部署服务的精彩实践。本次我们将分享个人实践:通过DeepSeekAPI+Word...

如何在Eclipse中搭建Zabbix源码的调试和开发环境

Zabbix是一款非常优秀的企业级软件,被设计用于对数万台服务器、虚拟机和网络设备的数百万个监控项进行实时监控。Zabbix是开放源码和免费的,这就意味着当出现bug时,我们可以很方便地通过调试源码来...

MySQL自我保护参数

#头条创作挑战赛#之前(MySQL自我保护工具--pt-kill)提到用pt-kill工具来kill相关的会话,来达到保护数据库的目的,本文再通过修改数据库参数的方式达到阻断长时间运行的SQL的目...

Python闭包深度解析:掌握数据封装的高级技巧

闭包作为Python高级编程特性之一,为开发者提供了一种优雅的方式来实现数据封装和状态保持。这一概念源于函数式编程理论,在现代Python开发中发挥着重要作用。理解和掌握闭包的使用不仅能够提升代码的表...

Java服务网格故障注入与熔断实战

在分布式系统的高可用性挑战中,服务网格的故障注入与熔断机制是检验系统韧性的终极试金石。以下是10道逐步升级的"地狱关卡",每个关卡都对应真实生产环境中可能遇到的致命场景,并附具体场景示...

MySQL数据库性能优化全攻略:程序员必知的七大核心策略

作为程序员,我们每天都要与数据库打交道。当系统用户量突破百万级时,数据库往往成为性能瓶颈的首要怀疑对象。本文将深入探讨MySQL优化的七大核心策略,并提供可直接落地的优化方案,助您构建高效稳定的数据库...

如何在 Windows 11 上使用单个命令安装 XAMPP

XAMPP是一种广泛使用的软件,用于在Windows操作系统上快速运行LAMP服务器包,包括Windows11。尽管LAMP通常用于Linux系统,但XAMPP并不使用Li...

uTorrent怎样将bt种子转换为磁力

如何用uTorrent把BT种子转为磁力链接?以下方法希望能帮到你。1、在uTorrent窗口里,点击工具栏的按钮,所示。2、在打开窗口里,选取要转为磁力的种子文件,然后点击打开按钮,参照图示操作...

支持向量机SVM 分类和回归的实例

支持向量机(SupportVectorMachine)是Cortes和Vapnik于1995年首先提出的,它在解决小样本、非线性及高维模式识别中表现出许多特有的优势,并能够推广应用到函数拟合等其他...