invokedynamic
虽然是一个从jdk1.7开始引入的字节码,但是,它对于很多Javaer来说都是一个新鲜玩意,但是其实这个字节码在目前的jdk内部扮演着一些关键角色。
那么今天这个主要是填坑,因为之前在字节码的那篇文章中我说过,我会补上invokedynamic
的部分,虽然我感觉写起来有点麻烦,但是不想鸽太久,所以简单给大家分享一下我对这个字节码的理解。As always,仅供参考。
还有就是,如果没有特殊说明,所有的代码的版本都是Java24。
从案例来简单认识invokedynamic
invokedynamic
这条指令和之前说过的invokestatic
、invokevirtual
这类指令相似,都是用来调用方法的。简单来说,它的作用就是将原本在编译期间确认好的方法调用,延迟到运行期间才进行动态决定。
而什么时候会在class文件里看见这个指令呢,目前最常见的场景就是lambda,或者说方法引用,比如这样的代码:
java
public class ShowMeInvokeDynamic {
public static void main(String[] args) {
Consumer<String> printer = System.out::println;
printer.accept("Hello world");
}
}
这是目前大部分Javaer喜欢的写法,那么直接编译这段代码,通过javap来观测,大概会看到这样的东西:

这就是我们想要看到的东西,那我们再去constant pool(后文简称cp)中找一下这一个entry,如下:
可以看到,这是一个方法的描述,我们来看一下第20条entry,可以简单的分为三部分,那么先解析一下这个方法是什么样的:
accept
:方法名(Ljava/io/PrintStream;)
:表明该方法接收的参数为PrintStreamLjava/util/function/Consumer
:表明该方法的返回值为Consumer
说句题外话,这里的L...
代表的是引用类型。那么很明显,是这么一个方法
java
Consumer accept(PrintStream arg) {...}
那么其实看起来很奇怪,似乎和我们的目标lambda完全偏离了,按照代码里面的lambda来说,应该只是一个简单的Consumer
形式的方法就好了。因为实际上,这个方法是给VM调用的,它的意义是让VM知道,我们需要一个Consumer
的实现类,并且其中包含一个方法,这个方法的参数为PrintStream
,名字为accept
,返回值为void
。
这就涉及到了invokedynamic
这条指令本身的用处:当执行到这条指令的时候,它会从操作数栈上拿走一些需要的参数,然后调用一个所谓的引导方法(BootstrapMethod)来获取一个CallSite
。
那么什么是BootstrapMethod
和CallSite
,暂时按下不表,只需要知道invokedynamic
最终会产生一个东西,并被放置到栈上就可以了。
现在再来看到这一整个字节码,从头到尾来解析一遍就很清晰了:
- 0~7:这一系列的操作可以说是对应了
System.out::println
这一行代码,获取到System的out变量,然后会有一个隐式的Objects.requireNonNull
。这里想说一下,这个null-check是javac编译器自动插入的,实际上在方法内部,javac为了保证程序的正常运行,会隐式的插入很多的null-check。它会返回out本身,所以这里需要把这个重复的out给pop掉,此时operand stack就剩下了一个out变量 - 8:通过invokedynamic生成了一个Consumer对象
- 13~14:可以看出来这是把
CallSite
给保存了起来,那么这个main方法的locals为2,第0个就是args数组,第1个就是我们定义的pointer这个Consumer
- 15~22:这里对应的就是
printer.accept()
这一个方法,然后就正常结束
还有一个比较有意思的点,并且也可以关联到我之前写过的一篇文章,那就是这里的null-check,在Java9之前,javac插入的null-check并不是Objects.requireNonNull
,而是一个getClass方法。至于后续版本替换成了Objects.requireNonNull
是因为,这个静态方法是一个标准的null-check实践,并且它是一个被intrinsic优化过的方法,相比于直接的getClass
,性能会好上不少。关于intrinsic,可以翻看我之前的《从toArray窥探JVM》。
那么出现这个invokedynamic
的作用是什么呢?
我们知道,在jdk1.8之前,也就是没有lambda之前,我们想要直接返回一个interface,只能通过匿名内部类来实现,比如:
java
public class WhenThereIsNoLambda {
public static void main(String[] args) {
Consumer<String> printer = new Consumer<String> {
@Override public void accept(String str) {
System.out.println(str);
}
}
}
}
而Javac会为我们生成一个对应的class文件,像这样:

