建立 Android Library 所需要知道的一切

mcryeasy發表於2016-12-12

Android 庫(Library)在結構上與 Android 應用模組相同。應用模組所可以包含的東西,在庫中都允許存在,包括程式碼檔案、資原始檔和manifest檔案等。

應用模組編譯後生成的是一個apk檔案,可以直接在裝置上執行,但是,庫模組編譯後生成的是一個Android Archive檔案,簡稱AAR。AAR檔案無法像apk檔案一樣直接在裝置上執行,我們一般用它作為Android app的依賴。

普通JAR檔案只能包含程式碼檔案和清單檔案,而ARR檔案不僅可以包含程式碼檔案,還可以包含Android的資原始檔和manifest檔案。這樣,我們就可以把資原始檔像佈局檔案、圖片檔案等和Java程式碼檔案一起分享出去。可以說ARR檔案是真正專屬於Android的“JAR”包。

庫模組在以下情況下非常有用:

  • 建立多個app,這些app需要使用多個相同的元件,像activity、service或UI 佈局等。
  • 建立一個app,而這個app可能需要根據需要編譯成多個APK版本,比如免費版和付費版,而兩個版本都需要使用到相同的元件。

在任何一種情況下,你只需要將要重用的檔案放到庫模組中,然後以依賴項的形式為每個應用模組新增庫即可。

建立庫模組

在你的工程中,建立一個新的庫模組,可以遵循如下的步驟:

  1. 點選 File > New > New Module.
  2. 在Create New Module的視窗中,選擇Android Library,並點選下一步(Next)。
    在該視窗中還有一個選項用於建立一個Java Library,Java Library就是我們所知的傳統的JAR檔案。JAR檔案在很多工程中十分有用,尤其當你想分享程式碼給其他工程的時候。但是JAR檔案並不允許包含Android資原始檔和manifest檔案,而資原始檔在Android專案中對程式碼重用具有很大的幫助。所以本篇主要對Android庫作介紹。
  3. 為你的庫命名並選擇最低SDK版本號,然後點選Finish,完成建立。

只要Gradle同步完成後,庫模組就會出現左邊的工程皮膚中。

應用模組轉成庫模組

如果你有一個已經存在的應用模組,並想重用它的所有程式碼,你可以把它轉成一個庫模組:

1.開啟屬於該應用模組下的build.gradle檔案,在最頂部,你可以看見如下的顯示:

java apply plugin: 'com.android.application'

2.把應用的外掛改成庫的外掛:

java apply plugin: 'com.android.library'

3.點選Sync Project with Gradle Files.

處理完上面這些,整個模組的結構不會被改變,但是該模組已經變為了庫模組,編譯後生成的是AAR檔案而不再是APK檔案了。

新增庫作為應用的依賴

為了在應用模組中使用庫模組,你需要作如下的處理:

1.新增庫到工程中有兩種方式(如果你是在相同專案中建立的庫模組,則該模組已經存在,您可以跳過此步驟)

  • 新增編譯後的ARR(或JAR)檔案:
    1.點選 File > New Module.
    2.在Create New Module的視窗中,點選 Import .JAR/.AAR Package 然後點選 Next.
    3.輸入ARR或JAR檔案所在的路徑,並點選Finish。建立後如下所示:
    image_1b3etje011peq1s2ruono51uqb9.png-17.3kB
  • 匯入外部庫模組到工程中:
    1.點選 File > New > Import Module.
    2.輸入Library模組所在的路徑,並點選Finish。建立後如下所示:
    image_1b3etmks81d4ld061fcigc517411g.png-19.7kB

這兩種引入庫的方式有所不同。如果直接引入的是庫模組,你可以對庫的程式碼進行編輯。但是如果匯入的是AAR檔案,那麼則無法進行編輯,就像JAR檔案一樣。

2.當庫模組或AAR檔案引入到工程後,請確保庫被列在settings.gradle檔案中,就如下所示,其中mylibrary是庫的名稱:

