关于方法id生成

1,800 阅读4分钟

什么是方法id

我们日常开发中,可能会遇到这么一个需求,就是想要把每一个方法,都转换为一个个Int类型/Long类型(推荐)来表示,这样的好处是效率非常高,比如在慢方法插桩里面,我们如果直接记录一个方法耗时,同时也要记录是哪个方法产生的耗时对吧,我们一般会以String类型方法名称去记录这么一条信息。但是以String可能会比较麻烦

原因在于String长度是不固定的,如果在插桩场景之下,如果需要插入的方法很多,那么它的消耗也是非常巨大的。

所以程序员们想啊想,既然字符串那么不可靠,那么如果我们能用一个数字代表一个方法,那么无论怎么变化,只要这个方法本身没有改变,那么是不是永远也能找到固定的值?这也通常被称为方法id

image.png

方法id生成

生成方法ID,最主要的是寻找一个映射关系,用于把方法映射成我们定义的ID,这里的ID可以是一个递增的数字,比如我们可以采用一个原子类型,比如AtomicInteger,每次遇到一个新方法的时候,这个数就递增即可,原子类型保证了我们能够在多线程环境下处理方法ID的正确生成,比如我们只需调用incrementAndGet即可处理并发问题。

那么还有一个问题,就是方法,我们怎么把一个方法跟一个id绑定起来呢?处理这个问题前,还有一个问题需要解决,怎么判断方法是不是同一个方法?

这里可以总结成一条公式:方法唯一值 = 方法名+方法签名+类特征

下面我们一一说明这几点:

方法名

一个方法的方法名,比如java/kotlin,就是一个方法的重要特征,当然,我们也不能仅仅依靠方法名去判断,因为java虚拟机是支持重载的,比如fun1(a:Int),跟fun1(a:Int,b:Int),虽然方法名一致,但是也不是同一个方法。因此需要另一个保证,方法签名

方法签名

代表着一个方法参数和返回值的特定字符串,比如一个fun1(param:String),它的签名就是(Ljava/lang/String;)V,括号中间代表着参数,右侧是返回值类型

以上两个,换算成字节码表示,以(ASM 中 MethodNode 为例子,方法名就是name属性,方法签名就是desc)

public class MethodNode extends MethodVisitor {
    public int access;
    方法名
    public String name;
    方法签名
    public String desc;
    public String signature;
    public List<String> exceptions;
    public List<ParameterNode> parameters;
    public List<AnnotationNode> visibleAnnotations;
    public List<AnnotationNode> invisibleAnnotations;
    public List<TypeAnnotationNode> visibleTypeAnnotations;
    public List<TypeAnnotationNode> invisibleTypeAnnotations;
    public List<Attribute> attrs;
    public Object annotationDefault;
    public List<AnnotationNode>[] visibleParameterAnnotations;
    public List<AnnotationNode>[] invisibleParameterAnnotations;
    public InsnList instructions;
    public List<TryCatchBlockNode> tryCatchBlocks;
    public int maxStack;
    public int maxLocals;
    public List<LocalVariableNode> localVariables;
    public List<LocalVariableAnnotationNode> visibleLocalVariableAnnotations;
    public List<LocalVariableAnnotationNode> invisibleLocalVariableAnnotations;
    private boolean visited;

通过方法名+方法签名,我们可以判断一个在类内的方法唯一值,注意这里特别表明是类内,因为如果同样的方法定义,在不同的类中,方法名与函数签名也是一致的,因此我们需要加入第三个维度,类特征

类特征

这里可以指代表一个类的唯一特征,比如我们常见的类名就是,由类加载验证过程,保证了类的唯一性,也可以是其他特有的信息。通过类特征,我们就能区别开不同类的方法

生成代码

通过上面的公式,我们就知道了一个方法唯一值,我们可以取类名+方法名+函数签名的方式拼凑而成,这个时候我们还需要一个容器,存储着两者的映射关系,这里我们可以用一个ConcurrentHashMap<String,Int>存储即可,这也我们可以在多线程环境下,也能收集到正确的方法id。

methodMap 是 ConcurrentHashMap<String,Int>,key是方法唯一值,value就是方法id类型
methodId 是一个AtomicInteger类型,用于原子的递增

fun methodCreate(){
    classNode.methods.forEach {
        if (!methodMap.containsKey(方法唯一值)) {
            设置方法id
            val id = methodId.incrementAndGet()
            methodMap[方法唯一值] = id
        }
    }
}

生成方法优劣

通过上文的生成方法,我们简单归纳一下好处与坏处

好处坏处
可以直接在编译时简单生成,上面我们讲到的信息,都是可以在编译时获取到,比如 classNode对应着一个Transform/Transform Action过程的一个类,可以用ASM获取,生成也比较简单,可以支持多线程只能处理编译时就确定的方法,因为依赖编译时生成id,对于动态生成的方法就没有办法

方法id生成实践

这种方法id生成思想,由于简单易用,所以也被很多大厂开源所采用,比如

MethodCollector

总结

通过本文,我们了解到了方法id以及常见的方法id生成过程,如果大家有多方法插桩的需求,不妨采用方法id的方式代替方法名等信息,希望对你有帮助!