Android组件化中使用Catalogs管理版本与对应封装
前言
说起 Android 的版本管理方案其实有很多种实现的,但是目前主流是三种方向,config.gradle 和 buildSrc 和 version Catalogs 这三种方案。
从网上的呼声来看目前大家比较喜欢 Catalogs 这种方案,因为如果现在出文章或出 Demo 还是用老的版本方式,会被读者善意的提醒过时了 😂
我之前的组件化方案【传送门】中我是使用 buildSrc + Kotin DSL 的方案来做的版本管理,然后有读者推荐使用 Catalogs 的方案来管理。
那本篇文章我们就带着几个问题往下看:
1、Catalogs 经过这么长时间的改善真的好用吗?代码提示完善吗?版本升级提示可以吗?
2、对比 buildSrc 这种方案会更好吗?他们之间的优缺点是什么?
3、使用 Catalogs 取代 config.gradle 有什么优势?中大型项目使用组件化开发的时候,Catalogs 如何进行集中管理和封装?
话不多说,直接开始!
一、使用BuildSrc的方式有什么弊端
关于 BuildSrc + Kotlin DSL 如何进行版本管理,我在之前的文章中已经详细的介绍了【传送门】。
我介绍了这种方案的一些优势
-
可以很方便的跳转查看依赖组件,哪些地方有被依赖到一点就能飞。
-
可以使用Kotlin语法,并且可以对依赖组进行编队,可以按需进行依赖组的依赖。
-
其次还可以通过 Gradle Task 的方式进行封装,子组件只需要依赖这个 GradleTask 即可实现默认的配置。
这里介绍一些缺点
-
buildSrc 被 Gradle 视为一个独立的项目,它在主项目构建之前自动编译。这意味着,当你对 buildSrc 中的代码做出更改时,Gradle需要重新编译它。如果 buildSrc 项目变得很大,编译时间可能会对总体构建时间产生显著影响。
-
语言成面,kotlin 编写脚本在编译时没有 groovy 的速度快,性能上差点,但是也是在编译时,并不影响运行时速度。
-
由于 buildSrc 是独立的项目,所以和主工程的 Gradle 可能有版本冲突,需要准备双份的版本强制指定。
在之前的文章我就说过我搞版本冲突搞了2晚上,就是因为这个问题导致 Hilt 无法使用,最后是强制指定双份版本才解决的冲突的问题,如果是其他的依赖不知道会不会也需要这么做。
对于 Kotlin DSL 和 Groovy DSL 可以看看我的之前的项目,我分别做了不同的分支,一模一样的依赖,分别做出三次清除缓存之后的编译测效果如下。
使用 Groovy 的传统方式:
使用 BuildSrc + Kotlin DSL 的方式:
取决于温度,电脑,CPU,其他环境因素,我不敢说原始的 Groovy 的方式一定比 BuildSrc + Kotlin DSL 的方案快,我只能说在我这边的测试结果是 Groovy 的方式相对于 BuildSrc 的方式稍微要快一些,如果你想要更精确的数据可以自行运行尝试。
当然他们对最终APK大小是没有影响的,最终的打包产物,包括最终性能都没有影响,只是在编译期间有区别。
二、使用 Catalogs 的集成
我使用的 Gradle 版本是 8.1.3 不算太高,也不算很低,使用 Catalogs 的方式不需要额外的声明。
我们直接创建文件就能使用
我们常用的四个标签,[versions] [libraries] [bundles] [plugins]
[versions]
作用: 定义项目中使用的各个依赖的版本号。
用途: 通过为每个版本分配一个标识符,您可以在整个版本目录中重用这些版本号。这样做的好处是,当需要升级依赖版本时,您只需在一个地方更新版本号,改动就会被应用到所有使用该版本的依赖上。
[libraries]
作用: 定义项目依赖的库。
用途: 在这个部分,您可以具体列出项目需要的库及其坐标(group,name,[version])。您还可以引用 [versions] 部分定义的版本号,从而避免在每个库定义中重复版本号。
[bundles]
作用: 定义一组相关的依赖,可以一起引入项目中。
用途: 通过创建包(bundle),您可以将相关的依赖分组在一起,然后在项目的不同模块中一次性引入这些依赖。这对于管理那些经常一起使用的库非常有用,如测试库集合、日志库集合等。
[plugins]
作用: 定义Gradle插件的依赖。
用途: 在这个部分,您可以列出项目中使用的Gradle插件及其版本。同样,您可以利用 [versions] 中定义的版本号来统一管理插件版本。
举个例子:
[versions]
compileSdk = "34"
minSdk = "21"
targetSdk = "33"
versionCode = "100"
versionName = "1.0.0"
applicationId = "com.newki.template"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
androidGradlePlugin = "8.1.3"
kotlinVersion = "1.8.22"
coroutinesVersion = "1.7.1"
appcompat = "1.6.1"
supportV4 = "1.0.0"
coreKtx = "1.9.0"
activityKtx = "1.8.0"
fragment = "1.5.4"
fragmentKtx = "1.5.4"
constraintLayout = "2.1.4"
cardView = "1.0.0"
material = "1.11.0"
recyclerView = "1.2.1"
multidex = "2.0.1"
viewpager = "1.0.0"
viewpager2 = "1.1.0-beta01"
lifecycleVersion = "2.7.0"
hiltVersion = "2.45"
navigationVersion = "2.5.3"
dataStoreVersion = "1.1.0-beta01"
workVersion = "2.8.1"
junit = "4.13.2"
androidJunit = "1.1.5"
espresso = "3.5.1"
#第三方
aRouterVersion = "1.0.3"
retrofitVersion = "2.9.0"
glideVersion = "4.11.0"
permissionVersion = "18.6"
gsonFactoryVersion = "9.5"
gsonVersion = "2.10.1"
[libraries]
#基础与UI
appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
supportV4 = { module = "androidx.legacy:legacy-support-v4", version.ref = "supportV4" }
coreKtx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
activityKtx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtx" }
fragment = { module = "androidx.fragment:fragment", version.ref = "fragment" }
fragmentKtx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" }
multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" }
#Widget
constraintLayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintLayout" }
recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerView" }
cardView = { module = "androidx.cardview:cardview", version.ref = "cardView" }
material = { module = "com.google.android.material:material", version.ref = "material" }
#lifecycle
lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "lifecycleVersion" }
lifecycle-runtimektx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleVersion" }
lifecycle-livedata = { module = "androidx.lifecycle:lifecycle-livedata", version.ref = "lifecycleVersion" }
lifecycle-livedataKtx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycleVersion" }
lifecycle-viewModel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycleVersion" }
lifecycle-viewModelKtx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleVersion" }
lifecycle-viewModelSavedState = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "lifecycleVersion" }
lifecycle-compiler = { module = "androidx.lifecycle:lifecycle-compiler", version.ref = "lifecycleVersion" }
#Kotlin与协程
stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlinVersion" }
reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlinVersion" }
stdlibJdk7 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk7", version.ref = "kotlinVersion" }
stdlibJdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlinVersion" }
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutinesVersion" }
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutinesVersion" }
#ViewPager
viewpager = { module = "androidx.viewpager:viewpager", version.ref = "viewpager" }
viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "viewpager2" }
#Hilt
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltVersion" }
hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltVersion" }
#Work
work-runtime = { module = "androidx.work:work-runtime", version.ref = "workVersion" }
work-runtimeKtx = { module = "androidx.work:work-runtime-ktx", version.ref = "workVersion" }
#Navigation
navigation-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigationVersion" }
navigation-ui = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigationVersion" }
navigation-dynamic = { module = "androidx.navigation:navigation-dynamic-features-fragment", version.ref = "navigationVersion" }
navigation-dynamicRuntime = { module = "androidx.navigation:navigation-dynamic-features-runtime", version.ref = "navigationVersion" }
navigation-testing = { module = "androidx.navigation:navigation-testing", version.ref = "navigationVersion" }
#DataStore
datastore-core = { module = "androidx.datastore:datastore-core", version.ref = "dataStoreVersion" }
datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "dataStoreVersion" }
#测试
junit = { module = "junit:junit", version.ref = "junit" }
androidJunit = { module = "androidx.test.ext:junit", version.ref = "androidJunit" }
espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" }
#第三方依赖库
#ARouter
arouter-core = { module = "com.github.jadepeakpoet.ARouter:arouter-api", version.ref = "aRouterVersion" }
arouter-compiler = { module = "com.github.jadepeakpoet.ARouter:arouter-compiler", version.ref = "aRouterVersion" }
#Retrofit
retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofitVersion" }
retrofit-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofitVersion" }
gson = { module = "com.google.code.gson:gson", version.ref = "gsonVersion" }
gsonFactory = { module = "com.github.getActivity:GsonFactory", version.ref = "gsonFactoryVersion" }
#图片加载
glide-core = { module = "com.github.bumptech.glide:glide", version.ref = "glideVersion" }
glide-annotation = { module = "com.github.bumptech.glide:annotations", version.ref = "glideVersion" }
glide-integration = { module = "com.github.bumptech.glide:okhttp3-integration", version.ref = "glideVersion" }
glide-compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glideVersion" }
gifDrawable = "pl.droidsonroids.gif:android-gif-drawable:1.2.28"
#Gradle插件
androidGradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "androidGradlePlugin" }
kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlinVersion" }
hilt-plugin = { module = "com.google.dagger:hilt-android-gradle-plugin", version.ref = "hiltVersion" }
arouter-plugin = { module = "com.github.jadepeakpoet.ARouter:arouter-register", version.ref = "aRouterVersion" }
[bundles]
appcompatBundles = ["appcompat", "supportV4", "coreKtx", "activityKtx", "fragment", "fragmentKtx", "multidex"]
lifecycleBundles = ["lifecycle-runtime", "lifecycle-runtimektx", "lifecycle-livedata", "lifecycle-livedataKtx", "lifecycle-viewModel", "lifecycle-viewModelKtx", "lifecycle-viewModelSavedState"]
widgetBundles = ["constraintLayout", "recyclerView", "cardView", "material", "viewpager", "viewpager2"]
kotlinBundles = ["stdlib", "reflect", "stdlibJdk7", "stdlibJdk8", "coroutines-core", "coroutines-android"]
navigationBundles = ["navigation-fragment", "navigation-ui", "navigation-dynamic", "navigation-dynamicRuntime"]
dataStoreBundles = ["datastore-core", "datastore-preferences"]
retrofitBundles = ["retrofit-core", "retrofit-gson", "gson", "gsonFactory"]
glideBundles = ["glide-core", "glide-annotation", "glide-integration"]
workBundles = ["work-runtime", "work-runtimeKtx"]
[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
jetbrains-kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinVersion" }
Catalogs 配合 Gradle.kts 使用:
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.jetbrains.kotlin) apply false
}
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin)
}
android {
namespace = "com.hgm.versioncatlogsguide"
compileSdk = 34
defaultConfig {
applicationId = "com.hgm.versioncatlogsguide"
minSdk = 26
targetSdk = 33
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.3"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}
dependencies {
implementation(libs.bundles.appcompatBundles)
implementation(libs.bundles.kotlinBundles)
implementation(libs.bundles.widgetBundles)
implementation(libs.bundles.lifecycleBundles)
kapt(libs.lifecycle.compiler)
...
}
具体可以参考 Google 的 nowinandroid 项目,内部带有 Catalogs + gradle.kts 的组件化示例。
https://github.com/android/nowinandroid
只不过它没有对 Catalogs 的依赖进行组件化的再封装,因为这个 Demo 也只有几个组件,在我们真实的中大型项目中,都是动辄十几个或几十个组件,如果我们每个组件都需要写一些重复的代码,那么任务量也是很大的,如果要修改某一个配置那也是需要每个组件都修改,岂不是烦死个人,所以我更推荐封装之后使用。
三、使用 Catalog 的进行组件化封装
像我们之前的 config.gradle 一样的方式,我们可以通过自定义 gradle 文件的方式,然后在各组件中引入对应的依赖即可。
从Gradle 7.0开始, Groovy DSL 也可以使用与 Kotlin DSL 类似的依赖语法,这种改进被称为Gradle箭头语法(Gradle Sugar Syntax)。
之前我们的依赖方式:
dependencies {
implementation 'androidx.appcompat:appcompat:1.4.2'
}
现在我们也能类似 Kotlin DSL 的方式:
dependencies {
implementation libs.arouter.core
implementation(libs.bundles.kotlin)
}
并且部分的 Groovy API 支持 DSL 的方法也支持跳转,并且对于 Catalogs 的支持也是很好可以直接跳转过去,但是支持有限,有些可以有些不性感,这是由于 Groovy 是一种动态语言,而 Kotlin 是一种静态语言,它们在IDE集成支持方面存在差异。即使在Gradle 8.0+ 中, Groovy DSL的导航和重构支持仍然有限的原因 。Kotlin DSL作为一种静态类型的DSL,天生得到了更好的 IDE 集成支持。
3.1 使用 BuildSrc + Groovy DSL 的方式
我们就配合 Groovy DSL 的方式来封装一个基类的公共配置:
configBasic.gradle
/**
* 最基类的,公共的配置模块,(一般不直接使用),只是给别的配置文件继承使用
*/
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'org.jetbrains.kotlin.android'
apply plugin: 'dagger.hilt.android.plugin'
android {
compileSdk = Integer.parseInt(libs.versions.compileSdk.get())
defaultConfig {
minSdk = Integer.parseInt(libs.versions.minSdk.get())
targetSdk = Integer.parseInt(libs.versions.targetSdk.get())
versionCode = Integer.parseInt(libs.versions.versionCode.get())
versionName = libs.versions.versionName.get()
testInstrumentationRunner = libs.versions.testInstrumentationRunner.get()
vectorDrawables {
useSupportLibrary = true
}
multiDexEnabled = true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
buildConfig = true
viewBinding = true
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}
kapt {
arguments {
arg("AROUTER_MODULE_NAME", project.getName())
}
}
dependencies {
//基础
implementation(libs.bundles.appcompatBundles)
implementation(libs.bundles.kotlinBundles)
implementation(libs.bundles.widgetBundles)
implementation(libs.bundles.lifecycleBundles)
kapt(libs.lifecycle.compiler)
//Hilt
implementation(libs.hilt.android)
kapt(libs.hilt.compiler)
//ARouter
implementation(libs.arouter.core)
kapt(libs.arouter.compiler)
//junit
testImplementation(libs.junit)
androidTestImplementation(libs.androidJunit)
androidTestImplementation(libs.espresso)
}
子模块的gradle基类,configModule.gradle:
/**
* 默认的模块初始化gradle配置,可以依赖这个配置 再配置别的依赖
* 上层的配置和running_config同级别-供组件化开发中的子组件依赖,如auth-component组件
*/
apply plugin: 'com.android.library'
apply from: rootProject.file('configBasic.gradle') //重复的配置统一由基类提供
dependencies {
//Module模块默认添加Service模块的
implementation(project(":cs-service"))
}
主要添加组件的服务类。
app宿主或独立运行模块需要的依赖,configRunning.gradle:
/**
* 运行模块初始化gradle配置,可以依赖这个配置 再配置别的依赖
* 上层的配置和module_config同级别-供真正能运行的模块配置,如app组件
*/
apply plugin: 'com.android.application'
apply plugin: 'com.alibaba.arouter'
apply from: rootProject.file('configBasic.gradle') //重复的配置统一由基类提供
android {
defaultConfig {
applicationId = "com.newki.running"
}
signingConfigs {
release {
storeFile file("${project.rootDir}/${project.store_file}")
storePassword project.store_password
keyAlias project.key_alias
keyPassword project.key_password
v1SigningEnabled true
v2SigningEnabled true
}
}
buildTypes {
release {
//默认系统混淆
minifyEnabled true
// 不显示Log
buildConfigField "boolean", "LOG_DEBUG", "false"
//是否可调试
debuggable false
//Zipalign优化
zipAlignEnabled true
//移除无用的resource文件
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
debug {
debuggable true
}
}
}
dependencies {
//Module模块默认添加Service模块的
implementation(project(":cs-service"))
}
主要是配置一些签名,编译信息。
使用也比较简单:
app的build.gradle:
apply from: rootProject.file('configRunning.gradle')
android {
namespace = "com.newki.template"
defaultConfig {
applicationId = libs.versions.applicationId.get()
}
}
dependencies {
implementation(project(":cpt-auth"))
implementation(project(":cpt-profile"))
//依赖到对应组件的Api模块
implementation(project(":app-api"))
}
profile组件的build.gradle:
apply from: rootProject.file('configModule.gradle')
android {
namespace = "com.newki.profile"
}
dependencies {
//依赖到对应组件的Api模块
implementation(project(":cpt-profile-api"))
}
3.2 使用 BuildSrc + Kotin DSL 的方式
理论上 Catalog + Kotlin DSL 的方式会更加优雅一些。
例如创建 common.gradle.kts :
// 示例共通配置
tasks.register("commonTask") {
doLast {
//做一些公共的配置
}
}
这里的公共配置和我们之前的BuildSrc中的 DefaultGradlePlugin 配置类似,然后我们在各模块引入这个基类。
apply(from = "../common.gradle.kts")
或者类似 NowInAndroid 中的单独定义模块定义 Plugin 然后注册之后各模块引入:
比如自定义模块 build-logic 中定义 Plugin :
class AndroidTestConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("com.android.test")
apply("org.jetbrains.kotlin.android")
}
extensions.configure<TestExtension> {
configureKotlinAndroid(this)
defaultConfig.targetSdk = 34
configureGradleManagedDevices(this)
}
}
}
}
build-logic 的 build.gradle 中 register。
gradlePlugin {
plugins {
register("androidTest") {
id = "nowinandroid.android.test"
implementationClass = "AndroidTestConventionPlugin"
}
...
}
}
使用:
plugins {
id("nowinandroid.android.test")
// 其他插件...
}
那么对比 BuildSrc + Kotlin DSL 的管理+封装方式,这种方案有什么缺点?
只是之前 BuildSrc + Kotlin DSL 的方式,我们把版本管理和封装都搞在一起了,现在是 Catalogs 做版本管理,使用另一个单独的模块做封装了。
好处是全程支持 Kotlin 代码提示和导航比 BuildSrc 还要好,其次是扩展性要比 BuildSrc 更好通过注册插件的方式,可以更容易地扩展和自定义构建逻辑,而不需要修改主项目的构建脚本,再次是分离了版本管理和插件注册的逻辑。
缺点是与buildSrc相比编译速度并没有提升,甚至还更慢,因为 buildSrc 是一个特殊的模块, Gradle加载过程略有优化。其次是复杂程度更高,插件太过碎片化导致管理难度提高。
总结
本文介绍了三种比较推荐的做法,BuildSrc + Kotlin DSL ,Catalogs + Groovy DSL,Catalogs + Kotlin DSL并且这三种方式各有利弊。
BuildSrc + Kotlin DSL 的方案的主要特点是编译会稍慢,可能需要处理依赖版本冲突问题,好处是熟悉的Kotlin语法,与良好的代码导航支持。
Catalogs + Groovy DSL 的方案特点是虽然支持了代码导航,但是支持程度没有 Kotlin DSL 好,好处是编译速度更快,都在一个 Project 中版本冲突问题会缓解。
Catalogs + Kotlin DSL 的方案特点中和个上面两种方案的优点,无需新项目编译,完美的代码导航,友好的语言环境,但是无法像上面两种方案进行封装使用。
如果你的项目是小项目,或者组件比较少,或者不是组件化项目,我推荐你使用 Catalogs + Kotlin DSL 的方式,相对更完美,只需要在每一个模块都写一份配置文件而已。
如果你的项目是大型项目,并且有很多的子组件,那么我推荐你使用 Catalogs + Groovy DSL 的方式,封装一份 gradle 基类方便统一集中的管理。
当然如果你想以Catalogs + Kotlin DSL ,封装的方式选择使用单独模块的方式实现Plugin 再 register 之后在其他模块使用也是可以的,方法有很多,各有利弊。
回归到标题上,Android版本管理BuildSrc过时了?Catalogs才是版本答案?我不信!好吧现在我信了,就单纯版本管理来说确实 Catalogs 已经很方便了,尤其是如今 Catalogs 更新迭代到现在,不管是代码导航还是版本升级提示都表现相对更完美,如果你有新项目开发或者老项目改造我都还是更推荐 Catalogs 的方案。
(PS:防杠)当然了,这并不是唯一答案,反正都能用,各位高工大佬爱用哪个用哪个,说到底只是一个编译工具,对最终的打包产物与性能并没有影响,我们应该把更多的关注点放在业务和性能上面。
好了闲话少说,如果你有其他的更多的更好的实现方式,也希望大家能评论区交流一起学习进步。如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,同学们都可以指出修正。
最后本文源码奉上,恳请各位大佬高工指点 【传送门】 。
内部的两个分支分别对应了这里的两种方案,大家可以按需进行参考或测试。
作者:Newki
链接:https://juejin.cn/post/7355738271693373479
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。