include ':app', ':mylibrary'

3.開啟應用模組下的build.gralde檔案,並在dependencies塊中新增新的一行,使之成為該應用的依賴,如下片段所示:

dependencies {
    compile project(":mylibrary")
}

4.點選 Sync Project with Gradle Files.

配置完上面的資訊後,名為mylibrary的庫模組就會成為應用的依賴。然後你就可以在應用模組中讀取任何屬於庫模組的程式碼和資原始檔。

另一種使用本地aar檔案的方式

其實我們還有一種引入本地aar檔案的方式,首先在工程的下先建立一個aar目錄,專門用於存放aar檔案,然後在應用的build.gradle新增如下配置:

repositories {
    flatDir {
        dirs '../aar'   // aar目錄
    }
}

然後將aar檔案拷貝到工程/aar目錄下,在應用模組的dependencies中加入aar引用:

compile(name: 'mylibrary-debug', ext: 'aar')

通過上面的配置,這樣aar就被引入過來了。這種方式與上面介紹的引入方式有點不同,上面作法是把引入的aar檔案封裝成一個獨立的模組,然後以compile project的方式引入。而現在的這種方式有點像jar包的引入方式。

注意:
根據上面的條件,如果把flatDir配置在project的gradle檔案中allprojects.repositories塊下面,發現app專案無法識別到aar檔案。通過規律發現,不管aar檔案放在哪裡,只要在app的gradle中配置flatDir都可以被識別。但是如果flatDir配置在project的gradle中,只能把aar檔案放到app的模組下才能被識別。

生成AAR檔案

我們可以通過點選Build > Make Project生成aar檔案,aar檔案會在project-name/module-name/build/outputs/aar/ 下生成。一般情況下會有兩個aar檔案,一個debug版本,一個release版本。

當我們拿到後aar檔案後,就可以把它釋出出去,其他小夥伴就可以利用上面的方式引入aar檔案到工程中了。

AAR檔案解刨

AAR 檔案的副檔名為 .aar,該檔案本身就是一個zip檔案,必須要包括以下內容:

  • /AndroidManifest.xml
  • /classes.jar
  • /res/
  • /R.txt (由R.java轉換而來)

此外,AAR檔案可能包含以下可選條目中的一個或多個:

  • /assets/
  • /libs/name.jar
  • /jni/abi_name/name.so(其中 abi_name 是 Android 支援的 ABI 之一)
  • /proguard.txt
  • /lint.jar

庫的私有資源

預設情況下庫中的所有資源都是公開狀態,也就是說允許應用模組直接訪問。但是如果你想讓庫中的資源僅供內部使用,而不想暴露給外部,您應通過宣告一個或多個公開資源的方式來使用這種自動私有標識機制。資源包括您專案的 res/ 目錄中的所有檔案,例如影像、佈局等。

首先,在庫的res/values/下新建一個public.xml檔案(如果不存在的話),然後在public.xml中定義公開的資源名,下面的示例程式碼可以建立兩個名稱分別為 lib_main_layout和 mylib_public_string的公開佈局資源和字串資源:

<resources>
    <public name="lib_main_layout" type="layout"/>
    <public name="mylib_public_string" type="string"/>
</resources>

上面的定義的兩個資源表示公開狀態,可以被外部依賴直接訪問。而沒有被定義在其中的資源都為隱式私有狀態,外部依賴無法合法訪問。其中name為資源名,type是資源型別有:string、layout、drawable、dimen等。

注意,如果想讓庫中的所有資源都為私有的,你必須要在public.xml中定義至少一個屬性。

在外部依賴使用庫私有資源的時候,你是無法通過R點的方式進行提示的,這也為了不暴露私有資源的一種手段。如果你強制使用了該資源,編譯器會發出警告:

1e12a2b6-bb40-4054-94d0-5e8f65ead49d.png-9.9kB

