前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Lambda引发的惨案 | Desugar顺序变更

Lambda引发的惨案 | Desugar顺序变更

作者头像
逮虾户
发布2021-01-20 10:00:41
1.2K0
发布2021-01-20 10:00:41
举报
文章被收录于专栏:逮虾户逮虾户

前言

这篇文章是紧接着上一篇文章的,原因就是因为有人在评论区留下了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 (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 {
  // 先从  method.instructions中找到`InvokeDynamicInsnNode`
    val lambdaMethodNodes = mutableListOf()
    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的,所以我就习惯性的按照之前的想法来了,质疑了大佬们的回复,我有罪,我错了。

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

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

源代码链接

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 什么是Lambada
    • 那么安卓中的lambda最后真的是java中的lambda吗?
    • 那么我们应该如何对Lambda进行字节码操作呢?
      • TODO
      • 总结
      相关产品与服务
      腾讯云代码分析
      腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档