深度探索 Gradle 自动化构建技术(一、Gradle 核心配置篇)

前言

成为一名优秀的Android开发,需要一份完备的 知识体系,在这里,让我们一起成长为自己所想的那样~。

一、重识 Gradle

工程构建工具从古老的 mk、make、cmake、qmake, 再到成熟的 ant、maven、ivy,最后到如今互联网时代的 sbt、gradle,经历了长久的历史演化与变迁

Gradle 作为一款新生代的构建工具无疑是有它自身的巨大优势的,因此,掌握好 Gradle 构建工具的各种使用姿势与使用场景其重要性不言而喻

此外,Gradle 已经成为 高级 Android 知识体系 必不可少的一部分。因此,掌握 Gradle,提升自身 自动化构建技术的深度, 能让我们更加地 如虎添翼

1、Gradle 是什么?

  • 1)、它是一款强大的构建工具,而不是语⾔。
  • 2)、它使用了 Groovy 这个语言,创造了一种 DSL,但它本身不是语⾔。

2、为什么使用 Gradle?

主要基于如下 三点 原因:

  • 1)、它是一个款最新的,功能最强大的构建工具,使用它我们能做很多事情。
  • 2)、使用程序替代传统的 XML 配置,使得项目构建更加灵活。
  • 3)、丰富的第三方插件,可以让我们随心所欲地使用。

3、Gradle 的构建流程

通常来说,Gradle 一次完整的构建过程通常分成如下 三个部分

  • 初始化阶段:首先,在初始化阶段 Gradle 会决定哪些项目模块要参与构建,并且为每个项目模块创建一个与之对应的 Project 实例。
  • 配置阶段:然后,配置工程中每个项目的模块,并执行包含其中的配置脚本。
  • 任务执行:最后,执行每个参与构建过程的 Gradle task。

二、打包提速

掌握 Gradle 构建提速的技巧能够帮助我们节省大量的编译构建时间,并且,依赖模块越多且越大的项目节省出来的时间越多,因此是一件投入产出比相当大的事情。

1、升级最新的 Gradle 版本

将 Gradle 和 Android Gradle Plugin 的版本升至最新,所带来的的构建速度的提升效果是显而易见的,特别是当之前你所使用的版本很低的时候。

2、开启离线模式

打开 Android Studio 的离线模式后,所有的编译操作都会走本地缓存,毫无疑问,这将会极大地缩短编译时间。

3、配置 AS 的最大堆内存

在默认情况下, AS 的最大堆内存为 1960MB,我们可以选择 Help => Edit Custom VM Options,此时,会打开一个 studio.vmoptions 文件,我们将第二行的 -Xmx1960m 改为 -Xmx3g 即可将可用内存提升到 3GB

4、删除不必要的 Moudle 或合并部分 Module

过多的 Moudle 会使项目中 Module 的依赖关系变得复杂,Gradle 在编译构建的时候会去检测各个 Module 之间的依赖关系,然后,它会花费大量的构建时间帮我们梳理这些 Module 之间的依赖关系,以避免 Module 之间相互引用而带来的各种问题。除了删除不必要的 Moudle 或合并部分 Module 的方式外,我们也可以将稳定的底层 Module 打包成 aar,上传到公司的本地 Maven 仓库,通过远程方式依赖

5、删除Module中的无用文件

  • 1)、如果我们不需要写单元测试代码,可以直接删除 test 目录。
  • 2)、如果我们不需要写 UI 测试代码,也可以直接删除 androidTest 目录。
  • 3)、此外,如果 Moudle 中只有纯代码,可以直接删除 res 目录。

6、去除项目中的无用资源

在 Android Studio 中提供了供了自动检测失效文件和删除的功能,即 Remove Unused Resource 功能,操作路径如下所示:

右键 => 选中 Refactor => 选中Remove Unused Resource => 直接点击REFACTOR

需要注意的是,这里不需要将 Delete unused @id declarations too 选中,如果你使用了 databinding 的话,可能会编译失败

7、优化第三方库的使用

一般的优化步骤有如下 三步:

1)、使用更小的库去替换现有的同类型的三方库。

2)、使用 exclude 来排除三方库中某些不需要或者是重复的依赖。

