Android依賴匯入全攻略

大頭呆發表於2018-04-11

在我們開發安卓專案的時候,不會所有的功能都自己去造輪子,經常要使用到各種的其他包,其中有谷歌給我們提供的各種support包,也有各種第三方的功能庫,有時候我們自己也會將一些功能封裝成包。這些包存在和匯入的形式也多種多樣,有遠端倉庫的,有直接拷貝到本地的,jar包、aar包、so包等。所幸我們都可以在主工程和各個Module的build.gradle裡進行統一管理。本文將在Android Studio3.0環境下來彙總下這些用法。

預備知識

先來看下Android Gradle plugin 3.0幾個引入依賴的方法:

Implementation

對於使用了該命令編譯的依賴,對該專案有依賴的專案將無法訪問到使用該命令編譯的依賴中的任何程式,也就是將該依賴隱藏在內部,而不對外部公開。

使用implementation會使編譯速度有所增快:比如我在一個library中使用implementation依賴了gson庫,然後我的主專案依賴了library,那麼,我的主專案就無法訪問gson庫中的方法。這樣的好處是編譯速度會加快,我換了一個版本的Gson庫,但只要library的程式碼不改動,就不會重新編譯主專案的程式碼。

api

等同於compile指令

compileOnly

等同於provided,只在編譯時有效,不會參與打包,不會包含到apk檔案中。可以用來解決重複匯入庫的衝突(下文會提到)。

更多細節內容可以看下這篇文章

遠端倉庫依賴

我們先來看下主工程下的build.gradle檔案

buildscript {
    ext.kotlin_version = '1.1.51'
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.1'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

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

allprojects {
    repositories {
        google()
        jcenter()
    }
}
複製程式碼

引入遠端倉庫依賴是很方便的,但在之前我們需要宣告遠端倉庫的地址。上面有兩個倉庫地址的宣告,一個在buildscript {},另一個在repositories {}。看程式碼中系統給我們的註釋就知道:前者是gradle指令碼自身執行所需依賴(Gradle外掛),後者是專案本身需要的依賴(普通程式碼庫)。所以如果你沒有引入遠端的Gradle外掛,那麼就不用在buildscript {}下的dependencies下新增依賴。

關於Gradle外掛的開發可以看下這篇文章:Gradle自定義外掛

再來看下幾種遠端依賴的新增方式:

 implementation 'commons-lang:commons-lang:2.6'
 
 implementation group: 'com.google.code.guice', name: 'guice', version: '1.0'
 
 implementation('org.hibernate:hibernate:3.1') {
        //不同版本同時被依賴時,那麼強制依賴這個版本的,預設false
        force = true
        //exclude可以設定不編譯指定的模組,有三種寫法:
        exclude module: 'cglib' 
        exclude group: 'org.jmock' 
        exclude group: 'org.unwanted', module: 'iAmBuggy' 
        //禁止依賴的傳遞,gradle自動新增子依賴項(依賴包所需的依賴),設定為false,則需要手動新增每個子依賴項,預設為true。
        transitive = false
    }
複製程式碼

同樣的配置下的版本衝突,會自動使用最新版;而不同配置下的版本衝突,gradle同步時會直接報錯。可使用exclude、force解決衝突。 比如你同時依賴了兩個版本的v7包:

implementation 'com.android.support:appcompat-v7:26.1.0'
implementation 'com.android.support:appcompat-v7:23.1.1'
複製程式碼

最終只會使用26.1.0版本。但是如implementation 'com.android.support:appcompat-v7:23.1.1',和androidTestImplementation 'com.android.support.test.espresso:espresso-core:2.1',所依賴的com.android.support:support-annotations版本不同,就會導致衝突。除了可以用exclude、force解決外,也可以自己統一為所有依賴指定support包的版本,不需要為每個依賴單獨排除了:

configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        def requested = details.requested
        if (requested.group == 'com.android.support') {
            if (!requested.name.startsWith("multidex")) {
                details.useVersion '26.1.0'
            }
        }
    }
}
複製程式碼

編譯期註解的依賴--annotationProcessor

用過butterknife或者Dagger的同學可能對這種annotationProcessor引入方式有所印象,這種方式是隻在編譯的時候執行依賴的庫,但是庫最終不打包到apk中。結合編譯期註解的作用,他是用來生成程式碼的,本身在執行時是不需要的。

本地依賴

jar包

jar包依賴的匯入還是比較簡單的:

  • implementation files('hibernate.jar', 'libs/spring.jar')//列出每個jar包的相對路徑
  • implementation fileTree(dir: 'libs', include: ['*.jar'])//列出包含jar包的資料夾路徑

但和遠端倉庫依賴引入方式不同,如果本地同時存在兩個不同的jar包,或者本地已有jar包,再去遠端依賴不同版本的jar包,就會報錯。

Android依賴匯入全攻略
解決方式:將其中的一個採用compileOnly替換implementation。顧名思義,compileOnly只在編譯時起作用,不會包含到APK裡面,在執行時也就避免找到重複的類了。

aar包

和jar包不同,aar包存放的路徑宣告和依賴引入是分開的:

repositories {
    flatDir {
        dir "../${project.name}/libs"
    }
}
dependencies {  
    implementation(name: 'aar名字', ext: 'aar')  
} 
複製程式碼

如果aar包有很多,也可以一樣象jar包統一新增一個資料夾下的所有包:

    def dir = new File('app/libs')
    dir.traverse(
            nameFilter: ~/.*\.aar/
    ) { file ->
        def name = file.getName().replace('.aar', '')
        implementation(name: name, ext: 'aar')
    }