從上面可以看出,lib_main_layout和mylib_public_string資源都可以直接使用的,而沒有定義的都為私有資源,外部依賴使用的時候,編譯器會發出警告資訊。

但是這裡有一點需要注意,使用私有資源並不會發生任何錯誤,應用模組可以正常的使用這些私有資源,之所以提供這種機制,是為了告訴你,庫模組並不想把這些資源暴露給你,可能這些資源有特殊用途之類的。如果你真想使用私有資源,而且不想編譯器發出如上的警告,你可以把私有資源拷到自己的應用模組下。

隱私的賦予資源私有屬性不僅可以一定程度上防止外部使用,而且還允許你重新命名或刪除私有資源時,不會影響到使用到該庫的應用模組。私有資源不在程式碼自動完成和 Theme Editor 的作用範圍內,並且如果您嘗試引用私有資源,Lint 將顯示警告。

庫開發注意事項

將庫模組引用新增至您的Android 應用模組後,庫模組會根據優先順序的順序與應用模組進行合併。

資源合併衝突

  1. 構建工具會將庫模組中的資源與相關應用模組的資源合併。如果在兩個模組中均定義了相同的資源 ID,那就預設使用應用模組的資源。
  2. 如果多個 AAR 庫之間發生衝突,將使用依賴項列表首先列出(位於 dependencies 塊頂部)的庫中的資源。

為了避免常用資源 ID 的資源衝突,請使用在模組(或在所有專案模組)中具有唯一性的字首或其他一致的命名方案。

我們舉個例子來證明觀點1,觀點2感興趣的同學可以自己驗證。首先在庫模組mylibraryone中定義瞭如下的string資源:

<resources>
    <string name="app_name">My Library</string>
    <string name="test_one">My name is Library</string>
    <string name="my_library">Library</string>
</resources>

通過該庫的R檔案,這三個資原始檔的id值為:app_name=0x7f020000、my_library=0x7f020001、test_one=0x7f020002

然後在應用模組mytesttwo中這也定義瞭如下的string資源:

<resources>
    <string name="app_name">MyTestTwo</string>
    <string name="test_one">My name is App</string>
</resources>

請注意,其中資源名app_name 和test_one 和庫中定義的string資源名一樣。

我們把mylibraryone庫該作為mytesttwo應用的依賴,並重新編譯,大家可以發現在應用模組生成了兩個R檔案:

e1c93a61-d052-4265-b73f-31fa34034906.png-6.5kB

其中第一個是庫合併過來後的R檔案,而第二個是應用自己的R檔案。

我們對比下,兩個R檔案的內容:
mylibraryone:

public final class R {
    public static final class string {
        public static final int app_name = 0x7f040000;
        public static final int my_library = 0x7f040001;
        public static final int test_one = 0x7f040002;
    }
}

mytesttwo:

public final class R {
     .....
    public static final class mipmap {
        public static final int ic_launcher=0x7f020000;
    }
    public static final class string {
        public static final int app_name=0x7f040000;
        public static final int my_library=0x7f040001;
        public static final int test_one=0x7f040002;
    }
}

mylibraryone庫的R檔案只包含自己的資源,並且所有的資源值都發生了改變。並且庫中的資源id也都合併到應用的R檔案中了。從上面的兩個檔案可以看出一個特性:

用庫的R檔案和應用的R檔案都能訪問到庫的資源,但是無法用庫的R檔案訪問應用資源。

既然現在庫的資源和應用的資源現在進行了合併,那當我們使用test_one字串的時候用的是哪一個呢?我們在應用模組下直接輸出id值來瞧瞧:

Log.d("cryc","App:"+Integer.toHexString(com.example.mytesttwo.R.string.test_one)+"");
Log.d("cryc","App:"+getString(com.example.mytesttwo.R.string.test_one)+"");
Log.d("cryc","Library:"+Integer.toHexString(com.example.mylibraryone.R.string.test_one)+"");
Log.d("cryc","Library:"+getString(com.example.mylibraryone.R.string.test_one));
Log.d("cryc","Library:"+Integer.toHexString(com.example.mylibraryone.R.string.my_library));
Log.d("cryc","Library:"+getString(com.example.mylibraryone.R.string.my_library));

