Gradle 是將軟件編譯、測試、部署等步驟聯繫在一塊兒自動化構建工具。html
對於Android開發人員已經瞭解build.gradle 的 android{} 和 dependencies{} ,可是他的編譯過程是什麼樣的?這個過程當中能夠幹些什麼事瞭解嗎?java
此文是學習Gradle時的學習筆記,讓你從新認識Gradle,讓Gradle加快並提效構建你的項目。此時分享給你們,與你們共勉linux
Gradle 最基礎的一個項目配置android
Groovy 基礎語法 並解釋 apply plugin: 'xxxx'和dependencies{}git
Gradle Project/Task 並自定義Task和Plugingithub
自定義一個重命名APP名的插件 流程json
APT 技術- Java AbstractProcessorwindows
Android 字節碼加強技術 - Transform (Android 中使用字節碼加強技術)api
文章內容略長,若是你已經掌握Gradle基礎知識,能夠直接經過目錄查看你想看的內容,回顧或者學習都還不錯。bash
gralde的項目配置是先識別 settings.gradle,而後在配置各個build.gradle.
爲了說明構建執行順序,在上述最基礎的gradle項目結構裏面設置了對應的代碼
// settings.gradle
println "settings.gradle start"
include ':app'
println "settings.gradle end"
複製代碼
//root build.gradle
println "project.root start"
buildscript {
repositories {
}
dependencies {
}
}
allprojects {
}
println "project.root end"
複製代碼
//app build.gradle
println "project.app start"
project.afterEvaluate {
println "project.app.afterEvaluate print"
}
project.beforeEvaluate {
println "project.app.beforeEvaluate print"
}
println "project.app end"
複製代碼
若是是mac/linux,執行./gradlew 獲得以下結果:
settings.gradle start
settings.gradle end
> Configure project :
project.root start
project.root end
> Configure project :app
project.app start
project.app end
project.app.afterEvaluate print
複製代碼
下面講一些關於groovy的語法,能夠打開Android Studio Tools-> Groovy Console練習Groovy 語法 ,以下
int vs = 1
def version = 'version1'
println(vs)
println(version)
複製代碼
println vs
println version
複製代碼
def s1 = 'aaa'
def s2 = "version is ${version}"
def s3 = ''' str is many '''
println s1
println s2
println s3
複製代碼
def list = ['ant','maven']
list << "gradle"
list.add('test')
println list.size()
println list.toString()
//map
def years = ['key1':1000,"key2":2000]
println years.key1
println years.getClass()
複製代碼
輸出結果
[ant, maven, gradle, test]
1000
class java.util.LinkedHashMap
複製代碼
groovy語法中支持閉包語法,閉包簡單的說就是代碼塊,以下:
def v = {
v -> println v
}
static def testMethod(Closure closure){
closure('閉包 test')
}
testMethod v
複製代碼
其中定義的v就爲閉包,testMethod 爲一個方法,傳入參數爲閉包,而後調用閉包.
咱們先把子項目的build.gradle改成以下形式
apply plugin: 'java-library'
repositories {
mavenLocal()
}
dependencies {
compile gradleApi()
}
複製代碼
這樣,咱們就能夠直接看gradle的源碼了,在External Libraries裏以下
進入build.gradle 點擊apply 會進入到gradle的源碼,能夠看到
//PluginAware
/** * Applies a plugin or script, using the given options provided as a map. Does nothing if the plugin has already been applied. * <p> * The given map is applied as a series of method calls to a newly created {@link ObjectConfigurationAction}. * That is, each key in the map is expected to be the name of a method {@link ObjectConfigurationAction} and the value to be compatible arguments to that method. * * <p>The following options are available:</p> * * <ul><li>{@code from}: A script to apply. Accepts any path supported by {@link org.gradle.api.Project#uri(Object)}.</li> * * <li>{@code plugin}: The id or implementation class of the plugin to apply.</li> * * <li>{@code to}: The target delegate object or objects. The default is this plugin aware object. Use this to configure objects other than this object.</li></ul> * * @param options the options to use to configure and {@link ObjectConfigurationAction} before 「executing」 it */
void apply(Map<String, ?> options);
複製代碼
用Groovy 語法很清楚的解釋,apply其實就是一個方法,後面傳遞的就是一個map,其中plugin爲key.
那麼dependencies{}也是同樣
//Project
/** * <p>Configures the dependencies for this project. * * <p>This method executes the given closure against the {@link DependencyHandler} for this project. The {@link * DependencyHandler} is passed to the closure as the closure's delegate. * * <h3>Examples:</h3> * See docs for {@link DependencyHandler} * * @param configureClosure the closure to use to configure the dependencies. */
void dependencies(Closure configureClosure);
複製代碼
dependencies是一個方法 後面傳遞的是一個閉包的參數.
問題:思考那麼android {}也是同樣的實現嗎? 後面講解
在前面章節中提到gralde初始化配置,是先解析並執行setting.gradle,而後在解析執行build.gradle,那麼其實這些build.gradle 就是Project,外層的build.gradle是根Project,內層的爲子project,根project只能有一個,子project能夠有多個.
咱們知道了最基礎的gradle配置,那麼怎麼來使用Gradle裏面的一些東西來爲咱們服務呢?
前面提到apply plugin:'xxxx',這些plugin都是按照gradle規範來實現的,有java的有Android的,那麼咱們來實現一個本身的plugin.
把build.gradle 改成以下代碼
//app build.gradle
class LibPlugin implements Plugin<Project>{
@Override
void apply(Project target) {
println 'this is lib plugin'
}
}
apply plugin:LibPlugin
複製代碼
運行./gradlew 結果以下
> Configure project :app
this is lib plugin
複製代碼
咱們在自定義的Plugin中要獲取Project的配置,能夠經過Project去獲取一些基本配置信息,那咱們要自定義的一些屬性怎麼去配置獲取呢,這時就須要建立Extension了,把上述代碼改成以下形式。
//app build.gradle
class LibExtension{
String version
String message
}
class LibPlugin implements Plugin<Project>{
@Override
void apply(Project target) {
println 'this is lib plugin'
//建立 Extension
target.extensions.create('libConfig',LibExtension)
//建立一個task
target.tasks.create('libTask',{
doLast{
LibExtension config = project.libConfig
println config.version
println config.message
}
})
}
}
apply plugin:LibPlugin
//配置
libConfig {
version = '1.0'
message = 'lib message'
}
複製代碼
配置完成後,執行./gradlew libTask 獲得以下結果
> Configure project :app
this is lib plugin
> Task :lib:libTask
1.0
lib message
複製代碼
看完上述代碼,咱們就知道android {} 其實他就是一個Extension, 他是由plugin 'com.android.application'或者'com.android.library' 建立。
上述代碼中,建立了一個名字爲libTask的task,gradle中建立task的方式由不少中, 具體的建立接口在TaskContainer類中
//TaskContainer
Task create(Map<String, ?> options) throws InvalidUserDataException;
Task create(Map<String, ?> options, Closure configureClosure) throws InvalidUserDataException;
Task create(String name, Closure configureClosure) throws InvalidUserDataException;
Task create(String name) throws InvalidUserDataException;
<T extends Task> T create(String name, Class<T> type) throws InvalidUserDataException;
<T extends Task> T create(String name, Class<T> type, Action<? super T> configuration) throws InvalidUserDataException;
複製代碼
Project不能夠執行跑起來,那麼咱們就要定義一些task來完成咱們的編譯,運行,打包等。com.android.application插件 爲咱們定義了打包task如assemble,咱們剛纔定義的插件爲咱們添加了一個libTask用於輸出。
咱們看到建立的task裏面能夠直接調用doLast API,那是由於Task類中有doLast API,能夠查看對應的代碼看到對應的API
gradle 爲咱們定義了一些常見的task,如clean,copy等,這些task能夠直接使用name建立,以下:
task clean(type: Delete) {
delete rootProject.buildDir
}
複製代碼
咱們知道Android打包時,會使用assemble相關的task,可是僅僅他是不能直接打包的,他會依賴其餘的一些task. 那麼怎麼建立一個依賴的Task呢?代碼以下
task A{
println "A task"
}
task B({
println 'B task'
},dependsOn: A)
複製代碼
執行./graldew B 輸出
A task
B task
複製代碼
經過上述的一些入門講解,大概知道了gradle是怎麼構建的,那如今來自定義一個安卓打包過程當中,重命名APP名字的一個插件。
上述在build.gradle直接編寫Plugin是OK的,那麼爲了複用性更高一些,那咱們怎麼把這個抽出去呢?
以下
其中build.gradle爲
apply plugin: 'groovy'
apply plugin: 'maven'
repositories {
mavenLocal()
jcenter()
}
dependencies {
compile gradleApi()
}
def versionName = "0.0.1"
group "com.ding.demo"
version versionName
uploadArchives{ //當前項目能夠發佈到本地文件夾中
repositories {
mavenDeployer {
repository(url: uri('../repo')) //定義本地maven倉庫的地址
}
}
}
複製代碼
apkname.properties爲
implementation-class=com.ding.demo.ApkChangeNamePlugin
複製代碼
ApkChangeNamePlugin
package com.ding.demo
import org.gradle.api.Project
import org.gradle.api.Plugin
class ApkChangeNamePlugin implements Plugin<Project>{
static class ChangeAppNameConfig{
String prefixName
String notConfig
}
static def buildTime() {
return new Date().format("yyyy_MM_dd_HH_mm_ss", TimeZone.getTimeZone("GMT+8"))
}
@Override
void apply(Project project) {
if(!project.android){
throw new IllegalStateException('Must apply \'com.android.application\' or \'com.android.library\' first!');
}
project.getExtensions().create("nameConfig",ChangeAppNameConifg)
ChangeAppNameConfig config
project.afterEvaluate {
config = project.nameConfig
}
project.android.applicationVariants.all{
variant ->
variant.outputs.all {
output ->
if (output.outputFile != null && output.outputFile.name.endsWith('.apk')
&& !output.outputFile.name.contains(config.notConfig)) {
def appName = config.prefixName
def time = buildTime()
String name = output.baseName
name = name.replaceAll("-", "_")
outputFileName = "${appName}-${variant.versionCode}-${name}-${time}.apk"
}
}
}
}
}
複製代碼
定義完成後,執行./gradlew uploadArchives 會在本目錄生成對應對應的插件
應用插件 在根build.gralde 配置
buildscript {
repositories {
maven {url uri('./repo/')}
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.1'
classpath 'com.ding.demo:apkname:0.0.1'
}
}
複製代碼
在app.gralde 設置
apply plugin: 'apkname'
nameConfig{
prefixName = 'demo'
notConfig = 'debug'
}
複製代碼
Gradle的基礎API差很少就介紹完了。
官網地址:docs.gradle.org/current/use…
能夠去查看對應的API,也能夠直接經過源碼的方式查看
可是筆記還沒完,學習了Gradle的基礎,咱們要讓其爲咱們服務。下面介紹幾個實際應用.
www.jianshu.com/p/94aee6b02…
blog.csdn.net/kaifa1321/a…
APT 全稱Annotation Processing Tool,編譯期解析註解,生成代碼的一種技術。經常使用一些IOC框架的實現原理都是它,著名的ButterKnife,Dagger2就是用此技術實現的,SpringBoot中一些注入也是使用他進行注入的.
在介紹APT以前,先介紹一下SPI (Service Provider Interface)它經過在ClassPath路徑下的META-INF/**文件夾查找文件,自動加載文件裏所定義的類。 上面自定義的ApkNamePlugin 就是使用這種機制實現的,以下.
SPI 技術也有人用在了組件化的過程當中進行解耦合。要實現一個APT也是須要這種技術實現,可是谷歌已經把這個使用APT技術從新定義了一個,定義了一個auto-service,能夠簡化實現,下面就實現一個簡單Utils的文檔生成工具。
咱們知道,項目中的Utils可能會不少,每當新人入職或者老員工也不能完成知道都有那些Utils了,可能會重複加入一些Utils,好比獲取屏幕的密度,框高有不少Utils.咱們經過一個小插件來生成一個文檔,當用Utils能夠看一眼文檔就很一目瞭然了.
定義一個註解
@Retention(RetentionPolicy.CLASS)
public @interface GDoc {
String name() default "";
String author() default "";
String time() default "";
}
複製代碼
而後引入谷歌的 auto-service,引入DocAnnotation
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.google.auto.service:auto-service:1.0-rc2'
implementation 'com.alibaba:fastjson:1.2.34'
implementation project(':DocAnnotation')
}
複製代碼
定義一個Entity類
public class Entity {
public String author;
public String time;
public String name;
}
複製代碼
定義註解處理器
@AutoService(Processor.class) //其中這個註解就是 auto-service 提供的SPI功能
public class DocProcessor extends AbstractProcessor{
Writer docWriter;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
}
@Override
public Set<String> getSupportedAnnotationTypes() {
//可處理的註解的集合
HashSet<String> annotations = new HashSet<>();
String canonicalName = GDoc.class.getCanonicalName();
annotations.add(canonicalName);
return annotations;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
Messager messager = processingEnv.getMessager();
Map<String,Entity> map = new HashMap<>();
StringBuilder stringBuilder = new StringBuilder();
for (Element e : env.getElementsAnnotatedWith(GDoc.class)) {
GDoc annotation = e.getAnnotation(GDoc.class);
Entity entity = new Entity();
entity.name = annotation.name();
entity.author = annotation.author();
entity.time = annotation.time();
map.put(e.getSimpleName().toString(),entity);
stringBuilder.append(e.getSimpleName()).append(" ").append(entity.name).append("\n");
}
try {
docWriter = processingEnv.getFiler().createResource(
StandardLocation.SOURCE_OUTPUT,
"",
"DescClassDoc.json"
).openWriter();
//docWriter.append(JSON.toJSONString(map, SerializerFeature.PrettyFormat));
docWriter.append(stringBuilder.toString());
docWriter.flush();
docWriter.close();
} catch (IOException e) {
//e.printStackTrace();
//寫入失敗
}
return true;
}
}
複製代碼
dependencies {
implementation project(':DocAnnotation')
annotationProcessor project(':DocComplie')
}
複製代碼
應用一個Utils
@GDoc(name = "顏色工具類",time = "2019年09月18日19:58:07",author = "dingxx")
public final class ColorUtils {
}
複製代碼
最後生成的文檔以下:
名稱 功能 做者
ColorUtils 顏色工具類 dingxx
複製代碼
固然最後生成的文檔能夠由本身決定,也能夠直接是html等.
在說Android Transform以前,先介紹Android的打包流程,在執行task assemble時
在.class /jar/resources編譯的過程當中,apply plugin: 'com.android.application' 這個插件支持定義一個回調 (com.android.tools.build:gradle:2.xx 以上),相似攔截器,能夠進行你本身的一些定義處理,這個被成爲Android的Transform
那麼這個時候,能夠動態的修改這些class,完成咱們本身想幹的一些事,好比修復第三方庫的bug,自動埋點,給第三方庫添加函數執行耗時,完成動態的AOP等等.
咱們所知道的 ARoute就使用了這種技術. 固然他是先使用了APT先生成路由文件,而後經過Transform加載.
如下內容引用自ARoute ReadMe
使用 Gradle 插件實現路由表的自動加載 (可選)
apply plugin: 'com.alibaba.arouter'
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "com.alibaba:arouter-register:?"
}
}
複製代碼
可選使用,經過 ARouter 提供的註冊插件進行路由表的自動加載(power by AutoRegister), 默認經過掃描 dex 的方式 進行加載經過 gradle 插件進行自動註冊能夠縮短初始化時間解決應用加固致使沒法直接訪問 dex 文件,初始化失敗的問題,須要注意的是,該插件必須搭配 api 1.3.0 以上版本使用!
看ARoute的LogisticsCenter 能夠知道,init時,若是沒有使用trasnform的plugin,那麼他將在註冊時,遍歷全部dex,查找ARoute引用的相關類,以下
//LogisticsCenter
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
if (registerByPlugin) {
logger.info(TAG, "Load router map by arouter-auto-register plugin.");
} else {
Set<String> routerMap;
// It will rebuild router map every times when debuggable.
if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
logger.info(TAG, "Run with debug mode or new install, rebuild router map.");
// These class was generated by arouter-compiler.
routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
if (!routerMap.isEmpty()) {
context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP,routerMap).apply();
}
PackageUtils.updateVersion(context); // Save new version name when router map update finishes.
} else {
logger.info(TAG, "Load router map from cache.");
routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
}
}
....
}
複製代碼
經過以上,咱們知道,回調的是.class文件或者jar文件,那麼要處理.class 文件或者jar文件就須要字節碼處理的相關工具,經常使用字節碼處理的相關工具都有
具體的詳細,能夠查看美團的推文 Java字節碼加強探祕
怎麼定義一個Trasnfrom內,回顧上面的gradle plugin實現,看如下代碼
public class TransfromPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
AppExtension appExtension = (AppExtension) project.getProperties().get("android");
appExtension.registerTransform(new DemoTransform(), Collections.EMPTY_LIST);
}
class DemoTransform extends Transform{
@Override
public String getName() {
return null;
}
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return null;
}
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return null;
}
@Override
public boolean isIncremental() {
return false;
}
}
}
複製代碼
結合字節碼增長技術,就能夠實現動態的一些AOP,因爲篇幅緣由,這裏就不在詳細把筆記拿出來了,若是想進一步學習,推薦ARoute做者的一個哥們寫的AutoRegister,能夠看看源碼
到這裏Gradle的學習筆記基本整理完成了,因爲做者水平有限,若是文中存在錯誤,還請指正,感謝. 也推薦閱讀個人另外一篇文章,Android 修圖(換證件照背景,污點修復)