关于创建Android Library所需要知道的一切

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。创建后如下所示:
  • android service library是什么 android service library是什么意思啊_库

  • 导入外部库模块到工程中:
    1.点击 File > New > Import Module.
    2.输入Library模块所在的路径,并点击Finish。创建后如下所示:
  • android service library是什么 android service library是什么意思啊_android_02

这两种引入库的方式有所不同。如果直接引入的是库模块,你可以对库的代码进行编辑。但是如果导入的是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点的方式进行提示的,这也为了不暴露私有资源的一种手段。如果你强制使用了该资源,编译器会发出警告:

android service library是什么 android service library是什么意思啊_android_03

从上面可以看出,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文件:

android service library是什么 android service library是什么意思啊_library_04

其中第一个是库合并过来后的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模块中存在私有资源,如果应用模块资源名和私有资源名冲突了,编译器会发出警告:

android service library是什么 android service library是什么意思啊_library_05

当我们在应用中使用该资源时,也会发出该警告:

android service library是什么 android service library是什么意思啊_library_06

虽然我们使用该资源时用的是应用模块的资源,但是库已经把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 文件可以指定在应用模块的压缩期间需要保留的代码。