在 JVM中 Class 文件分析 主要详细讲解了Class文件的格式,并且在上一篇文章中做了总结。 众所周知,JVM 在运行时, 加载并执行class文件, 这个class文件基本上都是由我们所写的java源文件通过 javac 编译而得到的。 但是, 我们有时候会遇到这种情况:在前期(编写程序时)不知道要写什么类,只有到运行时,才能根据当时的程序执行状态知道要使用什么类。 举一个常见的例子就是 JDK 中的动态代理。这个代理能够使用一套API代理所有的符合要求的类, 那么这个代理就不可能在 JDK 编写的时候写出来,因为当时还不知道用户要代理什么类。

当遇到上述情况时, 就要考虑这种机制:在运行时动态生成class文件。 也就是说, 这个 class 文件已经不是由你的 Java 源码编译而来,而是由程序动态生成。 能够做这件事的,有JDK中的动态代理API, 还有一个叫做 cglib 的开源库。 这两个库都是偏重于动态代理的, 也就是以动态生成 class 的方式来支持代理的动态创建。 除此之外, 还有一个叫做 ASM 的库, 能够直接生成class文件,它的 api 对于动态代理的 API 来说更加原生, 每个api都和 class 文件格式中的特定部分相吻合, 也就是说, 如果对 class 文件的格式比较熟练, 使用这套 API 就会相对简单。 下面我们通过一个实例来讲解 ASM 的使用, 并且在使用的过程中, 会对应 class 文件中的各个部分来说明。

ASM 库的介绍和使用

ASM 库是一款基于 Java 字节码层面的代码分析和修改工具,那 ASM 和访问者模式有什么关系呢?访问者模式主要用于修改和操作一些数据结构比较稳定的数据,通过前面的学习,我们知道 .class 文件的结构是固定的,主要有常量池、字段表、方法表、属性表等内容,通过使用访问者模式在扫描 .class 文件中各个表的内容时,就可以修改这些内容了。在学习 ASM 之前,可以通过深入浅出访问者模式 这篇文章学习一下访问者模式。

ASM 可以直接生产二进制的 .class 文件,也可以在类被加载入 JVM 之前动态修改类行为。下文将通过两个例子,分别介绍如何生成一个 class 文件和修改 Java 类中方法的字节码。

在刚开始使用的时候,可能对字节码的执行不是很清楚,使用 ASM 会比较困难,ASM 官方也提供了一个帮助工具 ASMifier,我们可以先写出目标代码,然后通过 javac 编译成 .class 文件,然后通过 ASMifier 分析此 .class 文件就可以得到需要插入的代码对应的 ASM 代码了。

ASM 生成 class 文件

下面简单看一个 java 类:

package work;

public class Example {public static void main(String[] var0) {
System.out.println("createExampleClass");
}
}

这个 Example 类很简单,只有简单的包名,加上一个静态 main 方法,打印输出 createExampleClass 。

现在问题来了,你如何生成这个 Example.java 的 class 文件,不能在开发时通过上面的源码来编译成, 而是要动态生成。

下面开始介绍如何使用 ASM 动态生成上述源码对应的字节码。

代码示例

