为什么要开发Android库?
不论是你要执行一个特定的任务,模块化你的代码,或者只是为了更优雅地重用你的代码,有些时候,作为开发者,通常会考虑开发库来实现。但开发库是个挺困难的事情。由 Bay Android Dev Group 主办,这次分享由我们的 Emanuele Zattin 分享一些他在开发 Java 和 C/C++ 库上的一些最佳实践。探讨下 API 设计,CI 技术,以及对于性能的看法,你会了解到对你工作很有帮助的一些工具。
为什么要开发 Android 库?
为了追求更简洁的代码和更好的代码管理,我们通常需要把代码拆分成不同的逻辑单元,所以,开发 Android 库的第一个原因就是为了模块化。这也引申出我们开发库的第二个原因:代码重用。一旦你的代码模块化后,你可以将它用在很多不同的地方。相比高耦合度的代码,基于库的代码管理让你更容易的替换其中的代码,以适应不同的场景。还有一个原因,其实就是 “虚荣”,如果你有一个很不错的想法或者提出一种新的解决特定问题的方案,写个库会是一个将这个想法分享给全世界,并且让大家都能用到的一个好办法。
为什么是开发 Android 库而不是 Java 库呢?如果你的库要和 Android 的 UI,消息系统,设备传感器,或者原生代码打交道,那么你只能开发 Android 库,而不是 Java 库。
我们开始前,首先,打开你的 Android Studio,并且新建一个项目!Android Studio 并不支持直接新建一个新的库,这里有几种解决办法,如下:
方法一 - Hack 方案:
- 创建 Application Project
- 添加 Library Project
- 删除 Application Module
简单粗暴效果好
方法二 - 使用命令行,Android 自带了很多很多的工具,创建 Android 库也是其中的一个命令。传入一个 ID 参数允许你指定特定的编译 版本,也可以通过传参指定项目包名,以及你想要运行的 Gradle 的版本。 Gradle 是一个极其有用和灵活的自动化构建工具,你可以非常容易地运行各种插件,来加速你的开发,比 Maven 和 Ant 不知道高到哪里去了。
第二步: 代码,代码,代码!
在开发库的时候,API 设计非常重要, Joshua Bloch 是这方面的专家,他的 Effective Java 尽管是基于 Java 1.5, 但仍然有很多有价值的技巧。他曾经做过一个 名叫 如何设计优秀的 API,为何如此重要 的精彩的演讲,因此我想分享一些他在 API 设计上的一些观点。
那么,一个好的 API 应该具备哪些特点?
简单易学,你并不想让开发者不断地去查看文档,因此你在设计时候的命名法、类名和参数名一定要尽可能的不言自明。
- API 要少出错,所以要把 API 设计得坚如磐石。
- API 一定要易读,易维护,毕竟你在很长一段时间内要和这些代码打交道,特别是它一旦流行开以后,你会收到很多的新功能的请求和漏洞报告。
- API 的可扩展性也至关重要。想想 Jenkins :虽然不是一个 API,但是他是一个非常成功的开源项目,一部分原因是因为他易于扩展,以及为他开发插件。
- 最后,你一定要明确你的受众,不论受众是你自己、你的团队、你工作所在的公司或是整个世界,你的 API 都要做到对你的受众友好。
测试很重要,对库而言更加重要,因为你无法预知你的受众将如何去使用它们。很幸运的是,在 Android 上测试库跟测试 App 差异并不大。你可以使用 Android TestCase ,也可以用一些其他的 Android 测试工具框架来做测试。
Android 的测试组件一个不好的地方就是大家通常会卡到 JUnit 3 上,尤其是你发现你不能用类似 Robolectric 这样不支持 Native 代码的工具。JUnit3 不支持一个对库开发很重要的测试特性:参数化测试。如果你想用一些列测试参数来测试的方法,你可以试试用 Square 开发的一个叫: Burst 的库。它很好地解决了这个问题。
自动化你的测试! Jenkins 是一个非常赞的工具来解决实现自动化测试。他提供了超过 1000 个插件,其中一些专为 Android 开发设计。我强烈推荐以下插件:
- Job Config History 插件, 它可以在出状况的时候通过配置文件重新恢复现场。
- Git 插件,以及与他类似的一些插件,比如: GitHub , GitHub pull request , GitLab , 等等。
- 让执行任务和自动化变得简单的 Gradle 插件。Gradle 非常擅长自动化,你可以在 Jenkins 的 Gradle 里运行很多你自己的操作逻辑。
- Android Emulator 插件,这个不仅仅是个模拟器。 当你想要测试不同屏幕分辨率以及内存使用的时候,这个插件非常有用。
另一个测试方案是写测试用的 App。这种方案在很多时候都很有用:它可以帮你验证你的测试用例,间接地测试,而且可以确保你的 App 不会崩溃或者无响应。想要做测试 App,推荐用 Gradle 的 这个插件 ,它同时支持发送指令给不同的 Android 设备。
在你准备要发布你的库的时候,你会作何选择?目前有两个选择:发布 Jar, 或者发布 Aar(Android 归档文件),如果你还不知道 Aar,我来做个简单介绍,Aar 是 Google 为了能让库文件包含 UI 元素而提出的一种文件格式,他可以让你不仅仅包含 Java 类,还可以存储数据和资源在里面。他在 Android 和 Android Studio 上非常有用,Ant 和 Eclipse 并不支持这种格式。
一个不好的事情是尽管使用本地的 Aar 是可行的,有两种方案来实现这个,但他们都很麻烦。尤其是你想用你刚刚生成的 Aar 来做 App 测试。所以,如何选择发布哪种格式,最后就归结为,你是否必须要支持 Eclipse,如果是,那么没得选,只能用 Jar。
问题产生的原因就是因为 Android Gradle 插件产生的是 Aar,而不是 Jar 文件,尽管如此,事实上 Jar 文件旧包含在 Aar 文件里,其实你只要复制出来 Jar 文件,然后重命名他就好了。下面的代码就是一个简单的 Gradle Demo 来做这件事情: 拷贝文件(通常会被命名为:’classes.jar’),然后重命名成你想要的名字。
task generateJar(type: Copy) { group 'Build' description 'blah blah...' dependsOn assemble from 'build/intermediates/bundles/release/classes.jar' into 'build/libs' rename('classes.jar', 'awesome-library.jar') }
下一个问题是:我们应该发布在哪?如果你计划开源它,那么想都不用想,就用酷炫的 Bintray 。Bintray 尽管需要你准备更多的东西,但依旧用起来非常简单。仅仅需要一个源码 Jar 文件,以及一个 Javadoc 的 Jar 文件,这两个都非常容易生成。这样一来,你基本上只要指定代码路径,就可以遍历所有的 Variant。
// sources Jar task androidSourcesJar(type: Jar) { from android.sourceSets.main.java.srcDirs } // Javadoc Jar android.libraryVariants.all { variant -> task("javadoc${variant.name.capitalize()}", type: Javadoc) { description "Generates Javadoc for $variant.name." group 'Docs' source = variant.javaCompile.source ext.androidJar = files(plugins .findPlugin("com.android.library") .getBootClasspath()) classpath = files(variant.javaCompile.classpath.files) + ext.androidJar exclude '**/BuildConfig.java' exclude '**/R.java' } }
Bintray 也为生产发布提供了一个很精美却不是那么好用的 Gradle 插件 ,尤其是你只是一个 Gradle 新手的话…… 另外,带 Web 界面的发布工具也很是有用。你只要上传这三个文件,再填一些信息,就可以了。一开始,你可以使用那些 web 方案,当你对 Gradle 插件的使用和自动化非常熟悉的时候,就让 Jenkins 去帮你做这些发布工作吧。
Advanced Topics
注解处理技术( Annotation processing technologies )现在已经非常流行了。在编译的时候,Javac 通常会找到你定义的注解并且在他们之上做操作,因此你能做的通常就是生成新的类和 Java 文件。你可以写你自己的注解处理器。虽然很不简单,但是可行。
注解技术非常擅长处理两件事情。一是减少重复代码(boilerplate),另一件拿手的就是优化运行时的反射(introspection)。在运行时执行反射操作是非常慢的,因此你最好能在编译时期就去优化你的程序,推荐一些比较流行的用注解来处理数据的库: Dagger , Butter Knife , AutoValue / Autoparcel , 以及 Realm 。
一个不好的消息是:Android API 不能给注解提供正确的包路径。解决这个问题的方法是创建两个子项目,一个用来指向注解,另一个指向注解处理器。注解同时需要注解处理器和你的代码。因此,你的 Android 库项目,将会有两个子项目。创建完两个子项目后,你需要将他们都打包在一起,最终打包出来的东西不仅仅是你的 jar 文件,同时还有注解和注解处理器。你还需要修改 javadoc 任务,把注解部分的文档也添加进去,以便让开发者能够读到所有的文档。
// Jar task androidJar(type: Jar) { dependsOn assemble group 'Build' description 'blah blah' from zipTree( 'build/intermediates/bundles/release/classes.jar') from zipTree( '../annotations-processor/build/libs/processor.jar') from zipTree( '../annotations/build/libs/annotations.jar') } // javadoc tasks android.libraryVariants.all { variant -> task("javadoc${variant.name.capitalize()}", type: Javadoc) { description "Generates Javadoc for $variant.name." group 'Docs' source = variant.javaCompile.source source "../annotations/src/main/java" ext.androidJar = files(plugins .findPlugin("com.android.library") .getBootClasspath()) classpath = files(variant.javaCompile.classpath.files) + ext.androidJar exclude '**/BuildConfig.java' exclude '**/R.java' } }
Native Code 基本上都是和 NDK 打交道,整个工作流掌握起来很多很麻烦。如果你了解 C/C++,那最好不过,不然你要花大量的时间去学习新东西。Gradle 和 Android Studio 对 NDK 的支持并不好,当你尝试着去使用 NDK 模块的时候,他会警告你说:当前 NDK 已经不被支持了。Google 现在在尽可能的去让 Gradle 支持去支持 Native 插件。在此之前,我们还是必须得手动的搭建整个工具链,手动的编译以及把编译出来的文件拷贝到正确的位置。
遇到 NDK 不被支持的警告要做呢?一个方法是忽略警告,因为它依然可以工作。得留意的是,现在没有对 ldFlags 的定义,因此你不能为链接器指定 flag 参数。如果你需要这些参数,另一个方法是使用 Native 插件。这个方法可能很快就被废弃了,他需要你自己处理单独的工具链的搭建以及将生成文件从一个项目转移到对应的项目下。
如果你使用 jar 文件,如何才能包含基于 Native 实现的库呢?其实只用在 Building 的时候动动手脚就好了。如下所示,修改你的 “jar” 任务就可以了。
task androidJar(type: Jar, dependsOn: ['assemble']) { group 'Build' description 'blah blah' from zipTree('build/intermediates/bundles/release/classes.jar') from(file('src/main/jniLibs')) { into 'lib' } }
- 拥抱 Gradle:可能需要花时间学习,但是非常值。
- 探索 Gradle 插件!有数不清的插件,总有一款适合你。
- 自动化你的测试。尽可能的用 Jenkins和自动化工具。
- 如果你要开源你的炫酷组件,Bintray 是个好选择。
Q: Jenkins 上用 Gradle 的优势有哪些? Emanuele:优势就是 Gradle 有一些设置选项,包括你在你电脑上运行的 Gralde 或者 Gradle Wrapper 版本,插件能在出问题的时候让你通过 log 更容易的定位到问题所在。
Q: 你提到注解很有用,因为他们是在编译的时候处理问题,那么运行时的注解有没有什么特殊之处? Emanuele: 如果你想的话,你也可以在运行时调用注解,执行反射操作。但是相比于在编译时期处理慢很多。在 Android 上就更慢了,所以你用着用着就不想这么搞了。
Q: 你之前介绍了这么多有用的库,他们都会对 Android 性能产生很大影响么? Emanuele:不,我只是说你可以在编译时做反射优化。有的时候,只有运行时反射才能拿到一些你程序执行时候需要的信息,所以那种情况你也只能那样做了。
Q: 在处理注解的时候, Javapoet 是个不错的库,但依然要读取注解并且在类继承关系中寻找,你有没有处理这个的好的方案? Emanuele: 很不幸,没有。事实上,javapoet 只能在你生成新的类的时候帮到你。如果没有那么多的代码量,你也可以不用任何的库,只用模板。问题通常出在对象反射的限制上。你可以拿到注解的类,等等,但是往往无法反射内部的方法。为了实现这个,你需要操作字节码,听起来很可怕,但是是可行的。有一个叫 Morpheus 的库,可以帮你做这些。