【我的ASM学习进阶之旅】ASM 实现 Hook Lambda 和方法引用
ASM 实现 Hook Lambda 和方法引用 | 数据采集
PS: 本文转载于:ASM 实现 Hook Lambda 和方法引用 | 数据采集
1. 前言
本文是前作「Lambda 设计参考」的实战部分,具体将介绍如何使用 ASM 对 Java 8 Lambda 表达式和方法引用进行 Hook 操作。在此之前会介绍一些基础概念和字节码相关的知识方便大家对这块内容的理解,最后会给出一个完整的代码供大家参考。
2. 脱糖
2.1. 概念介绍
Java 脱糖(Desugar):简单地说,就是在编译阶段将语法层面一些底层字节码不支持的特性转换为底层支持的结构。例如:可以在 Android 中使用 Java 8 的 Lambda 特性,就是使用了脱糖。使用脱糖的最主要原因是 Android 设备并没有提供 Java 8 的运行时环境。下面用一个例子来展示对 Lambda 脱糖需要做的工作。
class Java8 {interface Logger {void log(String s);}public static void main(String... args) {sayHi(s -> System.out.println(s));}private static void sayHi(Logger logger) {logger.log("Hello!");}
}
首先是将 Lambda 方法体中的内容从 main 方法中移到 Java8 类的内部方法中,改变后的结果如下:
public class Java8 {interface Logger {void log(String s);}public static void main(String... args) {//使用 lambda$main$0 替换原有的逻辑sayHi(s -> lambda$main$0(s));}private static void sayHi(Logger logger) {logger.log("Hello!");}//方法体中的内容移到这里static void lambda$main$0(String str){System.out.println(str);}
}
接着生成一个类,这个类实现了 Logger 接口,实现的方法中调用 lambda$main 0 方 法 , 并 且 使 用 实 现 类 替 换 代 码 ‘ s a y H i ( s − > l a m b d a 0 方法,并且使用实现类替换代码 `sayHi(s -> lambda 0方法,并且使用实现类替换代码‘sayHi(s−>lambdamain$0(s))`,改变后的代码如下:
public class Java8 {interface Logger {void log(String s);}public static void main(String... args) {//这里使用 Logger 的实现类 Java8$1sayHi(s -> new Java8$1());}private static void sayHi(Logger logger) {logger.log("Hello!");}//方法体中的内容移到这里static void lambda$main$0(String str){System.out.println(str);}
}
public class Java8$1 implements Java8.Logger {public Java8$1(){}@Overridepublic void log(String s) {//这里调用 Java8 方法的静态方法Java8.lambda$main$0(s);}
}
最后,因为 Lambda 并没有捕获外部作用的任何变量,所以这是一个无状态 Lambda。实现类会生成一个单例,在使用的地方用这个单例来替换 new Java8$1(),最终的代码如下:
class Java8 {interface Logger {void log(String s);}public static void main(String... args) {//此处使用单例替换原有代码sayHi(Java8$1.INSTANCE);}static void lambda$main$0(String s) {System.out.println(s);}private static void sayHi(Logger logger) {logger.log("Hello!");}
}
public class Java8$1 implements Java8.Logger { static final Java8$1 INSTANCE = new Java8$1();@Overridepublic void log(String s) {Java8.lambda$main$0(s);}
}
这个例子简单地展示了脱糖的过程,其中 lambda$main$0
方法会在编译的时候生成。需要注意的是:方法引用并不会生成额外的方法(关于方法引用和 lambda$main$0` 的生成规则以及上面提到的 无状态 lambdas 等知识可以通过「Lambda 设计参考」获取,读者如果对这部分内容不了解可以先看这篇文章)。
2.2. Android 中的脱糖
上一节介绍了什么是脱糖以及用一个简单的例子来演示 Lambda 表达式的脱糖逻辑,那么我们为什么要关注 Android 中的脱糖呢?
首先 Android 系统本身并不支持 Java 8,前面说了 Android 设备并没有提供 Java 8 的运行时环境。因此,App 项目使用 Java 8 编译产生的字节码是无法在 Android 设备上解析的,Android 使用 Gradle 在编译时会将 .class 文件中的一些 Java 8 语法特性脱糖成 Java 7 中支持的语法特性。我们看下图 2-1 描述的 Android 处理 Java 文件的流程,注意图中的 “Third-party plugins” 是 Android 为我们提供的可以在编译期有机会处理 .class 文件。关于插件开发,可以参考我司出版的《Android 全埋点解决方案》一书。
图 2-1 Android 处理 Java 文件的流程(来源:https://developer.android.com/studio/write/java8-support)
根据图 2-1 所示,自定义的 Android 插件是在 D8/R8 之前先操作 .class 文件。D8 是 Android 提供的脱糖工具,这就导致自定义插件获取的 .class 是原始未脱糖的 .class(注:多个自定义插件执行顺序跟引入顺序有关,我们自定义的插件获取到的 .class 可能是其他插件处理过的 )。现在我们来分析下面这段代码:
View.setOnClickListener(System.out::println)
Android 开发者对这段代码很容易理解。现在我们对这段代码进行处理,希望在执行点击事件的时候,除了执行 println 方法,同时还能够加入一些其他的逻辑,如下面代码的描述:
View.setOnClickListener(view->{System.out.println(view); // 方法引用SensorsDataAutoTrackHelper.trackViewClick(view); //添加的额外逻辑
})
因为这里是一个方法引用,并不会像 Lambda 表达式那样在编译时生成一个 lambd$ 开头的方法(注:关于这块的描述请参考 「Lambda 设计参考」),而且我们也不能在 println 方法中插入代码,本文就是给大家介绍如何处理这种情况。
注意
Android 可以选择在工程中关闭 D8 的脱糖功能(可以通过在 gradle.properties 里配置 android.enableD8.desugaring=false 来关闭),那么 .class 文件的处理流程会变成:.class → desugar → third-party plugins → dex。
3. invokedynamic 指令
在正式介绍如何使用 ASM 处理 Lambda 和方法引用之前,我们首先了解一下字节码指令 invokedynamic。invokedynamic 指令是在 JDK 7 引入的,用来实现动态类型语言功能,简单来说就是能够在运行时去调用实际的代码。在进一步介绍 invokedynamic 指令之前,我们先熟悉几个类:**MethodType、MethodHandle、CallSite 。**在介绍这几个类之前我们先来了解一个方法的构成:
- 方法名;
- 方法签名(参数类型和返回值类型);
- 方法所在的类;
- 方法体(方法中的代码)。
根据上面方法的构成,我们来依次介绍上面的几个类的用法。
3.1. MethodType
MethodType 代表一个方法所需的参数签名和返回值签名,MethodType 类有多个静态方法来构造 MethodType 对象,示例如下:
MethodType methodType = MethodType.methodType(String.class, int.class);
上面这个 MethodType 描述的是返回值为 String 类型,参数是一个 int 类型的方法签名,例如:int foo(String) 这个方法就符合这个描述。
3.2. MethodHandle
MethodHandle 翻译过来就是方法句柄,通过这个句柄可以调用相应的方法,MethodType 描述了方法的参数和返回值,MethodHandle 则是根据类名、方法名并且配合 MethodType 来找到特定方法然后执行它;MethodType 和 MethodHandle 配合起来完整表达了一个方法的构成。例如:我们调用 String.valueOf(int) 方法,可以这么做:
//声明参数和返回值类型
MethodType methodType = MethodType.methodType(String.class, int.class);
MethodHandles.Lookup lookup = MethodHandles.lookup();
//声明一个方法句柄:这里说明的是 String 类里面的 valueOf 方法,方法签名需要符合 methodType
MethodHandle methodHandle = lookup.findStatic(String.class, "valueOf", methodType);
//执行这个方法
String result = (String) methodHandle.invoke(99);
System.out.println(result);
这个跟反射很类似,从这个例子可以看出方法句柄里包含了需要执行的方法信息,只要传入所需的参数就可以执行这个方法了。
3.3. CallSite
CallSite 是方法调用点,调用点中包含了方法句柄信息,通常 invokedynamic 指令所描述的内容会使用 CallSite 来链接,关于这块内容的介绍也可以在 「Lambda 设计参考」找到。可以从调用点上获取 MethodHandle ,代码如下所示:
CallSite callSite = new ConstantCallSite(methodHandle);
MethodHandle mh = callSite.getTarget();
3.4. invokedynamic
前面介绍了一些跟 Lambda 相关的 API,下面正式介绍 invokedynamic,先看下面这段代码对应的字节码:
//源码部分
public class TestMain2 {public void test() {final Date date = new Date();Consumer<String> consumer = (String s) -> System.out.println(s + date.toString());}
}
//对应的部分字节码
Constant pool:#4 = InvokeDynamic #0:#30 // #0:accept:(Ljava/util/Date;)Ljava/util/function/Consumer;#5 = Fieldref #31.#32 // java/lang/System.out:Ljava/io/PrintStream;#6 = Class #33 // java/lang/StringBuilder#7 = Methodref #6.#23 // java/lang/StringBuilder."<init>":()V#8 = Methodref #6.#34 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;#9 = Methodref #2.#35 // java/util/Date.toString:()Ljava/lang/String;#10 = Methodref #6.#35 // java/lang/StringBuilder.toString:()Ljava/lang/String;#11 = Methodref #36.#37 // java/io/PrintStream.println:(Ljava/lang/String;)V#12 = Class #38 // cn/curious/asm/method_ref/TestMain2#19 = Utf8 lambda$test$0#20 = Utf8 (Ljava/util/Date;Ljava/lang/String;)V#21 = Utf8 SourceFile#22 = Utf8 TestMain2.java#23 = NameAndType #14:#15 // "<init>":()V#24 = Utf8 java/util/Date#25 = Utf8 BootstrapMethods#26 = MethodHandle #6:#40 // 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;#27 = MethodType #41 // (Ljava/lang/Object;)V#28 = MethodHandle #6:#42 // invokestatic cn/curious/asm/method_ref/TestMain2.lambda$test$0:(Ljava/util/Date;Ljava/lang/String;)V#29 = MethodType #43 // (Ljava/lang/String;)V#30 = NameAndType #44:#45 // accept:(Ljava/util/Date;)Ljava/util/function/Consumer;#31 = Class #46 // java/lang/System#32 = NameAndType #47:#48 // out:Ljava/io/PrintStream;#33 = Utf8 java/lang/StringBuilder#34 = NameAndType #49:#50 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;#35 = NameAndType #51:#52 // toString:()Ljava/lang/String;#36 = Class #53 // java/io/PrintStream#37 = NameAndType #54:#43 // println:(Ljava/lang/String;)V#38 = Utf8 cn/curious/asm/method_ref/TestMain2#39 = Utf8 java/lang/Object#40 = Methodref #55.#56 // 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;#41 = Utf8 (Ljava/lang/Object;)V#42 = Methodref #12.#57 // cn/curious/asm/method_ref/TestMain2.lambda$test$0:(Ljava/util/Date;Ljava/lang/String;)V#43 = Utf8 (Ljava/lang/String;)V#44 = Utf8 accept#45 = Utf8 (Ljava/util/Date;)Ljava/util/function/Consumer;=
{public void test();descriptor: ()Vflags: ACC_PUBLICCode:stack=2, locals=3, args_size=10: new #2 // class java/util/Date3: dup4: invokespecial #3 // Method java/util/Date."<init>":()V7: astore_18: aload_19: invokedynamic #4, 0 // InvokeDynamic #0:accept:(Ljava/util/Date;)Ljava/util/function/Consumer;14: astore_215: returnprivate static void lambda$test$0(java.util.Date, java.lang.String);descriptor: (Ljava/util/Date;Ljava/lang/String;)Vflags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETICCode:stack=3, locals=2, args_size=20: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;3: new #6 // class java/lang/StringBuilder6: dup7: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V10: aload_111: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;14: aload_015: invokevirtual #9 // Method java/util/Date.toString:()Ljava/lang/String;18: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;21: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;24: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V27: return
}
InnerClasses:public static final #61= #60 of #64; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:0: #26 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;Method arguments:#27 (Ljava/lang/Object;)V#28 invokestatic cn/curious/asm/method_ref/TestMain2.lambda$test$0:(Ljava/util/Date;Ljava/lang/String;)V#29 (Ljava/lang/String;)V
上面是部分主要的字节码信息,可以看下关键代码:
- 首先可以看第 58 行的 invokedynamic 指令:
invokedynamic #4, 0 // invokeDynamic #0:accept:(Ljava/util/Date;)Ljava/util/function/Consumer
,其中,0 是预留字段,#4 表示的是常量池中的字段; - 第 11 行
#4 = InvokeDynamic #0:#30 // #0:accept:(Ljava/util/Date;)Ljava/util/function/Consumer
,这里的 #0 表示的是第一个引导方法,假如有多个 Lambda 可能会有多个引导方法。所谓的引导方法指的是在执行invokedynamic
指令时,该指令所指向的、需要去执行的 Java 方法,通常在执行引导方法的时候会生成一些额外的类,例如前面介绍脱糖的时候 Java8.Logger 的实现类Java8$1
,这个类会在第一次执行引导方法的时候生成,大家有兴趣可以看一下引导方法的源码; - 第 83 行,这可以看到这个引导方法是
LambdaMetafacotry.metafactory
,此方法的定义如下:
public static CallSite metafactory(MethodHandles.Lookup caller,String invokedName,MethodType invokedType,MethodType samMethodType,MethodHandle implMethod,MethodType instantiatedMethodType)
这个方法会返回一个 Callsite 调用点,调用点中包括了方法句柄信息,我们现在来详细解释下这个方法的参数,其中前三个参数不需要关注,系统会自动生成,主要是看后面三个参数:
- samMethodType: 函数式接口中抽象方法的签名描述信息,关于 MethodType 前面的章节有介绍,这里的方法签名是 Consumer#apply 的签名,因为泛型参数,泛型 T 统一被转换成 Object(注:这里的 sam 指的是 Single Abstract Method,大家可以理解为函数式接口);
- implMethod: 是一个方法句柄,这个在前面也介绍了,方法句柄包含了具体需要执行的方法,从上面的字节码可以看到,这个方法句柄的内容是:
#23 invokestatic cn/curious/asm/method\_ref/TestMain2.lambda$main$0:(Ljava/lang/String;)V
,意思是调用静态方法 lambda$main$0,在前面有介绍 Lambda 脱糖的时候我们知道,Lambda 会生成一个方法,此方法默认是隐藏的,如果想查看,可以使用 java 的javap -p -v xxx.class
命令查看这个方法; - instantiatedMethodType: 是 samMethodType 的具体实现,源码传入的泛型类型是 String,所以这里就是 String。
接下来再看 invokedynamic 指令执行的前后代码:
public void test();descriptor: ()Vflags: ACC_PUBLICCode:stack=2, locals=3, args_size=10: new #2 // class java/util/Date3: dup4: invokespecial #3 // Method java/util/Date."<init>":()V7: astore_18: aload_19: invokedynamic #4, 0 // InvokeDynamic #0:accept:(Ljava/util/Date;)Ljava/util/function/Consumer;14: astore_215: return
从上面的指令可以看到,在执行 invokedynamic 指令的时候将创建的 Date 对象加载到栈顶,invokedynamic 指令对应的 accept:(Ljava/util/Date;)Ljava/util/function/Consumer;
中的 Date 就是动态参数,这个参数会添加在编译时生成的方法 lambda$test$0(java.util.Date, java.lang.String)
参数列表的前面。脱糖的具体规则在「Lambda 设计参考」中的 _Lambda 方法体脱糖_章节有详细的介绍。
4. 使用 ASM 实现
综合前三节的知识,我们知道方法句柄中包含了方法调用的信息,而且我们也说明了方法引用并不会生成一个 lambda$ 开头的中间方法,同时我们知道 MethodHandle 包含了方法调用的信息。因此,如果要去 Hook Lambda 和方法引用,我们可以创建一个新的 MethodHandle 替换原有的。具体做法是:我们会生成一个新的方法,新的方法中会实现 invokedynamic 指令中描述的代码逻辑。然后创建新的 MethodHandle,将这个 MethodHandle 替换原本的 MethodHandle。
现在整体的思路和方案已经有了,接下来就是使用 ASM 编写代码来实现,具体的实现如下:
public class MethodReferenceAdapter extends ClassNode {private final AtomicInteger counter = new AtomicInteger(0);private List<MethodNode> syntheticMethodList = new ArrayList<>();public MethodReferenceAdapter(ClassVisitor classVisitor) {super(Opcodes.ASM7);this.cv = classVisitor;}@Overridepublic void visitEnd() {super.visitEnd();this.methods.forEach(methodNode -> {ListIterator<AbstractInsnNode> iterator = methodNode.instructions.iterator();while (iterator.hasNext()) {AbstractInsnNode node = iterator.next();if (node instanceof InvokeDynamicInsnNode) {InvokeDynamicInsnNode tmpNode = (InvokeDynamicInsnNode) node;//形如:(Ljava/util/Date;)Ljava/util/function/Consumer; 可以从 desc 中获取函数式接口,以及动态参数的内容。//如果没有参数那么描述符的参数部分应该是空。String desc = tmpNode.desc;Type descType = Type.getType(desc);Type samBaseType = descType.getReturnType();//sam 接口名String samBase = samBaseType.getDescriptor();//sam 方法名String samMethodName = tmpNode.name;Object[] bsmArgs = tmpNode.bsmArgs;//sam 方法描述符Type samMethodType = (Type) bsmArgs[0];//sam 实现方法实际参数描述符Type implMethodType = (Type) bsmArgs[2];//sam name + desc,可以用来辨别是否是需要 Hook 的 lambda 表达式String bsmMethodNameAndDescriptor = samMethodName + samMethodType.getDescriptor();//中间方法的名称String middleMethodName = "lambda$" + samMethodName + "$sa" + counter.incrementAndGet();//中间方法的描述符String middleMethodDesc = "";Type[] descArgTypes = descType.getArgumentTypes();if (descArgTypes.length == 0) {middleMethodDesc = implMethodType.getDescriptor();} else {middleMethodDesc = "(";for (Type tmpType : descArgTypes) {middleMethodDesc += tmpType.getDescriptor();}middleMethodDesc += implMethodType.getDescriptor().replace("(", "");}//INDY 原本的 handle,需要将此 handle 替换成新的 handleHandle oldHandle = (Handle) bsmArgs[1];Handle newHandle = new Handle(Opcodes.H_INVOKESTATIC, this.name, middleMethodName, middleMethodDesc, false);InvokeDynamicInsnNode newDynamicNode = new InvokeDynamicInsnNode(tmpNode.name, tmpNode.desc, tmpNode.bsm, samMethodType, newHandle, implMethodType);iterator.remove();iterator.add(newDynamicNode);generateMiddleMethod(oldHandle, middleMethodName, middleMethodDesc);}}});this.methods.addAll(syntheticMethodList);accept(cv);}private void generateMiddleMethod(Handle oldHandle, String middleMethodName, String middleMethodDesc) {//开始对生成的方法中插入或者调用相应的代码MethodNode methodNode = new MethodNode(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC /*| Opcodes.ACC_SYNTHETIC*/,middleMethodName, middleMethodDesc, null, null);methodNode.visitCode();// 此块 tag 具体可以参考: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.invokedynamicint accResult = oldHandle.getTag();switch (accResult) {case Opcodes.H_INVOKEINTERFACE:accResult = Opcodes.INVOKEINTERFACE;break;case Opcodes.H_INVOKESPECIAL://private, this, super 等会调用accResult = Opcodes.INVOKESPECIAL;break;case Opcodes.H_NEWINVOKESPECIAL://constructorsaccResult = Opcodes.INVOKESPECIAL;methodNode.visitTypeInsn(Opcodes.NEW, oldHandle.getOwner());methodNode.visitInsn(Opcodes.DUP);break;case Opcodes.H_INVOKESTATIC:accResult = Opcodes.INVOKESTATIC;break;case Opcodes.H_INVOKEVIRTUAL:accResult = Opcodes.INVOKEVIRTUAL;break;}Type middleMethodType = Type.getType(middleMethodDesc);Type[] argumentsType = middleMethodType.getArgumentTypes();if (argumentsType.length > 0) {int loadIndex = 0;for (Type tmpType : argumentsType) {int opcode = tmpType.getOpcode(Opcodes.ILOAD);methodNode.visitVarInsn(opcode, loadIndex);loadIndex += tmpType.getSize();}}methodNode.visitMethodInsn(accResult, oldHandle.getOwner(), oldHandle.getName(), oldHandle.getDesc(), false);Type returnType = middleMethodType.getReturnType();int returnOpcodes = returnType.getOpcode(Opcodes.IRETURN);methodNode.visitInsn(returnOpcodes);methodNode.visitEnd();syntheticMethodList.add(methodNode);}
}
我们对前面介绍的示例中的 .class 文件使用 ASM 运行后输出的结果如下:
public class TestMain2 {public TestMain2() {}public void test() {Date var1 = new Date();Consumer var2 = TestMain2::lambda$accept$sa1;}private static void lambda$accept$sa1(Date var0, String var1) {//TODO 可以在此插桩lambda$test$0(var0, var1);}
}
其中,lambda$accept$sa1
是我们使用 ASM 生成的方法。在这个方法中,我们替换了原本的 lambda$test$0
(此方法的 tag 是 acc_synthetic
,表示代码是自动生成的,反编译默认不显示)方法,在我们生成的方法中调用编译器生成的 lambda$test$0
方法。这里需要再提醒一下,方法引用并不会生成类似 lambda$test$0
这样的方法,我们需要将方法引用的代码放在我们生成的方法中,这个读者可以写一个方法引用测试一下结果。
至此,如果我们想要对 Lambda 或者方法引用的代码进行插桩,只要在我们生成的方法中插入即可。
5. 总结
整体的原理是:我们自己生成一个中间方法,如果是 Lambda ,那么我们在方法中调用这个 Lambda 编译时生成的中间方法;如果是方法引用,就把方法引用里的内容放到我们生成的中间方法中,然后将自定义的 MethodHandle 指向生成的方法;最后替换掉 Bootstrap Method 中的 MethodHandle,达到偷梁换柱的效果。
不过,这种方式的弊端是会多生成一些中间方法。
至此,用了两篇文章介绍了 ASM Hook Lambda 和方法引用的知识,希望对大家有所帮助。
6. 参考资料
- D8 & R8: https://developer.android.com/studio/releases#r8-default
- Android’s Java 8 Support: https://jakewharton.com/androids-java-8-support/
- JVM invokeydynamic instruction: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.invokedynamic
- https://www.jianshu.com/p/d74e92f93752
- https://developer.android.com/studio/write/java8-support
- https://www.infoq.com/news/2018/04/android-studio-d8-compiler/
7. 本文作者
神策数据 | Android 研发工程师
我是张伟,神策数据 Android 研发工程师,主要从事神策 Android SDK 和 Android Plugin 的开发工作,希望通过开源社区这个平台与大家共同学习进步。生活中热爱篮球、看书、旅游,希望我们相聚神策,一起维护神策开源社区和打篮球。
8、交流合作
本文著作权归神策数据开源社区所有。商业转载请联系我们获得授权;非商业转载请注明出处,并附上神策数据开源社区公众号二维码。
你还可以扫描二维码,加入社区交流群,与大家一同讨论。
也欢迎关注我们的公众号,博客更新尽在掌握。
【我的ASM学习进阶之旅】ASM 实现 Hook Lambda 和方法引用相关推荐
- 【我的ASM学习进阶之旅】 介绍一个基于gradle transform api和ASM的字节码插件平台ByteX
原文链接: https://github.com/bytedance/ByteX/blob/master/README_zh.md 文章目录 ByteX(Infinite Possibilities) ...
- 【我的OpenGL学习进阶之旅】介绍一下 绘制图元
目录 一.绘制图元 1.1 `glDrawArrays` 1.1.1 `glDrawArrays`API说明 1.1.2 `glDrawArrays`API示例 1.2 `glDrawElements ...
- 【我的OpenGL学习进阶之旅】C++如何加载TGA文件?
一.TGA文件相关介绍 通过前面的博客 [我的OpenGL学习进阶之旅]什么是TGA文件以及如何打开TGA文件? 地址:https://ouyangpeng.blog.csdn.net/article ...
- 【我的OpenGL学习进阶之旅】【持续更新】关于学习OpenGL的一些资料
目录 一.相关书籍 OpenGL 方面 C方面 NDK 线性代数 二.相关博客 2.0 一些比较官方的链接 2.1 OpenGL着色器语言相关 2.2 [[yfan]](https://segment ...
- 【我的OpenGL学习进阶之旅】OpenGL ES 3.0新功能
目录 1.1 纹理 1.2 着色器 1.3 几何形状 1.4 缓冲区对象 1.5 帧缓冲区 OpenGL ES 2.0 开创了手持设备可编程着色器的时代,在驱动大量设备的游戏.应用程序和用户接口中获得 ...
- 【我的OpenGL学习进阶之旅】着色器和程序(上)------着色器
着色器和程序 一.前言 二.着色器和程序 2.1 创建和编译一个着色器 2.1.1 创建着色器 2.1.2 删除着色器 2.1.3 提供着色器源代码 2.1.4 编译色器 2.1.4 查询有关着色器对 ...
- 【我的C语言学习进阶之旅】介绍一下NDK开发中关于JNI函数的两种注册方式:静态注册和动态注册
目录 一.要介绍本篇博客的原因 二.静态注册 2.1 实现原理 2.2 实现过程 2.3 弊端 2.4 示例 三.动态注册 3.1 实现原理 3.2 实现过程 3.3 优点 3.4 示例 一.要介绍本 ...
- 【我的C/C++语言学习进阶之旅】NDK开发之解决错误:signal 5 (SIGTRAP), code 1 (TRAP_BRKPT), fault addr 0xXXX
一.错误描述 今天在使用C++实现一个OpenGL特效的时候,运行出错,如下所示: 错误描述为: signal 5 (SIGTRAP), code 1 (TRAP_BRKPT), fault addr ...
- 【我的OpenGL学习进阶之旅】关于3D模型知识之:什么是obj文件和mtl文件
文章目录 一.学习3D模型的背景 二.3D模型效果展示 三.好奇3D模型文件是啥内容? 3.1 打开.obj文件 3.2 打开.obj文件 3.3 在外部使用查看3D模型的软件打开.obj文件 3.3 ...
最新文章
- kdj超卖_三分钟学会KDJ三大买卖绝技,简单高效,把握最佳买卖点,不懂KDJ的股民值得一看!...
- Python编程语言简介
- java中try-catch另外一种用法:try(){}语法使用
- 算法竞赛入门经典(第二版) | 例题5-1 大理石在哪 (普适查找)(UVa10474,Where is the Marble?)
- leetcode283.移动零
- Go Web编程--应用ORM
- 域服务器内置用户组说明
- C++类中在构造器中调用本类的另外构造器
- java中this什么时候不能省略?this()的用法
- [Java反序列化]AspectJWeaver反序列化
- 案例分析 - OOM的内存分析
- mysql改列名_mysql怎么修改列名
- 那些便宜的vps,你敢用吗?企业该如何选择云服务器?
- 3大类6种排序 插入排序 选择排序 冒泡排序 希尔排序 堆排序 快速排序 —————— 开开开山怪
- Revit二次开发 获取模型截面线、截面轮廓
- Java 日期的各种操作
- redis关于set的常用命令
- 基于C语言的网络编程的项目
- matlab中洛伦兹拟合,基于MATLAB洛伦兹线型非线性拟合算法实现
- mysql查看主机名_mysql怎么看主机名
热门文章
- 计算机系统五大功能,操作系统的五大功能是什么
- mysql 变量 加加_MySQL命令
- 虚拟现实在招聘中的应用
- SVN操作 -- TortoiseSVN中的Excel文件比较
- 前端笔试题2019-玄武科技
- OAI 完全搭建手册
- C++ typeid详解
- Hibernate org.hibernate.hql.internal.ast.QuerySyntaxException: tb_supplier is not mapp
- 很落地的chatgpt应用场景——批量制作抖音爆款带货文案并直接SEO霸屏1 小时前
- javaEE Design Patter的几种模式详解