深入探索编译插桩技术(四、ASM 探秘,2024年最新kotlin匿名内部类

| String | superName | 父类类名,采用全地址 |
| List | interfaces | 实现的接口,采用全地址 |
| String | sourceFile | 源文件,可能为null |
| String | sourceDebug | debug源,可能为null |
| String | outerClass | 外部类 |
| String | outerMethod | 外部方法 |
| String | outerMethodDesc | 外部方法描述(包括方法参数和返回值) |
| List | visibleAnnotations | 可见的注解 |
| List | invisibleAnnotations | 不可见的注解 |
| List | attrs | 类的Attribute |
| List | innerClasses | 类的内部类列表 |
| List | fields | 类的字段列表 |
| List | methods | 类的方法列表 |

2)、获取指定字段的节点

获取一个字段节点的代码如下所示:

for(FieldNode fieldNode : (List)classNode.fields) {
// 1
if(fieldNode.name.equals(“password”)) {
// 2
fieldNode.access = Opcodes.ACC_PUBLIC;
}
}

字段节点列表 fields 是一个 ArrayList,它储存着类节点的所有字段。在注释1处,我们通过遍历 fields 集合的方式来找到目标字段节点。接着,在注释2处,我们将目标字段节点的访问权限置为 public。

除此之外,我们还可以为类添加需要的字段,代码如下所示:

FieldNode fieldNode = new FieldNode(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, “JsonChao”, “I”, null, null);
classNode.fields.add(fieldNode);

在上述代码中,我们直接给目标类节点添加了一个 “public static int JsonChao” 的字段,需要注意的是,第三个参数的 “I” 表示的是 int 的类型描述符。

那么,对于一个字段节点,又包含有哪些字段信息呢?

如下所示:

字段信息
类型名称说明
intaccess访问级
Stringname字段名
Stringsignature签名,通常是 null
Stringdesc类型描述,例如 Ljava/lang/String、D(double)、F(float)
Objectvalue初始值,通常为 null
ListvisibleAnnotations可见的注解
ListinvisibleAnnotations不可见的注解
Listattrs字段的 Attribute

接下来,我们看看如何获取一个方法节点。

3)、获取指定的方法节点

获取指定的方法节点的代码如下所示:

for(MethodNode methodNode : (List)classNode.methods) {
// 1、判断方法名是否匹配目标方法
if(methodNode.name.equals(“getName”)) {
// 2、进行操作
}
}

methods 同 fields 一样,也是一个 ArrayList,通过遍历并判断方法名的方式即可匹配到目标方法。

对于一个方法节点来说,它包含有如下信息:

方法节点包含的信息
类型名称说明
intaccess访问级
Stringname方法名
Stringdesc方法描述,其包含方法的返回值和参数
Stringsignature签名,通常是null
Listexceptions可能返回的异常列表
ListvisibleAnnotations可见的注解列表
ListinvisibleAnnotations不可见的注解列表
Listattrs方法的Attribute列表
ObjectannotationDefault默认的注解
List[]visibleParameterAnnotations可见的参数注解列表
List[]invisibleParameterAnnotations不可见的参数注解列表
InsnListinstructions操作码列表
ListtryCatchBlockstry-catch块列表
intmaxStack最大操作栈的深度
intmaxLocals最大局部变量区的大小
ListlocalVariables本地(局部)变量节点列表

4、操控操作码

在操控字节码之前,我们必须先了解下 instructions,即 操作码列表,它是 方法节点中用于存储操作码的地方,其中 每一个元素都代表一行操作码

ASM 将一行字节码封装为一个 xxxInsnNode(Insn 表示的是 Instruction 的缩写,即指令/操作码),例如 ALOAD/ARestore 指令被封装入变量操作码节点 VarInsnNode,INVOKEVIRTUAL 指令则会被封入方法操作码节点 MethodInsnNode 之中

对于所有的指令节点 xxxInsnNode 来说,它们都继承自抽象操作码节点 AbstractInsnNode。其所有的派生类使用详情如下所示。