例如,我在 Awesome-WanAndroid 项目中就使用到了这种技巧,在依赖 LeakCanary 时,发现它包含有 support 包,因此,我们可以使用 exclude 将它排除掉,代码如下所示:

   debugImplementation (rootProject.ext.dependencies["leakcanary-android"]) {
       exclude group: 'com.android.support'
   }
   releaseImplementation (rootProject.ext.dependencies["leakcanary-android-no-op"]) {
       exclude group: 'com.android.support'
   }
   testImplementation (rootProject.ext.dependencies["leakcanary-android-no-op"]) {
       exclude group: 'com.android.support'
   }

3)、使用 debugImplementation 来依赖仅在 debug 期间才会使用的库,如一些线下的性能检测工具。如下是一个示例代码:

// 仅在debug包启用BlockCanary进行卡顿监控和提示的话,可以这么用
debugImplementation 'com.github.markzhai:blockcanary-android:1.5.0'

8、利用公司 Maven 仓库的本地缓存

当第一个开发引入了新库或者更新版本之后,公司的 Maven 仓库中就会缓存对应的库版本,通过这样的方式,其他开发同事就能够在项目构建时直接从公司的 Maven 仓库中拿到缓存。

9、Debug 构建时设置 minSdkVersion 为 21

这样,我们就可以避免因使用 MutliDex 而拖慢 build 速度。在主 Moudle 中的 build.gradle 中加入如下代码:

    productFlavors {
        speed {
            minSdkVersion 21
        }
    }

同步项目之后,我们在Android Studio右侧的 Build Variants 中选中 speedDebug 选项即可,如下图所示:

深度探索 Gradle 自动化构建技术(一、Gradle 核心配置篇)_第1张图片

需要注意的是,要注意我们当前项目的实际最低版本,比如它为 18,现在我们开启了 speedDebug,项目编写时就会以 21 为标准,此时,就 需要注意 18 ~ 21 之间的 API,例如我在布局中使用了 21 版本新出的 Material Design 的控件,此时就是没问题的,但实际我们需要对 21 版本以下的对应布局做相应的适配。

此外,我们也可以定义不同的 productFlavors,并且在 src 目录下新建对应的 flavor 名称标识的目录资源文件,以此实现在不同的渠道 APK 中采用不同的资源文件。

10、配置 gradle.properties

通用的配置项如下所示:

    // 构建初始化需要执行许多任务,例如java虚拟机的启动,加载虚拟机环境,加载class文件等等,配置此项可以开启线程守护,并且仅仅第一次编译时会开启线程(Gradle 3.0版本以后默认支持)
    org.gradle.daemon=true  
    
    // 配置编译时的虚拟机大小
    org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8  
    
    // 开启并行编译,相当使用了多线程,仅仅适用于模块化项目(存在多个 Library 库工程依赖主工程)
    org.gradle.parallel=true  
    
    // 最大的优势在于帮助多 Moudle 的工程提速,在编译多个 Module 相互依赖的项目时,Gradle 会按需选择进行编译,即仅仅编译相关的 Module
    org.gradle.configureondemand=true   
    
    // 开启构建缓存,Gradle 3.5新的缓存机制,可以缓存所有任务的输出,
    // 不同于buildCache仅仅缓存dex的外部libs,它可以复用
    // 任何时候的构建缓存,设置包括其它分支的构建缓存
    org.gradle.caching=true

这里效果比较好一点的配置项就是 配置编译时的虚拟机大小 这项,我们来详细分析下其中参数的含义,如下所示:

  • -Xmx2048m:指定 JVM 最大允许分配的堆内存为 2048MB,它会采用按需分配的方式。
  • -XX:MaxPermSize=512m:指定 JVM 最大允许分配的非堆内存为 512MB,同上堆内存一样也是按需分配的。

11、配置 DexOptions

我们可以将 dexOptions 配置项中的 maxProcessCount 设定为 8,这样编译时并行的最大进程数数目就可以提升到 8 个。

12、使用 walle 提升打多渠道包的效率

