Gradle插件開發(2) - extensions和Taskjava
通過前邊兩篇介紹,咱們瞭解了Gradle的基礎知識和如何寫一個本身的插件,咱們今天,開始實戰,搞點有趣的東西。android
今天前面的介紹,咱們是能夠實現本身的自定義task,當時android在構建是一個很瑣碎的過程,以前的各個環節都是task,爲了讓開發人員更少也更好寫業務代碼,後來出了一套Transform API
。git
Transform
是特地爲Android打造的,按照官網的解釋以下:github
Starting with 1.5.0-beta1, the Gradle plugin includes a Transform API allowing 3rd party plugins to manipulate compiled class files before they are converted to dex files. (The API existed in 1.4.0-beta2 but it's been completely revamped in 1.5.0-beta1)api
The goal of this API is to simplify injecting custom class manipulations without having to deal with tasks, and to offer more flexibility on what is manipulated. The internal code processing (jacoco, progard, multi-dex) have all moved to this new mechanism already in 1.5.0-beta1.
Note: this applies only to the javac/dx code path. Jack does not use this API at the moment.app
簡單解釋下就是:ide
Transforms
是從新引入的,主要做用在對class的處理上,也是在生成dex文件前。Transforms
有很強大功能,避免了你們使用task,內部能夠處理jacoco, progard, multi-dex
等過程。在Android studio下咱們如果執行./gradlew tasks
就會發現不少以那麼Transforms
開頭的task。函數
咱們能夠註冊多個transform,這個相似於task
流式關係。 這寫API能夠參考如何理解 Transform API,能夠認爲transform
是碼頭搬運的師父,你加入一我的,不影響個總體的碼頭的雲端,前提是你要按照碼頭的規矩辦事。 那麼transform
遵循碼頭的那些規矩呢?固然就是Transforms
的API了,你要嚴格按照這個API定義每一步。 那麼Transforms 有哪些API呢:工具
public class XXXTransform 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;
}
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation);
}
}
複製代碼
來解釋下每一個函數的做用:測試
getName
定義transform
的名字,便於程序區分getInputTypes
攔截輸入的流的類型,哪些它會攔截呢?public class TransformManager extends FilterableStreamCollection {
public static final Set<ContentType> CONTENT_CLASS;
public static final Set<ContentType> CONTENT_JARS;
public static final Set<ContentType> CONTENT_RESOURCES;
public static final Set<ContentType> CONTENT_NATIVE_LIBS;
public static final Set<ContentType> CONTENT_DEX;
public static final Set<ContentType> CONTENT_JACK;
}
複製代碼
TransformManager
:public static final Set<Scope> SCOPE_FULL_PROJECT = Sets.immutableEnumSet(
Scope.PROJECT,
Scope.PROJECT_LOCAL_DEPS,
Scope.SUB_PROJECTS,
Scope.SUB_PROJECTS_LOCAL_DEPS,
Scope.EXTERNAL_LIBRARIES);
public static final Set<QualifiedContent.ScopeType> SCOPE_FULL_INSTANT_RUN_PROJECT =
new ImmutableSet.Builder<QualifiedContent.ScopeType>()
.addAll(SCOPE_FULL_PROJECT)
.add(InternalScope.MAIN_SPLIT)
.build();
public static final Set<Scope> SCOPE_FULL_LIBRARY = Sets.immutableEnumSet(
Scope.PROJECT,
Scope.PROJECT_LOCAL_DEPS);
複製代碼
transform(TransformInvocation transformInvocation)
是整個類的核心,inputs中是傳過來的輸入流,其中有兩種格式,一種是jar包格式一種是目錄格式。outputProvider 獲取到輸出目錄,最後將修改的文件複製到輸出目錄,這一步必須作否則編譯會報錯。Transform
註冊一個Transform
到項目裏,須要經過Plugin
實現,
public class TranPlugin implements Plugin<Project> {
void apply(Project project) {
System.out.println("------------------開始----------------------");
//AppExtension就是build.gradle中android{...}這一塊
def android = project.extensions.getByType(AppExtension)
//註冊本身的Transform
def classTransform = new ClassTransform(project);
android.registerTransform(classTransform);
System.out.println("------------------結束了嗎----------------------");
}
}
複製代碼
Transform
public class ClassTransform extends Transform {
private Project mProject;
public ClassTransform(Project p) {
this.mProject = p;
}
/** transform的名稱 * transformClassesWithMyClassTransformForDebug 運行時的名字 */
@Override
public String getName() {
return "MyClassTransform";
}
//須要處理的數據類型,有兩種枚舉類型
//CLASSES和RESOURCES,CLASSES表明處理的java的class文件,RESOURCES表明要處理java的資源
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
/** * 指Transform要操做內容的範圍,官方文檔Scope有7種類型: * EXTERNAL_LIBRARIES 只有外部庫 * PROJECT 只有項目內容 * PROJECT_LOCAL_DEPS 只有項目的本地依賴(本地jar) * PROVIDED_ONLY 只提供本地或遠程依賴項 * SUB_PROJECTS 只有子項目。 * SUB_PROJECTS_LOCAL_DEPS 只有子項目的本地依賴項(本地jar)。 * TESTED_CODE 由當前變量(包括依賴項)測試的代碼 */
@Override
public Set<QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}
/** * 指明當前Transform是否支持增量編譯 */
@Override
public boolean isIncremental() {
return false;
}
/** * Transform中的核心方法 * inputs中是傳過來的輸入流,其中有兩種格式,一種是jar包格式一種是目錄格式。 * outputProvider 獲取到輸出目錄,最後將修改的文件複製到輸出目錄,這一步必須作否則編譯會報錯 */
@Override
public void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
System.out.println("----------------進入transform了--------------")
System.out.println("--------------結束transform了----------------")
}
}
複製代碼
若這樣,什麼都不寫,是咱們的transform
會報錯,咱們能夠將遍歷input
文件,:
//遍歷input
inputs.each {
TransformInput input ->
//遍歷文件夾
input.directoryInputs.each {
DirectoryInput directoryInput ->
// 獲取output目錄
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
println(directoryInput.file.getPath() + " ---> " + dest.toPath())
// 將input的目錄複製到output指定目錄
FileUtils.copyDirectory(directoryInput.file, dest)
}
////遍歷jar文件 對jar不操做,可是要輸出到out路徑
input.jarInputs.each {
JarInput jarInput ->
// 重命名輸出文件(同目錄copyFile會衝突)
def jarName = jarInput.name println("jar = " + jarInput.file.getAbsolutePath()) def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
def dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(jarInput.file, dest)
}
}
複製代碼
能夠看到咱們什麼都沒作,就是copy,不過copy文件是須要從新命名一個文件。
這樣就能夠將碼頭搬運過程當中,從上一個工人人的東西,直接傳給了下一個工人,只是有少些的改動。 那咱們如果想在其中進行少些的改動呢?這就須要用到一個神奇的工具javassist
。
Javaassist能夠用 Javassist 改變 Java 類的字節碼,而無需真正瞭解關於字節碼或者 Java 虛擬機(Java virtual machine JVM)結構的任何內容,這樣他就能夠直接修改class文件的字節碼。
Javassist
使您能夠檢查、編輯以及建立 Java 二進制類。檢查方面基本上與經過 Reflection API 直接在 Java 中進行的同樣,可是當想要修改類而不僅是執行它們時,則另外一種訪問這些信息的方法就頗有用了。這是由於 JVM 設計上並無提供在類裝載到 JVM 中後訪問原始類數據的任何方法,這項工做須要在 JVM 以外完成。
Javassist 使用 javassist.ClassPool 類跟蹤和控制所操做的類。這個類的工做方式與 JVM 類裝載器很是類似,可是有一個重要的區別是它不是將裝載的、要執行的類做爲應用程序的一部分連接,類池使所裝載的類能夠經過 Javassist API 做爲數據使用。
裝載到類池中的類由 javassist.CtClass 實例表示。與標準的 Java java.lang.Class 類同樣, CtClass 提供了檢查類數據(如字段和方法)的方法。不過,這只是 CtClass 的部份內容,它還定義了在類中添加新字段、方法和構造函數、以及改變類、父類和接口的方法。奇怪的是,Javassist 沒有提供刪除一個類中字段、方法或者構造函數的任何方法。
字段、方法和構造函數分別由 javassist.CtField、javassist.CtMethod 和 javassist.CtConstructor 的實例表示。這些類定義了修改由它們所表示的對象的全部方法的方法,包括方法或者構造函數中的實際字節碼內容。
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("me.cyning.cc.Rectangle");
cc.setSuperclass(pool.get("me.cyning.cc.Point"));
cc.writeFile();
複製代碼
這樣就將test.Rectangle
的父類設置爲test.Point
,其中 pool.get("test.Rectangle")
就是用默認的ClassPool來裝寨對應的class,而且可使用裝載的CtClass
.
調用 writeFile() 後,這項修改會被寫入原始類文件. 怎麼驗證呢?
Class rc = cc.toClass();
System.out.println(rc.getSuperclass());
複製代碼
咱們能夠寨函數的前面和後面插入咱們的值,如何作呢?
public static void main(String[] args) throws Exception {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("me.cyning.Hello");
CtMethod m = cc.getDeclaredMethod("say");
m.insertBefore("{ System.out.println(\"before Hello.say():\"); }");
m.insertAfter("{ System.out.println(\"after Hello.say():\"); }");
Class c = cc.toClass();
Hello h = (Hello)c.newInstance();
h.say();
}
public class Hello {
public void say() {
System.out.println("Hello");
}
}
複製代碼
先將me.cyning.Hello
load到Javassist的ClassPool中,獲取到me.cyning.Hello
類中的say
方法,能夠在方法開始和結尾來加入函數。 是否是有點意思。
因而咱們能夠在咱們類中直接使用Javassist來修改咱們的class字節碼。 接着transform
中的transform
方法來處理,全部的class到transform
方法中都是以class文件存在的,因此攔截文件夾。
只須要在適當的方法裏注入以下代碼:
``````````````````````println("方法名 = " + mMethod.getName())
println("返回類型 = " + mMethod.getReturnType().getName())
// println("參數類型 = " + mMethod.getParameterTypes())
// 開始時間
mMethod.addLocalVariable("startMs", CtClass.longType);
mMethod.insertBefore("startMs = System.currentTimeMillis();");
String body = "{" +
"final long endMs = System.currentTimeMillis();" +
"System.out.println(\"Executed in ms: [" + className + "," + mMethod.getName() +
"] ---> \" + (endMs-startMs));}"
println(body)
mMethod.insertAfter(body)
複製代碼
找到對應的class的看下,生效:
最後的代碼: github.com/ownwell/Gra…
Transform API Android Plugin Transform 初探 Gradle經過Transform API實現代碼注入 深刻理解 Android 之 Gradle Tutorial 1 Android ASM 插樁初步實現 Android熱修復技術——QQ空間補丁方案解析(3)
網易樂得-Android AOP之字節碼插樁