咱们打开一个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打包的过程,致使编译速度变慢。