上一篇博客介绍了Gradle实践之多渠道自动化打包+版本号管理。因为我在公司里主要是作SDK开发的,此次我想介绍一下如何使用Gradle打出本身想要的jar包,并根据须要混淆、发布jar包。而后再介绍一下如何在打包的时候将自定义的Log输出工具关闭。java
前面咱们说过,在Android Studio里面使用Gradle来打包应用程序,通常都是build出来一个apk文件。可是有的同窗是作实现层的开发,不直接作View层的东西,例如sdk开发主要是给View层开发的同窗提供接口,一般是把代码打包成jar,再给开发者使用。android
如今有不少github上的开源项目也都是使用Android的library插件打包成aar,再提供给开发者用。这里说到aar
,它是随着Android Studio的出现而出现的,功能上相似一个library,能够在其余的项目里面调用这个aar提供的接口,aar也是一种zip包,与apk文件很是地类似,用解压工具打开它就会发现里面除了一个 classes.jar ,还有 res、assert、aidl、AndroidManifest.xml 等等文件,真的和apk太像了,不过apk压缩包里面的classes文件是一个dex文件,aar里面的classes文件仍是个jar。git
仍是以上一篇博客中创建的HelloGradle工程为例,如今向里面再添加一个新的Module。添加方法就是在项目面板的左侧,以Andrioid
视图查看工程结构,右键,在弹出的菜单中选择open module settings
,而后选择new a module
,接着在弹出的对话框中,选择新建一个Android Library Module
,这里我把它命名为HelloLib。以下图所示:github
这时你会发现,咱们的HelloGradle工程里,有了两个Module,一个是application类型的Module,一个是library类型的Module。正则表达式
它们的区别能够在各自的build.gradle
文件中一目了然。由于application module的build.gradle中引入的是com.android.application
插件来打包,而library module的build.gradle中引入的是com.android.library
插件进行打包。闭包
apply plugin: 'com.android.library'
可想而知,这个com.android.library
打包出的来的output必定就是aar
文件了。这个aar文件位于build/output/aar/
文件夹下。app
那么咱们要如何打包出一个jar呢?毕竟如今还有项目是用Eclipse开发的,使用jar文件比较方便,并且jar文件也能够在Android Studio中引入。工具
首先咱们在新建HelloLib Module中new一个class,做为咱们的库来提供给app module使用。以下所示,我新建了一个测试类。测试
package com.nought.hellolib; import android.util.Log; public class UncleNought { public static void Output() { Log.i(UncleNought.class.getSimpleName(), "I'm a library!"); } }
而后在app module的build.gradle文件中添加一行compile project(':hellolib')
,使得app module依赖咱们的HelloLib module。gradle
dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') testCompile 'junit:junit:4.12' compile 'com.android.support:appcompat-v7:22.2.1' compile 'com.android.support:design:22.2.1' compile project(':hellolib') }
这样就能够在app module中调用刚才的测试类了。咱们在app module中MainActivity的onCreate方法里调用UncleNought.Output();
,能够看到输出了LogI'm a library!
。
接着,咱们介绍两种生成jar的方法,有了jar之后,就能够在app module中以jar包的形式来调用HelloLib中的接口。
说到jar包,其实它就是把java源文件编译出来的class字节码,以一种zip的形式压缩在了一块儿。Android很大部分的开发都是用java写的,那么咱们能够将Android源代码编译出来的class字节码压缩到一个jar包里面,不就是咱们想要的jar包吗?没错就是这个,实际上在com.android.library
插件中,运行build命令是,也会有这样的操做,先把java源代码编译成class文件,再把文件打包成jar,再把jar压缩成dex。这其中就有jar的操做,生成的jar就位于build/intermediates/bundles/release/classes.jar
。若是你想直接使用这个jar也是能够的,只要本身在HelloLib Module的build.gradle中写一个copy类型的task,把这个classes.jar拷贝到指定的目录下就能够了。下面是一种示例:
task releaseMyLib(type: Copy, dependsOn: ['build']) { from( 'build/intermediates/bundles/release/') into( 'build/libs') include('classes.jar') rename('classes.jar', 'my-lib.jar') }
在HelloLib的build.gradle脚本添加完上面的task之后,打开Android Studio自带的命令行工具,依次输入下面两行,就能够打出一个my-lib.jar包了。
cd hellolib gradle releaseMyLib
这段脚本的含义很是简单,咱们自定义了一个名叫releaseMyLib的task,它是Gradle API自带的copy
类型的任务,这个任务依赖于 build
任务,前面咱们提到过,gradle有不少默认的任务,build即是其中的一个。因此当build任务结束后,会在build/intermediates/bundles/release/
下生成classes.jar
文件,咱们只要在这以后,把它拷贝出来,重命名为my-lib.jar
就能够了。
而后把这个jar包拷贝到app module下的libs文件夹中,去掉刚才在app module的build.gradle文件中添加的compile project(':hellolib')
,从新gradle sync一下,而后尝试运行你会发现和刚才的效果是同样的,这样就打出一个hellolib module的jar了。
可是上面这种作法太偷懒了,实际上这个classes.jar中,有一些是咱们不要的类,例如BuildConfig.class
这样的类。下图是用Java Decompiler反编译看到my-lib.jar里面的内容。
做为一个sdk开发者,不少时候须要本身的jar越小越好,因此咱们能够不须要把编译后自动生成的BuildConfig类加入到咱们本身的jar包中来,此外有时候咱们并不想把全部的类都打到这个my-lib.jar包中,这时应该怎么作呢?
咱们知道,Android Studio生成默认的jar包,是把源代码编译以后生成的全部的class字节码都压缩到这个classes.jar中,若是我想只打其中的一部分类该怎么办呢?
答案很简单:只须要在对编译出来的class字节码作Jar操做时,include我本身想要的类(或者exclude掉不想要的类)便可。那么全部的编译好的class字节码都在哪里呢?答案是build/intermediates/classes/release/
目录下,以下图所示:
Android Library打包插件在build时,会把全部的java文件编译成class文件,放在这个目录下。因此咱们接下来要作的事就是把这里面,全部须要的class,打成一个Jar包便可。下面是一个示例:
task jarMyLib(type: Jar, dependsOn: ['build']) { archiveName = 'my-lib.jar' from('build/intermediates/classes/release') destinationDir = file('build/libs') exclude('com/nought/hellolib/BuildConfig.class') exclude('com/nought/hellolib/BuildConfig\$*.class') exclude('**/R.class') exclude('**/R\$*.class') include('com/nought/hellolib/*.class') }
一样,打开Android Studio的终端,依次输入下面两行命令:
cd hellolib gradle jarMyLib
这样就经过Jar任务,本身打包出了一个jar包。咱们能够反编译一下这个jar包看看:
果真里面没有BuildCongfig这个类了,把这my-lib.jar
拷贝到app module下的libs文件夹下,从新Gradle sync一下,再运行这个app module,能够看到和以前方案1中同样的效果了。
这里是一种基本的自定义示例,若是还须要有别的需求,能够参考Gradle官方的DSL,里面介绍了各类Task接收的参数和使用方法。你们能够自行发挥实现本身想要的效果。
有一次我在Android开源群里,一个朋友问到“若是除了本身写的类,还想把第三方的OkHttp包打进来怎么办?”。其实这个问题很好解决,Gradle的Jar
任务是可配置多个from
来源的,因此咱们只须要在上面的代码里添加一行:
task jarMyLib(type: Jar, dependsOn: ['build']) { archiveName = 'my-lib.jar' from('build/intermediates/classes/release') from(project.zipTree("libs/xxx-x.x.x.jar")) // 添加这一行 destinationDir = file('build/libs') exclude('com/nought/hellolib/BuildConfig.class') exclude('com/nought/hellolib/BuildConfig\$*.class') exclude('**/R.class') exclude('**/R\$*.class') include('com/nought/hellolib/*.class') include('com/xxx/*.class') // 同时记得加上第三方的package
看上面的加了注释的两行,这样就能够把第三放依赖的jar包添加进来了。
前面咱们自定义jarMyLib
的时候,都依赖了build
任务,由于这个任务能够帮咱们把全部的java源代码编译成class文件,实际上build任务本身又依赖了不少其余的任务来实现打包。若是你想实现更快速的打包,运行一下gradle tasks
或者在Android Studio中点击右边的Gradle按钮弹出任务列表的面板,就会看到还有一个compileReleaseJavaWithJavac
,看名字就知道这个任务是编译全部的release type的java源文件,由于咱们能够把上面的代码改成dependsOn这个任务便可,改成task jarMyLib(type: Jar, dependsOn: ['compileReleaseJavaWithJavac'])
。可是记住了,必定要看清楚本身的gradle插件版本,我这个Android Gradle插件的版本是com.android.tools.build:gradle:1.3.0
,而com.android.tools.build:gradle:1.2.3
插件版本中对应的这个Compile任务的名字是叫作compileReleaseJava
,你们记得不要写错了。
另外你们可能会说,既然都本身自定义Jar任务,为啥不把compileJava
任务也自定义了,其实也是能够的,这样等于彻底不用依赖Android Gradle插件的默认任务了。但有的时候,假设咱们的代码中要把aidl打进来,依赖默认的compileReleaseJavaWithJavac
任务会把aidl生成的class文件也包含在里面,很是方便。若是本身去写JavaCompile任务的话,首先还要把aidl文件生成java文件,再来compile它,会有一点点麻烦。我们作sdk开发的,不须要打那么多渠道包,直接依赖默认的compileReleaseJavaWithJavac
其实多花个1-2s不是什么大问题。
刚才忘了提,混淆也是比较常见的一个需求,假设咱们不是打包apk,在buildTypes闭包里面也没有给release类型的任务设置``为混淆。那么咱们还能够本身定义一个混淆任务,话很少说,直接上代码:
def androidSDKDir = plugins.getPlugin('com.android.library').sdkHandler.getSdkFolder() def androidJarDir = androidSDKDir.toString() + '/platforms/' + "${android.compileSdkVersion}" + '/android.jar' task proguardMyLib(type: proguard.gradle.ProGuardTask, dependsOn: ['jarMyLib']) { injars('build/libs/my-lib.jar') outjars('build/libs/my-pro-lib.jar') libraryjars(androidJarDir) configuration 'proguard-rules.pro' }
这里的混淆Task——proguard.gradle.ProGuardTask
,也是来自Gradle标准的API,查看一下Gradle DSL,就知道怎么用了。injars、outjars和libraryjars以及混淆配置文件proguard-rules.pro这些参数,和原来使用Eclipse开发时是同样的,injars表示输入的须要被混淆的jar包,outsjars表示混淆后输出的jar包,libraryjars表示引用到的jar包不被混淆,proguard-rules.pro
里面写的是混淆配置,具体就不在这里详细发散了。
最后,仍是在终端中进入HelloLib目录,执行gradle proguardMyLib
,就能够获得混淆之后的jar包my-pro-lib.jar
了。
cd hellolib gradle proguardMyLib
一样,咱们反编译一下这个my-pro-lib.jar
,以下图所示:
有同窗就会说了,这个混淆的后的jar包和原来的jar包没啥区别啊… …没错,由于咱们这个类里面只调用了一句Log API,这个API又是来自于android.jar的,咱们在混淆的时候使用libraryjars(android.jar)保证了这个包里面的东西不会被混淆,因此这个示例里面看起来是没有什么变化的。若是你的HelloLib Module写的很复杂,里面代码有不少的话,混淆之后是有明显变化的,自定义打包jar文件就到这里结束了,你们能够本身体验一下。
在Android开发中,不少时候咱们会本身封装一个Log类,里面设置一个开关,在开发的时候将全部级别的Log所有打开输出。而后在发布应用前,把Log.i和Log.d这类级别的Log关闭,仅留下Log.e类型的输出。这样作是为了防止别人经过log来研究咱们的代码,同时也能够把一些没必要要给别人看的信息过滤掉。
其实这个需求很早就有,网上的大神们有不少的方法,这里我就举两个例子,说一下我本身的体会吧。
前面咱们一经发现,当你使用Android Gradle插件打包,执行默认的build任务时,会在build/intermediates/classes/release
中自动生成一个BuildConfig.class
,有class就应该有java源代码文件啊,那么这个class文件对应的java文件在哪里呢?答案是app/build/generated/source/buildConfig/
下。
关于这个生成的类文件,咱们能够经过在build.gradle脚本中的buildTypes闭包中指定参数,使得这个类生成出来的时候包含一个咱们自定义的boolean类型的静态常量ENABLE_DEBUG
,直接上代码:
buildTypes { release { // 不显示log buildConfigField "boolean", "ENABLE_DEBUG", "false" ... } debug { // 显示Log buildConfigField "boolean", "ENABLE_DEBUG", "true" ... } }
按照上面的脚本编写以后,生成的release版BuildConfig类中就会多出一个常量,即public static final boolean ENABLE_DEBUG = false;
;而debug版的BuildConfig类中的常量值则为true,即public static final boolean ENABLE_DEBUG = true;
。你能够分别在源代码中调用这两个常量,最后这两个类分别也会被打包到release和debug版各自的apk文件当中。
当你修改build.gradle脚本之后,按照Android Studio的提示,点击Gradle Sync
,就能够在以前咱们自定义的UncleNought测试类中调用BuildConfig类中常量,能够看到ENABLE_DEBUG
这个类已经自动生成出来了。下面是一段调用的示例:
package com.nought.hellolib; import android.util.Log; public class UncleNought { public static void Output() { if (BuildConfig.ENABLE_DEBUG) { Log.i(UncleNought.class.getSimpleName(), "I'm a library!"); } } }
我们能够打个包看一下,在命令行中运行:
gradle releaseMyLib
记住,这里必须执行releaseMyLib
这个任务,由于咱们用到了BuildConfig这个自动生成的类,假如不把它编译到咱们的jar包里,那么就无法去引用BuildConfig
里面的ENABLE_DEBUG
常量了。打包好了之后,咱们经过反编译再看一下这个jar,以下图:
把这个jar包给app module引用一下也会发现,如今Log已经不会输出了。
假设咱们不想把BuildConfig打包进来,只想在本身的类中定义一个常量,而后在release的时候修改这个动态去常量,应该怎么作呢?这个时候就能够利用gradle强大的能力了,话很少说,一步步看代码。
首先在测试类的代码里添加一个常量ENABLE_DEBUG
:
package com.nought.hellolib; import android.util.Log; public class UncleNought { public static boolean ENABLE_DEBUG = true; public static void Output() { if (ENABLE_DEBUG) { Log.i(UncleNought.class.getSimpleName(), "I'm a library!"); } } }
而后修改咱们的HelloLib打包脚本build.gradle文件,在前面的基础上添加:
def enableLoggerDebug(boolean flag) { def loggerFilePath = "src/main/java/com/qq/e/comm/util/GDTLogger.java" def updatedDebug = new File(loggerFilePath).getText('UTF-8') .replaceAll("DEBUG_ENABLE\\s?=\\s?" + (!flag).toString(), "DEBUG_ENABLE = " + flag.toString()) new File(loggerFilePath).write(updatedDebug, 'UTF-8') println(flag ? 'GDTLogger.DEBUG_ENABLE : [true]' : 'GDTLogger.DEBUG_ENABLE : [false]') } preBuild {}.doFirst { if (('jarMyLib' in gradle.startParameter.taskNames)) { enableLoggerDebug(false) } } jarMyLib {}.doLast { enableLoggerDebug(true) }
前面我提过,Gradle兼容Java的语法,因此我就想到,能够用正则表达式替换掉原来代码中的true
,让它变成false
。固然咱们要保证这该替换必须发生在complileReleaseJavaWithJavac
以前,而后咱们在打包完全完成之后,再把Log开关打开,即再false
变回true
,使得开发环境一直都是能够输出Debug Log的。
能够看到咱们在preBuild任务前把开关关闭了,而后在jarMyLib以后,又把开关打开了。doFirst
和doLast
都是经过闭包的方式向一个已有的任务里面添加可执行操做的语法。下面咱们打开终端进入到HelloLib目录下,执行下面的语句打一个包试试:
gradle jarMyLib
找到咱们的jar包,反编译一下看看:
果真,虽然咱们的代码里是public static boolean ENABLE_DEBUG = true;
,然而打出来的jar包倒是public static boolean ENABLE_DEBUG = false;
。
是否是很方便,若是你还有相似的动态修改代码的需求,也能够采用这种方法实现。其实还有其余的方式也能够实现一样的效果,在Android打包脚本的buildTypes和productFlavor支持下,咱们还能够为不一样类型的任务建立不一样的源代码或者资源类型的文件,前面的博客就提到过能够为不一样渠道包设置不一样的appname
,也能够采用一样的思路实现刚才这个需求,你们看本身的偏好吧,黑猫白猫,只要能抓到老鼠那都是好狗哇,哈哈哈!
最后上一下这个HelloGradle工程的代码示例https://github.com/unclechen/HelloGradle,里面有这两篇博客的打包示例,须要的同窗能够看看。