前言
这篇文章是紧接着上一篇文章的,原因就是因为有人在评论区留下了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
的语法,但是由于线上分布了大量低版本的设备,所以安卓在实际生成产物的时候,并不是一个java8
的INVOKEDYNAMIC
语法,而是被Desugar
脱糖成了一个匿名内部类了。
这样就能同时兼容到线上的所以旧版的安卓os设备,因为并没有新的字节码指令被引入,所以就不需要考虑兼容性问题了。所以相对来说安卓的Lambda
比java8的Lambda
更像是一个语法糖,因为是由Desugar脱糖器处理成匿名内部类。
那么我们应该如何对Lambda进行字节码操作呢?
上面只是介绍完了释义,下面我们才要动手解决这部分的问题。
这里我先说下我的想法和思路哦。首先我们只要能找到INVOKEDYNAMIC
这个指令,然后就可以根据这个指令的后续描述找到其所对应的静态方法名,之后再对这个静态方法进行后续的字节码操作。
如果从ClassVisitor去写这个,我感觉我可能要进行多次代码扫描,或者提早记录很多关键性的信息,才能进行对应的操作了。但是根据我对ClassNode
的理解,我感觉我可以在这个的基础上完成我的思路。
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的,所以我就习惯性的按照之前的想法来了,质疑了大佬们的回复,我有罪,我错了。
所以程序猿还是要谦逊,毕竟所有的代码都是动态迭代的。很多时候都会颠覆我们之前的认知,对代码保持着谦卑的姿态就是对代码的热爱了。
这次文章只有单点内容所以比较短大家见谅啊。
源代码链接