上一篇博客介紹了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,裏面有這兩篇博客的打包示例,須要的同窗能夠看看。