Gradle系列第(三)篇---Android Studio与Gradle那些事儿

版权声明:本文为LooperJing原创文章,转载请注明出处!

·

Android中的gradle.jpg

本来这篇要写Android性能优化的,个人时间比较少,每天加班到很晚,写博客的时间就很少了,但是Gradle系列的文章还没有写完,所以补一篇,在Gradle系列第(二)篇---Gradle编程主要对象主要写了Gradle中的几个对象(Project,Settings,Gradle,Task、Action),现在聊一聊Android Studio中的gradle常见的功能需求。如果你還沒有阅读过我的前两篇博客Gradle系列第(一)篇---Groovy语法初探Gradle系列2---Gradle编程主要对象,可以先看一下,有助于本文的理解,好啦,各位看官准备好瓜子花生,接下来一大篇文章哗啦啦的来了。不过不用担心,这篇博客仍然是面向基础。

读完这篇博客,你会了解到这些内容

  • 1、Android的构建文件
  • 2、全局参数配置
  • 3、用脚本更改项目结构
  • 4、多种apk的生成
  • 5、签名的配置与使用
  • 6、项目混淆(Proguard)
  • 7、gradle多渠道打包
  • 8、APK需求定制的案例
  • 9、动态参数配置
  • 10、gradle依赖管理
  • 11、gradle.properties文件配置
  • 12、jar文件输出
一、AS项目构建文件的简单解释

一个AS项目结构大概像下面这样子

项目构建文件.jpg

如蓝色条所示,项目中总共包含了6个构建文件(不算Library中的gradle),我们先从宏观的方面了解一下,每个构建文件的作用是啥?

  • 1、这个文件是app文件夹下这个Module的gradle配置文件,也可以算是整个项目最主要的gradle配置文件,比如自动打包debug,release,beta等环境,签名,多渠道打包,混淆等操作都可以在这里面写。每一个Module都需要有一个gradle配置文件。
  • 2、我们主要看下gradle-wrapper.properties这个文件的内容
#Mon Dec 28 10:00:20 PST 2015
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip

可以看到里面声明了gradle的目录与下载路径以及当前项目使用的gradle版本,这些默认的路径我们一般不会更改的,有時候导入一个新项目,gradle版本不对,可以在这里修改。

  • 3、这个文件是整个项目的gradle基础(全局)配置文件,内容主要包含了两个方面:一个是声明仓库的源,这里可以看到是指明的jcenter(), 之前版本则是mavenCentral(), jcenter可以理解成是一个新的中央远程仓库,兼容maven中心仓库,而且性能更优。另一个是声明了android gradle plugin的版本。allprojects:中定义的属性会被应用到所有 moudle 中,但是为了保证每个项目的独立性,我们一般不会在这里面操作太多共有的东西。

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.0'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

所有通过gradle导入的jar包都是从http://bintray.com/bintray/jcenter这个中央仓库上扒下来的。如果你需要的jar包在这个网站上没有,那就无法通过gradle的方式来导入哦。

  • 4、这个里面可以配置参数,然后在其他build.gradle中引用,后面会讲例子,如何动态配置参数。
  • 5、这里主要指定了ndk和SDK的路径
ndk.dir=G\:\\Users\\wangjing\\AppData\\Local\\Android\\sdk\\ndk-bundle
sdk.dir=G\:\\Users\\wangjing\\AppData\\Local\\Android\\sdk
  • 6、setting.gradle最关键的内容就是告诉Gradle这个multiprojects包含哪些子projects,当你的app只有一个模块的时候,你的setting.gradle将会是这样子的:
include ':app'

当你的app有多个模块的时候,你的setting.gradle将会是这样子的

include ':app', ':library',。。。。

setting.gradle文件将会在gradle初始化时期执行,关于初始化时期,可以查看上一篇博客,并且定义了哪一个模块将会被构建。举个例子,上述setting.gradle包含了app模块,setting.gradle是针对多模块操作的,所以单独的模块工程完全可以删除掉该文件。在这之后,Gradle会为我们创建一个Setting对象,每一个settings.gradle都会转换成一个Settings对象,并为其包含必要的方法,你不必知道Settings类的详细细节,但是你最好能够知道这个概念。另外可以在settings做一些初始化的工作,后面介绍。