輸出結果:

App:7f040002
App:My name is App
Library:7f040002
Library:My name is App
Library:7f040001
Library:Library

大家可以看出,如果庫和應用的資源名衝突了,不管使用哪個R檔案,都那預設使用應用的資源。

大家或許還有疑問,如果我在庫中使用test_one資源,那到底是使用庫的資源還是應用的資源?答案是應用的資源,因為庫被合併到應用後,庫的R檔案資源id值都發生了變化。而我們用R檔案去訪問資源的時候,都是拿變化後的R檔案去訪問,所以如果有資源衝突預設都是以應用資源為準。所以這裡我也可以得出另一個結論:

當庫和應用模組資源衝突的情形下,不管在應用中還是在庫中使用該資源,都預設以應用資源為主。前提是應用模組有依賴該庫模組。

所以為了避免常用資源 ID 的資源衝突,請使用在模組(或在所有專案模組)中具有唯一性的字首或其他一致的命名方案。比如庫名是PullToRefresh,那麼該庫下的資源命名可以用ptr作為字首。

關於R檔案:

R檔案(R.java)是由Android 資源打包工具AAPT(Android Asset Packaging Tool))自動生成,包含了res目錄下所有資源的Id。每當建立一個新資源,會自動地在R檔案中新增該資源的id。我們可以在程式碼中使用該id,執行任何有關該資源的操作。注意,如果我們手動刪除R檔案,編譯器會自動建立。

R檔案是一個java檔案,因為它是被自動建立的,所以Android studio 會把它進行隱藏,具體位置在 app/build/generated/source/r/debug

資源衝突和私有資源的問題

當Library模組中存在私有資源,如果應用模組資源名和私有資源名衝突了,編譯器會發出警告:

89572d52-d118-4238-90e0-aba61a4a44e5.png-10.1kB

當我們在應用中使用該資源時,也會發出該警告:

3603b9a9-76da-45ba-bf55-3f3fd2fedac2.png-7.5kB

雖然我們使用該資源時用的是應用模組的資源,但是庫已經把test_one標為私有資源,為了規範化,我可以採取如下措施:

  1. 在應用模組中更換不同的資源名,不要與庫中的資源名一樣。
  2. 如果真的要使用同名資源,使用tools標記為重寫狀態:
<resources xmlns:tools="http://schemas.android.com/tools">
    <string name="app_name">MyTestTwo</string>
    <string name="test_one" tools:override="true">My name is App</string>
</resources>

此方式並無法取消私此資源是私有資源的狀態,只不過取消了資原始檔中的警告而已。

asserts合併衝突

當應用依賴庫時,應用的assert目錄會和庫的asserts目錄進行合併,如果有相同路徑檔案,則以應用模組的為準。例如,應用模組存在asserts/ha.json檔案,庫模組下也有asserts/ha.json檔案,因為兩個路徑一樣,當合並後apk中只保留應用模組asserts/ha.json。如果庫模組的ha.json檔案是存放在assert/json目錄下,那麼當合並後,兩個json檔案都存在, 因為它們路徑不一樣,一個是asserts/ha.json 另一個是asserts/json/ha.json。

谷歌官方說:工具不支援在庫模組中使用原始資原始檔(儲存在 assets/ 目錄中),但是經過我的測試,在應用模組中可以隨意使用庫中的assets資源並無任何問題。

關於asserts目錄

Android資原始檔大致可以分為兩種:

第一種是res目錄下存放的可編譯的資原始檔:這種資原始檔系統會在R.java裡面自動生成該資原始檔的ID,所以訪問這種資原始檔比較簡單,通過R.XXX.ID即可;

