软件开发随着项目越来越大,我们会开始拆封出多个子项目(sub-project)或者库(library)来更好地独立维护。Android开发也不例外,我们也会需要创建各种库。如果使用Android Studio和Gradle,他们为我们提供了便捷的方式来创建libray。但是他们也带来了不少的维护问题,本文就来讲讲android-library里的坑来帮助大家避开。
android-library v.s. java-library
对于普通Android手机项目,我们有创建两种不同的Library可以选用: android-library 和 java-library
它们的gradle文件内容分别如下
[codesyntax lang=”groovy” lines=”normal” highlight_lines=”1″]
apply plugin: 'com.android.library' android { // ... } dependencies { // ... }
[/codesyntax]
[codesyntax lang=”groovy” lines=”normal” highlight_lines=”1″]
apply plugin: 'java-library' sourceCompatibility = "7" targetCompatibility = "7" dependencies { // ... }
[/codesyntax]
两种库在定义上的根本差别其实就是不同gradle plugin的使用。另一个小的差别就是android-library需要有一个 AndroidManifest.xml 文件,但它可以简单到只有一行内容(其中package的值通常是文件夹的路径)
[codesyntax lang=”xml” lines=”normal”]
<manifest package="com.ider.android.library" />
[/codesyntax]
使用上他们有一些差异,这些差异也是我们决定使用哪种Library的标准:
- android-library 可以使用 Android API(比如Activity, Service等等),java-library 则不能使用
- android-library 可以编译android资源文件(resources files),java-library 则无视这样内容
- android-library 编译出 arr 文件,java-library编译出 jar 文件(本质上他们都是 zip 文件)
- android-library 的配置可以包含各种编译类型(build variants)来控制,java-library没有那个复杂
- android-library 可以依赖于另一个android-library 或者 java-library, java-library只能依赖于java-library而不能依赖于android-library
总体而言 android-library 基本是 java-library 的一个超集, 但 android-library 比 java-libray 使用起来要更加复杂一些。从上边的对比来看,对于Android的项目,因为我们不确定什么时候会需要调用 Android 的API,或者定义一下 Android 的资源内容。再者最后一条的约束也决定了创建的 java-library 要保证在整个依赖树(dependency tree)上要在接近叶子节点的位置。所以不如优先选择创建android-library,以免掉入之后改 build.gradle 的坑中。
android-library的资源合并冲突
之前提到android-library里可以定义自己的资源文件,它也会生成相应的 R class。而这个 class 的 package 名字则由 AndroidManifest.xml 里我们所定义的。这一点在 <manifest> 里有介绍:
- It applies this name as the namespace for your app’s generated R.java class (used to access your app resources).For example, if package is set to “com.example.myapp”, the R class is created at com.example.myapp.R.
- It uses this name to resolve any relative class names that are declared in the manifest file.For example, if package is set to “com.example.myapp”, an activity declared as <activity android:name=”.MainActivity”> is resolved to be com.example.myapp.MainActivity.
但是这个 R 的限定有一个非常奇特的现象,让我们用例子来看看:
假如我有两个 android-library 项目 Ider 和 Iderson,Iderson项目依赖于 Ider 项目它们在AndroidManifest.xml 文件里定义的package名字分别是 com.ider
和 com.iderson
。然后Ider项目里包含了两个strings,iderson 里有一个名字相同但是值不同的string:
[codesyntax lang=”xml” lines=”normal”]
ider/src/main/res/strings.xml <resources> <string name="my_name">Ider</string> <string name="my_son_name">Iderson</string> </resources>
[/codesyntax]
[codesyntax lang=”xml” lines=”normal”]
iderson/src/main/res/strings.xml <resources> <string name="my_name">Iderson</string> </resources>
[/codesyntax]
最后我们在iderson项目里定义下边这个类
[codesyntax lang=”java” lines=”normal” highlight_lines=”7,8,9,10″]
import android.content.Context; import android.util.Log; public class IdersonClass { public static void readStrings(Context context) { int[] resIds = new int[] { com.iderson.R.string.my_name, com.iderson.R.string.my_son_name, com.ider.R.string.my_name, com.ider.R.string.my_son_name, }; for (int resId :resIds) { Log.d("android-library", context.getString(resId)); } } }
[/codesyntax]
这个类不仅能正常编译和运行,更神奇的是其输出结果全部都是 Iderson
这是因为整个应用程序其实最终只会生成一个包含所有资源内容的 R 文件,而这些资源标识本质上其实是 int
类型的(static final
)常量,所以编译后会自动内嵌这些数值,因此它们的引用标志并没有什么区别。
当出现重名冲突时,在同一个项目里,编译器会报出错误,对于不同的项目则使用覆盖的方式,其选用原则在官方文档中有介绍。
Resource merge conflicts
The build tools merge resources from a library module with those of a dependent app module. If a given resource ID is defined in both modules, the resource from the app is used.
If conflicts occur between multiple AAR libraries, then the resource from the library listed first in the dependencies list (toward the top of the dependencies block) is used.
To avoid resource conflicts for common resource IDs, consider using a prefix or other consistent naming scheme that is unique to the module (or is unique across all project modules).
这个坑倒是不容易掉进去,因为一般都会使用Library前缀来加以区分,但是一旦掉进去就会眼前一黑不知所措。
另一方面来说,这样的设计有一个好处,特别是设计UI的Library,我们可以定义一组主题资源在Library项目里,在依赖项目里,可以自由覆盖不同的值了自定义样式,但总体而言还是太复杂了。
(官方文档里有介绍了可以 public.xml 让资源变成Library私有的从而防止外部访问,但我试了一下,似乎并没有起作用)
android-library的编译变体
Gradle Android Plugin 在配置上提供了很两种解决方案来帮助开发者生应对不同的开发阶段和用户群里:版本类型(buildTypes)和产品特性(productFlavors)。
[codesyntax lang=”groovy” lines=”normal” highlight_lines=”8,25,26″]
apply plugin: 'com.android.library' android { defaultConfig { // ... } buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } debug { applicationIdSuffix ".debug" debuggable true } staging { initWith debug applicationIdSuffix ".debugStaging" } } flavorDimensions "tier" productFlavors { free { dimension "tier" applicationId 'com.iderzheng.free' } paid { dimension "tier" applicationId 'com.iderzheng.paid' } } }
[/codesyntax]
但是当编译变体和 android-library 配置方式混合在一起就会让人感到头昏眼花。最主要的问题出现在于依赖项目和被依赖项目之间有不同的变体定义,因为Gradle Android plugin 的识别机制是优先匹配相同编译变体的配置,如果找不到对应的变体,就会抛出编译错误。
Android 提供的解决方案是使用 matchingFallbacks 和 missingDimensionStrategy 来匹配Library中所定义的编译变体。matchingFallbacks是在同一维度(buildTypes 或 flavorDimnsions)里找编译变体(buildTypes 或 productFlavors);missingDimensionStrategy是当被依赖的Library里有不同维度(flavorDimnsions)时指定被依赖的Library中的维度和产品特性。
Android Studio的配置窗口会警告有编译变体的冲突,但是这不影响特定的编译,只要该编译依赖直接不会产生冲突。而且有可能我们永远都不会执行那个会造成冲突的编译变体。
不管怎样Android提供的解决方案都让配置文件看起来很复杂。(好在大多数情况下,我们不太会有这么复杂的配置)
[codesyntax lang=”groovy” lines=”normal”]
android { defaultConfig { // Specifies a sorted list of flavors that the plugin should try to use from // a given dimension. The following tells the plugin that, when encountering // a dependency that includes a "minApi" dimension, it should select the // "minApi18" flavor. You can include additional flavor names to provide a // sorted list of fallbacks for the dimension. missingDimensionStrategy 'minApi', 'minApi18', 'minApi23' // You should specify a missingDimensionStrategy property for each // dimension that exists in a local dependency but not in your app. missingDimensionStrategy 'abi', 'x86', 'arm64' // Do not configure matchingFallbacks in the defaultConfig block. // Instead, you must specify fallbacks for a given product flavor in the // productFlavors block, as shown below. } buildTypes { debug {} release {} staging { // Specifies a sorted list of fallback build types that the // plugin should try to use when a dependency does not include a // "staging" build type. You may specify as many fallbacks as you // like, and the plugin selects the first build type that's // available in the dependency. matchingFallbacks = ['debug', 'qa', 'release'] } } flavorDimensions 'tier' productFlavors { free { dimension 'tier' // Specifies a sorted list of fallback flavors that the plugin // should try to use when a dependency's matching dimension does // not include a "free" flavor. You may specify as many // fallbacks as you like, and the plugin selects the first flavor // that's available in the dependency's "tier" dimension. matchingFallbacks = ['demo', 'trial'] } paid { dimension 'tier' // You can override the default selection at the product flavor // level by configuring another missingDimensionStrategy property // for the "minApi" dimension. missingDimensionStrategy 'minApi', 'minApi23', 'minApi18' } } }
[/codesyntax]
在我的经验看来,如果这些Library都是自定义内部可控的,那么最简单的方式就是把这些共同的编译变体放在根目录下的独立.gradle文件里,比如我们把本节第一段代码存到文件android-build-variants.gradle 中,那边我们就可以在 android-library 项目的 build.gradle 文件里用 apply from:
来引入这些共同的配置来保证整个项目有统一的编译变体。
[codesyntax lang=”groovy” lines=”normal”]
apply from: "$rootDir/android-build-variants.gradle"
[/codesyntax]
在android-library项目的build.gradle里就可以去掉 android {}
配置模块,只有在两者存在差异时,我们再引入 android {}
配置并只在其中加入额外的部分。
[codesyntax lang=”groovy” lines=”normal”]
apply from: "$rootDir/android-build-variants.gradle" android { sourceSets { // Include more sources for free version free { java.srcDirs = ['src/commercial/java'] } } }
[/codesyntax]