Android編譯期插樁,讓程序本身寫代碼(一)中我介紹了APT
技術。html
Android編譯期插樁,讓程序本身寫代碼(二)中我介紹了AspectJ
技術。java
本文是這一系列的最後一篇,介紹如何使用Javassist
在編譯期生成字節碼。老規矩,直接上圖。android
Javassist
是一個可以很是方便操做字節碼的庫。它使Java程序可以在運行時新增或修改類。操做字節碼,Javassist
並非惟一選擇,經常使用的還有ASM
。相較於ASM
,Javassist
效率更低。可是,Javassist
提供了更友好的API,開發者們能夠在不瞭解字節碼的狀況下使用它。這一點,ASM
是作不到。Javassist
很是簡單,咱們經過兩個例子直觀的感覺一下。git
這個例子演示瞭如何經過Javassist
生成一個class二進制文件。github
public class Main {
static ClassPool sClassPool = ClassPool.getDefault();
public static void main(String[] args) throws Exception {
//構造新的Class MyThread。
CtClass myThread = sClassPool.makeClass("com.javassist.example.MyThread");
//設置MyThread爲public的
myThread.setModifiers(Modifier.PUBLIC);
//繼承Thread
myThread.setSuperclass(sClassPool.getCtClass("java.lang.Thread"));
//實現Cloneable接口
myThread.addInterface(sClassPool.get("java.lang.Cloneable"));
//生成私有成員變量i
CtField ctField = new CtField(CtClass.intType,"i",myThread);
ctField.setModifiers(Modifier.PRIVATE);
myThread.addField(ctField);
//生成構造方法
CtConstructor constructor = new CtConstructor(new CtClass[]{CtClass.intType}, myThread);
constructor.setBody("this.i = $1;");
myThread.addConstructor(constructor);
//構造run方法的方法聲明
CtMethod runMethod = new CtMethod(CtClass.voidType,"run",null,myThread);
runMethod.setModifiers(Modifier.PROTECTED);
//爲run方法添加註Override註解
ClassFile classFile = myThread.getClassFile();
ConstPool constPool = classFile.getConstPool();
AnnotationsAttribute overrideAnnotation = new AnnotationsAttribute(constPool,AnnotationsAttribute.visibleTag);
overrideAnnotation.addAnnotation(new Annotation("Override",constPool));
runMethod.getMethodInfo().addAttribute(overrideAnnotation);
//構造run方法的方法體。
runMethod.setBody("while (true){" +
" try {" +
" Thread.sleep(1000L);" +
" } catch (InterruptedException e) {" +
" e.printStackTrace();" +
" }" +
" i++;" +
" }");
myThread.addMethod(runMethod);
//輸出文件到當前目錄
myThread.writeFile(System.getProperty("user.dir"));
}
}
複製代碼
運行程序,當前項目下生成了如下內容:api
反編譯MyThread.class
,內容以下:架構
package com.javassist.example;
public class MyThread extends Thread implements Cloneable {
private int i;
public MyThread(int var1) {
this.i = var1;
}
@Override
protected void run() {
while(true) {
try {
Thread.sleep(1000L);
} catch (InterruptedException var2) {
var2.printStackTrace();
}
++this.i;
}
}
}
複製代碼
這個例子演示如何修改class字節碼。咱們爲第一個例子中生成的MyTread.class擴展一些功能。app
public class Main {
static ClassPool sClassPool = ClassPool.getDefault();
public static void main(String[] args) throws Exception {
//爲ClassPool指定搜索路徑。
sClassPool.insertClassPath(System.getProperty("user.dir"));
//獲取MyThread
CtClass myThread = sClassPool.get("com.javassist.example.MyThread");
//將成員變量i變成靜態的
CtField iField = myThread.getField("i");
iField.setModifiers(Modifier.STATIC|Modifier.PRIVATE);
//獲取run方法
CtMethod runMethod = myThread.getDeclaredMethod("run");
//在run方法開始處插入代碼。
runMethod.insertBefore("System.out.println(\"開始執行\");");
//輸出新的二進制文件
myThread.writeFile(System.getProperty("user.dir"));
}
}
複製代碼
運行,再反編譯MyThread.class
,結果以下:框架
package com.javassist.example;
public class MyThread extends Thread implements Cloneable {
private static int i;
public MyThread(int var1) {
this.i = var1;
}
@Override
protected void run() {
System.out.println("開始執行");
while(true) {
try {
Thread.sleep(1000L);
} catch (InterruptedException var2) {
var2.printStackTrace();
}
++this.i;
}
}
}
複製代碼
編譯期插樁對於Javassist
的要求並不高,掌握了上面兩個例子就能夠實現咱們大部分需求了。若是你想了解更高級的用法,請移步這裏。接下來,我只介紹兩個類:CtClass和ClassPool。ide
CtClass
表示字節碼中的一個類。CtClass爲咱們提供了能夠構造一個完整Class
的API,例如繼承父類、實現接口、增長字段、增長方法等。除此以外,CtClass
還提供了writeFile()
方法,方便咱們直接輸出二進制文件。
ClassPool
是CtClass的容器。ClassPool
能夠新建(makeClass)或獲取(get)CtClass
對象。在獲取CtClass對象時,即調用ClassPool.get()
方法,須要在ClassPool
中指定查找路徑。不然,ClassPool怎麼知道去哪裏加載字節碼文件呢。ClassPool經過鏈表維護這些查找路徑,咱們能夠經過insertClassPath()
\appendClassPath()
將路徑插入到鏈表的表頭\表尾。
Javassist
只是操做字節碼的工具。要實現編譯期生成字節碼還須要Android Gradle
爲咱們提供入口,而Transform
就是這個入口。接下來咱們進入了Transform
環節。
Transform是Android Gradle提供的,能夠操做字節碼的一種方式。App編譯時,咱們的源代碼首先會被編譯成class,而後再被編譯成dex。在class編譯成dex的過程當中,會通過一系列Transform
處理。
上圖是Android Gradle定義的一系列Transform
。Jacoco
、Proguard
、InstantRun
、Muti-Dex
等功能都是經過繼承Transform實現的。當前,咱們也能夠自定義Transform
。
咱們先來了解多個Transform
是如何配合工做的。直接上圖。
Transform
之間採用流式處理方式。每一個Transform
須要一個輸入,處理完成後產生一個輸出,而這個輸出又會做爲下一個Transform
的輸入。就這樣,全部的Transform
依次完成本身的使命。
Transform
的輸入和輸出都是一個個的class/jar文件。
Transform
接收輸入時,會把接收的內容封裝到一個TransformInput集合中。TransformInput
由一個JarInput集合和一個DirectoryInput集合組成。JarInput
表明Jar文件,DirectoryInput
表明目錄。
Transform
的輸出路徑是不容許咱們自由指定的,必須根據名稱、做用範圍、類型等由TransformOutputProvider生成。具體代碼以下:
String dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
複製代碼
咱們先看一下繼承Transform
須要實現的方法。
public class CustomCodeTransform 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():Transform
要處理的輸入類型。DefaultContentType提供了兩種類型的輸入方式:
TransformManager
爲咱們封裝了InputTypes。具體以下:
public static final Set<ContentType> CONTENT_CLASS = ImmutableSet.of(CLASSES);
public static final Set<ContentType> CONTENT_JARS = ImmutableSet.of(CLASSES, RESOURCES);
public static final Set<ContentType> CONTENT_RESOURCES = ImmutableSet.of(RESOURCES);
複製代碼
getScopes():Transform
的處理範圍。它約定了Input
的接收範圍。Scope中定義瞭如下幾種範圍:
TransformManager
也爲咱們封裝了經常使用的Scope。具體以下:
public static final Set<ScopeType> PROJECT_ONLY =
ImmutableSet.of(Scope.PROJECT);
public static final Set<Scope> SCOPE_FULL_PROJECT =
Sets.immutableEnumSet(
Scope.PROJECT,
Scope.SUB_PROJECTS,
Scope.EXTERNAL_LIBRARIES);
public static final Set<ScopeType> SCOPE_FULL_WITH_IR_FOR_DEXING =
new ImmutableSet.Builder<ScopeType>()
.addAll(SCOPE_FULL_PROJECT)
.add(InternalScope.MAIN_SPLIT)
.build();
public static final Set<ScopeType> SCOPE_FULL_LIBRARY_WITH_LOCAL_JARS =
ImmutableSet.of(Scope.PROJECT, InternalScope.LOCAL_DEPS);
複製代碼
isIncremental(): 是否支持增量更新。
transform(): 這裏就是咱們具體的處理邏輯。經過參數TransformInvocation,咱們能夠得到輸入,也能夠獲取決定輸出的TransformOutputProvider
。
public interface TransformInvocation {
/** * Returns the inputs/outputs of the transform. * @return the inputs/outputs of the transform. */
@NonNull
Collection<TransformInput> getInputs();
/** * Returns the output provider allowing to create content. * @return he output provider allowing to create content. */
@Nullable
TransformOutputProvider getOutputProvider();
}
複製代碼
下面到了集成Transform環節。集成Transform須要自定義gradle 插件。寫給Android 開發者的Gradle系列(三)撰寫 plugin介紹了自定義gradle插件的步驟,咱們跟着它就能夠實現一個插件。而後就能夠將CustomCodeTransform註冊到gradle的編譯流程了。
class CustomCodePlugin implements Plugin<Project> {
@Override
void apply(Project project) {
AppExtension android = project.getExtensions().getByType(AppExtension.class);
android.registerTransform(new RegisterTransform());
}
}
複製代碼
在Android領域,組件化通過多年的發展,已經成爲一種很是成熟的技術。組件化是一種項目架構,它將一個app項目拆分紅多個組件,而各個組件間互不依賴。
既然組件間是互不依賴的,那麼它們就不能像普通項目那樣進行Activity跳轉。那應該怎麼辦呢?下面咱們就來具體了學習一下。咱們的Activity路由框架有兩個module組成。一個module用來提供API,咱們命名爲common
;另外一個module用來處理編譯時字節碼的注入,咱們命名爲plugin
。
咱們先來看一下common
。它只有兩個類,以下:
public interface IRouter {
void register(Map<String,Class> routerMap);
}
複製代碼
public class Router {
private static Router INSTANCE;
private Map<String, Class> mRouterMap = new ConcurrentHashMap<>();
//單例
private static Router getInstance() {
if (INSTANCE == null) {
synchronized (Router.class) {
if (INSTANCE == null) {
INSTANCE = new Router();
}
}
}
return INSTANCE;
}
private Router() {
init();
}
//在這裏字節碼注入。
private void init() { }
/** * Activity跳轉 * @param context * @param activityUrl Activity路由路徑。 */
public static void startActivity(Context context, String activityUrl) {
Router router = getInstance();
Class<?> targetActivityClass = router.mRouterMap.get(activityUrl);
Intent intent = new Intent(context,targetActivityClass);
context.startActivity(intent);
}
}
複製代碼
common
的這兩個類十分簡單。IRouter
是一個接口。Router
對外的方法只有一個startActivity
。
接下來,咱們跳過plugin
,先學習一下框架怎麼使用。假如咱們的項目被拆分紅app、A、B三個module。其中app是一個殼工程,只負責打包,依賴於A、B。A和B是普通的業務組件,A、B之間互不依賴。如今,A組件中有一個AActivity,B組件想跳轉到AActivity。怎麼作呢?
在A組件中新建一個ARouterImpl
實現IRouter
。
public class ARouterImpl implements IRouter {
private static final String AActivity_PATH = "router://a_activity";
@Override
public void register(Map<String, Class> routerMap) {
routerMap.put(AActivity_PATH, AActivity.class);
}
}
複製代碼
在B組件中調用時,只須要
Router.startActivity(context,"router://a_activity");
複製代碼
是否是很神奇?其實奧妙就在plugin
中。編譯時,plugin
在Router
的init()
中注入了以下代碼:
private void init() {
ARouterImpl var1 = new ARouterImpl();
var.register(mRouterMap);
}
複製代碼
plugin
中的代碼有點多,我就不貼出來了。這一節的代碼都在這裏。
這個Demo很是簡單,可是它對於理解ARouter、WMRouter等路由框架的原理十分有用。它們在處理路由表的註冊時,都是採用編譯期字節碼注入的方式,只不過它們沒有使用javassit
,而是使用了效率更高的ASM
。它們用起來更方即是由於,它們利用APT技術把路徑和Activity之間的映射變透明瞭。即:相似於Demo中的ARouterImpl
這種代碼,都是經過APT生成的。