バックグラウンド
この章を読んだ後、ASMのツリーAPIを使用して匿名スレッドでフック操作を実行する方法を学びます。また、asm関連の操作と背景知識についても学ぶことができます。ASMインストルメンテーションの場合、多くの人がそれに精通しているかもしれませんが、それらのほとんどはコアAPIにとどまる可能性があります。市場に出回っている一部のインストルメンテーションライブラリでは、ツリーAPIのシンプルで明確な機能がますます増えているため、それらの多くはツリーAPIで記述されています。多くのオープンソースライブラリの選択肢になります。(ASMには、コアとツリーの2セットのAPIタイプがあります)
ASMの概要
ASMは実際にはバイトコードをコンパイルできるツールです。たとえば、日常の開発で多くのクラスライブラリを導入しますよね?またはプロジェクトが大きすぎます。特定のポイントを変更したい場合、間違いを犯しやすいです。統一された変更(プライバシーコンプライアンスの問題など)では、このとき、生成されたクラスファイルを編集するツールがあれば、フォローアップ作業を行うのに非常に便利です。
この章では主にツリーAPIを紹介します。下記のASMはツリーAPIの動作を示しています。コアAPIの紹介については、作者が書いたスパイダーの記事をご覧ください。
クラスファイル
私たちがよく言うクラスファイルは、実際にはバイナリの観点から次の部分に分割されています。ご覧のとおり、クラスファイルは実際には上の図の複数の部分で構成されており、ASMはこれらの構造を実行します。 、クラスファイルの場合、実際にはasmのクラスノードクラスに抽象化されます
クラスファイルの場合、バージョン(バージョン)、アクセス(プライベートなどの修飾子などのスコープ)、名前(名前)、署名(一般的な署名)、superName(親クラス)、インターフェイス(実装されたインターフェース)、フィールド(現在のプロパティ)、メソッド(現在のメソッド)。したがって、クラスを変更する場合は、対応するclassNodeを変更できます。
田畑
クラスの非常に重要な部分でもあるプロパティは、バイトコードで定義されています。
プロパティの場合、ASMはそれをFieldNodeとして抽象化します
属性フィールドの場合、アクセス(スコープ、プライベート変更などのクラス構造と同じ)、名前(属性名)、desc(署名)、署名(一般的な署名)、値(現在の対応する値)
メソッド
属性と比較すると、メソッドの構造はより複雑です。単一の属性と比較すると、メソッドは複数の命令で構成されている可能性があります。メソッドを正常に実行するには、ローカル変数テーブルとオペランドスタックの連携も必要です。ASMでは、メソッドは次のような定義に抽象化されます。メソッド=メソッドヘッダー+メソッド本体
- メソッドヘッダー:アクセス(スコープ)、名前(メソッド名)、desc(メソッドシグネチャ)、シグネチャ(ジェネリックシグネチャ)、例外(メソッドがスローできる例外)など、メソッドを識別する基本的な属性
- メソッド本体:メソッドヘッダーと比較すると、メソッド本体の概念は実際には比較的単純です。実際、メソッド本体は、主に命令(メソッドの命令セット)、tryCatchBlocks(メソッドの命令セットを含む、メソッドのさまざまな命令のコレクションです。異常なノードセット)、maxStack(オペランドスタックの最大深度)、maxLocals(ローカル変数テーブルの最大長)
メソッド内のInsnListオブジェクトは、特定のメソッドの命令セットを抽象化したものであることがわかります。これについては、ここで説明します。
InsnList
public class InsnList implements Iterable<AbstractInsnNode> {
private int size;
private AbstractInsnNode firstInsn;
private AbstractInsnNode lastInsn;
AbstractInsnNode[] cache;
...
主なオブジェクトはfirstInsnとlastInsnであり、メソッド命令セットのヘッド命令とテール命令を表します。各命令は実際にはAbstractInsnNodeのサブクラスに抽象化されます。AbstractInsnNodeは命令の最も基本的な情報を定義します。このクラスのサブクラスを見ることができます
ここでは、最も一般的に使用されるmethodInsnNodeを見てみましょう。
public class MethodInsnNode extends AbstractInsnNode {
/**
* The internal name of the method's owner class (see {@link
* org.objectweb.asm.Type#getInternalName()}).
*
* <p>For methods of arrays, e.g., {@code clone()}, the array type descriptor.
*/
public String owner;
/** The method's name. */
public String name;
/** The method's descriptor (see {@link org.objectweb.asm.Type}). */
public String desc;
/** Whether the method's owner class if an interface. */
public boolean itf;
这个就是一个普通方法指令最根本的定义了,owner(方法调用者),name(方法名称),desc(方法签名)等等,他们都有着相似的结构,这个也是我们接下来会实战的重点。
Signature
嗯!我们最后介绍一下这个神奇的东西!不知道大家在看介绍的时候,有没有一脸疑惑,这个我解释为泛型签名,这个跟desc(函数签名)参数有什么区别呢?当然,这个不仅仅在函数上有出现,在属性,类的结构上都有出现!是不是非常神奇!
其实Signature属性是在JDK 1.5发布后增加到了Class文件规范之中,它是一个可选的定长属性, 可以出现于类、属性表和方法表结构的属性表中。我们想想看,jdk1.5究竟是发生什么了!其实就是对泛型的支持,那么1.5版本之前的sdk怎么办,是不是也要进行兼容了!所以java标准组就想到了一个折中的方法,就是泛型擦除,泛型信息编译(类型变量、参数化类型)之后 都通通被擦除掉,以此来进行对前者的兼容。那么这又导致了一个问题,擦除的泛型信息有时候正是我们所需要的,所以Signature就出现了,把这些泛型信息存储在这里,以提供运行时反射等类型信息的获取!实际上可以看到,我们大部分的方法或者属性这个值都为null,只有存在泛型定义的时候,泛型的信息才会被存储在Signature里面
实战部分
好啦!有了理论基础,我们也该去实战一下,才不是口水文!以我们线程优化为例子,在工作项目中,或者在老项目中,可能存在大多数不规范的线程创建操作,比如直接new Thread等等,这样生成的线程名就会被赋予默认的名字,我们这里先把这类线程叫做“匿名线程”!当然!并不是说这个线程没有名字,而是线程名一般是“Thread -1 ”这种没有额外信息含量的名字,这样对我们后期的线程维护会带来很大的干扰,时间长了,可能就存在大多数这种匿名线程,有可能带来线程创建的oom crash!所以我们的目标是,给这些线程赋予“名字”,即调用者的名字
解决“匿名”Thread
为了达到这个目的,我们需要对thread的构造有一个了解,当然Thread的构造函数有很多,我们举几个例子
public Thread(String name) {
init(null, null, name, 0);
}
public Thread(ThreadGroup group, String name) {
init(group, null, name, 0);
}
可以看到,我们Thread的多个构造函数,最后一个参数都是name,即Thread的名称,所以我们的hook点是,能不能在Thread的构造过程,调用到有name的构造函数是不是就可以实现我们的目的了!我们再看一下普通的new Thread()字节码
那么我们怎么才能把new Thread()的方式变成 new Thread(name)的方式呢?很简单!只需要我们把init的这条指令变成有参的方式就可以了,怎么改变呢?其实就是改变desc!方法签名即可,因为一个方法的调用,就是依据方法签名进行匹配的。我们在函数后面添加一个string的参数即可
node是methidInsnNode
def desc =
"${node.desc.substring(0, r)}Ljava/lang/String;${node.desc.substring(r)}"
node.desc = desc
那么这样我们就可以完成了吗,非也非也,我们只是给方法签名对加了一个参数,但是这并不代表我们函数就是这么运行的!因为方法参数的参数列表中的string参数我们还没放入操作数栈呢!那么我们就可以构造一个string参数放入操作数栈中,这个指令就是ldc指令啦!asm为我们提供了一个类是LdcInsnNode,我们可以创建一个该类对象即可,构造参数需要传入一个字符串,那么这个就可以把当前方法的owner(解释如上,调用者名称)放进去了,是不是就达到我们想要的目的了!好啦!东西我们又了,我们要在哪里插入呢?
所以我们的目标很明确,就是在init指令调用前插入即可,asm也提供了insertBefore方法,提供在某个指令前插入的便捷操作。
method.instructions.insertBefore(
node,
new LdcInsnNode(klass.name)
)
我们看看最后插入后的字节码
もちろん、Androidから提供されたTransformステージにasmコードを挿入するのが一般的です(新しいバージョンのagpは変更されていますが、一般的なワークフローは同じです)。したがって、transfromでのクラスへの過度の干渉を避けるために、不要にする必要がありますステージは早くなくなります!たとえば、新しいスレッドのみを操作する場合、Opcodes.INVOKESPECIALではない操作をフィルタリングできます。非initステージ(つまり、非コンストラクターステージ)もあります。所有者がThreadクラスでない場合は、変更に参加せずに事前にフィルター処理できます。
次に、完全なコード(Transformで実行する必要のあるコード)が表示されます。
static void transform(ClassNode klass) {
println("ThreadTransformUtils")
// 这里只处理Thread
klass.methods?.forEach { methodNode ->
methodNode.instructions.each {
// 如果是构造函数才继续进行
if (it.opcode == Opcodes.INVOKESPECIAL) {
transformInvokeSpecial((MethodInsnNode) it, klass, methodNode)
}
}
}
}
private static void transformInvokeSpecial(MethodInsnNode node, ClassNode klass, MethodNode method) {
// 如果不是构造函数,就直接退出
if (node.name != "<init>" || node.owner != THREAD) {
return
}
println("transformInvokeSpecial")
transformThreadInvokeSpecial(node, klass, method)
}
private static void transformThreadInvokeSpecial(
MethodInsnNode node,
ClassNode klass,
MethodNode method
) {
switch (node.desc) {
// Thread()
case "()V":
// Thread(Runnable)
case "(Ljava/lang/Runnable;)V":
method.instructions.insertBefore(
node,
new LdcInsnNode(klass.name)
)
def r = node.desc.lastIndexOf(')')
def desc =
"${node.desc.substring(0, r)}Ljava/lang/String;${node.desc.substring(r)}"
// println(" + $SHADOW_THREAD.makeThreadName(Ljava/lang/String;Ljava/lang/String;) => ${this.owner}.${this.name}${this.desc}: ${klass.name}.${method.name}${method.desc}")
println(" * ${node.owner}.${node.name}${node.desc} => ${node.owner}.${node.name}$desc: ${klass.name}.${method.name}${method.desc}")
node.desc = desc
break
}
}
やっと
これを見ると、asmツリーAPIの関連する使用法と実際の戦闘を理解できるはずです。お役に立てば幸いです。
ナゲッツテクノロジーコミュニティのクリエイター署名プログラムの募集に参加しています。リンクをクリックして登録し、送信してください。