所有的指令码节点说明

名称说明参数
FieldInsnNode用于 GETFIELD 和 PUTFIELD 之类的字段操作的字节码String owner 字段所在的类
String name 字段的名称
String desc 字段的类型
FrameNode栈映射帧的对应的帧节点待补充
IincInsnNode用于 IINC 变量自加操作的字节码int var:目标局部变量的位置
int incr: 要增加的数
InsnNode一切无参数值操作的字节码,例如 ALOAD_0,DUP(注意不包含 POP)
IntInsnNode用于 BIPUSH、SIPUSH 和 NEWARRAY 这三个直接操作整数的操作int operand:操作的整数值
InvokeDynamicInsnNode用于 Java7 新增的 INVOKEDYNAMIC 操作的字节码String name:方法名称
String desc:方法描述
Handle bsm:句柄
Object[] bsmArgs:参数常量
JumpInsnNode用于 IFEQ 或 GOTO 等跳转操作字节码LabelNode lable:目标 lable
LabelNode一个用于表示跳转点的 Label 节点
LdcInsnNode使用 LDC 加载常量池中的引用值并进行插入的字节码Object cst:引用值
LineNumberNode表示行号的节点int line:行号
LabelNode start:对应的第一个 Label
LookupSwitchInsnNode用于实现 LOOKUPSWITCH 操作的字节码LabelNode dflt:default 块对应的 Lable
List keys 键列表
List labels:对应的 Label 节点列表
MethodInsnNode用于 INVOKEVIRTUAL 等传统方法调用操作的字节码,不适用于 Java7 新增的 INVOKEDYNAMICString owner :方法所在的类
String name :方法名称
String desc:方法描述
MultiANewArrayInsnNode用于 MULTIANEWARRAY 操作的字节码String desc:类型描述
int dims:维数
TableSwitchInsnNode用于实现 TABLESWITCH 操作的字节码int min:键的最小值
int max:键的最大值
LabelNode dflt:default 块对应的 Lable
List labels:对应的 Label 节点列表
TypeInsnNode用于实现 NEW、ANEWARRAY 和 CHECKCAST 等类型相关操作的字节码String desc:类型
VarInsnNode用于实现 ALOAD、ASTORE 等局部变量操作的字节码int var:局部变量

下面,我们就开始来讲解下字节码操控有哪几种常见的方式。

1、获取操作码的位置

获取指定操作码位置的代码如下所示:

for(AbstractInsnNode ainNode : methodNode.instructions.toArray()) {
if(ainNode.getOpcode() == Opcodes.SIPUSH && ((IntInsnNode)ainNode).operand == 16) {
…//进行操作
}
}

由于一般情况下我们都无法确定操作码在列表中的具体位置,因此 通常会通过遍历的方式去判断其关键特征,以此来定位指定的操作码,上述代码就能定位到一个 SIPUSH 16 的字节码,需要注意的是,有时一个方法中会有多个相同的指令,这是我们需要靠判断前后字节码识别其特征来定位,也可以记下其命中次数然后设定在某一次进行操作,一般情况下我们都是使用的第二种

2、替换指定的操作码

替换指定的操作码的代码如下所示:

for(AbstractInsnNode ainNode : methodNode.instructions.toArray()) {
if(ainNode.getOpcode() == Opcodes.BIPUSH && ((IntInsnNode)ainNode).operand == 16) {
methodNode.instructions.set(ainNode, new VarInsnNode(Opcodes.ALOAD, 1));
}
}

这里我们 直接调用了 InsnList 的 set 方法就能替换指定的操作码对象,我们在获取了 “BIPUSH 64” 字节码的位置后,便将封装它的操作码替换为一个新的 VarInsnNode 操作码,这个新操作码封装了 “ALOAD 1” 字节码, 将原程序中 将值设为16 替换为 将值设为局部变量1

3、删除指定的操作码

methodNode.instructions.remove(xxx);

xxx 表示的是要删除的操作码实例,我们直接调用用 InsnList 的 remove 方法将它移除掉即可。