walle 是 Android Signature V2 Scheme 签名下的新一代渠道包打包神器,它在 Apk 中的 APK Signature Block 区块添加了自定义的渠道信息以生成渠道包,因而提高了渠道包的生成效率。此外,它也可以作为单机工具来使用,也可以部署在 HTTP 服务器上来实时处理渠道包 Apk 的升级网络请求,有需要的同学可以参考美团的 walle。

13、设置应用支持的语言

如果应用没有做国际化,我们可以让应用仅仅支持 中文的资源配置,即将 resConfigs 设置为 “zh”。如下所示:

    android {
        defaultConfig {
            resConfigs "zh"
        }
    }

14、使用增量编译

Gradle 的构建方式通常来说细分为以下 三种:

  • 1)、Full Build:全量构建,即从0开始构建。
  • 2)、Incremental build java change:增量构建Java改变,修改源代码后的构建,且之前构建过。
  • 3)、Incremental build resource change:修改资源文件后的构建,且之前构建过。

在 Gradle 4.10 版本之后便默认使用了增量编译,它会测试自上次构建以来是否已更改任何 gradle task 任务输入或输出。如果还没有,Gradle 会将该任务认为是最新的,因此跳过执行其动作。由于 Gradle 可以将项目的依赖关系分析精确到类级别,因此,此时仅会重新编译受影响的类。如果在更老的版本需要启动增量编译,可以使用如下配置:

    tasks.withType(JavaCompile) {
        options.incremental = true
    }

15、使用循环进行依赖优化()

在 Awesome-WanAndroid 项目的 app moudle 的 build.gradle 中,有将近几百行的依赖代码,如下所示:

    dependencies {
        implementation fileTree(include: ['*.jar'], dir: 'libs')

        // 启动器
        api files('libs/launchstarter-release-1.0.0.aar')
        
         //base
        implementation rootProject.ext.dependencies["appcompat-v7"]
        implementation rootProject.ext.dependencies["cardview-v7"]
        implementation rootProject.ext.dependencies["design"]
        implementation rootProject.ext.dependencies["constraint-layout"]
        
        annotationProcessor rootProject.ext.dependencies["glide_compiler"]
        
         //canary
        debugImplementation (rootProject.ext.dependencies["leakcanary-android"]) {
            exclude group: 'com.android.support'
        }
        releaseImplementation (rootProject.ext.dependencies["leakcanary-android-no-op"]) {
            exclude group: 'com.android.support'
        }
        testImplementation (rootProject.ext.dependencies["leakcanary-android-no-op"]) {
            exclude group: 'com.android.support'
        }
        
        ...

有没有一种好的方式不在 build.gradle 中写这么多的依赖配置?

有,就是 使用循环遍历依赖。答案似乎很简单,但是要想处理在依赖时遇到的所有情况,并不简单。下面,我直接给出相应的适配代码,大家可以直接使用。

首先,在 app 下的 build.gradle 的依赖配置如下所示:

    // 处理所有的 aar 依赖
    apiFileDependencies.each { k, v -> api files(v)}

    // 处理所有的 xxximplementation 依赖
    implementationDependencies.each { k, v -> implementation v }
    debugImplementationDependencies.each { k, v -> debugImplementation v }
    releaseImplementationDependencies.each { k, v -> releaseImplementation v }
    androidTestImplementationDependencies.each { k, v -> androidTestImplementation v }
    testImplementationDependencies.each { k, v -> testImplementation v }
    debugApiDependencies.each { k, v -> debugApi v }
    releaseApiDependencies.each { k, v -> releaseApi v }
    compileOnlyDependencies.each { k, v -> compileOnly v }
    
    // 处理 annotationProcessor 依赖
    processors.each { k, v -> annotationProcessor v }
    
    // 处理所有包含 exclude 的依赖
    implementationExcludes.each { entry ->
        implementation(entry.key) {
            entry.value.each { childEntry ->
                exclude(group: childEntry)
            }
        }
    }
    debugImplementationExcludes.each { entry ->
        debugImplementation(entry.key) {
            entry.value.each { childEntry ->
                exclude(group: childEntry.key, module: childEntry.value)
            }
        }
    }
    releaseImplementationExcludes.each { entry ->
        releaseImplementation(entry.key) {
            entry.value.each { childEntry ->
                exclude(group: childEntry.key, module: childEntry.value)
            }
        }
    }
    testImplementationExclude.each { entry ->
        testImplementation(entry.key) {
            entry.value.each { childEntry ->
                exclude(group: childEntry.key, module: childEntry.value)
            }
        }
    }
    androidTestImplementationExcludes.each { entry ->
        androidTestImplementation(entry.key) {
            entry.value.each { childEntry ->
                exclude(group: childEntry.key, module: childEntry.value)
            }
        }
    }

然后,在 config.gradle 全局依赖管理文件中配置好对应名称的依赖数组即可。代码如下所示:

    dependencies = [
            // base
            "appcompat-v7"                      : "com.android.support:appcompat-v7:${version["supportLibraryVersion"]}",
            ...
    ]
    
    annotationProcessor = [
            "glide_compiler"                    : "com.github.bumptech.glide:compiler:${version["glideVersion"]}",
            ...
    ]
    
    apiFileDependencies = [
            "launchstarter"                                   :"libs/launchstarter-release-1.0.0.aar"
    ]
    
    debugImplementationDependencies = [
            "MethodTraceMan"                                  : "com.github.zhengcx:MethodTraceMan:1.0.7"
    ]
    
    ...
    
    implementationExcludes = [
            "com.android.support.test.espresso:espresso-idling-resource:3.0.2" : [
                    'com.android.support' : 'support-annotations'
            ]
    ]
    
    ...

具体的代码示例可以在 Awesome-WanAndroid 的 build.gradle 和 config.gradle 上进行查看。

三、Gradle 常用命令

1、Gradle 查询命令

1)、查看主要任务

    ./gradlew tasks

