Lambda引发的惨案 | Transform进阶教程

2022-03-06 09:39:04 浏览数 (1)

前言

这篇文章是紧接着上一篇文章的,原因就是因为有人在评论区留下了Lambad如何处理。根据我以往的经验,卧槽这个不是送分题吗,根据以往的经验,Lambda都会被脱糖成匿名内部类,然后才会走到Transform流程上来,所以lambda不就是个匿名内部类吗。

但是往往经验这个东西会害死人啊,我以前在写编译流程的时候介绍过了新的混淆规则R8,而Desugar的任务也被移动到了Dex合并ShrinkResourcesTask的环节上了。

那么Transform在这个时候其实就是一个完全没有被脱糖过的代码,所以还是要感谢那位同学的评论,让我在马上意识到了这个问题,所以才有了这篇水文。

什么是Lambada

Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。 Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。使用 Lambda 表达式可以使代码变的更加简洁紧凑。

上面是java对于lambda的释义,那么什么是lambda的本质呢。我以前的文章简单介绍过java的函数调用的四个指令(invokevirtual、invokespecial、invokestatic、invokeinterface),而在java8之后专门给lambda新增了一个指令,就是invokedynamic

我们可以简单的把lambda理解问一个动态的链接,他将一个lambda表达式指向的其实是一个静态的方法调用,而这个方法调用会返回他所需要的表述类型等等信息。

接下来,又到了各位基本看不懂的字节码阶段了,从本质上看lambda。

代码语言:javascript复制
public class OtherTestViewHolder extends ViewHolder {
    private int i = 100;

    public OtherTestViewHolder(@NonNull View itemView) {
        super(itemView);
        itemView.setOnClickListener((v) -> {
            Log.i("", "");
            ToastHelper.toast(v, v, "1234");
        });
    }
}
代码语言:javascript复制
//删除无效的不想阅读的代码
// access flags 0x1
 public <init>(Landroid/view/View;)V
  L2
   LINENUMBER 19 L2
   ALOAD 1

   // 通过INVOKEDYNAMIC 将当前的的setOnClickListener 链接到lambda$new$0静态方法上去。
   INVOKEDYNAMIC onClick()Landroid/view/View$OnClickListener; [
     // handle kind 0x6 : INVOKESTATIC
     java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
     // arguments:
     (Landroid/view/View;)V, 
     // handle kind 0x6 : INVOKESTATIC
     com/wallstreetcn/sample/adapter/OtherTestViewHolder.lambda$new$0(Landroid/view/View;)V, 
     (Landroid/view/View;)V
   ]
   INVOKEVIRTUAL android/view/View.setOnClickListener (Landroid/view/View$OnClickListener;)V
  L3
   LINENUMBER 22 L3
   RETURN
  L4
   LOCALVARIABLE this Lcom/wallstreetcn/sample/adapter/OtherTestViewHolder; L0 L4 0
   LOCALVARIABLE itemView Landroid/view/View; L0 L4 1
   MAXSTACK = 2
   MAXLOCALS = 2


   
// access flags 0x100A
private static synthetic lambda$new$0(Landroid/view/View;)V
 L0
  LINENUMBER 20 L0
  LDC ""
  LDC ""
  INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
  POP
 L1
  LINENUMBER 21 L1
  ALOAD 0
  ALOAD 0
  LDC "1234"
  INVOKESTATIC com/wallstreetcn/sample/ToastHelper.toast (Ljava/lang/Object;Landroid/view/View;Ljava/lang/Object;)V
  RETURN
 L2
  LOCALVARIABLE v Landroid/view/View; L0 L2 0
  MAXSTACK = 2
  MAXLOCALS = 1

上面是一个非常简单的java中的Lambad,以及其对应的字节码翻译。我们可以看到,其中Lambda的部分被翻译出来的就是INVOKEDYNAMIC,然后将这部分指向了一个静态方法而已,而静态方法中就是我们原始的java代码的那部分。

那么安卓中的lambda最后真的是java中的lambda吗?

这只是一个小展开而已,虽然安卓在后续的版本上支持了java8的语法,但是由于线上分布了大量低版本的设备,所以安卓在实际生成产物的时候,并不是一个java8INVOKEDYNAMIC语法,而是被Desugar脱糖成了一个匿名内部类了。