4、插入指定的操作码

InsnList 主要提供了 四类 方法用于插入字节码,如下所示:

  • 1)、add(AbstractInsnNode insn)将一个操作码添加到 InsnList 的末尾
  • 2)、insert(AbstractInsnNode insn)将一个操作码插入到这个 InsnList 的开头
  • 3)、insert(AbstractInsnNode insnNode,AbstractInsnNode insn)将一个操作码插入到另一个操作码的下面
  • 4)、insertBefore(AbstractInsnNode insnNode,AbstractInsnNode insn) 将一个操作码插入到另一个操作码的上面

接下来看看如何使用这些方法插入指定的操作码,代码如下所示:

for(AbstractInsnNode ainNode : methodNode.instructions.toArray()) {
if(ainNode.getOpcode() == Opcodes.BIPUSH && ((IntInsnNode)ainNode).operand == 16) {
methodNode.instructions.insert(ainNode, new MethodInsnNode(Opcodes.INVOKEVIRTUAL, “java/awt/image/BufferedImage”, “getWidth”, “(Ljava/awt/image/ImageObserver;)I”));
methodNode.instructions.insert(ainNode, new InsnNode(Opcodes.ACONSTNULL));
methodNode.instructions.set(ainNode, new VarInsnNode(Opcodes.ALOAD, 1));
}
}

这样,我们就能将

BIPUSH 16

替换为

ALOAD 1
ACONSTNULL
INVOKEVIRTUAL java/awt/image/BufferedImage.getWidth(Ljava/awt/image/ImageObserver;)I

当我们操控完指定的类节点之后,就可以使用 ASM 的 ClassWriter 类来输出字节码,代码如下所示:

// 1、让 ClassWriter 自行计算最大栈深度和栈映射帧等信息
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTEFRAMES);
classNode.accept(classWriter);
return classWriter.toByteArray();

关于 ClassWriter 的具体用法,我们会在 ASM Core API 这部分来进行逐步讲解。下面👇,我们就先来看看 ASM 的事件模型。

三、ASM 的事件模型(ASM Core API)

对象模型是由事件模型封装而成,因此事件模型的上手难度会更大一些。

对于事件模型来说,它 采用了设计模式中的访问者模式。它的出现是为了更好地解决这样一种需求:有 A 个元素和 N 种算法,每个算法都能作用于任意一个元素,并且在不同的元素上有不同的运行方式

在访问者模式出现之前,我们通常会在每一个元素对应的类中添加 N 个方法,然后在每一个方法中去实现一个算法,但是,这样的做法容易导致代码耦合性过高,并且可维护性差。

因此,访问者模式应运而生,我们可以 建立 N 个访问者,并且每一个访问者拥有一个算法及其内部的 A 种运行方式。当我们需要调用一个算法时,就让相应的访问者去访问元素,然后让访问者根据被访问对象选择相应的算法

需要注意的是,访问者并没有直接去操作元素,而是先让元素类调用 accept 方法接收访问者,然后,访问者在元素类的内部方法中开始调用 visit 方法访问当前的元素类。这样,访问者便能直接访问元素类中的内部私有成员,其优势在于 避免了暴露不必要的内部细节

要理解 ASM 的事件模型,我们就需要对其中的 两个重要成员的工作原理 有较深的了解。它们便是 类访问者 ClassVisitor 与 类读取(解析)者 ClassReader

从字节码的视角中,一个 Java 类由很多组件凝聚而成,而这之中便包括超类、接口、属性、域和方法等等。当我们在使用 ASM 进行操控时,可以将它们视为一个个与之对应的事件。因此 ASM 提供了一个 类访问者 ClassVisitor,以通过它来访问当前类的各个组件,当解析器 ClassReader 依次遇到上述的各个组件时,ClassVisitor 上对应的 visitor 事件处理器方法均会被一一调用