2)、查看所有任务,包括缓存任务等等

    ./gradlew tasks --all

2、Gradle 执行命令

1)、对某个module [moduleName] 的某个任务[TaskName] 运行

    ./gradlew :moduleName:taskName

3、Gradle 快速构建命令

Gradle 提供了一系列的快速构建命令来替代 IDE 的可视化构建操作,如我们最常用的 clean、build 等等。需要注意的是,build 命令会把 debug、release 环境的包都构建出来。

1)、查看构建版本

    ./gradlew -v

2)、清除 build 文件夹

    ./gradlew clean

3)、检查依赖并编译打包

    ./gradlew build

4)、编译并安装 debug 包

    ./gradlew installDebug

5)、编译并打印日志

    ./gradlew build --info

6)、编译并输出性能报告,性能报告一般在构建工程根目录 build/reports/profile 下

    ./gradlew build --profile

7)、调试模式构建并打印堆栈日志

    ./gradlew build --info --debug --stacktrace

8)、强制更新最新依赖,清除构建后再构建

    ./gradlew clean build --refresh-dependencies

9)、编译并打 Debug 包

    ./gradlew assembleDebug
    # 简化版命令,取各个单词的首字母
    ./gradlew aD

10)、编译并打 Release 的包

    ./gradlew assembleRelease
    # 简化版命令,取各个单词的首字母
    ./gradlew aR

4、Gradle 构建并安装命令

1)、Release 模式打包并安装

    ./gradlew installRelease

2)、卸载 Release 模式包

    ./gradlew uninstallRelease

3)、debug release 模式全部渠道打包

    ./gradlew assemble

5、Gradle 查看包依赖命令

1)、查看项目根目录下的依赖

    ./gradlew dependencies

2)、查看 app 模块下的依赖

    ./gradlew app:dependencies

3)、查看 app 模块下包含 implementation 关键字的依赖项目

    ./gradlew app:dependencies --configuration implementation

四、使用 Build Scan 诊断应用的构建过程

在了解 Build Scan 之前,我们需要先来一起学习下旧时代的 Gradle build 诊断工具 Profile report。

1、Profile report

通常情况下,我们一般会使用如下命令来生成一份本地的构建分析报告:

    ./gradlew assembleDebug --profile

这里,我们在 Awesome-WanAndroid App的根目录下运行这个命令,可以得到四块视图。下面,我们来了解下。

1)、Summary