public class Main extends ClassLoader {
// 此处记得替换成自己的文件地址
public static final String PATH = "/Users/xxx/IdeaProjects/untitled/src/work/"; public static void main(String[] args) {
createExampleClass();
} private static void createExampleClass() {
ClassWriter cw = new ClassWriter(0);
// 定义一个叫做Example的类,并且这个类是在 work 目录下面
cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "work/Example", null, "java/lang/Object", null);
// 生成默认的构造方法
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
// 生成构造方法的字节码指令
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
// 构造函数访问结束
mv.visitEnd(); // 生成main方法中的字节码指令
mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
mv.visitCode();
// 获取该方法
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
// 加载字符串参数
mv.visitLdcInsn("createExampleClass");
// 调用该方法
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 1);
mv.visitEnd(); // 获取生成的class文件对应的二进制流
byte[] code = cw.toByteArray(); // 将二进制流写到本地磁盘上
FileOutputStream fos = null;
try {
fos = new FileOutputStream(PATH + "Example.class");
fos.write(code);
System.out.println(fos.getFD());
fos.close();
} catch (Exception e) {
System.out.print(" FileOutputStream error " + e.getMessage());
e.printStackTrace();
}
loadclass("Example.class", "work.Example");
} private static void loadclass(String className, String packageNamePath) {
//通过反射调用main方法
MyClassLoader myClassLoader = new MyClassLoader(PATH + className);
// 类的全称,对应包名
try {
// 加载class文件
Class<?> Log = myClassLoader.loadClass(packageNamePath);
System.out.println("类加载器是:" + Log.getClassLoader());
// 利用反射获取main方法
Method method = Log.getDeclaredMethod("main", String[].class);
String[] arg = {"ad"};
method.invoke(null, (Object) arg); } catch (Exception e) {
e.printStackTrace();
}
}
}

为了证明表示我们生成的 class 可以正常调用,还需要将其加载,然后通过反射调用该类的方法,这样才能说明生成的 class 文件是没有问题并且可运行的。

下面是自定义的一个 class 加载类:

public class MyClassLoader extends ClassLoader {
// 指定路径
private String path; public MyClassLoader(String classPath) {
path = classPath;
} /**
* 重写findClass方法
*
* @param name 是我们这个类的全路径
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class log = null;
// 获取该class文件字节码数组
byte[] classData = getData(); if (classData != null) {
// 将class的字节码数组转换成Class类的实例
log = defineClass(name, classData, 0, classData.length);
}
return log;
} /**
* 将class文件转化为字节码数组
*
* @return
*/
private byte[] getData() { File file = new File(path);
if (file.exists()) {
FileInputStream in = null;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(file);
out = new ByteArrayOutputStream(); byte[] buffer = new byte[1024];
int size = 0;
while ((size = in.read(buffer)) != -1) {
out.write(buffer, 0, size);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return out.toByteArray();
} else {
return null;
}
}
}

代码详解

下面详细介绍生成class的过程:

首先定义一个类

相关代码片段如下:
 ClassWriter cw = new ClassWriter(0);
// 定义一个叫做Example的类,并且这个类是在 work 目录下面
cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "work/Example", null, "java/lang/Object", null);

ClassWriter 类是 ASM 中的核心 API , 用于生成一个类的字节码。 ClassWriter 的 visit 方法定义一个类。

  • 第一个参数 V1_8 是生成的 class 的版本号, 对应class文件中的主版本号和次版本号, 即 minor_version 和 major_version 。

  • 第二个参数ACC_PUBLIC表示该类的访问标识。这是一个public的类。 对应class文件中的access_flags 。

  • 第三个参数是生成的类的类名。 需要注意,这里是类的全限定名。 如果生成的class带有包名, 如com.jg.xxx.Example, 那么这里传入的参数必须是com/jg/xxx/Example  。对应 class 文件中的 this_class  。

  • 第四个参数是和泛型相关的, 这里我们不关新, 传入null表示这不是一个泛型类。这个参数对应class文件中的Signature属性(attribute) 。

  • 第五个参数是当前类的父类的全限定名。 该类直接继承Object。 这个参数对应class文件中的super_class 。

  • 第六个参数是 String[] 类型的, 传入当前要生成的类的直接实现的接口。 这里这个类没实现任何接口, 所以传入null 。 这个参数对应class文件中的interfaces 。

定义默认构造方法, 并生成默认构造方法的字节码指令

相关代码片段如下:
        // 生成默认的构造方法
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
// 生成构造方法的字节码指令
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
// 构造函数访问结束
mv.visitEnd();