与类相似,方法也是由多个组件凝聚而成的,其对应着方法属性、注解及编译后的代码(Class 字节码)。ASM 的 MethodVisitor 提供了一种 hook(钩子)机制,以便能够访问方法中的每一个操作码,这样我们便能够对字节码文件进行细粒度地修改

下面,我们便来一一分析下它们。

1、类访问者 ClassVisitor

通常我们在使用 ASM 的访问者模式有一个模板代码,如下所示:

InputStream is = new FileInputStream(classFile);
// 1
ClassReader classReader = new ClassReader(is);
// 2
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
// 3
ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
// 4
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);

首先,在注释1处,我们 将目标文件转换为流的形式,并将它融入类读取器 ClassReader 之中。然后,在注释2处,我们 构建了一个类写入器 ClassWriter,其参数 COMPUTE_MAXS 的作用是将自动计算本地变量表最大值和操作数栈最大值的任务托付给了ASM。接着,在注释3处,新建了一个自定义的类访问器,这个自定义的 ClassVisitor 的作用是为了在每一个方法的开始和结尾处插入相应的记时代码,以便统计出每一个方法的耗时。最后,在注释4处,类读取器 ClassReader 实例这个被访问者调用了自身的 accept 方法接收了一个 classVisitor 实例,需要注意的是,第二个参数指定了 EXPAND_FRAMES,旨在说明在读取 class 的时候需要同时展开栈映射帧(StackMap Frame),如果我们需要使用自定义的 MethodVisitor 去修改方法中的指令时必须要指定这个参数,。

上面,我们说到了栈映射帧(StackMap Frame),它到底是什么呢?

栈映射帧 StackMap Frame

它是 Java 6 以后引入的一种验证机制,用于 检验 Java 字节码的正确性。它的工作方式是 记录每一个关键步骤完成后其方法中操作数栈的理论状态,然后,在实际运行的时候,ASM 会将其实际状态和理论状态对比,如果状态不一致则表明出现了错误

但栈映射帧的实现并不简单,因此通过调用 classReader 实例的 accept 方法我们便可以让 ASM 自动去计算栈映射帧,尽管这 会增加 50% 的额外运算。此外,可能会有小概率的情况遇到 栈映射帧验证失败 的情况,例如:VerifyError: Inconsistent stackmap frames at branch target 这个错误。

最常见的原因可能就是由于 字节码写错造成的,此时,我们应该去检查对应的字节码实现代码。此外,也可能是 JDK 版本的支持问题或是 ASM 自身的缺陷,但是,这种情况几乎不会发生。

2、类读取(解析)者 ClassVisitor