Gradle 构建信息的概览界面,用于 查看 Total Build Time、初始化(包含 Startup、Settings and BuildSrc、Loading Projects 三部分)、配置、任务执行的时间。如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NPORskRT-1586753696609)(http://github.com/JsonChao/Awesome-Android-Performance/blob/master/screenshots/gradle_profile_report1.png?raw=true)]

2)、Configuaration

Gradle 配置各个工程所花费的时间,我们可以看到 All projects、app 模块以及其它模块单个的配置时间。如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t396apgo-1586753696609)(https://github.com/JsonChao/Awesome-Android-Performance/blob/master/screenshots/gradle_profile_report2.png?raw=true)]

3)、Dependency Resolution

Gradle 在对各个 task 进行依赖关系解析时所花费的时间。如下图所示:

深度探索 Gradle 自动化构建技术(一、Gradle 核心配置篇)_第2张图片

4)、Task Execution

Gradle 在执行各个 Gradle task 所花费的时间。如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P53ZqExq-1586753696610)(https://github.com/JsonChao/Awesome-Android-Performance/blob/master/screenshots/gradle_profile_report4png.png?raw=true)]

需要注意的是,Task Execution 的时间是所有 gradle task 执行时间的总和,实际上 多模块的任务是并行执行的

2、Build Scan

Build Scan 是官方推出的用于诊断应用构建过程的性能检测工具,它能分析出导致应用构建速度慢的一些问题。在项目下使用如下命令即可开启 Build Scan 诊断:

    ./gradlew build --scan 

如果你使用的是 Mac,使用上述命令时出现

    zsh: permission denied: ./gradlew

可以加入下面的命给 gradlew 分配执行权限:

    chmod +x gradlew

执行完 build --scan 命令之后,在命令的最后我们可以看到如下信息:

可以看到,在 Publishing build scan 点击下面的链接就可以跳转到 Build Scan 的诊断页面。

需要注意的是,如果你是第一次使用 Build Scan,首先需要使用自己的邮箱激活 Build Scan。如下图界面所示:

这里,我输入了我的邮箱 [email protected],点击 Go!之后,我们就可以登录我们的邮箱去确认授权即可。如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6oCPrBFj-1586753696612)(https://github.com/JsonChao/Awesome-Android-Performance/blob/master/screenshots/build_scan2.png?raw=true)]

直接点击 Discover your build 即可。

授权成功后,我们就可以看到 Build Scan 的诊断页面了。如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VJlXA2Ge-1586753696612)(https://github.com/JsonChao/Awesome-Android-Performance/blob/master/screenshots/build_scan4.png?raw=true)]

可以看到,在界面的右边有一系列的功能 tab 可供我们选择查看,这里默认是 Summary 总览界面,我们的目的是要查看 应用的构建性能,所以点击右侧的 Performance tab 即可看到如下图所示的构建分析界面:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fDoIY8EM-1586753696612)(https://github.com/JsonChao/Awesome-Android-Performance/blob/master/screenshots/build_scan5.png?raw=true)]

从上图可以看到,Performance 界面中除了 Build、Configuration、Dependency resolution、Task execution 这四项外,还有 Daemon、Network activity、Settings and suggestions

在 Build 界面中,共有三个子项目,即 Total build time、Total garbage collection time、Peak heap memory usage,Total build time 里面的配置项前面我们已经分析过了,这里我们看看其余两项的含义,如下所示:

  • Total garbage collection time:总的垃圾回收时间。
  • Peak heap memory usage:最大堆内存使用。

对于 Peak heap memory usage 这一项来说,还有三个子项,其含义如下:

  • 1)、PS Eden Space:Young Generation 的 Eden(伊甸园)物理内存区域。程序中生成的大部分新的对象都在 Eden 区中。
  • 2)、PS Survivor Space:Young Generation 的 Eden 的 两个Survivor(幸存者)物理内存区域。当 Eden 区满时,还存活的对象将被复制到其中一个 Survivor 区,当此 Survivor 区满时,此区存活的对象又被复制到另一个 Survivor 区,当这个 Survivor 区也满时,会将其中存活的对象复制到年老代。
  • 3)、PS Old Gen:Old Generation,一般情况下,年老代中的对象生命周期都比较长。