而lambda的出现,让我们可以直接用方法引用的形式来表示一个FunctionalInterface
的实现,把它改造成lambda,那么我们就可以看到没有显式的class文件被生成,所以,通过invokedynamic
的形式实现的lambda可以为我们省略掉额外的class文件的开销。
那么,具体是怎么实现的呢?那就请继续往下看。
invokedynamic & Java Lambda
此时,需要回到字节码那部分,前面说到,invokedynamic
最终需要去通过BootstrapMethod
来生成一个CallSite
,而心细的朋友会发现,它既然生成的是CallSite
,那为什么后面的字节码,我说它是直接把invokedynamic
的结果给store到了Consumer
类型的printer
去了,这当中是有歧义的,因为CallSite
很明显不是一个Consumer
,那么其中肯定是有一些什么转换,让CallSite
变成了我们需要的Consumer
。
后面,我们简称invokedynamic
为indy
。
那么现在,我们来看看所谓的BootstrapMethod
以及CallSite
。
BootstrapMethod
在最初的那份class文件中,通过javap我们其实可以看到,完整的class文件多出了一块BootstrapMethods
,这就是我们需要的东西,如下图:
可以看到,这里只有一个entry,关联的是
LambdaMetafactory
类的metafactory
方法,并且这个方法参数列表非常的长,但是实际给到的参数只有三个,这又是为什么呢?
这里实际上可以这么区分,前三个属于是JVM动态提供的,后三个是编译器静态提供的。怎么理解呢,前三个属于是在运行期间,JVM来动态获取的,也是每个BSM必须要有的三个参数,因为JVM需要这三个东西来找到具体的方法实现;而后面三个,某种程度来说是可以不需要的。这方面就不多展开,只是防止大伙有疑惑,了解一下即可。
那么首先,我们先去看这个方法的源码,由于在ide中打开会看到,有非常多的冗余代码,那么就用简单的伪代码表示即可:
java
public static CallSite metafactory(...) {
AbstractValidatingLambdaMetafactory mf = creatingInnerClassLambdaMetafactory(...);
mf.validateMetafactoryArgs();
return mf.buildCallSite();
}
实际上的关键步骤只有:
- 创建一个
InnerClassLambdaMetafactory
- 通过这个
Metafactory
创建一个CallSite
那么找到这个InnerClassLambdaMetafactory
类,从名字我们可以猜测,它实际上也是通过InnerClass也就是内部类的形式来实现Lambda的一个factory,从它的源码注释也可以看到:
那么,直接看到它的
buildCallSite
方法:
看上去嵌套了挺多的,其实逻辑非常简单。
首先看到spinInnerClass
这个方法,根据它的注释总结一下它的作用就是:获取一个class,这个class可能是
- 从CDS中获取
- 动态生成
为了直观一点,源码如下:

CDS不是本文的重点,简单介绍一下,全称是class data sharing ,用来减少JVM应用的启动时间和内存占用,从名字就知道,是一份可以在多个VM之间共享的class data。
这里还有一点要提的就是,实际上是可以看到这些动态生成的类的,在运行的时候配置一个参数 -Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles
即可。配置了这一个参数之后,所有动态生成的lambda实现类都会在项目的根目录下的DUMP_LAMBDA_PROXY_CLASS_FILES
文件里存放。
回到正题,一般来说,大部分情况下我们都会直接走到动态生成这一步,那么我们看到generateInnerClass
这个方法,这个方法体比较长,为了不影响观感,我就不打算贴源码了,用一段简单的伪代码来表示如下:
java
private Class<?> generateInnerClass() throws LambdaConversionException {
List<ClassDesc> interfaces = decideInterfaces();
byte[] classBytes = buildClassFileData();
defineGeneratedClassInTheVM();
return caller.makeHiddenClassDefiner(...).defineClass(...);
}
这里其实大概从伪代码的命名就可以看出具体的作用,但是这里打算聊一下第二步。
buildClassFileData是怎么build的
讲这一步之前提一嘴第一步,它主要是解决这样的情况:
java
Consumer<String> printer = (Consumer<String> & MarkInterfaceWithoutAPI) str -> {};
这时候,这个lambda表示的接口就有两个,一个是Consumer
,一个是MarkInterfaceWithoutAPI
,但是这里需要注意的是,不管这里有多少个接口,总共只能有一个方法声明,因为lambda只能表示一个方法实现。
所以,第一步就是把所有这些接口给收集起来,但是这些接口全部加起来只能有一个未实现的方法。
现在回到第二步这里。
通过名字很明显,就是来生成一个类的数据。我们用c来简称这个类。
首先,它会先处理被lambda捕获的变量。什么叫被lambda捕获,看到这样的代码:
java
final int x = 42;
Runnable r = () -> {System.out.println(x);}
那么此时,这个x就是被捕获的变量,在这一步,会为这些变量生成对应的private final
变量,并生成对应的构造器来赋值。
所以此时,这个c大概会长这样:
java
public class c {
private final YourType yourField;
...
public c(YourType yourField, ...) {...}
}
然后,会生成lambda的sam(全称Single Abstrac Method)方法,我们直接贴一下部分源码:
java
clb.withMethodBody(interfaceMethodName, methodDesc(interfaceMethodtype). ACC_PUBLIC,
forwardingMethod(interfaceMethodType));
可以看到就是生成一个方法,重点就是forwardingMethod
这里,它的注释告知了它的作用:生成一个调用lambda实现的方法。直观一点,我们用伪代码来表示:
java
public class c {
// fields...
public InterfaceMethodType interfaceMethodName(Args arg) {
invokeYourLambdaByStaticCall(convertIfNeeded(args));
}
}
这么看就比较直观了,所以,我们可以知道我们的自定义lambda是通过间接调用来实现的。
然后,这个类就会被创建并加载到vm中。只是可以关注的是它这里的方法名:makeHiddenClassDefiner
。注意到它叫做hiddenClass
,这种类和普通的类,相比于普通的类,这种类就像它的名字,它是hidden的,也就是它不能被其他的类发现。
那么,它的优势就是,不会被应用代码错误的引用,并且它有个特点,就是它的生命周期和它的Class对象绑定,如果这个Class是unreachable的,那么这个类可以直接被卸载,也就可以让metaspace的空间不会过于吃紧。
回到CallSite
那么这个内部类从此被创建,这时候就会拿着这个类去创建一个CallSite
,看回到buildCallSite
的源码,创建出的CallSite
会链接到某些代码,区别就在于对于捕获/非捕获的lambda,链接到的代码位置不同。
把情况总结一下,如下: 首先,对于所有lambda,都是生成一个新的inner class,这一点是没变的。
- 对于捕获lambda callsite链接到inner class的构造函数
- 对于非捕获lambda
- 如果开启了disable eager initialization 在inner class中会单独生成一个
private static final
的静态instance,此时,callsite会链接到一个static getter
- 如果没有开启disable eager initialization callsite会链接到一个pre-computed的对象,也就是说,会在第一次碰到
indy
的时候创建这个对象,后续的访问就直接返回这个提前计算好的对象
- 如果开启了disable eager initialization 在inner class中会单独生成一个
对于CallSite
,本质上它就是一个MethodHandle
的包装类。
得到CallSite之后
实际上,JVM内部实现也是通过方法调用来解析indy
指令的,在对应的方法内部,在拿到CallSite
之后,后续还有一些操作。
简单来说的话,就是JVM拿到CallSite
之后,会存入当前方法的一个常量池缓存 里面,这个缓存里本身会有indy
,然后把这个CallSite
和它包装的MethodHandle
给包装成一个高效率的入口,这里的包装,主要就是通过一个Invoker$Holder
类来做统一的入口。
这里的这个Invoker$Holder
实际上就是很多文章会提到的适配器,也就是,indy实际上最后会关联到一块调用Invoker$Holder
的入口方法的代码,可以简单的理解成一小段机器码,只有一个作用,就是发起对Invoker$Holder
的方法调用,通过它的方法去调用CallSite
包装的MethodHandle
。
这一块不再深入聊,大伙只要知道,在拿到CallSite
之后,vm会对它做一系列的操作,然后整合成一个高性能的方法,再和indy
关联起来方便后续调用。如果想要深入源码去看具体的步骤的话,具体的方法入口为InterpreterRuntime::resolve_invokedynamic
方法,这是在hotspot里的源码,但是还是不建议去看,没啥太大意义。
短暂的总结
所以,把上面的所有步骤总结一下,可以得到这么一个流程:
- 代码运行期间碰到一个没有解析过的indy指令
- 根据indy去找到ConstantPool的对应的entry,然后触发对应的
BootstrapMethod
(BSM) - 以lambda为例,触发
LambdaMetafactory.metafactory
,获取到一个CallSite
- vm通过统一的方法来处理callsite,也就是前面说的
linkToTargetMethod
- 后续再碰到这一个
indy
指令的时候,vm知道它被解析过,直接调用对应的方法即可
所以可以看到,indy
带来的优势就是全部都是动态执行的,对于lambda来说,indy
通过针对不同形式的lambda进行特殊处理,来最大程度的优化lambda实现,并且结合hidden class这一特性来降低metaspace的内存压力。
这也是indy
相比于传统的invoke
指令的最大区别,传统的invoke
指令,它们的共同点就是,它们具体的符号引用是要在编译期间就解析完并固定在类文件里的。indy
的出现赋予了Java更强的动态能力,它可以让方法引用的解析真正的推迟到了运行期间,并通过繁琐的优化来获取和普通调用相差无两的性能。
indy在jdk中的其他应用
还有一个比较常见的就是string拼接的场景,比如:
java
String s = randomStr() + randomStr();
在Java9之前,这段代码会生成一个StringBuilder
来进行拼接,在Java9之后,则使用indy
来动态获取要拼接的字符串,直接进行字符串的拼接,而不需要额外创建一个StringBuilder
。
通过javac后javap可以很清晰的看见,篇幅问题这里不贴全图了,只看Java24下的编译结果:
可以看到它对应的BSM是另外一个方法,
StringConcatFactory.makeConcatWithConstants
:

具体的就不深入看了,只要知道从Java9之后,字符串拼接的实现就是:分析这个拼接行为,然后把所有的常量部分提取出来,对于其他动态参数,用一个特殊字符作为占位符,然后形成一个recipe字符串
。比如:
java
String s = "UserName:" + name + " Age:" + age;
// Java 9之前
new StringBuilder().append("UserName").append(name).append(" Age").append(age);
// Java 9之后
invokedynamic makeConcatWithConstants("UserName: \u0001 Age: \u0001");
而在这个makeConcatWithConstants
内部,还会根据拼接的字符串的复杂度选择不同的策略:
- String::concat 如果recipe非常简单,那么会采用这种
- StringBuilder 如果是比较复杂的recipe,可能会采用这种
- 字节/字符数组拼接 在Java11之后的版本,绝大多数的场景下都会采用这个策略。它会先遍历整个字符串,然后精确的计算出最终字符串的长度,然后直接分配一个大小刚刚好的
byte[]
,然后直接把内容复制进去
具体可以看到源码里面的这部分,很明显:

所以,可以看到indy
的出现确实是增强了很多Java内部的动态能力。
怎么在日常开发中使用
那么,前面说到了indy这么多的好处,怎么在日常开发中用上这个东西呢?
答案是做不到。
因为,字节码是由javac编译器生成的,所以,什么东西会通过indy来实现,是由javac决定的。并且前面说到,indy
需要绑定BSM,我们是没有办法在代码中手动针对某个方法,让javac为他生成一条indy
,并调用我们自定义的BSM的。
但是,虽然我们没有办法控制javac,但是我们可以手动创建字节码。那么,这就是我们能在日常开发中,使用到indy
的唯一手段。
那么我们结合前面说到的lambda的实现,特别是在buildCallSite
源码里有这么一部分: 重点在这个pre-computing,这个是不是非常符合我们需要的经典场景lazy construction?
那么说明,可以通过类似lambda的实现来实现惰性单例,惰性单例需要有这么几个特点:
- 自定义
Supplier
- 惰性
- 单例
其实关键特性就在于这个自定义Supplier
,因为在Java9之后的VarHandle
已经可以很好的实现高效的DCL,但是DCL的缺点就在于,我们必须硬编码,也就是固定一个值,而不能在运行期间动态的修改构造函数的参数值。
那么参考lambda,我们尝试来搞一个使用indy
实现惰性单例的工具。
回顾lambda的实现,我们大概需要:
- 根据需要创建类
- 通过
indy
让单例接口的get方法链接到BSM - 想办法实现pre-computing
那么后面就大致给出代码以及部分解析。
首先,我们需要有一个接口,并且只能有一个方法,那其实Supplier
本身就可以很好的满足我们的需要。那么定义这么一个接口:
java
interface LazySupplier<T> extends Supplier<T> {
@Override T get();
public static <T> LazySupplier<T> of(Supplier<T> s) {
return LazySupplierMetafactory.of(s);
}
}
很明显,LazySupplierMetafactory
参考了LambdaMetafactory
:
java
public class LazySupplierMetafactory {
// indy关联的bsm方法
public static CallSite indyBsm(Lookup lk, String name, MethodType mt) {...}
// 参考 LambdaMetafactory的内部逻辑
// 其实核心就是生成内部类 然后把这个类的get方法修改为indy 然后加载这个类
public static <T> LazySupplier<T> of(Supplier<T> s) {...}
}
核心其实就是生成内部类这里,因为我们需要在这里来实现LazySupplier
接口的get
方法,通过这个get
方法来把我们实际的supplier
给替换掉这个实现。可能有点绕,以防有的朋友搞不明白,搞点简单的代码最直观:
java
LazySupplier<String> sls = LazySupplier.of(() -> "returned by...");
// 为这个接口动态生成一个实现类,比如
public class DynamicInnerClass_1 implements LazySupplier<String> {
@Override String get() {
invokedynamic OurCustomBSM;
// 在这里手动填入indy
// 那么当我们的代码里遇见 sls.get() 的时候
// 会触发 BSM
// 那么 BSM会帮我们做一些处理,上面的 indy就会消失了
return "returned by...";
}
}
相信这段简单的代码就可以很好的解释这个工具类的核心了,那么我们还需要实现的就是:
- 类生成逻辑
- BSM
在文章最后我会贴完整的代码,本来我是想贴个github链接的,但是我的github还有很多其他杂七杂八的东西,就不贴了。这里就简单的解释一下大致的逻辑:
java
private static final Map<String, Supplier<?>> supplierMap;
public static Class<?> generateClass() {
// LazySupplierMetafactory.class.getPackage() + LazySupplier.class.getName() + _1
String cName = getGeneratedClassName();
// todo:这里还需要把supplier存到一个map里面
// 借助 Java 24的class file api
// 大致步骤为
// 1. 定义父接口为 LazySupplier
// 2. 创建 get 方法的实现 里面的指令就是 invokedynamic
byte[] classByte = generateClassByte(cName);
return MethodHandles.lookup().defineClass(classByte)
.newInstance();
}
目的应该还是比较清晰直观的。这里需要提到一点,就是注释说到的,把Supplier
暂存到一个map中,存起来是为了后续调用BSM的时候,以全局唯一的classname当做key来获取对应的Supplier
来生成对象。所以上面的代码缺了个map的定义,大家知道就好。
那么最后就是对应的BSM,也是比较简单的,大概如下:
java
public static CallSite indyBSM(Lookup lk, String name, MethodType mt) {
Class<?> c = lk.lookupClass();
// supplierRegistry就是前面说的那个map
Supplier<?> s = supplierRegistry.remove(c.getName());
Objects.requireNonNull(s);
Object preComputingInst = s.get();
return new ConstantCallSite(MethodHandles.constant(mt.returnType(),
preComputingInst));
}
同样,尽量简单的表达大致的框架即可,可以看到和我最前面截图出来的,lambda的实现的那部分代码非常的类似。
那么到这里,我们实现的这个LazySupplier
接口就实现了。在代码里,我们不需要硬编码它的实现类,而是在运行期间让vm为我们动态的创建实现类,并借助vm处理indy指令的线程安全性来避免手动管理并发问题。
但是,这种方法的缺点就在于,我们这里是直接定义的普通类,而不是lambda的那种hidden class,这里直接改成hidden class也可以,只是稍微修改一下字节码即可。这里就不做实现了,大家可以自行去试试。
这里可能大伙有个疑问就是,如果是hidden class,它可以直接去访问Metafactory里面的map吗?答案是可以的,相关的权限处理问题都被lookup给处理掉了。关于hidden class,更多的也需要大伙自行去了解。
还有一个点就在于,可能会有人觉得,为什么不做成:所有LazySupplier
的实现类共用同一个,把Supplier
做成构造参数,这样就可以避免类过多的问题。但是,这种做法是不可行的,因为indy
实际上绑定的是类的行为,简单来说就是,这种实现方法只会对代码中的第一个LazySupplier.get
调用有效,后续所有关联了其他Supplier
的实现类都是不能正确返回想要的对象的,大家也可以自行去实验一下。这里就不多说了。
总结
至此,个人认为关于indy
的内容,已经基本介绍完毕了,其实关于这个opcode,更多的还是了解即可,在大部分的开发场景下并不需要使用到这一个字节码,但是它对Java的意义还是挺大的,因为正如前文所说,indy
赋予了Java更强的动态能力,并且可以通过一些巧妙的手段为我们日常开发提供一些巧思。
完整的LazySupplier
java
public interface LazySupplier<T> extends Supplier<T> {
public static LazySupplier<T> of(Supplier<T> s) {
return LazySupplierMetafactory.of(s);
}
}
public class LazySupplierMetafactory {
private static final ConcurrentHashMap<String, Supplier<?>> supplierRegistry = new ConcurrentHashMap<>();
private static final AtomicInteger counter = new AtomicInteger();
private static final String baseName = LazySupplier.class.getName() + "Impl$";
public static <T> LazySupplier<T> of(Supplier<T> s) {
String curClassName = className();
supplierRegistry.put(curClassName, s);
try {
Class<?> generatedClass = generateClass(curClassName);
return (LazySupplier)(generatedClass.getDeclaredConstructor().newInstance());
} catch (Exception e) {
supplierRegistry.remove(curClassName);
throw new RuntimeException(e);
}
}
private static String className() {
return baseName + counter.getAndIncrement();
}
private static final ClassDesc cdLazySingleton = ClassDesc.of(LazySupplier.class.getName());
private static final ClassDesc cdBsmProvider = ClassDesc.of(LazySupplierMetafactory.class.getName());
private static Class<?> generateClass(String cName) throws Exception {
final ClassDesc cdThisClass = ClassDesc.of(cName);
byte[] classBytes = ClassFile.of().build(cdThisClass, classBuilder -> {
classBuilder.withFlags(ClassFile.ACC_PUBLIC | ClassFile.ACC_FINAL | ClassFile.ACC_SYNTHETIC)
.withInterfaceSymbols(cdLazySingleton);
generatePublicNoArgConstructor(classBuilder);
generateGetImplByIndy(classBuilder);
});
return MethodHandles.lookup().defineClass(classBytes);
}
private static void generatePublicNoArgConstructor(ClassBuilder clb) {
clb.withMethodBody(
ConstantDescs.INIT_NAME,
ConstantDescs.MTD_void,
ClassFile.ACC_PUBLIC,
cob -> {
cob.aload(0);
cob.invokespecial(ConstantDescs.CD_Object, ConstantDescs.INIT_NAME, ConstantDescs.MTD_void);
cob.return_();
});
}
private static void generateGetImplByIndy(ClassBuilder clb) {
var bsmHandle = MethodHandleDesc.ofMethod(
DirectMethodHandleDesc.Kind.STATIC,
cdBsmProvider,
"indyBootstrap",
MethodTypeDesc.of(ConstantDescs.CD_CallSite, ConstantDescs.CD_MethodHandles_Lookup, ConstantDescs.CD_String, ConstantDescs.CD_MethodType)
);
var bsmCSD = DynamicCallSiteDesc.of(
bsmHandle,
"get",
MethodTypeDesc.of(ConstantDescs.CD_Object));
clb.withMethodBody(
"get",
MethodTypeDesc.of(ConstantDescs.CD_Object),
ClassFile.ACC_PUBLIC,
codeBuilder -> {
codeBuilder.invokedynamic(bsmCSD);
codeBuilder.areturn();
}
);
}
public static CallSite indyBootstrap(MethodHandles.Lookup lookup, String name, MethodType type) {
Class<?> generatedClass = lookup.lookupClass();
String className = generatedClass.getName();
Supplier<?> supplier = supplierRegistry.get(className);
if (supplier == null) {
// 一般到不了这里
throw new RuntimeException("Cannot find supplier for " + className);
}
Object inst = supplier.get();
return new ConstantCallSite(MethodHandles.constant(type.returnType(), inst));
}
}
这里其实还有一个问题,就是这里的indyBootstrap
方法是有优化空间的,这里就留给大伙自行解决了,相信大家在知道indy
的具体步骤之后,就可以很快的发现优化空间在哪,该怎么优化了。
最后再提一点,这一个只是一个小工具,仅供参考,大家可以参考这个去实现自己想要的工具。