现在,让我们再回到上述注释4处的代码,在这里,我们调用了 classReader 的 accept 方法接收了一个访问者 classVisitor,下面,我们来看看其内部的实现,代码如下所示(源码实现较长,这里我们只需关注注释处的代码即可:

/**

  • Makes the given visitor visit the Java class of this {@link ClassReader}
  • . This class is the one specified in the constructor (see
  • {@link #ClassReader(byte[]) ClassReader}).
  • @param classVisitor
  •        the visitor that must visit this class.
    
  • @param flags
  •        option flags that can be used to modify the default behavior
    
  •        of this class. See {@link #SKIP_DEBUG}, {@link #EXPAND_FRAMES}
    
  •        , {@link #SKIP_FRAMES}, {@link #SKIP_CODE}.
    

*/
public void accept(final ClassVisitor classVisitor, final int flags) {
accept(classVisitor, new Attribute[0], flags);
}

在 accept 方法中又继续调用了 classReader 的另一个 accept 重载方法,如下所示:

public void accept(final ClassVisitor classVisitor,
final Attribute[] attrs, final int flags) {
int u = header; // current offset in the class file
char[] c = new char[maxStringLength]; // buffer used to read strings

Context context = new Context();
context.attrs = attrs;
context.flags = flags;
context.buffer = c;

// 1、读取类的描述信息,例如 access、name 等等
int access = readUnsignedShort(u);
String name = readClass(u + 2, c);
String superClass = readClass(u + 4, c);
String[] interfaces = new String[readUnsignedShort(u + 6)];
u += 8;
for (int i = 0; i < interfaces.length; ++i) {
interfaces[i] = readClass(u, c);
u += 2;
}

// 2、读取类的属性信息,例如签名 signature、sourceFile 等等。
String signature = null;
String sourceFile = null;
String sourceDebug = null;
String enclosingOwner = null;
String enclosingName = null;
String enclosingDesc = null;
int anns = 0;
int ianns = 0;
int tanns = 0;
int itanns = 0;
int innerClasses = 0;
Attribute attributes = null;

u = getAttributes();
for (int i = readUnsignedShort(u); i > 0; --i) {
String attrName = readUTF8(u + 2, c);
// tests are sorted in decreasing frequency order
// (based on frequencies observed on typical classes)
if (“SourceFile”.equals(attrName)) {
sourceFile = readUTF8(u + 8, c);
} else if (“InnerClasses”.equals(attrName)) {
innerClasses = u + 8;
} else if (“EnclosingMethod”.equals(attrName)) {
enclosingOwner = readClass(u + 8, c);
int item = readUnsignedShort(u + 10);
if (item != 0) {
enclosingName = readUTF8(items[item], c);
enclosingDesc = readUTF8(items[item] + 2, c);
}
} else if (SIGNATURES && “Signature”.equals(attrName)) {
signature = readUTF8(u + 8, c);
} else if (ANNOTATIONS
&& “RuntimeVisibleAnnotations”.equals(attrName)) {
anns = u + 8;
} else if (ANNOTATIONS
&& “RuntimeVisibleTypeAnnotations”.equals(attrName)) {
tanns = u + 8;
} else if (“Deprecated”.equals(attrName)) {
access |= Opcodes.ACC_DEPRECATED;
} else if (“Synthetic”.equals(attrName)) {
access |= Opcodes.ACC_SYNTHETIC
| ClassWriter.ACC_SYNTHETIC_ATTRIBUTE;
} else if (“SourceDebugExtension”.equals(attrName)) {
int len = readInt(u + 4);
sourceDebug = readUTF(u + 8, len, new char[len]);
} else if (ANNOTATIONS
&& “RuntimeInvisibleAnnotations”.equals(attrName)) {
ianns = u + 8;
} else if (ANNOTATIONS
&& “RuntimeInvisibleTypeAnnotations”.equals(attrName)) {
itanns = u + 8;
} else if (“BootstrapMethods”.equals(attrName)) {
int[] bootstrapMethods = new int[readUnsignedShort(u + 8)];
for (int j = 0, v = u + 10; j < bootstrapMethods.length; j++) {
bootstrapMethods[j] = v;
v += 2 + readUnsignedShort(v + 2) << 1;
}
context.bootstrapMethods = bootstrapMethods;
} else {
Attribute attr = readAttribute(attrs, attrName, u + 8,
readInt(u + 4), c, -1, null);
if (attr != null) {
attr.next = attributes;
attributes = attr;
}
}
u += 6 + readInt(u + 4);
}

// 3、访问类的描述信息
classVisitor.visit(readInt(items[1] - 7), access, name, signature,
superClass, interfaces);

// 4、访问源码和 debug 信息
if ((flags & SKIP_DEBUG) == 0
&& (sourceFile != null || sourceDebug != null)) {
classVisitor.visitSource(sourceFile, sourceDebug);
}

// 5、访问外部类
if (enclosingOwner != null) {
classVisitor.visitOuterClass(enclosingOwner, enclosingName,
enclosingDesc);
}

// 6、访问类注解和类型注解
if (ANNOTATIONS && anns != 0) {
for (int i = readUnsignedShort(anns), v = anns + 2; i > 0; --i) {
v = readAnnotationValues(v + 2, c, true,
classVisitor.visitAnnotation(readUTF8(v, c), true));
}
}
if (ANNOTATIONS && ianns != 0) {
for (int i = readUnsignedShort(ianns), v = ianns + 2; i > 0; --i) {
v = readAnnotationValues(v + 2, c, true,
classVisitor.visitAnnotation(readUTF8(v, c), false));
}
}
if (ANNOTATIONS && tanns != 0) {
for (int i = readUnsignedShort(tanns), v = tanns + 2; i > 0; --i) {
v = readAnnotationTarget(context, v);
v = readAnnotationValues(v + 2, c, true,
classVisitor.visitTypeAnnotation(context.typeRef,
context.typePath, readUTF8(v, c), true));
}
}
if (ANNOTATIONS && itanns != 0) {
for (int i = readUnsignedShort(itanns), v = itanns + 2; i > 0; --i) {
v = readAnnotationTarget(context, v);
v = readAnnotationValues(v + 2, c, true,
classVisitor.visitTypeAnnotation(context.typeRef,
context.typePath, readUTF8(v, c), false));
}
}

// 7、访问类的属性
while (attributes != null) {
Attribute attr = attributes.next;
attributes.next = null;
classVisitor.visitAttribute(attributes);
attributes = attr;
}

// 8、访问内部类
if (innerClasses != 0) {
int v = innerClasses + 2;
for (int i = readUnsignedShort(innerClasses); i > 0; --i) {
classVisitor.visitInnerClass(readClass(v, c),
readClass(v + 2, c), readUTF8(v + 4, c),
readUnsignedShort(v + 6));
v += 8;
}
}

// 9、访问字段和方法
u = header + 10 + 2 * interfaces.length;
for (int i = readUnsignedShort(u - 2); i > 0; --i) {
u = readField(classVisitor, context, u);
}
u += 2;
for (int i = readUnsignedShort(u - 2); i > 0; --i) {
u = readMethod(classVisitor, context, u);
}

// 访问当前类结束时调用
classVisitor.visitEnd();
}

首先,在 classReader 实例的 accept 方法中的注释1和注释2处,我们会 先开始进行类相关的字节码解析的工作:读取了类的描述和属性信息。接着,在注释3 ~ 注释8处,我们调用了 classVisitor 一系列的 visitxxx 方法访问 classReader 解析完字节码后保存在内存的信息。然后,在注释9处,分别调用了 readField 方法和 readMethod 方法去访问类中的方法和字段。最后,调用 classVisitor 的 visitEnd 标识已访问结束

1)、类内字段的解析

