咱們打開一個Activity的時候是否想知道它徹底加載所須要的時間,若是要分析一個頁面,那咱們直接在代碼中修改就能夠了,那麼若是是多個頁面呢?html
這個時候咱們能夠利用AOP
的原理,在既有class文件的基礎上修改生成咱們須要的class文件。java
前面咱們已經會自定義插件了,此次咱們經過ASM來實現編譯插樁的操做。android
咱們先來看一下打包的流程: git
以上流程咱們能夠看到:github
因此咱們要作的就是在生成dex以前的.class文件上作文章。這就要用到 Teansform
。api
Android官方從gradle1.5版本開始,提供了Transform
來用於在項目構建階段,修改class文件的一個api。Transform會在被註冊以後被Gradle包裝成一個Task
,在java compile Task
執行完以後執行。數組
咱們來看下它的幾個重要方法bash
/** 指明transform的task名字 */
@Override
String getName() {
return null
}
/**
指明輸入類型:
CLASSES:class文件,來自jar或者文件夾
RESOURCES: java資源
*/
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return null
}
/**
指明輸入文件所屬範圍:
PROJECT:當前項目代碼,
SUB_PROJECTS:子工程代碼,
EXTERNAL_LIBRARIES:外部庫代碼,
TESTED_CODE:測試代碼,
PROVIDED_ONLY:provided庫代碼,
*/
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return null
}
/** 指明是不是增量構建 */
@Override
boolean isIncremental() {
return false
}
複製代碼
最最重要的方法是transform
方法,經過其中的transformInvocation
得到TransformInput
、DirectoryInput
、JarInput
以及TransformOutputProvider
。框架
TransformInput
: 輸入文件的抽象,包括DirectoryInput
集合以及JarInput
集合。DirectoryInput
: 表明以源碼方式參與編譯的目錄結構以及下面的源文件,能夠用來修改輸出文件的結構及其字節碼文件。JarInput
:全部參與編譯的jar文件包括本地和遠程jar文件。TransformOutputProvider
:Transform的輸出,能夠經過它來獲取輸出路徑。我這裏使用的是ASM的方式進行編譯時插樁,ASM
是一個通用的java字節碼操做和分析框架。能夠生成、轉換和分析已編譯的java class文件,可以使用ASM工具讀、寫、轉換JVM指令集。也就是說來處理jacac編譯以後的class文件。ide
咱們來看下ASM框架的幾個核心類:
ClassReader
:該類用來解析字節碼class文件,能夠接受一個實現了ClassVisitor接口的對象做爲參數,而後依次調用ClassVisitor接口的各個方法,進行本身的處理。ClassWriter
:ClassVisitor的子類,用來對class文件輸出和生成。在對類或者方法進行處理的時候,經過FieldVisitor
和MethodVisitor
進行處理。他們各自都有本身重要的子類:FiledWriter
和MethodWriter
。對於每個方法的調用會建立類的相應部分,例如調用visit方法會建立一個類的聲明部分,調用visitMethod會在這個類中建立一個新的方法,調用visitEnd會代表對該類的建立已經完成了,最終會經過toByteArray方法返回一個數組,這個數組包含了整個class文件的完整字節碼內容。ClassAdapter
:實現了ClassVisitor接口,其構造方法須要ClassVisitor隊形,並保存字段爲protected ClassVisitor。在它的實現中,每一個方法都是原裝不動的調用classVisitor對應方法,並傳遞一樣的參數。能夠經過集成ClassAdapter並修改其中的部分方法達到過濾的做用。它能夠堪稱事件的過濾器。好了,基本的知識咱們已經瞭解了,如今咱們開始一步步實現咱們須要的功能。
首先,咱們先自定義兩個註解以及計算時間的工具類。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface OnStartTime {
}
複製代碼
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface OnEndTime {
}
複製代碼
只在註解了這兩個的方法中進行耗時統計。
工具類:
public class TimeCache {
private static volatile TimeCache mInstance;
private static byte[] mLock = new byte[0];
private Map<String, Long> mStartTimes = new HashMap<>();
private Map<String, Long> mEndTimes = new HashMap<>();
private TimeCache() {}
public static TimeCache getInstance() {
if (mInstance == null) {
synchronized (mLock) {
if (mInstance == null) {
mInstance = new TimeCache();
}
}
}
return mInstance;
}
public void putStartTime(String className, long time) {
mStartTimes.put(className, time);
}
public void putEndTime(String className, long time) {
mEndTimes.put(className, time);
}
public void printlnTime(String className) {
if (!mStartTimes.containsKey(className) || !mEndTimes.containsKey(className)) {
System.out.println("className ="+ className + "not exist");
}
long currTime = mEndTimes.get(className) - mStartTimes.get(className);
System.out.println("className ="+ className + ",time consuming " + currTime+ " ns");
}
}
複製代碼
只有在onStart 和onEnd都註解了以後,纔會計算耗時。
新建Transform類,處理transform邏輯。
@Override
String getName() {
return "custom_plugin"
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
// 輸入類型:class文件
return TransformManager.CONTENT_CLASS
}
@Override
Set<? super QualifiedContent.Scope> getScopes() {
// 輸入文件範圍:project包括jar包
return TransformManager.SCOPE_FULL_PROJECT
}
複製代碼
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
println("//============asm visit start===============//")
def startTime = System.currentTimeMillis()
Collection<TransformInput> inputs = transformInvocation.inputs
TransformOutputProvider outputProvider = transformInvocation.outputProvider
if (outputProvider != null) {
outputProvider.deleteAll()
}
inputs.each { TransformInput input ->
input.directoryInputs.each { DirectoryInput directoryInput ->
handleDirectoryInput(directoryInput, outputProvider)
}
input.jarInputs.each { JarInput jarInput ->
handleJarInput(jarInput, outputProvider)
}
}
def customTime = (System.currentTimeMillis() - startTime) / 1000
println("plugin custom time = " + customTime + " s")
println("//============asm visit end===============//")
}
複製代碼
input分爲兩類:一個是項目中的,一個是jar包中的。咱們目前只處理項目中的。
static void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
if (directoryInput.file.isDirectory()) {
directoryInput.file.eachFileRecurse { File file ->
def name = file.name
// 排除不須要修改的類
if (name.endsWith(".class") && !name.startsWith("R\$") && !"R.class".equals(name) && !"BuildConfig.class".equals(name)) {
println("name =="+ name + "===is changing...")
ClassReader classReader = new ClassReader(file.bytes)
//
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
//
ClassVisitor classVisitor = new CustomClassVisitor(classWriter)
classReader.accept(classVisitor, EXPAND_FRAMES)
byte [] code = classWriter.toByteArray()
FileOutputStream fos = new FileOutputStream(file.parentFile.absolutePath + File.separator + name)
fos.write(code)
fos.close()
}
}
}
//處理完輸入文件以後,要把輸出給下一個任務
def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}
複製代碼
在ClassVisitor中處理咱們要過濾的類,而後對其進行修改。
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
methodVisitor = new AdviceAdapter(Opcodes.ASM5, methodVisitor, access, name, desc) {
private boolean isStart = false;
private boolean isEnd = false;
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
if ("Lcom/cn/lenny/annotation/OnStartTime;".equals(desc)) {
isStart = true;
}
if ("Lcom/cn/lenny/annotation/OnEndTime;".equals(desc)) {
isEnd = true;
}
return super.visitAnnotation(desc, visible);
}
@Override
protected void onMethodEnter() {
// 方法開始
if (isStart) {
// mv.visitLdcInsn(name);
mv.visitMethodInsn(INVOKESTATIC, "com/cn/lenny/annotation/TimeCache", "getInstance", "()Lcom/cn/lenny/annotation/TimeCache;", false);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getSimpleName", "()Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "com/cn/lenny/annotation/TimeCache", "putStartTime", "(Ljava/lang/String;J)V", false);
}
super.onMethodEnter();
}
@Override
protected void onMethodExit(int opcode) {
// 方法結束
if (isEnd) {
mv.visitLdcInsn(name);
mv.visitMethodInsn(INVOKESTATIC, "com/cn/lenny/annotation/TimeCache", "getInstance", "()Lcom/cn/lenny/annotation/TimeCache;", false);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getSimpleName", "()Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "com/cn/lenny/annotation/TimeCache", "putEndTime", "(Ljava/lang/String;J)V", false);
mv.visitLdcInsn(name);
mv.visitMethodInsn(INVOKESTATIC, "com/cn/lenny/annotation/TimeCache", "getInstance", "()Lcom/cn/lenny/annotation/TimeCache;", false);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getSimpleName", "()Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "com/cn/lenny/annotation/TimeCache", "printlnTime", "(Ljava/lang/String;)V", false);
}
super.onMethodExit(opcode);
}
};
return methodVisitor;
}
複製代碼
關於增長字節碼,能夠去看一個關於字節碼的文檔,也能夠經過插件ASM Bytecode Outline
來幫助咱們。
咱們來看下編譯以後的類是否達到咱們想要的效果了
public class TestActivity extends Activity {
public TestActivity() {
}
@OnStartTime
protected void onCreate(@Nullable Bundle savedInstanceState) {
TimeCache.getInstance().putStartTime(this.getClass().getSimpleName(), System.currentTimeMillis());
super.onCreate(savedInstanceState);
this.setContentView(2131296285);
}
@OnEndTime
protected void onResume() {
super.onResume();
String var10000 = "onResume";
TimeCache.getInstance().putEndTime(this.getClass().getSimpleName(), System.currentTimeMillis());
String var10001 = "onResume";
TimeCache.getInstance().printlnTime(this.getClass().getSimpleName());
}
}
複製代碼
哇,成功了。
看到這裏,我以爲你也能夠本身寫一個編譯插樁的代碼了。
利用AOP的思路來統計耗時,避免了對於原有代碼的修改,減小了大量的重複性工做,而且減小了代碼的耦合性;缺點在於ASM操做理解都有必定的難度,而且干預了APK打包的過程,致使編譯速度變慢。