使用上面创建的 ClassWriter 对象, 调用该对象的 visitMethod 方法, 得到一个 MethodVisitor 对象, 这个对象定义一个方法。 对应 class 文件中的一个 method_info 。

  • 第一个参数是 ACC_PUBLIC , 指定要生成的方法的访问标志。 这个参数对应 method_info 中的 access_flags 。

  • 第二个参数是方法的方法名。 对于构造方法来说, 方法名为 <init> 。 这个参数对应 method_info 中的 name_index , name_index 引用常量池中的方法名字符串。

  • 第三个参数是方法描述符, 在这里要生成的构造方法无参数, 无返回值, 所以方法描述符为 ()V  。 这个参数对应 method_info 中的descriptor_index 。

  • 第四个参数是和泛型相关的, 这里传入null表示该方法不是泛型方法。这个参数对应 method_info 中的 Signature 属性。

  • 第五个参数指定方法声明可能抛出的异常。 这里无异常声明抛出, 传入 null 。 这个参数对应 method_info 中的 Exceptions 属性。

接下来调用 MethodVisitor 中的多个方法, 生成当前构造方法的字节码。 对应 method_info 中的 Code 属性。

  1. 调用 visitVarInsn 方法,生成 aload 指令, 将第 0 个本地变量(也就是 this)压入操作数栈。

  2. 调用 visitMethodInsn方法, 生成 invokespecial 指令, 调用父类(也就是 Object)的构造方法。

  3. 调用 visitInsn 方法,生成 return 指令, 方法返回。

  4. 调用 visitMaxs 方法, 指定当前要生成的方法的最大局部变量和最大操作数栈。 对应 Code 属性中的 max_stack 和 max_locals 。

  5. 最后调用 visitEnd 方法, 表示当前要生成的构造方法已经创建完成。

定义main方法, 并生成main方法中的字节码指令

这里与构造函数一样,就不多说了。

生成class数据, 保存到磁盘中, 加载class数据

 // 获取生成的class文件对应的二进制流
byte[] code = cw.toByteArray(); // 将二进制流写到本地磁盘上
FileOutputStream fos = null;
try {
fos = new FileOutputStream(PATH + "Example.class");
fos.write(code);
fos.close();
} catch (Exception e) {
System.out.print(" FileOutputStream error " + e.getMessage());
e.printStackTrace();
}
loadclass("Example.class", "work.Example");

这段代码执行完, 可以看到控制台有以下输出:

生成 ASM 代码

那么还有个问题是前面的 ASM 代码是如何生成的呢?

还是以前文提到的 EXample.java 为例:

javac Example.java  // 生成 Example class 文件
java -classpath asm-all-6.0_ALPHA.jar org.objectweb.asm.util.ASMifier Example.class // 利用 ASMifier 将class 文件转为 asm 代码

在 Terminal 窗口中输入这两个命令,就可以得到下面的 asm 代码:

import java.util.*;
import org.objectweb.asm.*;
public class ExampleDump implements Opcodes { public static byte[] dump () throws Exception { ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0; cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "Example", null, "java/lang/Object", null); {
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("createExampleClass");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 1);
mv.visitEnd();
}
cw.visitEnd(); return cw.toByteArray();
}
}

可以看到输出结果与前面的生成的 class 文件的代码是一样的。

到这里,相信你对 ASM 的使用已经有了初步的了解了,当然可能不是很熟悉,但是多写写练练掌握格式就好多了。

利用 ASM 修改方法

下面介绍如何修改一个 class 文件的方法。

还是在原来的代码基础上,Main 类下面新增一个方法 modifyMethod 方法,具体代码如下:

private static void modifyMethod() {
byte[] code = null;
try {
// 需要注意把 . 变成 /, 比如 com.example.a.class 变成 com/example/a.class
InputStream inputStream = new FileInputStream(PATH + "Example.class");
ClassReader reader = new ClassReader(inputStream); // 1. 创建 ClassReader 读入 .class 文件到内存中
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS); // 2. 创建 ClassWriter 对象,将操作之后的字节码的字节数组回写
ClassVisitor change = new ChangeVisitor(writer); // 3. 创建自定义的 ClassVisitor 对象
reader.accept(change, ClassReader.EXPAND_FRAMES);
code = writer.toByteArray();
System.out.println(code);
FileOutputStream fos = new FileOutputStream(PATH + "Example.class");
fos.write(code);
fos.close();
} catch (Exception e) {
System.out.println("FileInputStream " + e.getMessage());
e.printStackTrace();
}
try {
if (code != null) {
System.out.println(code);
FileOutputStream fos = new FileOutputStream(PATH + "Example.class");
fos.write(code);
fos.close();
}
} catch (Exception e) {
System.out.println("FileOutputStream ");
e.printStackTrace();
}
loadclass("Example.class", "work.Example");
}

新建一个 adapter,继承自 AdviceAdapter,AdviceAdapter 本质也是一个 MethodVisitor,但是里面对很多对方法的操作逻辑进行了封装,使得我们不用关心 ASM 内部的访问逻辑,只需要在对应的方法下面添加代码逻辑即可。

public class ChangeAdapter extends AdviceAdapter {
private String methodName = null;
ChangeAdapter(int api, MethodVisitor mv, int access, String name, String desc) {
super(api, mv, access, name, desc);
methodName = name;
} @Override
protected void onMethodEnter() {
super.onMethodEnter();
Label l0 = new Label();
Label l1 = new Label();
Label l2 = new Label();
mv.visitTryCatchBlock(l0, l1, l2, "java/lang/InterruptedException");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
// 把当前的时间戳存起来
mv.visitVarInsn(LSTORE, 1);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("ChangeAdapter onMethodEnter ");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitLabel(l0);
mv.visitLdcInsn(new Long(100L));
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Thread", "sleep", "(J)V", false);
mv.visitLabel(l1);
Label l3 = new Label();
mv.visitJumpInsn(GOTO, l3);
mv.visitLabel(l2);
mv.visitFrame(Opcodes.F_FULL, 2, new Object[] {"[Ljava/lang/String;", Opcodes.LONG}, 1, new Object[] {"java/lang/InterruptedException"});
mv.visitVarInsn(ASTORE, 3);
mv.visitVarInsn(ALOAD, 3);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/InterruptedException", "printStackTrace", "()V", false);
mv.visitLabel(l3);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
// 把当前的时间戳存起来
mv.visitVarInsn(LSTORE, 3);
} @Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
// 把之前存储的时间戳取出来
mv.visitVarInsn(LLOAD, 3);
mv.visitVarInsn(LLOAD, 1);
mv.visitInsn(LSUB);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V", false);
} @Override
public void visitMaxs(int i, int i1) {
super.visitMaxs(i, i1); }

在 adapter 中,有两个非常重要的方法:

  • onMethodEnter:表示正在进入一个方法,在执行方法里的内容前会调用。因此,此处是对一个方法添加相关处理逻辑的很好的办法。

  • onMethodExit:表示正在退出一个方法,在执行 return 之前。如果一个方法存在返回值,只能再该方法添加静态方法。

 上面的代码是为了计算某个方法的耗时,我们先是在方法开始前记录了当前的时间戳,同时为了避免程序执行过快,还让该线程睡了100ms。在方法结束前,将之前的时间戳取出来,同时获取当前的时间戳,两者相减,就是方法运行耗时。
public class ChangeVisitor extends ClassVisitor {
ChangeVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM5, classVisitor);
} @Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
System.out.print(name);
if (name.equals("main")) {
return new ChangeAdapter(Opcodes.ASM4, methodVisitor, access, name, desc);
}
return methodVisitor;
}
}

ChangeVisitor 主要就是对 ASM 访问 class 文件方法的时候,做个拦截。如果发现方法名是 main,就让其走前面写好的 ChangeAdapter,这样,我们就可以改写 class 文件的方法了。

运行结果

可以看到输出结果,是 100 ms,成功的对 main 方法的耗时进行了计算。