这里,我们先来看看 readField 的源码实现,如下所示:

/**

  • Reads a field and makes the given visitor visit it.
  • @param classVisitor
  •        the visitor that must visit the field.
    
  • @param context
  •        information about the class being parsed.
    
  • @param u
  •        the start offset of the field in the class file.
    
  • @return the offset of the first byte following the field in the class.
    */
    private int readField(final ClassVisitor classVisitor,
    final Context context, int u) {
    // 1、读取字段的描述信息
    char[] c = context.buffer;
    int access = readUnsignedShort(u);
    String name = readUTF8(u + 2, c);
    String desc = readUTF8(u + 4, c);
    u += 6;

// 2、读取字段的属性
String signature = null;
int anns = 0;
int ianns = 0;
int tanns = 0;
int itanns = 0;
Object value = null;
Attribute attributes = null;

for (int i = readUnsignedShort(u); i > 0; --i) {
String attrName = readUTF8(u + 2, c);
// tests are sorted in decreasing frequency order
// (based on frequencies observed on typical classes)
if (“ConstantValue”.equals(attrName)) {
int item = readUnsignedShort(u + 8);
value = item == 0 ? null : readConst(item, c);
} else if (SIGNATURES && “Signature”.equals(attrName)) {
signature = readUTF8(u + 8, c);
} else if (“Deprecated”.equals(attrName)) {
access |= Opcodes.ACC_DEPRECATED;
} else if (“Synthetic”.equals(attrName)) {
access |= Opcodes.ACC_SYNTHETIC
| ClassWriter.ACC_SYNTHETIC_ATTRIBUTE;
} else if (ANNOTATIONS
&& “RuntimeVisibleAnnotations”.equals(attrName)) {
anns = u + 8;
} else if (ANNOTATIONS
&& “RuntimeVisibleTypeAnnotations”.equals(attrName)) {
tanns = u + 8;
} else if (ANNOTATIONS
&& “RuntimeInvisibleAnnotations”.equals(attrName)) {
ianns = u + 8;
} else if (ANNOTATIONS
&& “RuntimeInvisibleTypeAnnotations”.equals(attrName)) {
itanns = u + 8;
} else {
Attribute attr = readAttribute(context.attrs, attrName, u + 8,
readInt(u + 4), c, -1, null);
if (attr != null) {
attr.next = attributes;
attributes = attr;
}
}
u += 6 + readInt(u + 4);
}
u += 2;

// 3、访问字段的声明
FieldVisitor fv = classVisitor.visitField(access, name, desc,
signature, value);
if (fv == null) {
return u;
}

// 4、访问字段的注解和类型注解
if (ANNOTATIONS && anns != 0) {
for (int i = readUnsignedShort(anns), v = anns + 2; i > 0; --i) {
v = readAnnotationValues(v + 2, c, true,
fv.visitAnnotation(readUTF8(v, c), true));
}
}
if (ANNOTATIONS && ianns != 0) {
for (int i = readUnsignedShort(ianns), v = ianns + 2; i > 0; --i) {
v = readAnnotationValues(v + 2, c, true,
fv.visitAnnotation(readUTF8(v, c), false));
}
}
if (ANNOTATIONS && tanns != 0) {
for (int i = readUnsignedShort(tanns), v = tanns + 2; i > 0; --i) {
v = readAnnotationTarget(context, v);
v = readAnnotationValues(v + 2, c, true,
fv.visitTypeAnnotation(context.typeRef,
context.typePath, readUTF8(v, c), true));
}
}
if (ANNOTATIONS && itanns != 0) {
for (int i = readUnsignedShort(itanns), v = itanns + 2; i > 0; --i) {
v = readAnnotationTarget(context, v);
v = readAnnotationValues(v + 2, c, true,
fv.visitTypeAnnotation(context.typeRef,
context.typePath, readUTF8(v, c), false));
}
}

// 5、访问字段的属性
while (attributes != null) {
Attribute attr = attributes.next;
attributes.next = null;
fv.visitAttribute(attributes);
attributes = attr;
}

// 访问字段结束时调用
fv.visitEnd();

return u;
}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

