文檔當前狀態:**beta0.2**
* [x] 選題收集:2017/10/25
* [x] 初稿整理:
* [ ] 補充校對:
* [ ] 入庫存檔:
---
[TOC]
---
## gradle配置
Android工程的每個module都有一個自己私有的**build.gradle**(綠色部分),而整個項目的根目錄中也有一個**build.gradle**(灰色部分),我們這里談論的全局配置基本都是在根build.gradle中進行的。
![image_1bp80no6ubmgo4itssrg7kfam.png-117.1kB][1]
### 設定UTF-8
一個項目的根目錄的build.gradle決定了項目的全局配置,對于編碼這種所有module的通用配置自然就是在這里定義的:
```
allprojects {
repositories {
jcenter()
mavenCentral()
}
tasks.withType(JavaCompile){
options.encoding = "UTF-8"
}
}
```
**題外話:**
> UTF-8是Unicode的實現方式之一,IDE默認的編碼也是UTF-8。
### 支持Google倉庫
```
buildscript {
repositories {
// ...
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.0'
}
}
allprojects {
repositories {
google()
jcenter()
}
}
```
我們可以在項目根目錄中的**build.gradle**給單個項目或全部工程啟用google的倉庫,配置后我們就可以讓其自動下載最新的Android plugin了,再也無需我們手動干預。
![image_1bp7vae1vc1sbosedr1vaf1i209.png-18kB][2]
目前所有的support包都是通過google倉庫進行遠程依賴,如果不配置倉庫的依賴就必然會出現support庫依賴異常:
> Could not find com.android.support:appcompat-v7:25.4.0.
如果我們想要看下google這個倉庫的地址,可以打印一下它的url:
```
buildscript {
repositories {
google()
jcenter()
mavenCentral()
}
repositories.each {
println it.getUrl() // 輸出url
}
}
```
輸出:
```
file:/D:/android-studio-ide-171.4195411-windows/android-studio/gradle/m2repository/
https://dl.google.com/dl/android/maven2/
https://jcenter.bintray.com/
https://repo1.maven.org/maven2/
```
### 支持Groovy
在根目錄的build.gradle中:
```
allprojects {
// ...
}
apply plugin: 'groovy'
dependencies {
compile localGroovy()
}
```
這個是可選配置,配置后可以減少一些Groovy的warnning。但如果你的項目中用到了自己寫的Gradle插件,那么添加**apply plugin: 'groovy'**就是必須的了。
### 配置Java版本
如果工程中的大多數module都是支持到Java7,那么可以在根目錄中的build.gradle中配置最低Java版本:
```
allprojects {
repositories {
jcenter()
}
tasks.withType(JavaCompile) {
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
}
}
```
對于某個支持到Java8的module,當然可以在它里面的build.gradle配置Java8的支持:
```
android {
// ...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
```
配置好后可以通過項目的圖形界面進行查看:
![image_1bp8s6d521v9gk2u10gcfq31qnu3q.png-46.3kB][3]
如果你的項目比較老,可以考慮使用[Gradle Retrolambda Plugin](https://github.com/evant/gradle-retrolambda)來引用Java8的語法。
### 變量、依賴版本 統一管理
當我們在開發多module工程的時候,最麻煩的就是為每個module管理**targetSdkVersion**。老的module的minSdkVersion很低,新的module因為是Android Studio自動建立的,經常會把targetSdkVersion升級到最新。我們十分希望全部的module的targetSdkVersion都能進行統一的管理,這時我們就可以考慮定義全局變量了。
#### 寫法一
在project根目錄下的build.gradle定義全局變量:
```
ext {
minSdkVersion = 16
targetSdkVersion = 24
}
buildscript {
// ...
}
```
然后在各module的build.gradle中可以通過**rootProject.ext**來引用:
```
android {
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
}
}
```
這里添加**rootProject.ext**是因為這個變量定義在根build.gradle中的,如果是在私有build.gradle文件中定義的話就不用加了。
#### 寫法二(推薦)
除了在build.gradle中定義全局變量外,我們還可以新增一個config.gradle文件的方式實現(config文件一般私有 不提交到版本庫上)。
config.gradle可以這樣寫:
```
ext {
signingConfig = [
storePassword: "xxxxx",
keyAlias : "xxxxx",
keyPassword : "xxxx"
]
android = [
compileSdkVersion: 26,
buildToolsVersion: "26.0.0",
minSdkVersion : 16,
targetSdkVersion : 22,
versionCode : 32,
versionName : "3.4"
]
supportV4_Ver = "25.3.1"
supportV7_Ver = "25.3.1"
supportV13_Ver = "25.3.1"
constraint_layout = "1.0.2"
leakcanary_Ver = "1.5"
glide_Ver = "3.7.0"
eventbus_Ver = "3.0.0"
}
```
在根gradle中:apply from: "config.gradle"
在模塊的gradle中,
```
android {
compileSdkVersion rootProject.ext.android.compileSdkVersion
buildToolsVersion rootProject.ext.android.buildToolsVersion
defaultConfig {
applicationId 'com.xin.newcar2b'
minSdkVersion rootProject.ext.android.minSdkVersion
targetSdkVersion rootProject.ext.android.targetSdkVersion
versionCode rootProject.ext.android.versionCode
versionName rootProject.ext.android.versionName
}
signingConfigs {
myConfig {
storeFile file("../attachfiles/xxxx.jks")
storePassword rootProject.ext.signingConfig.storePassword
keyAlias rootProject.ext.signingConfig.keyAlias
keyPassword rootProject.ext.signingConfig.keyPassword
}
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "com.android.support:appcompat-v7:$rootProject.ext.supportV7_Ver"
implementation "com.github.bumptech.glide:glide:$rootProject.ext.glide_Ver"
}
```
這樣寫的好處,除了能多人開發提供更友好的依賴管理外,修改版本號 并不需要重新sync,提升了開發效率,如果團隊對 版本號有嚴格限制,這個方法也能較好的實現。
需要提的一點是,一旦用這種方式管理了support庫,那么Android Studio的升級提示就完全失效了,其余類似的官方庫也是同理。當然 和上面的好處相比,這并不算什么。
## 自定義Task
### 自定義apk保存路徑與命名、自動保存對應的混淆mapping文件
在開發的過程中,Android Studio默認會生成app開頭的apk。但如果我們有多個團隊,打包機器肯定肯定會要求多個團隊的apk有明顯的名稱區分,所以我們需要在輸出的時候讓apk的名稱有意義:
```
static def packageTime() {
return new Date().format("yyMMddHHmmss", TimeZone.getTimeZone("GMT+8"))
}
android {
buildTypes {
// ...
}
//自動重命名 生成的apk包、并且保存對應的混淆 mapping文件
applicationVariants.all { variant ->
variant.outputs.all { output ->
//自動重命名 生成的apk包
def outputFile = output.outputFile
def packageTime = packageTime()
if (outputFile != null && outputFile.name.endsWith('.apk')) {
def fileName
if (variant.buildType.name == "release") {
fileName = "appname_release_v${defaultConfig.versionName}_${defaultConfig.versionCode}_${packageTime}.apk"
} else {
fileName = "appname_debuger_v${defaultConfig.versionName}_${defaultConfig.versionCode}_${packageTime}.apk"
}
outputFileName = fileName
}
//自動保存mapping文件、避免發版遺失
if (variant.getBuildType().isMinifyEnabled()) {
variant.assemble.doLast {
copy {
from variant.mappingFile
into "${projectDir}/mappings"
rename { String fileName ->
"mapping_appname_v${defaultConfig.versionName}_${defaultConfig.versionCode}_${packageTime}.txt"
}
}
}
}
}
}
}
```
上面的方式,會自動保存apk和對應的maping文件(相同的時間戳),apk發布后,可以尋找相同時間戳的mapping上傳對相關的平臺上,便于bug的追蹤定位。
### 更改AAR的輸出的位置
在插件項目的開發過程中,在調試模式時輸出的是apk,在插件模式中時aar。宿主App會在運行時自動加載SD卡根目錄中的aar,將其當作一個資源來進行管理。
在Android Studio中我們的aar都是輸出在outputs目錄(最新的Android Studio的輸出路徑可能變更)下的,每次輸出后都扔到手機里很麻煩。我們可以通過copy命令將輸出的文件復制到想要的路徑中,節約人力成本。
```
android.libraryVariants.all { variant ->
variant.outputs.all { output ->
if (output.outputFile != null
&& output.outputFile.name.endsWith('.aar')
&& output.outputFile.size() != 0) {
copy {
from output.outputFile
into "${rootDir}/libs/"
}
}
}
}
```
這里的**${rootDir}**關鍵字是項目的跟路徑。通過這個路徑變量,我們可以屏蔽不同開發者電腦路徑不同產生的差異性。
## 編譯提速
gradle笨重緩慢的build速度令人發指,除了提升cpu、ram、ssd外,還可以使用以下方式提速。
### 使用新版gradle 依賴接口替代老的
android studio 3 中開始使用新版的依賴 接口,一般狀況下 對編譯提速有很大的幫助。
新舊 接口 對應關系。
* compile 替換為 implementation,
* provided 替換為 compileOnly,
* releaseCompile 替換為 releaseImplementation
* debugCompile 替換為 debugImplementation,
* testCompile 替換為 testImplementation
對于一些跨層次依賴(app module中使用了了輔助module的依賴庫中的接口),此時使用implementation 代替 compile 會報錯,你可以使用api 來替代 compile ,同compile相比, 除了名稱,功能幾乎一樣。
```
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "com.android.support:appcompat-v7:$rootProject.ext.supportV7_Ver"
```
至于為什么為提速?
當本module依賴的lib(也可以是module)發生變化時,由于本module對外暴露的接口并不發生變化,在構建工程時gradle將會只重新編譯本module,所有依賴于本module的module并不會發生編譯。
詳細的講解情況可以參考這里:[安卓工程依賴方式:Implementation vs API dependency](http://blog.csdn.net/cysion1989/article/details/73442034)
### 跳過不必要的Task
我們在項目構建的過程中會有很多的task,有些是Android默認的,有些是我們自定義的。在很多時候我們并不需要運行test相關的Task,我們可以通過**task.enable**來強制跳過它。
這里有兩個配置:全局統一配置、局部配置。一般直接用統一配置,如果某些module需要定制,那就在每個module中寫。
**全局配置**
~~~
// Top-level build file where you can add configuration options common to all sub-projects/modules.
apply from: "config.gradle"
apply from: "configpersonal.gradle"
buildscript {
repositories {
//maven 私服地址,如不能使用,嘗試切換到maven.aliyun
maven { url 'http://10.10.4.43:8083/repository/maven-public/' }
// maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
}
}
allprojects {
repositories {
//maven 私服地址,如不能使用,嘗試切換到maven.aliyun
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
flatDir {
dirs '../Library/libs'
}
}
//全局配置,移除不必要的gradle task,加快編譯
if (rootProject.ext.isRemoveSomeTask) gradle.taskGraph.whenReady {
tasks.each { task ->
if (task.name.contains("lint")
//如果instant run不生效,把clean這行干掉
//||task.name.equals("clean")
//如果項目中有用到aidl則不可以舍棄這個任務
|| task.name.contains("Aidl")
//用不到測試的時候就可以先關閉
|| task.name.contains("mockableAndroidJar")
|| task.name.contains("UnitTest")
|| task.name.contains("AndroidTest")
//用不到NDK和JNI的也關閉掉
|| task.name.contains("Ndk")
//|| task.name.contains("Jni")
) {
task.enabled = false
}
}
}
}
~~~
***局部配置**
將以下代碼放到每個module中的 android{} 內
```
android {
//自定義任務 減少build時間
tasks.whenTaskAdded { task ->
if (task.name.contains("lint")
//如果instant run不生效,把clean這行干掉
//||task.name.equals("clean")
//如果項目中有用到aidl則不可以舍棄這個任務
||task.name.contains("Aidl")
//用不到測試的時候就可以先關閉
||task.name.contains("mockableAndroidJar")
||task.name.contains("UnitTest")
||task.name.contains("AndroidTest")
//用不到NDK和JNI的也關閉掉
|| task.name.contains("Ndk")
|| task.name.contains("Jni")
) {
task.enabled = false
}
}
}
```
之前:
![image_1bp8i68921q101o70pk929a12221g.png-162.8kB][5]
之后:
![image_1bp8iah1jv618c53561vu310di3d.png-174.5kB][6]
每一個Task都有inputs和outputs,如果在執行一個Task時,如果它的輸入和輸出與前一次執行時沒有發生變化(通過快照來判斷),那么Gradle便會認為該Task是沒變的,Gradle將不予執行,這就是所謂的增量構建。為了更好的說明這點,我們可以定義一個查看輸出/輸出詳細信息的Task:
```
gradle.taskGraph.afterTask { task ->
StringBuffer taskDetails = new StringBuffer()
taskDetails << """"-------------\nname:$task.name"""
taskDetails << "\nInputs:\n"
task.inputs.files.each{ inp ->
taskDetails << " ${inp}\n"
}
taskDetails << "Outputs:\n"
task.outputs.files.each{ out ->
taskDetails << " ${out.absolutePath}\n"
}
println taskDetails
}
```
示例:
```
-------------
:lib:compileDebugRenderscript UP-TO-DATE
"-------------
name:compileDebugRenderscript
Inputs:
D:\studio\kaleExample\lib\src\main\rs
D:\studio\kaleExample\lib\src\debug\rs
Outputs:
D:\studio\kaleExample\lib\build\intermediates\rs\debug\lib
D:\studio\kaleExample\lib\build\intermediates\rs\debug\obj
D:\studio\kaleExample\lib\build\generated\res\rs\debug
D:\studio\kaleExample\lib\build\generated\source\rs\debug
```
順便一提,一個任務如果沒有定義輸出的話, 那么Gradle永遠都沒用辦法判斷是UP-TO-DATE。
### 禁用lint
todo: 暫時可以看這里[禁用lint來解決](http://www.jianshu.com/p/326c91e344a8)
### 定制腳本
todo:可以參考 自帶腳本installDebug的實現 ,來寫一個相同的
### 抽離Task腳本
一個成熟的項目必然有著龐大的app.gradle,腳本多了自然就想要抽離出去。我們可以建立一個xxx.gradle來存放這些腳本,引用者只需要通過**apply from**依賴即可。
taskcode.gradle:
```
buildscript {
repositories {
jcenter()
}
}
ext.autoVersionName = { ->
def branch = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-parse', '--abbrev-ref', 'HEAD'
standardOutput = branch
}
def cmd = 'git describe --tags'
def version = cmd.execute().text.trim()
return branch.toString().trim() == "master" ? version :
version.split('-')[0] + '-' + branch.toString().trim() // v1.0.1-dev
}
ext.autoVersionCode = {
def cmd = 'git tag --list'
def code = cmd.execute().text.trim()
return code.toString().split("\n").size()
}
tasks.whenTaskAdded { task ->
if (task.name.contains('AndroidTest')) {
task.enabled = false
}
}
android {
applicationVariants.all { variant ->
variant.assemble.doLast {
//If this is a 'release' build, reveal the compiled apk in finder/explorer
if (variant.buildType.name.contains('release')) {
def path = null
variant.outputs.each { output ->
path = output.outputFile
}
if (path != null) {
if (System.properties['os.name'].toLowerCase().contains('mac os x')) {
['open', '-R', path].execute()
} else if (System.properties['os.name'].toLowerCase().contains('windows')) {
['explorer', '/select,', path].execute()
}
}
}
}
}
}
```
build.gradle:
```
apply plugin: 'com.android.application'
// ...
apply from: 'taskcode.gradle'
```
這樣配置后,上面的的**taskcode.gradle**中的腳本就能和之前一樣引用進來了,十分方便。
## 動態定制
### 動態設置BuildConfig
在測試開發階段,開發人員并不會修改老的版本號,每次打包提測給不同的測試人員的時候就會遇到不知道當前是在什么節點上的包。這種問題十分難查,有時候是開發人員自己失誤沒有merge,有時候是測試人員失誤打錯包了。
為了解決這個問題,我們可以在App的詳情頁面增加一個commit值,讓測試和開發人員可以迅速定位當前包的節點。
![image_1bpauii7v1c6hgv11oaj15immnh4k.png-39.2kB][7]
項目中的BuildConfig文件隨著編譯環境的不同BuildConfig的內容也是不同的,所以我們可以利用它來做一些事情。
```
/**
* Automatically generated file. DO NOT MODIFY
*/
package com.example.kale;
public final class BuildConfig {
public static final boolean DEBUG = Boolean.parseBoolean("true");
public static final String APPLICATION_ID = "com.example.kale";
public static final String BUILD_TYPE = "debug";
public static final String FLAVOR = "dev";
public static final int VERSION_CODE = 1;
public static final String VERSION_NAME = "1.0";
}
```
我們可以看到這里有構建的Type和Flavor,VersionCode和VersionName也是可以直接拿到。Gradle提供了一個buildConfigField的DSL,在編譯的時候我們可以直接設置其中的一些參數,從而在項目中利用這些參數進行邏輯判斷。
```
android {
defaultConfig {
// String中的引號記得加轉義符
buildConfigField 'String', 'API_URL', '"http://www.kale.com/api"'
buildConfigField "boolean", "IS_FOR_TEST", "true"
buildConfigField "String" , "LAST_COMMIT" , "\""+ revision() + "\""
resValue "string", "build_host", hostName()
}
}
def hostName() {
return System.getProperty("user.name") + "@" + InetAddress.localHost.hostName
}
def revision() {
def code = new ByteArrayOutputStream()
exec {
// 執行:git rev-parse --short HEAD
commandLine 'git', 'rev-parse', '--short', 'HEAD'
standardOutput = code
}
return code.toString().substring(0, code.size() - 1) // 去掉最后的\n
}
```
BuildConfig的參數會變成靜態變量:
```
public final class BuildConfig {
public static final boolean DEBUG = Boolean.parseBoolean("true");
public static final String APPLICATION_ID = "com.example.kale";
public static final String BUILD_TYPE = "debug";
public static final String FLAVOR = "dev";
public static final int VERSION_CODE = 1;
public static final String VERSION_NAME = "1.0";
// Fields from default config.
public static final String API_URL = "http://www.kale.com/api";
public static final boolean IS_FOR_TEST = true;
public static final String LAST_COMMIT = "2b07344";
}
```
res生成的參數會在**build/generated/res/resValue/.../generated.xml**中看到,在代碼中可以通過getString來拿到:
![image_1bpau5ijj64rdr217n11mf91ii047.png-25.4kB][8]
現在我們可以通過LAST_COMMIT來拿到最近的commit的SHA,并且在有多個測試Flavor的時候,可以通過IS_FOR_TEST來判斷了。
### 填充Manifest中的值
我們在開發第三方庫的時候可能需要根據引用者的不同來定義Manifest中的值,但是Manifest本身就是一個寫死的xml文件,并非擁有Java類那種靈活性。比如我開發一個了第三方登錄分享的庫([ShareLoginLib](https://github.com/tianzhijiexian/ShareLoginLib/)),這個庫的Manifest中必須配置一個騰訊的id,但是這個id肯定是根據使用的app來定義的。這就是變和不變的矛盾,為了解決這個問題,我希望將變化的部分抽離出去,讓Mainfest中的變化元素變成變量。
[[代碼地址]](https://github.com/tianzhijiexian/ShareLoginLib/blob/master/share/src/main/AndroidManifest.xml#L71)
```xml
<!-- 騰訊的認證activity -->
<activity
android:name="com.tencent.tauth.AuthActivity"
android:launchMode="singleTask"
android:noHistory="true"
>
<intent-filter>
<!-- 僅僅是用來占位的key -->
<data android:scheme="${tencentAuthId}" />
</intent-filter>
</activity>
```
我們用**${tencentAuthId}**來做占位,使用者在編譯的時候會動態設置**tencentAuthId**這個的值:
[[代碼地址]](https://github.com/tianzhijiexian/ShareLoginLib/blob/master/app/build.gradle#L35)
```
defaultConfig {
manifestPlaceholders = [
"tencentAuthId": "tencent123456",
]
}
```
如果你想要一次性填充某些或者所有Flavor的Apk中的Manifest,使用遍歷是最快速的方案:
```
android {
// ...
productFlavors {
google {
}
baidu {
}
}
productFlavors.all { flavor ->
manifestPlaceholders.put("UMENG_CHANNEL",name)
}
}
```
通過這種方式我們就可以把所有的靈活改變的東西都變為動態配置,讓本身很死板的xml文件具有動態化的特性。
### 讓BuildType支持繼承
一個復雜項目的buildType是很多的。如果我們想要新增加一個buildType,又想要新的buildType繼承之前配置好的參數,那么用**init.with()**就很適合了:
```
buildTypes {
release {
zipAlignEnabled true
minifyEnabled true
shrinkResources true // 是否去除無效的資源文件
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
signingConfig signingConfigs.release
}
rtm.initWith(buildTypes.release) // 繼承release的配置
rtm {
zipAlignEnabled false // 覆蓋release中的一些配置
}
}
```
### 讓Flavor支持繼承
在開發的過程中,我們有開發版本、提測版本、內部測試版本、預發版本、正式版本等多個版本。為了標識這些版本,我們就需要用到Flavor來區分了。
Flavor也是一個多維度的,可以類比為中國-上海-黃埔,變為渠道就是:
```
flavorDimensions "china", "shanghai", "huangpu" // 按照先后進行排序
```
每個Flavor可以定義自己屬于的維度:
```
flavorDimensions "china", "shanghai", "huangpu"
productFlavors {
country {
dimension "china"
}
city {
dimension "shanghai"
}
town {
dimension "huangpu"
}
```
![image_1bpb67drbrpg4e3m6f1hulcvk51.png-18.8kB][9]
說的實際一點:
> [是否免費]+[渠道]+[針對用戶]+[Debug/Release]
```
flavorDimensions("isfree", "channel", "sex")
productFlavors {
// 是否免費的維度
free { dimension "isfree" }
paid { dimension "isfree" }
// 渠道維度
googleplay { dimension "channel" }
wandoujia { dimension "channel" }
// 用戶維度
male { dimension "sex" }
female { dimension "sex" }
}
```
![image_1bpb9n0mlnn91hu718gc80ubbp8m.png-38.3kB][10]
這其實就是間接實現了Flavor的繼承,有了這種維度的幫助,我們可以實現父Flavor做通用配置,子Flavor做差異化配置。比方說我們有多個內部測試渠道,但對于開發者來說內部測試的代碼都是幾乎一樣的,所以只需要一個**IS_FOR_TEST**的變量來標識:
```
forJackTest {
buildConfigField "boolean", "IS_FOR_TEST", "true"
}
forTonyTest {
buildConfigField "boolean", "IS_FOR_TEST", "true"
}
forSamTest {
buildConfigField "boolean", "IS_FOR_TEST", "true"
}
```
這種重復寫多次的變量肯定是有更優解的,而**dimension**就給了我們一個優雅的處理方式:
```
flavorDimensions("innertest", "channel")
productFlavors {
innertest{
dimension "innertest"
buildConfigField "boolean", "IS_FOR_TEST", "true"
}
forJackTest {
dimension "channel"
}
forTonyTest {
dimension "channel"
}
forSamTest {
dimension "channel"
}
}
```
![image_1bpfi2buhvi71g28sdcmps1eod9.png-26.7kB][11]
在Java代碼中我們可以很容易的進行這種二維的判斷:
```
if (BuildConfig.IS_FOR_TEST) {
switch (BuildConfig.FLAVOR_channel) {
case "forJackTest":
break;
case "forTonyTest":
break;
case "forSamTest":
break;
}
}
```
**題外話:**
> FLAVOR_channel是系統自動生成的,FLAVOR_channel這種大小寫混合的寫法特別奇怪,但官方推薦的flavorDimensions中定義的都是小寫字母,所以這點可以暫時不用管它。
### 測試App有獨特Icon
測試人員的手機經常被借來借去,他們很難知道當前手機上的包是否是他們想要的版本。為了方便測試人員區分包和避免測錯包的情況,我們希望開發版本和測試版本的圖標和app的名字是不同的,這樣一眼就可以分辨出是正式包還是測試包了。
![image_1bpb8ners1nov10ai108c1nlrg287s.png-252.1kB][12]
不同的Flavor在目錄結構中映射不同的文件夾。我們可以在src中建立以Flavor命名的包,然后在里面做一些某個Flavor私有的操作.
![image_1bpfjitgi1vap12fl1kb6ihm1o6f1g.png-33.2kB][13]
為了區別于正式包的Icon,我們得給InnerTest建立自己私有的啟動Icon:
![image_1bpfjcjga5bi14cs11ar17cm9ci13.png-89.2kB][14]
在FemaleApplication這個類推薦繼承自原始App的Application類,它只需要做自己的差異化工作就好:
```
package com.example.kale;
public class InnerTestApplication extends MyApplication {
@Override
public void onCreate() {
super.onCreate();
// do something for test
// 差異化工作,比如Debug功能
Stetho.initialize(
Stetho.newInitializerBuilder(this)
.enableDumpapp(Stetho.defaultDumperPluginsProvider(this))
.enableWebKitInspector(
Stetho.defaultInspectorModulesProvider(this)).build());
HttpHelper.getInstance().openDebugMode();
}
}
```
然后,在Manifest中我們可以進行需要項目的替換:
```
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.example.kale"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
>
<application
android:name=".GooglePlayApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="GooglePlay-Example"
tools:replace="android:name,android:icon,android:label"
/>
</manifest>
```
很多時候我們可以把那些不必打包到正式版本中的類定義在InnerTest的src下,這樣測試環境可以引用而且有時候還可以省去了no-op的依賴。這種方法可玩性很大,大家可以多多思考把玩。
### 不同渠道不同包名
```
flavorDimensions("innertest", "channel")
productFlavors {
innertest {
dimension "innertest"
applicationIdSuffix '.test' // 包名后綴
buildConfigField "boolean", "IS_FOR_TEST", "true"
}
dev {
dimension "channel"
applicationIdSuffix '.dev' // 包名后綴
minSdkVersion 21 // 設置某個Flavor的minSdkVersion
// ...
versionNameSuffix "-minApi21" // 版本名后綴
}
}
```
以dev渠道為例,通過**applicationIdSuffix**可以給原始包名增加后綴,通過**versionNameSuffix**可以給原始版本名字增加后綴。
最終得到:
名稱| 內容 | 來源
--|--|--
基礎包名 | com.example.kale | applicationId的值
包名|com.example.kale.test.dev |test來自innertest,dev來自channel
版本名| 1.0-minApi21| -minApi21來自dev
不同包名不同的簽名就決定了不同的App,通過這種方式我們可以讓手機上同時安裝測試版本和正式版本。因為后綴會根據Flavor的維度層層添加,所以我們甚至可以**把基本包名定為com或org,然后根據輸出的方案拼接包名**,大大增加了打包的靈活性。
對于某些不想打包的Flavor或者維度,我們可以利用variantFilter進行操作,下面的代碼會將“minApi21”和“demo”的類型直接跳過:
```
android {
buildTypes {...}
flavorDimensions "api", "mode"
productFlavors {
demo {...}
full {...}
minApi24 {...}
minApi23 {...}
minApi21 {...}
}
variantFilter { variant ->
def names = variant.flavors*.name
// To check for a build type instead, use variant.buildType.name == "buildType"
if (names.contains("minApi21") && names.contains("demo")) {
// Gradle ignores any variants that satisfy the conditions above.
setIgnore(true)
}
}
}
```
### 自動升級版本號
#### 自動填寫versionName
我們知道build.gradle中管理了versionCode和versionName:
```
android {
// ...
defaultConfig {
// ...
versionCode 1
versionName "1.0"
}
}
```
versionName和git的tag是有相關性的,我們希望可以將每次的tag和當前的versionName進行關聯,實現自動化設置版本名稱的功能。
![image_1bpi867g2oie1t21bl71vi8qbam.png-8.8kB][15]
我們執行**git describe --tag**就可以看到最近的一次tag,所以靠這個就可以實現自動版本名了:
```
def autoVersionName() {
def cmd = 'git describe --tags'
def version = cmd.execute().text.trim()
return version.toString()
}
```
如果不是在master分支,那么得到的結果就可能是:v1.0.1-1-g0cb4465,所以我們可以處理一下:
```
def autoVersionName() {
def branch = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-parse', '--abbrev-ref', 'HEAD'
standardOutput = branch
}
def cmd = 'git describe --tags'
def version = cmd.execute().text.trim()
return branch.toString().trim() == "master" ? version :
version.split('-')[0] + '-' + branch.toString().trim() // v1.0.1-dev
}
```
**題外話:**
> 上面是tag和verionName完全相同時的例子,如果你的tag和versionName不同,你可以在autoVersionName()利用String的Api對原始的tag進行處理,處理后返回即可。
#### 實現versionCode自增
有了通過tag來映射versionName的經驗后,我們可以考慮通過tag數量來映射versionCode。每一次發版我們就會打一個tag,tag的數量也會增加1個,和我們版本號的遞增邏輯是符合的。
```
def autoVersionCode() {
def cmd = 'git tag --list'
def code = cmd.execute().text.trim()
return code.toString().split("\n").size()
}
```
最終結果:
```
android {
// ...
defaultConfig {
// ...
versionCode autoVersionCode() // 4
versionName autoVersionName() // v1.0.1
}
}
```
這里有一點需要注意,打tag是在即將發版的時候才進行的,如果我們想要在調試的時候先升級一下versionCode的話,那肯定不能走這套自動化方案。在實際中我推薦在dev的Flavor中將版本名和版本號手動填寫,在正式版中用tag做自動化處理。最后再寫個腳本在打tag的時候自動修改開發版本的versionCode,一切都變得輕松許多。
### 隱藏Release簽名
```
signingConfigs {
storeFile file('../test_key.jks')
storePassword 'test123'
keyAlias 'kale'
keyPassword 'test123'
}
```
通常情況下我們是這么配置簽名的,但這樣的話安全性就是一個問題。簽名會被自動提交到git倉庫中,所有倉庫的只讀權限的人員都可以看到,十分不安全。我們的目標應該是少數人掌握release的簽名,所有人可以有debug版本的簽名。這樣的話,在別的部門想要看下這個工程的代碼做參考的時候,項目組長就可以放心大膽的給別人開權限了。
很多人推薦在**gradle.properties**中存放配置信息:
```
STORE_FILE_PATH ../test_key.jks
STORE_PASSWORD test123
KEY_ALIAS kale
KEY_PASSWORD test123
```
```
signingConfigs {
release {
storeFile file(STORE_FILE_PATH)
storePassword STORE_PASSWORD
keyAlias KEY_ALIAS
keyPassword KEY_PASSWORD
}
}
```
但是gradle.properties也是被git管理的文件,如果你ignore掉了gradle.properties,就會出現文件找不到的錯誤,所以我**強烈不建議在gradle.properties中存放正式版的簽名信息**。
我們可以參考[ShareLoginLib](https://github.com/tianzhijiexian/ShareLoginLib)的方式,可以建立一個**signing.properties**的文件,然后在里面寫上信息:
```
STORE_FILE_PATH = ../signing.keystore
STORE_PASSWORD = jack2017
KEY_ALIAS = jack
KEY_PASSWORD = jack@2017
```
在**gradle.properties**中寫上debug的簽名:
```
STORE_FILE_PATH ../test_key.jks
STORE_PASSWORD test123
KEY_ALIAS kale
KEY_PASSWORD test123
```
build.gradle:
```
Properties props = new Properties()
File f = file(rootProject.file("signing.properties"))
// 如果這個簽名文件存在則用,如果不存在就從gradle.properties中取
if (!f.exists()) {
f = file(rootProject.file("gradle.properties"))
}
props.load(new FileInputStream(f))
android {
signingConfigs {
release {
storeFile file(props['STORE_FILE_PATH'])
storePassword props['STORE_PASSWORD']
keyAlias props['KEY_ALIAS']
keyPassword props['KEY_PASSWORD']
}
}
}
```
因為signing.properities是被ignore掉的,所以這個文件不會被git管理,增加了簽名的可控性。
還有一種寫法是將簽名放入環境變量:
```
android {
// ...
signingConfigs {
def appStoreFile = System.getenv("STORE_FILE")
def appStorePassword = System.getenv("STORE_PASSWORD")
def appKeyAlias = System.getenv("KEY_ALIAS")
def appKeyPassword = System.getenv("KEY_PASSWORD")
// 四要素中的任何一個沒有獲取到,就使用默認的簽名信息
if(!appStoreFile||!appStorePassword||!appKeyAlias||!appKeyPassword){
appStoreFile = "debug.keystore"
appStorePassword = "android"
appKeyAlias = "androiddebugkey"
appKeyPassword = "android"
}
release {
storeFile file(appStoreFile)
storePassword appStorePassword
keyAlias appKeyAlias
keyPassword appKeyPassword
}
}
}
```
這里用到了System.getenv()方法,你可以參考java中System下的getenv()來理解,就是當前機器的環境變量。比如**System.getenv().get("ADB")**在我機器上得到的就是:H:\Android\sdk\platform-tools;H:\Android\sdk\tools。
### 自動打開apk的目錄
開發人員通常情況下是不Build Apk的,開發都是直接run app,但是測試人員經常要編譯各種版本,很少去run app。Android Studio在每次生成apk后都會提示去打開本地目錄,那么我們能否在編譯成功Release版本的時候自動打開本地目錄呢?
![image_1bpi4qa3eblt1gu71n6a15olb1i13.png-23kB][16]
![image_1bpi4p7gj1q1m1nkba0abc6fko9.png-9.5kB][17]
下面的腳本通過**applicationVariants**來監聽apk生成的時機,如果是Rlease版本就打開文件管理器:
```
android {
applicationVariants.all { variant ->
variant.assemble.doLast {
//If this is a 'release' build, reveal the compiled apk in finder/explorer
if (variant.buildType.name.contains('release')) {
def path = null
variant.outputs.each { output ->
path = output.outputFile
}
if (path != null) {
if (System.properties['os.name'].toLowerCase().contains('mac os x')) {
['open', '-R', path].execute()
} else if (System.properties['os.name'].toLowerCase().contains('windows')) {
['explorer', '/select,', path].execute()
}
}
}
}
}
}
```
### 組件化相關的配置
在組件化的模式中,我們每個業務部門都是一個獨立的項目,所以在獨自開發的時候我們的項目都是一個獨立的App;在需要整體打包測試的時候就需要每個部門的項目變成library了。通過上述的代碼,我們可以很輕易的用配置文件的方式進行環境切換,更方便進行CI的配置化處理。
除了在build.gradle中定義全局變量外,我們還可以在**gradle.properties**中定義變量:
```
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
isFusion = false
```
使用時:
```
if (!isFusion.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
```
## 遠程依賴
### 配置倉庫
Gradle管理依賴是它的一大特點,想當年還在Eclipse時代的時候,所有的依賴都必須打包成jar,資源文件還得依次復制到工程中,十分難以管理。
無論是遠程依賴還是本地依賴,配置依賴的倉庫總是我們的第一步:
```
buildscript {
repositories {
maven { url 'https://maven.fabric.io/public' }
}
dependencies {
// These docs use an open ended version so that our plugin
// can be updated quickly in response to Android tooling updates
classpath 'io.fabric.tools:gradle:1.+'
classpath 'com.antfortune.freeline:gradle:0.8.'
classpath 'me.tatarka:gradle-retrolambda:3.2.5'
}
}
```
配置多個maven倉庫:
```
allprojects {
repositories {
jcenter()
maven {
url="http://maven.mbd.qiyi.domain/nexus/content/repositories/mbd-vertical/"
}
maven {
url "https://jitpack.io"
}
maven {
url 'http://repo.xxxx.net/nexus/'
name 'maven name'
credentials {
username = 'username'
password = 'password'
}
}
}
}
```
其中name和credentials是可選項,視具體情況而定。
### 使用nexus私服 加速 依賴
當我們添加新的依賴時,gradle或去中央倉庫download相關的文件到本地,眾所周知的原因,編譯時下載依賴的網速又著實令人蛋疼不已。除了一些特殊的穿越技巧外,更推薦使用nexus私服。
這方面,有條件的公司可以自己搭建nexus私服,也可以使用國內的Maven鏡像倉庫,如[~~開源中國的Maven庫~~](http://maven.oschina.net/index.html)(已停止維護),或[阿里云的Maven服務](http://maven.aliyun.com/nexus/),又或者是換成自建的Maven私服,那想必是極好的。
一個簡單的辦法,修改項目根目錄下的build.gradle,將`jcenter()`或者`mavenCentral()`替換掉即可:
```
allprojects {
repositories {
maven{ url 'http://maven.aliyun.com/nexus/content/groups/public/'}
}
}
```
但是架不住項目多,難不成每個都改一遍么?
自然是有省事的辦法,將下面這段Copy到名為`init.gradle`文件中,并保存到 `USER_HOME/.gradle/`文件夾下即可。
```
allprojects {
repositories {
def REPOSITORY_URL = 'http://maven.aliyun.com/nexus/content/groups/public/'
all { ArtifactRepository repo ->
if (repo instanceof MavenArtifactRepository && repo.url != null) {
def url = repo.url.toString()
if (url.startsWith('https://repo1.maven.org/maven2') || url.startsWith('https://jcenter.bintray.com/')) {
project.logger.lifecycle "Repository ${repo.url} replaced by $REPOSITORY_URL."
remove repo
}
}
}
maven {
url REPOSITORY_URL
}
}
}
```
`init.gradle`文件其實是Gradle的`初始化腳本`(Initialization Scripts),也是運行時的全局配置。
更詳細的介紹請參閱 [http://gradle.org/docs/current/userguide/init_scripts.html](http://gradle.org/docs/current/userguide/init_scripts.html)
目前已知、可用nexus私服:
* 阿里云:http://maven.aliyun.com/nexus/content/groups/public/
### 基礎Api
![image_1bpi8nt3m1ulotlo160ac7lc8q2d.png-82.7kB][18]
Gradle提供了多種依賴方式的Api:
配置 | 解釋
-- | --
api|編譯時依賴和運行時依賴
implementation | 基礎依賴方式,運行時依賴,對于依賴結構做了優化
compileOnly | 類似于**provided**,僅僅在編譯時進行依賴,不會將依賴打包到app中
runtimeOnly | 類似于**apk**,它僅僅將依賴打包到apk中,在編譯時無法獲得依賴的類
annotationProcessor | 類似于**apt**,是注解處理器的依賴
testImplementation | Java測試庫的依賴,僅僅在測試環境生效
androidTestImplementation | Android測試庫的依賴,僅僅在測試環境生效
[Flavor]Api | 針對于某個Flavor的依賴,寫法是Flavor的名稱+依賴方式
舉例:
```
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
// 基礎依賴方式
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:' + rootProject.ext.support_version
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
// 依賴注解,注解處理器不會打包到apk中
compileOnly 'com.baoyz.treasure:treasure:0.7.4'
annotationProcessor 'com.baoyz.treasure:treasure-compiler:0.7.4'
annotationProcessor 'com.google.dagger:dagger-compiler:<version-number>'
// buildTypes是debug的時候才能被依賴
debugImplementation 'com.github.nekocode.ResourceInspector:resinspector:0.5.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.0'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.0'
// 編寫時無法訪問lib中的資源,lib會被打包到apk中
runtimeOnly project(':lib')
}
```
### 組合依賴
有時候一些庫是一并依賴的,刪除的時候也是要一并剔除的,如果像上面一樣多條引用的話,很容易不知道哪些庫是要一并刪除的。為了解決這個問題,我們可以像下面這樣進行統一引入:
```
implementation([
'com.github.tianzhijiexian:logger:2e5da00f0f', // logger和timber總是結合使用的
'com.jakewharton.timber:timber:4.1.2'
])
```
這樣整合起來的庫就成了一組,開發者一眼就知道這些庫是有相關性的,在刪除庫的時候十分方便。
```
implementation([
'io.reactivex.rxjava2:rxjava:2.1.3',
'io.reactivex.rxjava2:rxandroid:2.0.1'
])
```
rxandroid本身是自帶rxjava的依賴的,但是rxjava的升級很快,rxandroid十分穩定,幾乎不怎么升級。在保證Api穩定的前提下,我們通過這種聚合依賴的方式可以很方便升級rxjava,讓核心代碼的升級不被rxandroid限制。
### 依賴傳遞
我們配置Crashlytics的依賴的時候一般會這樣寫:
```
api('com.crashlytics.sdk.android:crashlytics:2.6.8@aar') {
transitive = true;
}
```
為什么要寫**transitive = true**呢?其實@符號的作用是僅僅下載文件本身,不下載它自身的依賴,等于關閉了以來傳遞。如果你要支持依賴傳遞,那么就必須要寫**transitive = true**。
更多配置方案可參考:[Crashlytics for Android \- Fabric Install](https://fabric.io/kits/android/crashlytics/install)
### 動態版本號(不推薦)
如果想要自己的依賴庫永遠保持最新版本,那么就可以利用**版本名+**或**-SNAPSHOT**的方式來做:
```
implementation 'com.android.support:appcompat-v7:23.0.+'
implementation 'com.kale.business:CommonAdapter:1.0.6-SNAPSHOT'
```
同時還要記得開啟offline功能:
![image_1bpialnaf9g51jfi1umu1vjp1ubf2q.png-84.4kB][19]
Gradle默認24小時自動檢查一次更新,我們可通過resolutionStrategy來修改檢查周期:
```
configurations.all {
// check for updates every build
resolutionStrategy.cacheDynamicVersionsFor 0, 'seconds'
resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
}
dependencies {
implementation 'com.android.support:appcompat-v7:23.0.+'
implementation 'com.kale.business:CommonAdapter:1.0.6-SNAPSHOT'
}
```
在實際中我**不建議采用動態版本的方式做依賴**。動態依賴就必然會出現版本不穩定的情況,你無法確定所有項目組的成員是否都保持了一致的依賴版本,而且一旦依賴版本的最新版出現了bug,你會不自覺的將bug引入進來,十分難以排查。對于這種不受到版本控制系統管理的危險方案,請不要隨意嘗試。
此外,每次run時 都會向服務器請求最新版本,當前的網絡環境會讓這一過程漫長的令人發指。
### 強制版本號
有時候第三方的lib中用到了很高版本的support包,而那個高版本的support包可能有一個bug,我們肯定不想因為它而引入這個bug。事實上Gradle的默認機制是有高版本則用高版本,這就讓我們處于了一種進退兩難的境地。幸好,**configurations**提供了強制約束庫版本的能力。
我們先在根build.gradle中配置一個task:
```
subprojects {
task allDeps(type: DependencyReportTask) {}
}
```
使用命令行**gradlew alldeps**得到輸出:
![image_1bpnccniu9oi1ffr15869q5b8k19.png-146.9kB][20]
配置強制的support版本號:
```
android {
configurations.all {
// 指定某個庫的版本
resolutionStrategy.force "com.android.support:appcompat-v7:25.4.0"
// 一次指定多個庫的版本
resolutionStrategy {
force 'com.android.support.test.espresso:espresso-core:3.0.0',
"com.android.support:appcompat-v7:25.4.0"
}
}
}
```
![image_1bpnch7h511ak1g66p1god8i1e1m.png-155kB][21]
此外,我們還可以通過**force**來強制指定某個庫的版本號:
```
implementation group: 'com.android.support', name: 'appcompat-v7', version: '26.0.2', force: true
```
### exclude關鍵字
如果我們引用的庫多了,各個庫之間可能會出現相互引用。Gradle的默認處理是進行依賴分析的時候自動將多個相同庫的最高版本定位最終依賴。但有時候會出現一個jar包打包了庫A,而我們依賴的庫B也有庫A。在實際中,我們經常會通過**exclude**關鍵字來剔除某些依賴:
```
implementation('com.android.support:appcompat-v7:23.2.0') {
exclude group: 'com.android.support', module: 'support-annotations' // 寫全稱
exclude group: 'com.android.support', module: 'support-compat'
exclude group: 'com.android.support', module: 'support-v4'
exclude group: 'com.android.support', module: 'support-vector-drawable'
}
```
剔除整個組織的庫(一下子剔除所有support庫):
```
implementation('com.facebook.fresco:animated-webp:0.13.0') {
exclude group: 'com.android.support' // 僅僅寫組織名稱
}
```
exclude的參數有group和module,可以分別單獨使用。如果你想要全局剔除某個庫,可以在**configurations**中進行配置:
```
configurations {
all*.exclude group: 'org.hamcrest', module: 'hamcrest-core'
}
```
順便一提,大廠部門眾多,不同部門的庫很容易就出現了依賴沖突。早期Google的Espresso庫就和support-annotations有沖突,所以Android官方給出了下面的方案:
```
// Espresso UI Testing
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
```
現在3.x的版本就沒有這方面的問題了:
```
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
```
### 依賴沖突
強制版本號和exclude關鍵字 其實 提了下 依賴沖突 的處理。都是先打印依賴樹,查看沖突的位置后,進行exclude。
兩種方法可以用來打印依賴樹:
```
//其中app是指你的module名字(gradle alldeps 也可以),命名行運行:
gradle -q app:dependencies
//或者 在對應 module中 apply plugin: 'project-report',命名行運行:
gradle htmlDependencyReport
```
首先在你需要插在的module比如app的下的build.gradle 文件上加上apply plugin: 'project-report'
然后在項目的根目錄下執行gradle命令./gradlew htmlDependencyReport 之后會在Build目錄下面生成report文件夾,里面生成的有html,里面會有compile的標簽,打開即可看到相關的依賴包情況。
搜索CompileClasspath 或者 RuntimeClasspath 查看
示例,如果多余 v4 和design包 可以這樣
```
compile ('com.wdullaer:materialdatetimepicker:3.2.2') {
exclude group: 'com.android.support', module: 'support-v4'
exclude group: 'com.android.support', module: 'design'
}
```
也可以整個移除
```
compile ('com.android.support:design:22.2.1')
{
exclude group: 'com.android.support'
}
```
### 動態依賴第三方庫
#### 用變量判斷
在Dev版本的時候我們可能會依賴很多測試庫,整合很多開發插件,App很容易就突破了65535的方法數限制。但是在Release版本中方法數卻很少,可能只需要一個Dex(一個Dex大概是10M)。最好的方式是我們可以通過判斷當前的開發狀態來決定是否需要依賴multidex這個庫。
除了可以通過BuildType、Flavor來判斷不同的開發環境外,我們還可以通過內部變量的邏輯判斷來做:
```
def needMultidex = true
android {
buildTypes {
release {
multiDexEnabled = false // 關閉multiDex
// ...
}
debug {
multiDexEnabled true // 開啟multiDex
// ...
}
}
}
dependencies {
if (!needMultidex.toBoolean()) {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// ....
} else {
implementation 'com.android.support:multidex:1.0.0'
// ...
}
}
```
#### 用Flavor實現
如果這里的needMultidex修改的很頻繁,每次打包都需要改代碼,那么這樣的方案就不太合理了。在這種情況下,我建議通過Flavor來做依賴配置:
![image_1bppsjsu67kmdep10d81d801qda9.png-18.2kB][22]
```
debugImplementation 'com.android.support:multidex:1.0.0'
```
通過Flavor的方式可以讓我們通過切換Build Variants的方式來切換環境,可以將環境切換和代碼分開,解決硬編碼的情況。但是如果我們不同環境的差異性很大,仍舊會出現不方便管理的情況。
以插件化方案舉例子,我們定義兩個Flavor:
```
android {
productFlavors {
// 插件
plugin {
buildConfigField "boolean", "IS_PLUGIN", "true"
}
// 獨立App
single {
buildConfigField "boolean", "IS_PLUGIN", "false"
}
}
}
// 這里的baseLibxxx在插件的時候是不需要打包的,而獨立App的時候是需要打包的
dependencies {
pluginImplementation project(':pluginLib')
pluginCompileOnly project(':baseLib01')
pluginCompileOnly project(':baseLib02')
pluginCompileOnly project(':baseLib03')
pluginCompileOnly project(':baseLib04')
singleImplementation 'com.android.support:multidex:1.0.0'
singleImplementation project(':singleLib')
singleImplementation project(':baselib01')
singleImplementation project(':baselib02')
singleImplementation project(':baselib03')
singleImplementation project(':baselib04')
}
```
#### 用回變量
通過區分Flavor的方式固然可以實現根據環境來依賴不同的東西,但如果更復雜一些呢?涉及到Task呢?對于復雜的需求,我們只有通過建立判斷邏輯來解決了。
增加判斷邏輯的好處是省去了一個Flavor,順便增加了動態程度:
```
// 每次切換插件/獨立App模式都得要改一次代碼,十分麻煩
ext { IS_PLUGIN = true; }
apply plugin: 'com.android.application'
// 判斷是否引入某個插件
if (IS_PLUGIN) {
apply plugin: "build-time-tracker"
buildtimetracker {
reporters {
csv {
output "build/times.csv"
append true
header false
}
summary {
ordered false
threshold 50
barstyle "unicode"
}
csvSummary {
csv "build/times.csv"
}
}
}
android {
defaultConfig {
// 判斷是否使用multiDex
if (IS_PLUGIN) {
multiDexEnabled false
} else {
multiDexEnabled true
}
}
dexOptions {
// 判斷內存配置
if (!IS_PLUGIN) {
javaMaxHeapSize "4g"
jumboMode = true
}
}
buildTypes {
debug {
buildConfigField("boolean", "IS_PLUGIN", "$IS_PLUGIN")
}
release {
buildConfigField("boolean", "IS_PLUGIN", "$IS_PLUGIN")
}
}
sourceSets {
main {
// 判斷資源
if (!IS_PLUGIN) {
jniLibs.srcDirs = ['libs']
}
res.srcDirs += ['src/main/res' , 'src/main/res-v7-appcompat']
}
}
}
dependencies {
// 這里還是出現了大量的相似依賴,說明可以進一步的進行優化
if (IS_PLUGIN) {
compileOnly fileTree(dir: 'libs-common', include: ['*.jar'])
compileOnly fileTree(dir: 'libs', include: ['*.jar'])
compileOnly 'com.android.support:support-annotations:23.0.1'
} else {
implementation fileTree(dir: 'libs-compile', include: ['*.jar'])
implementation fileTree(dir: 'libs-common', include: ['*.jar'])
implementation(name: 'lintaar-release', ext: 'aar')
implementation 'com.android.support:multidex:1.0.0'
}
}
```
**優化依賴**
有些庫在插件的宿主中是有的,但是調試的時候是獨立的App,所以只需要在調試時依賴。為了聚合這些類似的庫,我們可以將其封裝為數組,最終進行一次性的依賴判斷:
```
dependencies {
ext.libs =
['com.baoyz.treasure:treasure:0.7.4',
'com.squareup.okhttp3:okhttp:3.9.0',
'io.reactivex.rxjava2:rxjava:2.1.3']
if (isPlugin()) {
compileOnly(libs) // 不將依賴打入App中
} else {
implementation(libs)
}
if (isPlugin()) {
implementation project(':releaselib')
} else {
implementation project(':debuglib')
}
}
```
**優化配置**
如果我們切換一次獨立App/插件模式,那么**IS_PLUGIN**字段就得修改一遍。一個項目組內有些同事在調試插件模式,一些同事在調試獨立App,那么每次的Git提交就很容易在這個字段上沖突。為了解決這個問題,我們可以通過Gradle的打包命令,在執行命令行的時候動態設置這個字段,讓所有的修改和代碼分離:
```
// 并不定義PLUGIN這個變量
//ext.PLUGIN = true
ext.isPlugin = {
try {
if (PLUGIN.toBoolean()) {
return true
}
} catch (Exception ignore) {
}
return false
}
```
默認是獨立App模式,執行命令行時可進行修改:
```
gradlew clean -P PLUGIN=true installInnertestDevDebug
```
![image_1bpq3onor1m5i8715l0u88mmv9.png-62.1kB][23]
總的來說,如果你的需求不是很復雜,那么推薦用Flavor的方式,如果你的需求十分復雜,對于動態化和靈活性的要求很高,那么建議通過變量的方式來做。
## 本地依賴
### 引用aar
有時候我們有部分代碼需要給多個項目組共用,在不方便上傳倉庫的時候,可以做一個本地的aar依賴。
1.把aar文件放在某目錄內,比如就放在app的libs目錄內
![image_1bpkjae8p1n1l117i1jnv1k141hmn9.png-15.1kB][25]
2.在app的build.gradle文件中添加:
```
apply plugin: 'com.android.application'
repositories {
flatDir {
dirs 'libs' // this way we can find the .aar file in libs folder
}
}
```
3.之后在其他項目中添加下面的代碼后就引用了該aar
```
dependencies {
// name不用加aar的后綴
implementation(name:'lib-release', ext:'aar')
}
```
目前暫不知曉如何依賴根目錄中的aar文件。
### 依賴module/jar
#### module
依賴module:
```
implementation project(':lib')
```
如果的module在多級目錄中,那么首先要在settings.gradle中進行配置:
```
include ':app', ':lib', ':libraries:lib01', ':libraries:lib02'
```
![image_1bpkouc9iah616lv2go473lig2n.png-6.1kB][26]
依賴方式:
```
implementation project(':libraries:lib01')
implementation project(':libraries:lib02')
```
#### jar
依賴指定路徑下的全部jar文件
![image_1bpko53ob1a6hd5ivnd1b621c5k1t.png-39kB][27]
```
dependencies {
// 依賴當前module的libs目錄下的所有jar
implementation fileTree(dir: 'libs', include: ['*.jar'])
// 依賴外部目錄,librarymodule中libs目錄下的所有jar
implementation fileTree(dir: '../somedir/libstore', include: '*.jar')
}
```
如果用這種模糊依賴的話,我們只需要把要依賴的jar放入某個目錄中就好,但是這就有難以被版本控制系統管理的問題。一般情況下,**我建議通過指定依賴的方式來做**:
```
// 依賴當前目錄下的某個jar
implementation files('libs/guava-19.0.jar') // 指定依賴某個jar
// 依賴其他目錄下的某個jar
implementation files('../somedir/libstore/gson-2.8.1.jar')
```
為了方便管理和維護,放入jar文件的時候記得帶上版本號:
![image_1bpkn7e2vo919ik1k9ubct1g8613.png-8.8kB][28]
**題外話:**
> jar所在的目錄的名字可以隨便定義的,不局限于libs和是否在當前工程,見名之意即可。
### 自建倉庫
除了直接依賴aar或某個module外,我們可以將自己的module變成本地依賴的方式提供出去。
一個倉庫通常具有如下參數:
```
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<groupId>com.kale.github.example</groupId>
<artifactId>LocalLib</artifactId>
<versioning>
<release>1.1.1</release>
<versions>
<version>1.1.1</version>
</versions>
<lastUpdated>20170910025547</lastUpdated>
</versioning>
</metadata>
```
這里的groupId和庫名字一定要和公司的其余項目組協調定義,一般情況下**一個公司的庫的groupId都是一致的,這個是要寫入wiki的**。
可以參考Gson的信息:
![image_1bpkuj8ffenb1vta1bdf1gc8121n58.png-40.5kB][29]
#### 生成庫
1.在根路徑下的gradle.properties添加:
```
#org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# 組織信息
GROUP_ID=com.kale.github.example
# Licence信息(一般用apache的就行)
PROJ_LICENCE_NAME=The Apache Software License, Version 2.0
PROJ_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
PROJ_LICENCE_DEST=repo
```
2.在module(library)的build.gradle中定義發布配置:
```
apply plugin: 'com.android.library'
apply plugin: 'maven'
uploadArchives {
repositories.mavenDeployer {
pom.artifactId = 'LocalLib'
pom.groupId = GROUP_ID
pom.version = '1.1.1'
repository(url: "file:///${rootDir}/localstorage/locallib")
}
}
```
3.執行發布命令,等待文件生成完畢:
```
// 我演示的library叫做locallib
./gradlew -p <Library name> clean build uploadArchives --info
```
![image_1bpksi0upj281djvece1g9p1b2i34.png-9.9kB][30]
![image_1bpksmia45d21edq1tic5hens73h.png-35.7kB][31]
#### 依賴庫
依賴本地庫的方式將在依賴React Native的時候講,這里直接列代碼:
```
repositories {
// ...
maven {
url "$rootDir/localstorage/locallib/"
}
}
implementation 'com.kale.example:LocalLib:1.1.1'
```
順便一提,你也可以在Libary的根目錄下新建gradle.properties文件來填寫配置參數:
```
ARTIFACTID = androidLib
LIBRARY_VERSION = 2.2.2
LOCAL_REPO_URL = file:///D:/kale/my/local/repo // 可以是絕對路徑,但一定是file:開頭的
```
在build.gradle中:
```
apply plugin: 'com.android.library'
apply plugin: 'maven'
uploadArchives{
repositories.mavenDeployer{
repository(url:LOCAL_REPO_URL)
pom.groupId = GROUP_ID
pom.artifactId = ARTIFACTID
pom.version = LIBRARY_VERSION
}
}
```
### 本地依賴React Native
FaceBook的React Native因為更新速度很快,在它不支持遠程依賴的時候,我們可以考慮將項目作為一個倉庫進行配置,而倉庫的地址就是本地的目錄。
1.先將庫文件放入一個module的libs目錄中:

2.配置maven的url為本地地址:
```
allprojects {
repositories {
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url "$rootDir/module_name/libs/android" // 路徑是根據放置的目錄來定的
}
}
}
```
3.正常使用:
```
dependencies {
implementation 'com.facebook.react:react-native:0.32.0'
}
```
這里用到了**$rootDir**來屏蔽多個開發者機器環境的差異性,保證了項目的兼容性。
#### 依賴沖突
我們依賴本地jar的時候可能會出現jar中也打包了別的庫代碼的情況,如果是aar我們可以通過gradle來做處理,但在面對依賴沖突的時候,jar文件就變得令人棘手了。
[shevek/jarjar](https://github.com/shevek/jarjar)是一個再次打包工具,它可以為我們提供一次性更換包名的功能,是一個解決一來沖突的利器。
它還提供了gradle的腳本來操作你依賴的jar文件:
```
dependencies {
// Use jarjar.repackage in place of a dependency notation.
compile jarjar.repackage {
from 'com.google.guava:guava:18.0'
classDelete "com.google.common.base.**"
classRename "com.google.**" "org.private.google.@1"
}
}
```
這回我們嘗試通過手動的方式來操作gson.jar,我們希望把原本的**com.google.gson**的包換為**com.gg.gson**。
1.先建立一個**rule.txt**的文本文件,內容:
```
rule com.google.gson.** com.gg.gson.@1
```
2.執行命令:
```
java -jar jarjar.jar process rule.txt gson.jar gg.jar
```
執行后我們可以看到在當前目錄生成了一個gg.jar的文件,分析后就可以發現其內容已經變了:
![image_1bpo183ba1jhu1u7g5ibmln2m423.png-20.9kB][32]
jarjar并不提供修改META-INF的功能,但這并不影響我們使用。
如果你想要刪除特定包或特定的類,那么就在rule.txt中加入**zap**命令。
```
rule com.google.gson.** com.gg.gson.@1
zap com.google.gson.reflect.TypeToken // 刪除某個類
zap com.google.gson.stream.**
zap com.google.gson.annotations.**
zap com.google.gson.internal.**
```
原始的gson:
![image_1bpo1tnunvgabiki9jsl813m12g.png-29.3kB][33]
刪除后:
![image_1bpo1u8rk179i1991qurps81igr2t.png-16.8kB][34]
除了上面提到的rule、zap外還是有keep。首先zap會刪除需要刪除的所有類,然后執行rule替換符合要求的類,最后如果配置了keep的話,將不符合規則的所有類的移除,只保留keep指定的包。總結來說,這三條命令的執行優先級是:zap > rule > keep。
需要注意的是:jarjar無法支持反射,**如果jar包內有使用反射調用的情況,替換操作是十分危險的**。
另一個插件[dinuscxj/ClassPlugin](https://github.com/dinuscxj/ClassPlugin)還提供了替換依賴中的類的功能,有興趣可以嘗試一下。
**題外話:**
> 對于aar文件,我們只有將aar解壓后對解壓的jar進行處理,最后再打包成aar。
## 更多待續。。。
## 尾注
#### 附加說明:
* **版權**。文章大部分內容 來著 網絡,基于自己的使用情況,做了些許增刪改動,算是一個歸納筆記,僅供自己復習或內部交流;
* **gradle適用版本**。筆記時間跨度較長,文章中并不是同一個version的android studio上測試(1.5、2.1、2.3、3.0),實際使用可能會有部分差異,但大體是可以,如果有差異的話,可以稍微google下,應該都可以解決。
* **勘誤、貢獻**。文章中存在的錯誤或者尚未提到的gradle使用技巧,歡迎加入PR來勘誤補充;
#### 參考
* [gradle知識點總結分享](http://www.jianshu.com/p/d935357588bb)
* [Gradle插件用戶指南(譯)](http://rinvay.github.io/android/2015/03/26/Gradle-Plugin-User-Guide(Translation)/)
* [Gradle 修改 Maven 倉庫地址](https://yrom.net/blog/2015/02/07/change-gradle-maven-repo-url/)
* [gradledoc 項目報告插件](http://gradledoc.qiniudn.com/1.12/userguide/project_reports_plugin.html)
* [gradledoc翻譯](https://github.com/msdx/gradledoc)
* [gradle 中文用戶手冊](https://github.com/GradleCN/gradledoc)
[1]: http://static.zybuluo.com/shark0017/fuy6g22wbov6hr6dm2nq9vwg/image_1bp80no6ubmgo4itssrg7kfam.png
[2]: http://static.zybuluo.com/shark0017/ea6679q508r7cheld5c9hkgm/image_1bp7vae1vc1sbosedr1vaf1i209.png
[3]: http://static.zybuluo.com/shark0017/7qcl5lcqbjzxwog1h18vnbxz/image_1bp8s6d521v9gk2u10gcfq31qnu3q.png
[4]: http://static.zybuluo.com/shark0017/3am0o8lsp1c4082hvhpkpen8/image_1bp8f00691rmc1q9len5snt19n913.png
[5]: http://static.zybuluo.com/shark0017/6884pwtvgnne7n64ajytmkot/image_1bp8i68921q101o70pk929a12221g.png
[6]: http://static.zybuluo.com/shark0017/knc24xyxfch0hmrurhvaqjrm/image_1bp8iah1jv618c53561vu310di3d.png
[7]: http://static.zybuluo.com/shark0017/uuzh3f48fceteeztskf2yn2u/image_1bpauii7v1c6hgv11oaj15immnh4k.png
[8]: http://static.zybuluo.com/shark0017/k3qvytsb56sjcgdhohxshnkw/image_1bpau5ijj64rdr217n11mf91ii047.png
[9]: http://static.zybuluo.com/shark0017/u54dmheosm4d1m7cdn2n0t6n/image_1bpb67drbrpg4e3m6f1hulcvk51.png
[10]: http://static.zybuluo.com/shark0017/tl0jxghf9wwbiobknxkj2qhn/image_1bpb9n0mlnn91hu718gc80ubbp8m.png
[11]: http://static.zybuluo.com/shark0017/bol18npfie0wc40s82au53n9/image_1bpfi2buhvi71g28sdcmps1eod9.png
[12]: http://static.zybuluo.com/shark0017/wyiq7vcmyx2phn4npuy94nxi/image_1bpb8ners1nov10ai108c1nlrg287s.png
[13]: http://static.zybuluo.com/shark0017/emufnh1e2wqseluodeoa4oag/image_1bpfjitgi1vap12fl1kb6ihm1o6f1g.png
[14]: http://static.zybuluo.com/shark0017/0vp6ktmp2m9tws2ix0iybprx/image_1bpfjcjga5bi14cs11ar17cm9ci13.png
[15]: http://static.zybuluo.com/shark0017/1o6pec8yj5i6fo7mwn93654j/image_1bpi867g2oie1t21bl71vi8qbam.png
[16]: http://static.zybuluo.com/shark0017/og5eyv2460q2n814eeavg3oa/image_1bpi4qa3eblt1gu71n6a15olb1i13.png
[17]: http://static.zybuluo.com/shark0017/3r89uk61i3dp4y2cqjaovaip/image_1bpi4p7gj1q1m1nkba0abc6fko9.png
[18]: http://static.zybuluo.com/shark0017/rktt8zmd7ssmp7yo9j4ect6y/image_1bpi8nt3m1ulotlo160ac7lc8q2d.png
[19]: http://static.zybuluo.com/shark0017/3zugs7m6dzlcud6zrwbzzht5/image_1bpialnaf9g51jfi1umu1vjp1ubf2q.png
[20]: http://static.zybuluo.com/shark0017/na34vhd03qwmz2g0milxs31h/image_1bpnccniu9oi1ffr15869q5b8k19.png
[21]: http://static.zybuluo.com/shark0017/x7rj8qs0wq5hdhd93z971w8g/image_1bpnch7h511ak1g66p1god8i1e1m.png
[22]: http://static.zybuluo.com/shark0017/uv3na0un1t96igl38nb2gsyb/image_1bppsjsu67kmdep10d81d801qda9.png
[23]: http://static.zybuluo.com/shark0017/xw6ziprqbf437jepmw63xuzu/image_1bpq3onor1m5i8715l0u88mmv9.png
[24]: http://static.zybuluo.com/shark0017/61baz9yoc6amv1bsun6po4wv/image_1bpijpqvh1n23sjva9b14fp85v9.png
[25]: http://static.zybuluo.com/shark0017/r2f3hm71dsdi4jvofmvldj7h/image_1bpkjae8p1n1l117i1jnv1k141hmn9.png
[26]: http://static.zybuluo.com/shark0017/et59xla3vypuoknehfzxjl6z/image_1bpkouc9iah616lv2go473lig2n.png
[27]: http://static.zybuluo.com/shark0017/8csawzbedyc48f1xspepn8ao/image_1bpko53ob1a6hd5ivnd1b621c5k1t.png
[28]: http://static.zybuluo.com/shark0017/uoek1cgf1wx07guilsssc6hr/image_1bpkn7e2vo919ik1k9ubct1g8613.png
[29]: http://static.zybuluo.com/shark0017/pu0cgsdr7i5g0fie6c2fs0f4/image_1bpkuj8ffenb1vta1bdf1gc8121n58.png
[30]: http://static.zybuluo.com/shark0017/wehwhgf1myalfhtjbazigvev/image_1bpksi0upj281djvece1g9p1b2i34.png
[31]: http://static.zybuluo.com/shark0017/4oi27gbrr9b17c1vyfnkwkbo/image_1bpksmia45d21edq1tic5hens73h.png
[32]: http://static.zybuluo.com/shark0017/4ytqyxbs6xmufp2mf5aec4hc/image_1bpo183ba1jhu1u7g5ibmln2m423.png
[33]: http://static.zybuluo.com/shark0017/3e1wewyzaiod5zhimd2ovkrn/image_1bpo1tnunvgabiki9jsl813m12g.png
[34]: http://static.zybuluo.com/shark0017/03w4zw2sbuh6v52hl0jn4ye4/image_1bpo1u8rk179i1991qurps81igr2t.png
- 0-發現
- AndroidInterview-Q-A
- Android能讓你少走彎路的干貨整理
- LearningNotes
- temp
- temp11
- 部分地址
- 0-待辦任務
- 待補充列表
- 0-未分類
- AndroidView事件分發與滑動沖突處理
- Spannable
- 事件分發機制詳解
- 1-Java
- 1-Java-01基礎
- 未歸檔
- 你應該知道的JDK知識
- 集合框架
- 1-Java-04合集
- Java之旅0
- Java之旅
- JAVA之旅01
- JAVA之旅02
- JAVA之旅03
- JAVA之旅04
- JAVA之旅05
- JAVA之旅06
- JAVA之旅07
- JAVA之旅08
- JAVA之旅09
- java之旅1
- JAVA之旅10
- JAVA之旅11
- JAVA之旅12
- JAVA之旅13
- JAVA之旅14
- JAVA之旅15
- JAVA之旅16
- JAVA之旅17
- JAVA之旅18
- JAVA之旅19
- java之旅2
- JAVA之旅20
- JAVA之旅21
- JAVA之旅22
- JAVA之旅23
- JAVA之旅24
- JAVA之旅25
- JAVA之旅26
- JAVA之旅27
- JAVA之旅28
- JAVA之旅29
- java之旅3
- JAVA之旅30
- JAVA之旅31
- JAVA之旅32
- JAVA之旅33
- JAVA之旅34
- JAVA之旅35
- 1-Java-05辨析
- HashMapArrayMap
- Java8新特性
- Java8接口默認方法
- 圖解HashMap(1)
- 圖解HashMap(2)
- 2-Android
- 2-Android-1-基礎
- View繪制流程
- 事件分發
- AndroidView的事件分發機制和滑動沖突解決
- 自定義View基礎
- 1-安卓自定義View基礎-坐標系
- 2-安卓自定義View基礎-角度弧度
- 3-安卓自定義View基礎-顏色
- 自定義View進階
- 1-安卓自定義View進階-分類和流程
- 10-安卓自定義View進階-Matrix詳解
- 11-安卓自定義View進階-MatrixCamera
- 12-安卓自定義View進階-事件分發機制原理
- 13-安卓自定義View進階-事件分發機制詳解
- 14-安卓自定義View進階-MotionEvent詳解
- 15-安卓自定義View進階-特殊形狀控件事件處理方案
- 16-安卓自定義View進階-多點觸控詳解
- 17-安卓自定義View進階-手勢檢測GestureDetector
- 2-安卓自定義View進階-繪制基本圖形
- 3-安卓自定義View進階-畫布操作
- 4-安卓自定義View進階-圖片文字
- 5-安卓自定義View進階-Path基本操作
- 6-安卓自定義View進階-貝塞爾曲線
- 7-安卓自定義View進階-Path完結篇偽
- 8-安卓自定義View進階-Path玩出花樣PathMeasure
- 9-安卓自定義View進階-Matrix原理
- 通用類介紹
- Application
- 2-Android-2-使用
- 2-Android-02控件
- ViewGroup
- ConstraintLayout
- CoordinatorLayout
- 2-Android-03三方使用
- Dagger2
- Dagger2圖文完全教程
- Dagger2最清晰的使用教程
- Dagger2讓你愛不釋手-終結篇
- Dagger2讓你愛不釋手-重點概念講解、融合篇
- dagger2讓你愛不釋手:基礎依賴注入框架篇
- 閱讀筆記
- Glide
- Google推薦的圖片加載庫Glide:最新版使用指南(含新特性)
- rxjava
- 這可能是最好的RxJava2.x入門教程完結版
- 這可能是最好的RxJava2.x入門教程(一)
- 這可能是最好的RxJava2.x入門教程(三)
- 這可能是最好的RxJava2.x入門教程(二)
- 這可能是最好的RxJava2.x入門教程(五)
- 這可能是最好的RxJava2.x入門教程(四)
- 2-Android-3-優化
- 優化概況
- 各種優化
- Android端秒開優化
- apk大小優化
- 內存分析
- 混淆
- 2-Android-4-工具
- adb命令
- 一鍵分析Android的BugReport
- 版本控制
- git
- git章節簡述
- 2-Android-5-源碼
- HandlerThread 源碼分析
- IntentService的使用和源碼分析
- 2-Android-9-辨析
- LRU算法
- 什么是Bitmap
- 常見圖片壓縮方式
- 3-Kotlin
- Kotlin使用筆記1-草稿
- Kotlin使用筆記2
- kotlin特性草稿
- Kotlin草稿-Delegation
- Kotlin草稿-Field
- Kotlin草稿-object
- 4-JavaScript
- 5-Python
- 6-Other
- Git
- Gradle
- Android中ProGuard配置和總結
- gradle使用筆記
- Nexus私服搭建
- 編譯提速最佳實踐
- 7-設計模式與架構
- 組件化
- 組件化探索(OKR)
- 1-參考列表
- 2-1-組件化概述
- 2-2-gradle配置
- 2-3-代碼編寫
- 2-4-常見問題
- 2-9-值得一讀
- 8-數據結構與算法
- 0臨時文件
- 漢諾塔
- 8-數據-1數據結構
- HashMap
- HashMap、Hashtable、HashSet 和 ConcurrentHashMap 的比較
- 遲到一年HashMap解讀
- 8-數據-2算法
- 1個就夠了
- Java常用排序算法(必須掌握的8大排序算法)
- 常用排序算法總結(性能+代碼)
- 必須知道的八大種排序算法(java實現)
- 9-職業
- 閱讀
- 書單
- 面試
- 面試-01-java
- Java面試題全集駱昊(上)
- Java面試題全集駱昊(下)
- Java面試題全集駱昊(中)
- 面試-02-android
- 40道Android面試題
- 面試-03-開源源碼
- Android圖片加載框架最全解析(二),從源碼的角度理解Glide的執行流程
- 面試-07-設計模式
- 面試-08-算法
- 面試-09-其他
- SUMMARY
- 版權說明
- temp111