前言
关于asm,一个非常有名的字节码操作工具。具体到android应用程序上来说,上到拦截系统守护进程。下到简单的在编译器实现一些特定方法拦截,修改全局变量等统统不在话下。具体原理请参考java虚拟机规范,使用方法可以参考asm 使用指南,网上翻译版本众多,这里就不一一介绍了。本文的主要内容如题,目的是为了帮助大家可以快速的理解这一工具库。
准备工作
首先,你要先熟悉gradle的插件流程,首先你需要创建一个gradle插件。具体流程直接上代码。
第一步,先创建一个java 项目,这里注意的是要创建的不是android项目而是纯java项目。
第二步,接入groovy配置meta信息
class TestPlugin implements Plugin<Project> {
@Override
void apply(Project target) {
println("------------------开始----------------------")
println("自定义插件!")
target.android.registerTransform(new DemoTransform())
println("------------------结束----------------------->")
}
}
第三步,创建DemoTransform类文件,这里就可以用纯java写了。
class DemoTransform : Transform() {
private val projectScopes = hashSetOf(QualifiedContent.Scope.PROJECT)
override fun getName(): String = "InjectTest"
override fun getInputTypes(): Set<QualifiedContent.ContentType> = setOf(QualifiedContent.DefaultContentType.CLASSES)
override fun getScopes(): MutableSet<in QualifiedContent.Scope>? = hashSetOf(QualifiedContent.Scope.PROJECT,
QualifiedContent.Scope.SUB_PROJECTS,
QualifiedContent.Scope.EXTERNAL_LIBRARIES
)
override fun isIncremental(): Boolean = true
override fun transform(ti: TransformInvocation) {
val startTime = System.currentTimeMillis()
val outputProvider = ti.outputProvider
val outDir = outputProvider.getContentLocation("inject", outputTypes, projectScopes, Format.DIRECTORY)
val executor = ThreadPoolExecutor(8, 10, 1, TimeUnit.SECONDS,
LinkedBlockingQueue<Runnable>())
if (ti.isIncremental) {
ti.inputs.forEach {
for (jarInput in it.jarInputs) {
val jarFile = jarInput.file
val status = jarInput.status
if (status == Status.NOTCHANGED) {
continue
}
println("Status of file " + jarFile + " is " + jarInput.status)
val uniqueName = jarFile.name + "_" + jarFile.absolutePath.hashCode()
val jarOutFile = outputProvider.getContentLocation(uniqueName, outputTypes, jarInput.scopes, Format.JAR)
if (status == Status.REMOVED) {
jarOutFile.delete()
continue
}
executor.execute {
processJarFile(jarFile, jarOutFile)
}
}
it.directoryInputs.forEach {
val pathBitLen = it.file.toString().length + 1
val changedFiles = it.changedFiles
for ((file, status) in changedFiles.entries) {
val classPath = file.toString().substring(pathBitLen)
if (status == Status.NOTCHANGED) {
continue
}
println("Status of file $file is $status")
val outputFile = File(outDir, classPath)
if (status == Status.REMOVED) {
outputFile.delete()
continue
}
executor.execute {
processClassFile(classPath, file, outputFile)
}
}
}
}
} else {
outputProvider.deleteAll()
outDir.mkdirs()
ti.inputs.forEach {
it.jarInputs.forEach {
val jarFile = it.file
val uniqueName = "${jarFile.name}_${jarFile.absolutePath.hashCode()}"
val jarOutDir = outputProvider.getContentLocation(uniqueName, outputTypes, it.scopes, Format.JAR)
executor.execute {
processJarFile(jarFile, jarOutDir)
}
}
it.directoryInputs.forEach {
val pathBitLen = it.file.toString().length + 1
it.file.walk().forEach {
if (!it.isDirectory) {
val f = it
val classPath = f.toString().substring(pathBitLen)
val outputFile = File(outDir, classPath)
executor.execute {
processClassFile(classPath, f, outputFile)
}
}
}
}
}
}
executor.shutdown()
executor.awaitTermination(Int.MAX_VALUE.toLong(), TimeUnit.SECONDS)
println("Inject done, cost ${System.currentTimeMillis() - startTime}ms")
}
private fun processJarFile(file: File, outFile: File) {
val crc32 = CRC32()
outFile.delete()
outFile.parentFile.mkdirs()
ZipOutputStream(FileOutputStream(outFile)).use { zos ->
val zipFile = ZipFile(file)
zipFile.entries().toList().forEach {
val name = it.name
var bytes = zipFile.getInputStream(it).readBytes()
if (name.endsWith(".class")) {
bytes = doTransfer(name, bytes)
}
crc32.reset()
crc32.update(bytes)
val zipEntry = ZipEntry(name).apply {
method = ZipEntry.STORED
size = bytes.size.toLong()
compressedSize = bytes.size.toLong()
crc = crc32.value
}
zos.putNextEntry(zipEntry)
zos.write(bytes)
zos.closeEntry()
}
zos.flush()
}
}
private fun processClassFile(classPath: String, file: File, outputFile: File) {
if (file.isDirectory) {
return
}
outputFile.parentFile.mkdirs()
val classFileBuffer = file.readBytes()
outputFile.writeBytes(doTransfer(classPath, classFileBuffer))
}
private fun doTransfer(classPath: String, input: ByteArray): ByteArray {
val className = classPath.substring(0, classPath.lastIndexOf('.'))
val s = String(input)
//这里处理判断语句,当前情况是对于包含toast的类文件做处理
if (s.contains("android/widget/Toast")) {
val reader = ClassReader(input)
val writer = ClassWriter(reader, ClassWriter.COMPUTE_MAXS)
val cv = TestVisitor(writer)
reader.accept(cv, ClassReader.EXPAND_FRAMES)
println("inject $classPath end")
return writer.toByteArray()
}
return input
}
}
如果仔细阅读DemoTransform这个类,会发现其作用主要是把所有的类文件源码丢给dotransfer方法然后再输出。这里有一个叫做TestVisitor的类,而该类就是用来处理关键逻辑的部分。asm框架使用最麻烦的部分主要是使用者必须对字节码操作有比价深刻的理解。然而在日常使用过程中,其实大部分情况只需要对特定类类文件跟特定方法做一定的拦截就好了。这里会提到一个类,叫做ASMifier, 通过它,我们可以把需要替换实现的代码用纯java实现,然后编译成类文件,最后通过java -classpath asm.jar:asm-util.jar org.objectweb.asm.util.ASMifier xxxx.class方法获取该类文件的字节码操作代码。这样就可以相对简单的完成操作。那么下面我们来看一下例子。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Toast.makeText(this, "4321", Toast.LENGTH_SHORT)
}
}
这里有一个简单的activity,里面有调用toast方法。而我们的目的是把该应用内全部的toast.makeText方法都指向下面的工具类里。
public class ToastUtils {
public static Toast makeText(Context context, CharSequence text, int duration){
return Toast.makeText(context, "1234", duration);
}
}
为了实现上述功能,我们先通过as自带的kotlinbytecode工具查看当前activity的字节码
final class qingting/fm/myapplication/MainActivity$onCreate$1 implements android/view/View$OnClickListener {
// access flags 0x11
public final onClick(Landroid/view/View;)V
L0
LINENUMBER 16 L0
ALOAD 0
GETFIELD qingting/fm/myapplication/MainActivity$onCreate$1.this$0 : Lqingting/fm/myapplication/MainActivity;
CHECKCAST android/content/Context
LDC "4321"
CHECKCAST java/lang/CharSequence
ICONST_0
INVOKESTATIC android/widget/Toast.makeText (Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
INVOKEVIRTUAL android/widget/Toast.show ()V
L1
LINENUMBER 17 L1
RETURN
L2
LOCALVARIABLE this Lqingting/fm/myapplication/MainActivity$onCreate$1; L0 L2 0
LOCALVARIABLE it Landroid/view/View; L0 L2 1
MAXSTACK = 3
MAXLOCALS = 2
// access flags 0x0
<init>(Lqingting/fm/myapplication/MainActivity;)V
ALOAD 0
ALOAD 1
PUTFIELD qingting/fm/myapplication/MainActivity$onCreate$1.this$0 : Lqingting/fm/myapplication/MainActivity;
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
MAXSTACK = 2
MAXLOCALS = 2
}
ps: 通过java -classpath asm.jar:asm-util.jar:ToastUtils.class org.objectweb.asm.util.ASMifier xxxx.class 也可以查看特定class文件的字节码
修改为我们希望修改后的代码如下
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
ToastUtils.makeText(this, "4321", Toast.LENGTH_SHORT)
}
}
再次查看字节码:
final class qingting/fm/myapplication/MainActivity$onCreate$1 implements android/view/View$OnClickListener {
// access flags 0x11
public final onClick(Landroid/view/View;)V
L0
LINENUMBER 16 L0
ALOAD 0
GETFIELD qingting/fm/myapplication/MainActivity$onCreate$1.this$0 : Lqingting/fm/myapplication/MainActivity;
CHECKCAST android/content/Context
LDC "4321"
CHECKCAST java/lang/CharSequence
ICONST_0
INVOKESTATIC qingting/fm/myapplication/ToastUtils.makeText (Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
INVOKEVIRTUAL android/widget/Toast.show ()V
L1
LINENUMBER 17 L1
RETURN
L2
LOCALVARIABLE this Lqingting/fm/myapplication/MainActivity$onCreate$1; L0 L2 0
LOCALVARIABLE it Landroid/view/View; L0 L2 1
MAXSTACK = 3
MAXLOCALS = 2
// access flags 0x0
<init>(Lqingting/fm/myapplication/MainActivity;)V
ALOAD 0
ALOAD 1
PUTFIELD qingting/fm/myapplication/MainActivity$onCreate$1.this$0 : Lqingting/fm/myapplication/MainActivity;
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
MAXSTACK = 2
MAXLOCALS = 2
}
对比发现主要是
INVOKESTATIC android/widget/Toast.makeText 被替换成为了
INVOKESTATIC qingting/fm/myapplication/ToastUtils.makeText 。
所以相关的TextVisiter的作用就是要完成上述替换
public class TestVisitor extends ClassVisitor {
private String className;
private boolean doInject = false;
public TestVisitor(ClassVisitor cv) {
super(ASM5, cv);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
className = name;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
mv = new MethodVisitor(ASM5, mv) {
String methodName = name;
String methodDesc = descriptor;
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
if (owner.equalsIgnoreCase("android/widget/Toast") && name.equalsIgnoreCase("makeText") && desc.equalsIgnoreCase("(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;")) {
logHook(methodName, methodDesc);
mv.visitMethodInsn(INVOKESTATIC, "qingting/fm/myapplication/ToastUtils", "makeText", "(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;", false);
return;
}
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
};
return mv;
}
private void logHook(String method, String desc) {
System.out.println("Hooking in " + className + ":" + method + desc);
}
}
网友评论