最近学习插桩就尝试了一下用插桩的方式做一个统计Activity生命周期方法耗时的小demo
本文所用技术要点
1、自定义gradle插件
2、asm插桩
要点讲解
- 自定义插件
新建一个module(library),gradle文件如下:
apply plugin: 'groovy'
apply plugin: 'maven'
repositories {
mavenCentral()
jcenter()
}
dependencies {
//gradle sdk
implementation gradleApi()
//groovy sdk
implementation localGroovy()
implementation 'com.android.tools.build:gradle:3.1.3'
}
group='com.benz.ams'
version='1.0.0'
uploadArchives {
repositories {
mavenDeployer {
repository(url: uri('./plugin'))
}
}
}
创建groovy文件
class AmsPlugin extends Transform implements Plugin<Project> {
static File tempFile
@Override
void apply(Project project) {
//registerTransform
initDir(project);
def android = project.extensions.getByType(AppExtension)
android.registerTransform(this)
}
@Override
String getName() {
return "AmsPlugin"
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
static void initDir(Project project) {
if (!project.buildDir.exists()) {
project.buildDir.mkdirs()
}
tempFile = new File(project.buildDir, "ams")
if (!tempFile.exists()) {
tempFile.mkdir()
}
}
@Override
void transform(TransformInvocation transformInvocation) {
println '--------------- AmsPlugin visit start --------------- '
def startTime = System.currentTimeMillis()
Collection<TransformInput> inputs = transformInvocation.inputs
TransformOutputProvider outputProvider = transformInvocation.outputProvider
//删除之前的输出
if (outputProvider != null)
outputProvider.deleteAll()
//遍历inputs
inputs.each { TransformInput input ->
//遍历directoryInputs
input.directoryInputs.each { DirectoryInput directoryInput ->
handleDirectoryInput(directoryInput, outputProvider)
}
//遍历jarInputs
input.jarInputs.each { JarInput jarInput ->
handleJarInputs(jarInput, outputProvider)
}
}
def cost = (System.currentTimeMillis() - startTime) / 1000
println '--------------- AmsPlugin visit end --------------- '
println "AmsPlugin cost : $cost s"
}
/**
* 处理文件目录下的class文件
*/
static void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
//是否是目录
if (directoryInput.file.isDirectory()) {
//列出目录所有文件(包含子文件夹,子文件夹内文件)
directoryInput.file.eachFileRecurse { File file ->
def name = file.name
if (checkClassFile(name)) {
println '----------- deal with "class" file <' + name + '> -----------'
ClassReader classReader = new ClassReader(file.bytes)
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new AmsClassVisitor(classWriter)
classReader.accept(cv, EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
FileOutputStream fos = new FileOutputStream(
file.parentFile.absolutePath + File.separator + name)
fos.write(code)
fos.close()
saveModifiedClassForCheck(file);
}
}
}
//处理完输入文件之后,要把输出给下一个任务
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}
/**
* 处理Jar中的class文件
*/
static void handleJarInputs(JarInput jarInput, TransformOutputProvider outputProvider) {
if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
//重名名输出文件,因为可能同名,会覆盖
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
JarFile jarFile = new JarFile(jarInput.file)
Enumeration enumeration = jarFile.entries()
File tmpFile = new File(jarInput.file.getParent() + File.separator + "classes_temp.jar")
//避免上次的缓存被重复插入
if (tmpFile.exists()) {
tmpFile.delete()
}
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
//用于保存
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement()
String entryName = jarEntry.getName()
ZipEntry zipEntry = new ZipEntry(entryName)
InputStream inputStream = jarFile.getInputStream(jarEntry)
//插桩class
if (checkClassFile(entryName)) {
//class文件处理
println '----------- deal with "jar" class file <' + entryName + '> -----------'
jarOutputStream.putNextEntry(zipEntry)
ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new AmsClassVisitor(classWriter)
classReader.accept(cv, EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
jarOutputStream.write(code)
} else {
jarOutputStream.putNextEntry(zipEntry)
jarOutputStream.write(IOUtils.toByteArray(inputStream))
}
jarOutputStream.closeEntry()
}
//结束
jarOutputStream.close()
jarFile.close()
def dest = outputProvider.getContentLocation(jarName + md5Name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(tmpFile, dest)
tmpFile.delete()
}
}
/*
保存插桩后的文件到临时目录 方便查看是否插桩正确
*/
static void saveModifiedClassForCheck(File tempClass) {
File dir = tempFile;
File checkJarFile = new File(dir, tempClass.getName().replace("/", "_"));
if (checkJarFile.exists()) {
checkJarFile.delete();
}
FileUtils.copyFile(tempClass, checkJarFile);
}
/**
* 检查class文件是否需要处理
* @param fileName
* @return
*/
static boolean checkClassFile(String name) {
//只处理需要的class文件
return (name.endsWith(".class") && !name.startsWith("R\$")
&& !"R.class".equals(name) && !"BuildConfig.class".equals(name)
&& name.contains("Activity") && !name.contains("android/"))
}
}
- 插桩关键代码
创建ClassVisitor和MethodVisitor
public class AmsClassVisitor extends ClassVisitor implements Opcodes {
private static final String[] methodNames = {"onCreate", "onStart", "onResume", "onPause", "onStop", "onDestroy"};
private String mClassName;
private ArrayList<String> names;
public AmsClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM5, cv);
List<String> list = Arrays.asList(methodNames);
names = new ArrayList<>(list);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
this.mClassName = name;
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
if (this.mClassName.contains("Activity") && !this.mClassName.contains("android/")) {
if (match(name)) {
System.out.println("AmsClassVisitor : change method ----> " + name);
return new AmsMethodVisitor(mv, name);
}
}
return mv;
}
private boolean match(String name) {
return names.contains(name);
}
@Override
public void visitEnd() {
super.visitEnd();
}
}
public class AmsMethodVisitor extends MethodVisitor {
private String mMethodName;
public AmsMethodVisitor(MethodVisitor mv, String name) {
super(Opcodes.ASM4, mv);
mMethodName = name;
}
@Override
public void visitCode() {
super.visitCode();
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitVarInsn(Opcodes.LSTORE, 2);
}
@Override
public void visitInsn(int opcode) {
if (((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) && mv != null) {
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitVarInsn(Opcodes.LSTORE, 5);
Label l9 = new Label();
mv.visitLabel(l9);
mv.visitLineNumber(21, l9);
mv.visitLdcInsn("benz");
mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getSimpleName", "()Ljava/lang/String;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitLdcInsn(" ----> " + mMethodName + " execute cost ");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitVarInsn(Opcodes.LLOAD, 5);
mv.visitVarInsn(Opcodes.LLOAD, 2);
mv.visitInsn(Opcodes.LSUB);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
mv.visitLdcInsn("ms");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(Opcodes.POP);
}
super.visitInsn(opcode);
}
}
这里的代码不会写没关系,可以使用as中的一个插件(Ams Bytecode Outline)生成
具体使用方式如下:
long start = System.currentTimeMillis();
long end = System.currentTimeMillis();
Log.i("benz", this.getClass().getSimpleName() + " ----> OnCreate execute cost " + (end - start) + "ms");
先写成如上所示的代码然后选中代码右键选择show bytecode outline就能看到入下图所示的代码段
image.png
怎么让插件生效?
如下图,当修改了代码时使用uploadArchives生成插件
image.png
- 主module中使用
- 配置项目gradle文件
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
jcenter()
maven {
url uri('./lib-ams/plugin')//使用上面创建的插件
}
}
dependencies {
classpath "com.android.tools.build:gradle:3.1.3"
classpath 'com.benz.ams:lib-ams:1.0.0'//使用上面创建的插件
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
- 配置主module gradle文件
apply plugin: 'com.android.application'
apply plugin: 'com.benz.ams' //使用上面创建的插件
android {
compileSdkVersion 27
defaultConfig {
applicationId "com.benz.ams.demo"
minSdkVersion 21
targetSdkVersion 27
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation 'com.android.support:appcompat-v7:27.1.1'
}
-
运行代码
我们可以在build目录下找到MainActivity的源码对比自己写的MainActivity的代码会发现插桩成功,如下所示
image.png -
运行结果
image.png
网友评论