複製程式碼

當一個library型別的module需要引用aar檔案時,也要在所在模組的build.gradle檔案中加入上面的話,但是當其他 Module引用此library的module時,也需要在他的build.gradle中加入如下配置,否則會提示找不到檔案:

repositories {  
    flatDir {  
        dirs 'libs', '../包含aar包的模組名/libs'  
    }  
}  
複製程式碼

即如果當前Module需要一個aar包內容,不論aar包是不是在當前Module中,都需要在build.gradle中宣告它所在的路徑。如果專案中這樣的Module比較多,每個都需要宣告路徑,不便於管理的話,推薦在專案的根build.gradle中統一新增,將所有包含aar包的模組名列出,這樣不論是本Module或其他Module都不需要單獨配置路徑了:

allprojects {
    repositories {
        jcenter()
        google()
        flatDir {
             dirs "../moudle-A/libs,../moudle-B/libs,../moudle-C/libs".split(",")
        }
    }
}
複製程式碼

so檔案

這個和jar包差不多,宣告下so檔案的存放路徑就行了:

   sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }
複製程式碼

或者直接在main目錄下新建jniLibs目錄,這是so檔案預設的放置目錄,不過不常用。值得一提的是aar包裡面也可以包含so檔案,但依賴這種包含so檔案的aar包時不需要做特定的配置,編譯時so檔案會自動包含到引用AAR壓縮包的APK中。

但比較特殊的一點是,so檔案需要放到具體的ABI目錄下,不能直接放libs目錄下所以你見到的結果可能是這樣的:

Android依賴匯入全攻略

所有的x86/x86_64/armeabi-v7a/arm64-v8a裝置都支援armeabi架構的so檔案。所以為了減小包體積,為了減小 apk 體積,可以只保留 armeabi 一個資料夾。但如果你想引入多個平臺的,那麼需要保持 so 檔案的數量一致,就是說 armeabi 檔案下的每個so檔案都要在armeabi-v7a下找到對應的so檔案,但這樣apk包的體積就會增大。

還有一種做法是生成指定ABI版本的APK,然後按需上傳到應用商店,讓使用者自己選擇下載適合自己手機的版本,這個可能更多的用在安卓遊戲APP上,build.gradle配置如下:

android {
    ... 
    splits {
        abi {
            enable true  //啟用ABI拆分機制
            reset()  //重置ABI列表為只包含一個空字串
            include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' //與include一起使用來可以表示要使用哪一個ABI
             universalApk
             true//是否打包一個通用版本(包含所有的ABI)。預設值為 false。
        }
    }
 
    // ABI的code碼
    project.ext.versionCodes = ['armeabi': 1, 'armeabi-v7a': 2, 'arm64-v8a': 3, 'mips': 5, 'mips64': 6, 'x86': 8, 'x86_64': 9]
 
    android.applicationVariants.all { variant ->
        // 最終標記
        variant.outputs.each { output ->
            output.versionCodeOverride =
                    project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) * 1000000 + android.defaultConfig.versionCode
        }
    }
 }
複製程式碼

問題和小結

1.aar包中的資原始檔重複了

1.aar包中的資原始檔重複了

資原始檔重複了,主工程的資原始檔會直接覆蓋aar包中的檔案,並且不會有任何報錯或者提示,最終aar包中也會直接用主工程的資原始檔,所以需要注意命名方式。暫時沒有更好的解決方法。

2.AndroidManifest合併錯誤

同樣也是發生在aar包上, Android Studio 專案每個module中都可以有一個AndroidManifest.xml檔案,但最終的APK 檔案只能包含一個 AndroidManifest.xml 檔案。在構建應用時,Gradle 構建會將所有清單檔案合併到一個封裝到 APK 的清單檔案中。aar包的清單檔案和我們的app清單檔案屬性衝突時:用tools:replace="屬性名"解決。

3.annotationProcessor與compileOnly的區別

上文說了annotationProcessor與compileOnly都是隻編譯並不打入apk中,他倆到底有什麼區別呢?扮演的角色不一樣,annotationProcessor作用是編譯時生成程式碼,編譯完真的就不需要了,compileOnly是有重複的庫,為的是剃除只保留一個庫,最終還是需要的。

4.模組的依賴分析

上面說了如果我們的專案如果間接依賴了相同庫的不同版本,在編譯時就直接會報錯:

Android依賴匯入全攻略
專案依賴如下:

Android依賴匯入全攻略
解決方法很簡單,用exclude就行了,但我們並不知道哪兩個依賴他們依賴了相同庫的不同版本,該把exclude放到哪裡呢?這就可以用到一個命令:

./gradlew -q <模組名>:dependencies
複製程式碼

就能列印出該模組所有的依賴樹資訊:

Android依賴匯入全攻略

Android依賴匯入全攻略

可以看到com.android.support.test:runner:1.0.2com.android.support:appcompat-v7:26.1.0依賴的庫版本不同導致的,而com.android.support.test.espresso:espresso-core:3.0.2又依賴了前者,所以把這兩個庫的衝突的依賴排除掉就行了:

androidTestImplementation('com.android.support.test:runner:1.0.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    androidTestImplementation('com.android.support.test.espresso:espresso-core:3.0.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })

複製程式碼

或者:

 implementation ('com.android.support:appcompat-v7:26.1.0', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
複製程式碼

兩者二選一,衝突就解決啦。

相關文章