如果方法带有返回值

前面修改的 main 是没有返回值的,那么如果存在返回值?这么写还合适吗?

如果你添加了非静态方法的调用,去看生成的 class 文件也许可能是对的,但是在调用的时候就会报错。示例如下:

    protected void onMethodExit(int opcode) {
mv.visitVarInsn(LLOAD, longT);
mv.visitInsn(LSUB);
mv.visitVarInsn(LSTORE, longT);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("work2 createExampleClass");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitVarInsn(LLOAD, longT);
}

这里是调用了一些非静态方法,接下去看生成的 class 文件:

从class 文件来看,生成的 class 文件是没有问题的,结果在反射调用的时候报了异常:

通过  javap -c Example.class 将反编译结果输出如下:

$ javap -c Example.class
public class work2.Example {
public work2.Example();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return public long computer();
Code:
0: ldc2_w #29 // long 32423l
3: lstore_1
4: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #18 // String work2 createExampleClass
9: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: invokestatic #27 // Method java/lang/System.currentTimeMillis:()J
15: lstore_2
16: invokestatic #27 // Method java/lang/System.currentTimeMillis:()J
19: lstore 4
21: lload 4
23: lload_2
24: lsub
25: lstore 6
27: lload 6
29: lload_1
30: lsub
31: lstore_1
32: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
35: ldc #18 // String work2 createExampleClass
37: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
40: lload_1
41: lreturn
}

下面的是修改前的带有返回值的反编译结果:

$ javap  -c Example.class
public class work2.Example {
public work2.Example();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return public long computer();
Code:
0: ldc2_w #29 // long 32423l
3: lstore_1
4: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #18 // String work2 createExampleClass
9: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: invokestatic #27 // Method java/lang/System.currentTimeMillis:()J
15: lstore_2
16: invokestatic #27 // Method java/lang/System.currentTimeMillis:()J
19: lstore 4
21: lload 4
23: lload_2
24: lsub
25: lstore 6
27: lload 6
29: lreturn
}

可以发现 27 行前面的代码都是一样的,27 后面我们尝试修改 class 文件,同时替换返回值,但是最终还是失败了。这里原因我没有去寻找,应该就是我们的修改导致堆栈信息存在变化,从而导致校验失败。

如果我们实在需要对带有返回值的返回值进行修改,可以参考下面的实例,使用静态方法:

    protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
mv.visitLdcInsn("main");
mv.visitMethodInsn(INVOKESTATIC, "work2/Main", "test", "(Ljava/lang/String;)V", false);
mv.visitLdcInsn("ssss");
mv.visitMethodInsn(INVOKESTATIC, "work2/Main", "test", "(Ljava/lang/String;)V", false);
}

可以从 INVOKESTATIC 关键字看出,这些都是静态方法。

到这里,关于 ASM 使用说明到这里就结束了。

源码已上传到 CSDN : ASM-demo.zip 。

参考文章:

从 Java 字节码到 ASM 实践

Class文件格式实战:使用ASM动态生成class文件