这样就能同时兼容到线上的所以旧版的安卓os设备,因为并没有新的字节码指令被引入,所以就不需要考虑兼容性问题了。所以相对来说安卓的Lambda比java8的Lambda更像是一个语法糖,因为是由Desugar脱糖器处理成匿名内部类。

那么我们应该如何对Lambda进行字节码操作呢?

上面只是介绍完了释义,下面我们才要动手解决这部分的问题。

这里我先说下我的想法和思路哦。首先我们只要能找到INVOKEDYNAMIC这个指令,然后就可以根据这个指令的后续描述找到其所对应的静态方法名,之后再对这个静态方法进行后续的字节码操作。

如果从ClassVisitor去写这个,我感觉我可能要进行多次代码扫描,或者提早记录很多关键性的信息,才能进行对应的操作了。但是根据我对ClassNode的理解,我感觉我可以在这个的基础上完成我的思路。

代码语言:javascript复制
fun ClassNode.lambdaHelper(): MutableList<MethodNode> {
  // 先从  method.instructions中找到`InvokeDynamicInsnNode`
    val lambdaMethodNodes = mutableListOf<MethodNode>()
    methods?.forEach { method ->
        method.instructions.iterator()?.forEach {
            if (it is InvokeDynamicInsnNode) {
              // 判断是不是我想要修改的类 举例View$OnClickListener
                if (it.name == "onClick" && it.desc.contains(")Landroid/view/View$OnClickListener;")) {
                    Log.info("dynamicName:${it.name} dynamicDesc:${it.desc}")
                    val args = it.bsmArgs
                    args.forEach { arg ->
                      // 根据其中的name和desc等找到其所对应的静态方法,之后加入list中
                        if (arg is Handle) {
                            val methodNode = findMethodByNameAndDesc(arg.name, arg.desc, arg.tag)
                            Log.info("findMethodByNameAndDesc argName:${arg.name}  argDesc:${arg.desc} "  
                                    "method:${method?.name} ")
                            //    if (methodNode?.access == ACC_PRIVATE or ACC_STATIC or ACC_SYNTHETIC) {
                            if (methodNode != null) {
                                lambdaMethodNodes.add(methodNode)
                            }
                            //     }
                        }
                    }
                }

            }
        }
    }
    //然后返回当前类所有要修改的lambda
    lambdaMethodNodes.forEach {
        Log.info("lambdaName:${it.name} lambdaDesc:${it.desc} lambdaAccess:${it.access}")
    }
    return lambdaMethodNodes

}
// 根据名字和描述以及操作类型找到对应的方法
fun ClassNode.findMethodByNameAndDesc(name: String, desc: String, access: Int): MethodNode? {
    return methods?.firstOrNull {
        it.name == name && it.desc == desc
    }
}

NO BB show me the xxx source code。上面的代码我做了简单的代码注释,简单的说这里就是字符串匹配。逻辑也基本和我上面的描述是一样的。对于ClassNode来说,所有的栈帧上的方法调用都会被转化成AbstractInsnNode,而一个INVOKEDYNAMIC则对应的是InvokeDynamicInsnNode实现类,所以方法上只要有lambda调用,就会生成一个InvokeDynamicInsnNode类,其中会包含包括参数以及信息相关的。我就是根据这个类里面获取到的动态链接所指向的函数信息,找到了对应的静态方法。

那么到这里,我们已经成功获取到了Lambda指向的静态方法了,所以后续我们也就又可以为所欲为了啊。

TODO

但是这个由于是一个静态方法,所以当前如果只是插入一些静态方法应该都是没问题的,如果是要生成相对来说更复杂的代码,比如之前写的双击优化就不行了啊。这个我后面在盘一盘想一想。

总结

我觉得我这个人最大的毛病就是有时候会想当然,就比如我最新线上搞出的问题和对于lambda的这个问题一样。因为之前在写的时候是ok的,所以我就习惯性的按照之前的想法来了,质疑了大佬们的回复,我有罪,我错了。

所以程序猿还是要谦逊,毕竟所有的代码都是动态迭代的。很多时候都会颠覆我们之前的认知,对代码保持着谦卑的姿态就是对代码的热爱了。

这次文章只有单点内容所以比较短大家见谅啊。

源代码链接

0 人点赞