第二種是assets目錄下存放的原生資原始檔:
因為系統在編譯的時候不會編譯assets下的資原始檔,所以我們不能通過R.XXX.ID的方式訪問它們。那我麼能不能通過該資源的絕對路徑去訪問它們呢?因為apk安裝之後會放在/data/app/**.apk目錄下,以apk形式存在,asset/res和被繫結在apk裡,並不會解壓到/data/data/YourApp目錄下去,所以我們無法直接獲取到assets的絕對路徑,因為它們根本就沒有。

還好Android系統為我們提供了一個AssetManager工具類。檢視官方API可知,AssetManager提供對應用程式的原始資原始檔進行訪問;這個類提供了一個低階別的API,它允許你以簡單的位元組流的形式開啟和讀取和應用程式繫結在一起的原始資原始檔。

應用模組的 minSdkVersion 必須大於或等於庫定義的版本

庫作為相關應用模組的一部分編譯,因此,庫模組中使用的 API 必須與應用模組支援的平臺版本相容。

每個庫模組都會建立自己的 R 類

在您構建相關應用模組時,庫模組將先編譯到 AAR 檔案中,然後再新增到應用模組中。因此,每個庫都有其自己的 R 類,並根據庫的軟體包名稱命名。從主模組和庫模組生成的 R 類會在所需的所有軟體包(包括主模組的軟體包和庫的軟體包)中建立。

庫模組可能包含自己的 ProGuard 配置檔案

通過將 ProGuard 配置檔案新增到包含其 ProGuard 指令的庫,您可以在自己的庫上啟用程式碼壓縮。構建工具會為庫模組將此檔案嵌入到生成的 AAR 檔案中。在您將庫新增到應用模組時,庫的 ProGuard 檔案將附加至應用模組的 ProGuard 配置檔案 (proguard.txt)。

通過將 ProGuard 檔案嵌入到您的庫模組中,您可以確保依賴於此庫的應用模組不必手動更新其 ProGuard 檔案即可使用庫。當 ProGuard 在 Android 應用模組上執行時,它會同時使用來自應用模組和庫的指令,因此您不應當只在庫上執行 ProGuard。

要指定您的庫的配置檔名稱,請將其新增到 consumerProguardFiles 方法中,此方法位於您的庫的 build.gradle 檔案的 defaultConfig 塊內。例如,以下片段會將 lib-proguard-rules.txt 設定為庫的 ProGuard 配置檔案:

android {
    defaultConfig {
        consumerProguardFiles 'lib-proguard-rules.txt'
    }
    ...
}

預設情況下,應用模組會使用庫的釋出構建,即使在使用應用模組的除錯構建型別時亦是如此。要使用庫中不同的構建型別,您必須將依賴項新增到應用的 build.gradle 檔案的 dependencies 塊中,並在庫的 build.gradle 檔案中將 publishNonDefault 設定為 true。例如,您應用的 build.gradle 檔案中的以下程式碼段會使應用在應用模組於除錯模式下構建時使用庫的除錯構建型別,以及在應用模組於釋出模式下構建時使用庫的釋出構建型別:

dependencies {
    debugCompile project(path: ':library', configuration: 'debug')
    releaseCompile project(path: ':library', configuration: 'release')
}

您還必須在自己庫的 build.gradle 檔案的 android 塊內新增以下程式碼行,以便將此庫的非釋出配置展示給使用它的專案:

android {
    ...
    publishNonDefault true
}

不過請注意,設定 publishNonDefault 會增加構建時間。

為了確保您的庫的 ProGuard 規則不會將意外的壓縮副作用施加到應用模組,請僅包含適當規則,停用不適用於此庫的 ProGuard 功能。嘗試協助開發者的規則可能會與應用模組或它的其他庫中的現有程式碼衝突,因此不應包含這些規則。例如,您的庫的 ProGuard 檔案可以指定在應用模組的壓縮期間需要保留的程式碼

注:Jack 工具鏈僅支援 ProGuard 的部分壓縮和模糊選項

參考文件

相關文章