通过 ASM 库生成和修改 class 文件的更多相关文章

  1. oracle11g文件系统库迁移到ASM库上面

    最近把oracle11g的文件系统库迁移到了asm库上面. 迁移过程大致如下: 最少停机方案: 实例joinpay02 | |数据库joinpay02 需要改动的文件: 数据文件 控制文件 redo文 ...

  2. Windows环境下编译Assimp库生成Android可用的.so或.a文件

    在做项目过程中需要使用Assimp这个3D模型读取库来读取obj格式的模型,因为项目是基于Android平台,采用NDK开发,所以就打算编译Assimp库并生成.so文件.本文使用Assimp-v.5 ...

  3. 用pandas库修改excel文件里的内容,并把excel文件格式存为csv格式,再将csv格式改为html格式

    假设有Excel文件data.xlsx,其中内容为: ID  age  height     sex  weight张三   1   39     181  female      85李四   2  ...

  4. boost库生成文件命名和编译

    生成文件命名规则:boost中有许多库,有的库需要编译.而有的库不需要编译,只需包含头文件就可以使用.编译生成的文件名字普遍较长,同一个库根据编译链接选项不同,又可以生成多个不同名字的文件.生成的文件 ...

  5. 修改Sharepoint 文档库列表点击Excel文件默认跳转到Excel Service服务 xlviewer.aspx页面

    在Sharepoint 文档库中,当点击库中的一个Excel文件时,Sharepoint默认为转跳到Excel Services上,无论是Sharepoint 的是否开启了Excel Service, ...

  6. [转]使用ant让Android自动打包的build.xml,自动生成签名的apk文件(支持android4.0以上的版本)

    在android4.0以后的sdk里那个脚本就失效了,主要是因为 apkbuilder这个程序不见了: 人家sdk升级,我们的脚本也要跟上趟,修改一下喽. 上网一查,大家的文章还停留在我去年的脚本程度 ...

  7. iOS 静态库生成(引用第三方SDK、开源库、资源包)

    一.静态库创建 打开Xcode, 选择File ----> New ---> Project  选择iOS ----> Framework & Library ---> ...

  8. iOS - Bundle 资源文件包生成和常见资源文件使用

    1.Bundle 文件 Bundle 文件,就是资源文件包.我们将许多图片.XIB.文本文件组织在一起,打包成一个 Bundle 文件.方便在其他项目中引用包内的资源. Bundle 文件是静态的,也 ...

  9. 自动生成项目的Makefile文件

    自动生成项目的Makefile文件 理论基础 跟我一起写 Makefile:   http://bbs.chinaunix.net/forum.php?mod=viewthread&tid=4 ...

随机推荐

  1. npm fetch All In One

    npm fetch All In One fetch for TypeScript { "compilerOptions": { "lib": ["D ...

  2. API & YApi

    API & YApi 接口管理服务 YApi http://yapi.demo.qunar.com/ https://ued.qunar.com/ build bug https://gith ...

  3. React useMemo

    React useMemo react hooks https://reactjs.org/docs/hooks-reference.html#usememo useCallback & us ...

  4. alipay 小程序 & online IDE & demos

    alipay 小程序 & online IDE & demos system-info iOS / Andriod https://opendocs.alipay.com/mini/a ...

  5. 线上 S1 故障是什么, 线上 S1 故障, 运维故障分级, 运维, 故障分级, P1 级别故障, 故障, P1 , S1

    线上 S1 故障是什么 线上 S1 故障, 运维故障分级, 运维, 故障分级, P1 级别故障, 故障, P1 , S1 故障复盘 https://time.geekbang.org/column/a ...

  6. 最小生成树---普里姆算法(Prim算法)和克鲁斯卡尔算法(Kruskal算法)

    普里姆算法(Prim算法) #include<bits/stdc++.h> using namespace std; #define MAXVEX 100 #define INF 6553 ...

  7. 24_MySQL插入insert语句

    本节涉及SQL语句: -- MYSQL 基础操作 1.插入insert语句 INSERT INTO t_dept(deptno,dname,loc) VALUES(70,"后勤部" ...

  8. 用注解开发SpringMVC

    Spring2.5以后,用注解开发SpringMVC的功能十分强大,注解也是SpringMVC的精髓.在实际开发中,都会使用注解来实现. 这让SpringMVC开发工作量最小化,开发者只要专注于业务逻 ...

  9. Linux安装与使用

    1.安装 1.1安装VMware 1.1.1VM12版本安装 1)下载:网盘:链接:https://pan.baidu.com/s/1Jnr--KIy3bSTvRhtB8nfiQ 提取码:czna 2 ...

  10. SpringBoot Test 多线程报错:dataSource already closed

    1:前言 最近在项目中使用多线程对大任务拆分处理时,进行数据库操作的时候报错了. 业务代码大概是这样的: @Service public calss TestServiceImpl implement ...