读到这里做个总结

  • build.gradle:控制每个Module的构建过程
  • gradle.properties:设置gradle脚本中的参数
  • local.properties:gradle的SDK和NDK环境变量配置
  • gradle.properties:用于配置参数信息
  • setting.gradle :配置gradle的多项目管理
二、实用技能精讲
1、全局参数配置

通常我们的项目都有很多的Module,像我现在公司的项目就有十几个,那么每个Module里面的gradle文件通常都有类似这样的配置。

android {
    compileSdkVersion 24
    buildToolsVersion "24.0.0"
    defaultConfig {
        applicationId "com.zhangwan.www.gradle"
        minSdkVersion 15
        targetSdkVersion 24
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
}

这些配置对于每个Module来说,最好统一,我把它定义在项目根目录的gradle文件中,如下。

//全局配置
ext {
    minSdkVersion =15
    targetSdkVersion =24
    compileSdkVersion =24
    buildToolsVersion ="24.0.0"
    versionCode =1
    versionName="1.0"
}

定义好了,我们可以在各个Module的gradle文件文件中引用,如下:

android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion  rootProject.ext.buildToolsVersion

    defaultConfig {
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
        versionCode rootProject.ext.versionCode
        versionName rootProject.ext.versionName
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
}

利用Gradle全局变量,对于多Module有很大的好处,方便统一,除了上面的列子,在举个例子。上面全部按照单个的属性配置的,对于相关的属性,可以将他们写到一个列表中,下面定义了一个dependencies_config的列表。

ext{
    dependencies_config=[supportv7:"com.android.support:appcompat-v7:25.0.0"]
}

在Module中,这样引用

dependencies {
    compile rootProject.ext.dependencies_config.supportv7
     .....
}
2、项目结构更改

sourceSets 的作用是重新定义资源文件位置,比如

android {
    compileSdkVersion 24
    buildToolsVersion "24.0.0"
 
    sourceSets{
        main{
            res.srcDirs=['src/main/res','src/main/res/layout/activity','src/main/res/layout/fragment']
        }
    }
}

在你Sync Now之后,会出现activity和fragment两个文件夹


更改项目目录.png

最常见的是下面这块代码,当Eclipse项目转到Studio的时候,需要重新指定一些文件的位置。

sourceSets {  
    main {  
        manifest.srcFile 'AndroidManifest.xml'  
        java.srcDirs = ['src']  
        resources.srcDirs = ['src']  
        aidl.srcDirs = ['src']  
        renderscript.srcDirs = ['src']  
        res.srcDirs = ['res']  
        assets.srcDirs = ['assets']  
        jniLibs.srcDirs = ['libs']  
    }  
}  

sourceSets的用法就是这样,可以重新指定文件目录,但是读到这,可能有些人心中有个问题,为什么sourceSets ,defaultConfig这样的东东要写在android的大括号中。换言之,android这个大括号里面还能写什么东西,我来列举一下。

android {
    defaultConfig {
        //默认配置项,defaultConfig就是程序的默认配置,注意,如果在   AndroidMainfest.xml里面定义了与这里相同的属性,会以这里的为主。
    }

    buildTypes {
      // 编译配置,release或debug版本的内容
    }

    compileOptions {
      // Java 的版本配置
    }

    sourceSets {
        //源码设置(项目目录结构的设置)
    }

    packagingOptions {
       //打包时的相关配置  
    }

    lintOptions {
        //编译的 lint 开关,程序在buid的时候,会执行lint检查,有任何的错误或者警告提示,都会终止构建,我们可以将其关掉。
        //abortOnError false  
    }

    productFlavors {
        //产品发布的一些东西,比如渠道、包名等
        flavor1 {
        }

        flavor2 {
        }
    }

    signingConfigs {
        //签名的配置
        release {
        }
    }

    testOptions{
        //测试配置,TestOptions类型
    } 
    aaptOptions{
      //aapt配置,AaptOptions类型 
    } 
     lintOptions{
       //lint配置,LintOptions类型
    } 
    dexOptions{
       //dex配置,DexOptions类型
    } 
    compileOptions{
     // 编译配置,CompileOptions类型
    } 
    packagingOptions{
       // PackagingOptions类型
    } 
    jacoco{ 
       //JacocoExtension类型。 用于设定 jacoco版本
    } 
    splits{
       //Splits类型。
    } 
}

在DSL文档中,以上每个类型都有它的详细配置选项,一般常见的设置就是上面啦,如果你觉得有的不太了解,看下面之后就了解了。

3、多种apk的生成

默认studio生成的buildTypes是像下面这样的,但是呢,我还想要其他的变种类型。

 buildTypes {
        release {
            minifyEnabled false// 不混淆
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

我们可以这样添加

 buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        r1{
            applicationIdSuffix ".r1"
        }
        r2{
            applicationIdSuffix ".r2"
        }
        r3{
            applicationIdSuffix ".r3"
        }
    }

通过这样就可以得到多种变种app,执行assemble这个task,打出所有apk。

build_types.png

总共得到系统默认有的release和debug两个apk,额外还有r1,r2,r3三个不同的apk。
那么applicationIdSuffix是什么呢,逆向r1包看看。

r1包逆向结果.png

系统通过包名来区分应用,这种方式无非就是在包名后面加上了一个后缀r1。

4、签名的配置与使用

上面打出的包都是没有指定签名的,我们要配置一个签名,首先需要生成签名文件。我生成的签名文件是1.jks

 signingConfigs{
        signR1{
            storeFile file("build/1.jks");
            storePassword "123456"
            keyAlias "xxx"
            keyPassword "123456"
        }
        signR2{
            storeFile file("build/2.jks");
            storePassword "123456"
            keyAlias "xxx"
            keyPassword "123456"
        }
    }

签名在signingConfigs中配置,signR1,signR2是签名的名字,在buildTypes中使用。

 buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        r1{
            signingConfig signingConfigs.signR1
            applicationIdSuffix ".r1"
        }
        r2{
            signingConfig signingConfigs.signR2
            applicationIdSuffix ".r2"
        }

    }

加上签名后打的包是这样,跟未加签名相比较,多了app-r1.apk,app-r2.apk。


签名apk生成.png
5、项目混淆(Proguard)

面对众多的渠道,打包也有很多不同的需求。 比如 debug版,release版,dev版等等。 有时候不同的版本中使用到的不同的服务端api域名也不相同。 比如 debug_api.com,release_api.com,dev_api.com等等。不同的版本对应了不同的 api 域名,还可能对应不同的 icon 等。渠道首发包通常需要要求在欢迎页添加渠道的logo等。下面我们开始进行打包。首先进行混淆设置,混淆需要buildTypes中配置,在上面说过,默认生成的buildTypes是这样子的

  buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

其中,proguard-android.txt是在你的sdk\tools\proguard目录下。minifyEnabled:表示是否开启混淆,默认为false;proguardFiles:混淆配置文件,一般就采用项目中默认的proguard-rules.pro文件。在这个文件中写我们的混淆规则,比如:

-keepclasseswithmembernames class * {                                           # 保持 native 方法不被混淆
    native <methods>;
}

-keepclassmembers enum * {                                                      # 保持枚举 enum 类不被混淆
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

-keep class * implements android.os.Parcelable {                                # 保持 Parcelable 不被混淆
  public static final android.os.Parcelable$Creator *;
}

有这些还不够,还需要在gradle中开启混淆

   buildTypes {
        release {
           // 不显示 Log 
            buildConfigField "boolean", "LOG_DEBUG", "false"
            shrinkResources true
            signingConfig signingConfigs.release
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard.cfg'
          
        }
        debug {
          // 显示 Log 
            buildConfigField "boolean", "LOG_DEBUG", "true"
            signingConfig signingConfigs.debug
        }
    }

我们设置minifyEnabled true,就会在打包的时候进行代码混淆处理. 其中proguard-android.txt不用管,在sdk目录里面,我们主要是配置了proguard.cfg文件。可能大家直接在android studio创建项目不会有这个文件,而是proguard-rules.pro文件,其实一样的,我这里是因为项目是从eclipse迁移过来的,之前在eclipse上混淆是proguard.cfg文件.

6、gradle多渠道打包
  • 1、第一步 在AndroidManifest.xml里配置PlaceHolder
 <meta-data
            android:name="MY_CHANNEL"
            android:value="${MY_CHANNEL}" />
  • 2、第二步 在build.gradle设置productFlavors
   productFlavors{
        xiaomi {
            //用gradle修改AndroidManifest.xml中的meta-data元素值
            manifestPlaceholders = [MY_CHANNEL: "xiaomi"]
        }

        _360 {
            manifestPlaceholders = [MY_CHANNEL: "_360"]
        }
        baidu {
            manifestPlaceholders = [MY_CHANNEL: "baidu"]
        }

        huawei{
            manifestPlaceholders = [MY_CHANNEL: "huawei"]
        }
    }

或者批量修改

 productFlavors{
        xiaomi {}
        _360 {}
        baidu {}
        huawei{}
    }

    productFlavors.all {
        flavor -> flavor.manifestPlaceholders = [MY_CHANNEL: name]
    }

最后,最好在defaultConfig中定义一个默认的渠道

defaultConfig{
        manifestPlaceholders = [ MY_CHANNEL:"xiaomi" ]
}

到此配置完成,可以执行命令了。

  • 3、去工程的根目录,也就是有gradlew文件的目录,打开命令行,输入命令:
    ./gradlew assemble
    这时候你去app/build/outputs/apk中就能看到自动打好的渠道包了。
    ./gradlew assembleRelease
    只打Release包
    ./gradlew assembleDebug
    只打Debug包
    ./gradlew assemblebaidu
    只打360的渠道包
    ./gradlew assemblebaiduRelease

不想敲命令行的,调起下面这个面板打包

打包Task.png
7、APK需求定制

上面说了一下打包,在打包的時候,一些特殊化的操作,比如修改指定apk Logo,apk重命名,这些怎么搞?

  • 渠道包重命名
android.applicationVariants.all { variant ->
        variant.outputs.each { output ->
            def outputFile = output.outputFile
            if (outputFile != null && outputFile.name.endsWith('.apk')) {
                File outputDirectory = new File(outputFile.parent);
                def fileName
                if (variant.buildType.name == "release") {
                    fileName = "wangjing_${variant.productFlavors[0].name}.apk"
                } else {
                    fileName = "wangjing_${variant.productFlavors[0].name}_beta.apk"
                }
                output.outputFile = new File(outputDirectory, fileName)
            }
        }
    }
  • 根据渠道修改APP名称
buildTypes {
        debug {
            // 显示Log
            buildConfigField "boolean", "LOG_DEBUG", "true"
            //重命名
            resValue("string","app_name","DEBUG")
            versionNameSuffix "-debug"
            minifyEnabled false
            zipAlignEnabled false
            shrinkResources false
            signingConfig signingConfigs.debug
        }
        release {
            // 不显示Log
            buildConfigField "boolean", "LOG_DEBUG", "false"
            //重命名
            resValue("string","app_name","DEBUG")
            //混淆
            minifyEnabled true
            //加载默认混淆配置文件
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            //签名
            signingConfig signingConfigs.release

        }
    }

其中 resValue("string","app_name","DEBUG") 表示一个string 类型的变量app_name的值是DEBUG,做了上面的配置之后,需要将string.xml的app_name删掉,因为gradle编译的时候,会将脚本中的配置跟string.xml的合并。

8、动态参数配置
signingConfigs{
        release{
            storeFile file("build/mykey.jks")
            storePassword   "123456"
            keyAlias "123456"
            keyPassword   "123456"
        }
    }

上面的这段配置,有个缺点,就是值直接写死了,我们可以动态配置参数。在哪里配置呢,一开始就说了,在gradle.properties中配置参数。如下:

 systemPro.keyAliasPassword=123456
systemPro.keyAlias=123456
systemPro.keyStorePassword=123456
systemPro.keyStore=mykey.jks

配置好了,就可以“到处”使用了

 signingConfigs{
        release{
            storeFile       System.properties["keyStore"]
            storePassword   System.properties["keyStorePassword"]
            keyAlias        System.properties["keyAlias"]
            keyPassword     System.properties["keyAliasPassword"]
        }
        debug{
            storeFile file("mykey.jks")
            storePassword  "123456"
            keyAlias"123456"
            keyPassword  "123456"
        }
    }

9、gradle依赖管理

比如我们想依赖个support-v4包,直接一句话:

compile 'com.android.support:support-v4:23.1.1'

一个依赖需要定义三个元素:group,name和version。group意味着创建该library的组织名,通常这会是包名,name是该library的唯一标示。
上述的代码是基于groovy语法的,所以其完整的表述应该是这样的:

compile group: 'com.android.support:', name: 'support-v4', version:'23.1.1'

有些时候,你可能需要和sdk协调工作。为了能顺利编译你的代码,你需要添加SDK到你的编译环境。你不需要将sdk包含在你的APK中,因为它早已经存在于设备中,不需要在compile,我们总共有5个不同的配置:

  • compile是默认的那个,其含义是包含所有的依赖包,即在APK里,compile的依赖会存在。

  • apk的意思是apk中存在,但是不会加入编译中,这个貌似用的比较少。

  • provided的意思是提供编译支持,但是不会写入apk。

  • testCompileandroidTestCompile会添加额外的library支持针对测试。

通常项目的Module很多,依赖也非常多,为了方便管理,我们应该将这些依赖写到一个全局的地方,可以供其他module使用。这种思想也是第一小节所提的全局参数的配置。依赖管理可以参考:http://stormzhang.com/android/2016/03/13/gradle-config/

10、gradle.properties文件配置
  • gradle.properties常见配置比如有:
    开启并行编译:加快gradle 的编译
    org.gradle.parallel=true

  • 开启编译守护进程:该进程在第一次启动后回一直存在,当你进行二次编译的时候,可以重用该进程。
    org.gradle.daemon=true

  • 加大可用编译内存:
    org.gradle.jvmargs=-Xms256m -Xmx1024m

11、jar文件输出

android Studio常常有输出jar包的需求,只要下面这段代码即可:

task makeJar(type: Copy) {
    delete 'build/libs/my.jar'
    from('build/intermediates/bundles/release/')
    into('build/libs/')
    include('classes.jar')
    rename ('classes.jar', 'my.jar')
}

OK,Gradle研究了一个多星期,这篇博客耗时两个晚上,终于结束,另外如果时间来的急,在写一篇Gradle系列4或5,因为感觉自己还没有讲清楚,侧重多个Module中gradle的使用与Gradle常用命令的使用,下篇博客继续性能优化系列的更新,每一次写博客都花费很多的时间和精力也是一次锻炼,跟他人分享自己的学习成果,最后附上参考资料,比我写的好。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,800评论 25 707
  • 1.介绍 如果你正在查阅build.gradle文件的所有可选项,请点击这里进行查阅:DSL参考 1.1新构建系统...
    Chuckiefan阅读 12,122评论 8 72
  • 这一章主要针对项目中可以用到的一些实用功能来介绍Android Gradle,比如如何隐藏我们的证书文件,降低风险...
    acc8226阅读 7,575评论 3 25
  • ① 朗费罗说:“不要老叹息过去,它是不再回来的;要明智地改善现在。 人生是条无名的河,是深是浅...
    玉儿说阅读 687评论 3 3
  • 这是最近打磨出来的一个小技巧,也是之前思考时间概念时,迸发出来的一个小感悟。 干货总结语:穿越到现在的未来的自己,...
    果大喵喵阅读 712评论 0 0