Android高级架构师

由于篇幅问题,我呢也将自己当前所在技术领域的各项知识点、工具、框架等汇总成一份技术路线图,还有一些架构进阶视频、全套学习PDF文件、面试文档、源码笔记。

  • 330页PDF Android学习核心笔记(内含上面8大板块)

  • Android学习的系统对应视频

  • Android进阶的系统对应学习资料

  • Android BAT部分大厂面试题(有解析)

好了,以上便是今天的分享,希望为各位朋友后续的学习提供方便。觉得内容不错,也欢迎多多分享给身边的朋友哈。

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img
MZ8H6-1712744918378)]

Android高级架构师

由于篇幅问题,我呢也将自己当前所在技术领域的各项知识点、工具、框架等汇总成一份技术路线图,还有一些架构进阶视频、全套学习PDF文件、面试文档、源码笔记。

  • 330页PDF Android学习核心笔记(内含上面8大板块)

[外链图片转存中…(img-ANX5m4gD-1712744918379)]

[外链图片转存中…(img-WAaz6U6t-1712744918379)]

  • Android学习的系统对应视频

  • Android进阶的系统对应学习资料

[外链图片转存中…(img-bWD27ZHl-1712744918379)]

  • Android BAT部分大厂面试题(有解析)

[外链图片转存中…(img-citPAapM-1712744918380)]

好了,以上便是今天的分享,希望为各位朋友后续的学习提供方便。觉得内容不错,也欢迎多多分享给身边的朋友哈。

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-9pg99FCU-1712744918380)]

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值