由于我们的目的是关注项目的 build 时间,所以,我们直接关注到 Task execution 这一项。如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QkZPpIsu-1586753696613)(https://github.com/JsonChao/Awesome-Android-Performance/blob/master/screenshots/build_scan6.png?raw=true)]

可以看到,Awesome-WanAndroid 项目中所有的 task 都是 Not cacheable 的。此时,我们往下滑动界面,可以看到所有 task 的构建时间。如下所示:

如果,我们想查看一个 tinyPicPluginSpeedRelease 这一个 task 的执行详细,可以点击 :app:tinyPicPluginSpeedRelease 这一项,然后,就会跳转到 Timeline 界面,显示出 tinyPicPluginSpeedRelease 相应的执行信息。如下图所示:

此外,这里我们点击弹出框右上方的第一个图标:Focus on task in timeline 即可看到该 task 在整个 Gradle build 时间线上的精确位置,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dDl2JUoe-1586753696614)(https://github.com/JsonChao/Awesome-Android-Performance/blob/master/screenshots/build_scan9.png?raw=true)]

至此,我们可以看到 Build Scan 的功能要比 Profile report 强大不少,所以我强烈建议优先使用它进行 Gradle 构建时间的诊断与优化。

五、总结

Gradle 每次构建的运行时间会随着项目编译次数越来少,因此为了准确评估 Gradle 构建提速的优化效果,我们可以在优化前后分别执行以下命令进行对比分析,如下所示:

    gradlew --profile --recompile-scripts --offline --rerun-tasks assembleDebug

参数含义如下:

  • profile:开启性能检测。
  • recompile-scripts:不使用缓存,直接重新编译脚本。
  • offline:启用离线编译模式。
  • return-task:运行所有 gradle task 并忽略所有优化。

此外,Facebook 的 Buck 以及 Google 的 Bazel 都是优秀的编译工具,那么他们为什么没有使用开源的构建工具呢,主要有如下 三点原因:

  • 1)、统一编译工具:内部的所有项目都使用同一套构建工具,包括 Android、Java、iOS、Go、C++ 等。编译工具的统一优化会使所有项目受益。
  • 2)、代码组织管理架构:Facebook 和 Google 的所有项目都放到同一个仓库里面,因此整个仓库非常庞大,并且,他们也不会使用 Git。目前 Google 使用的是Piper,Facebook 是基于HG修改的,也是一种基于分布式的文件系统。
  • 3)、极致的性能追求:Buck 和 Bazel 的性能的确比 Gradle 更好,内部包含它们的各种编译优化。但是它们的定制型太强,而且对 Maven、JCenter 这样的外部依赖支持也不好。

但是,Buck 和 Bazel 编译构建工具内部的优化思路 还是很值得我们学习和参考的,有兴趣的同学可以去研究下。下一篇文章,我们将一起来学习 Gradle 中的必备基础 — groovy,这将会给我们后续的 Gradle 学习打下坚实的基础,敬请期待。

参考链接:


1、Gradle Github 地址

2、Gradle配置最佳实践

3、提升 50% 的编译速度!阿里零售通 App 工程提效实践

4、Gradle 提速:每天为你省下一杯喝咖啡的时间

5、[大餐]加快gradle构建速度

6、Gradle模块化配置:让你的gradle代码控制在100行以内

7、Gradle Android-build 常用命令参数及解释

8、Android打包提速实践

9、GRADLE构建最佳实践

Contanct Me

● 微信:

欢迎关注我的微信:bcce5360

● 微信群:

由于微信群已超过 200 人,麻烦大家想进微信群的朋友们,加我微信拉你进群。

● QQ群:

2千人QQ群,Awesome-Android学习交流群,QQ群号:959936182, 欢迎大家加入~

About me

  • Email: [email protected]

  • Blog: https://jsonchao.github.io/

  • 掘金: https://juejin.im/user/5a3ba9375188252bca050ade

很感谢您阅读这篇文章,希望您能将它分享给您的朋友或技术群,这对我意义重大。

希望我们能成为朋友,在 Github、掘金上一起分享知识。

你可能感兴趣的:(android,进阶,android,面试)