背景

 JDK 动态代理存在的一些问题:

调用效率低

 JDK 通过反射实现动态代理调用,这意味着低下的调用效率:

  1. 每次调用 Method.invoke() 都会检查方法的可见性、校验参数是否匹配,过程涉及到很多 native 调用,具体参见JNI 调用开销

  2. 反射调用涉及动态类解析,这种不可预测性,导致被反射调用的代码无法被 JIT 内联优化,具体参见反射调用方法

 可以通过java.lang.invoke.MethodHandle来规避以上问题,但是这不在本文讨论的范围。

只能代理接口

 java.lang.reflect.Proxy只支持通过接口生成代理类,这意味着 JDK 动态代理只能代理接口,无法代理具体的类。

 对于一些外部依赖或者现有模块来说,无法通过该方式实现动态代理。

应用场景

 CGLib 是一款用于实现高效动态代理的字节码增强库,通过字节码生成技术,动态编译生成代理类,从而将反射调用转换为普通的方法调用。

 下面通过两个案例体验一下 CGLib 的使用方式。

案例一:Weaving

 现有一个输出问候语句的类 Greet,现在有个新需求:在输出内容前后加上姓名,实现个性化输出。下面通过 CGLib 实现该功能:


class Greet { // 需要被增强目标类
public String hello() { return "hello"; }
public String hi() { return "hi"; }
public String toString() { return "@Greet"; }
} public class Weaving { // 模拟切面织入过程 // 增加 before: 前缀(模拟前置通知)
static MethodInterceptor adviceBefore = (target, method, args, methodProxy) -> "before:" + methodProxy.invokeSuper(target, args); // 增加 :after 后缀(模拟后置通知)
static MethodInterceptor adviceAfter = (target, method, args, methodProxy) -> methodProxy.invokeSuper(target, args) + ":after"; // 通知
static Callback[] advices = new Callback[] { NoOp.INSTANCE/*默认*/, adviceBefore, adviceAfter }; // 切入点
static CallbackFilter pointCut = method -> {
switch (method.getName()) {
case "hello" : return 1; // hello() 方法植入前置通知
case "hi" : return 2; // hi() 方法植入后置通知
default: return 0; // 其他方法不添加通知
}
}; public static void main(String[] args) throws InterruptedException {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Greet.class); // 设置目标类
enhancer.setCallbacks(advices); // 设置通知 Advice
enhancer.setCallbackFilter(pointCut); // 设置切点 PointCut
Greet greet = (Greet) enhancer.create(); // 创建 Proxy 对象
System.out.println(greet.hello());
System.out.println(greet.hi());
System.out.println(greet.toString());
TimeUnit.HOURS.sleep(1);
}
}

案例二:Introduction

 随着业务发展,系统需要支持法语的问候 FranceGreet,在不修改现有业务代码的前提下,可以通过 CGLib 实现该功能:

interface FranceGreet { // 支持新功能的接口
String bonjour();
} class FranceGreeting implements Dispatcher { // 新接口的实现
private final FranceGreet delegate = () -> "bonjour";
@Override
public Object loadObject() throws Exception {
return delegate;
}
} class FranceGreetingMatcher implements CallbackFilter { // 将新接口调用委托给 Dispatcher
@Override
public int accept(Method method) {
return method.getDeclaringClass().equals(FranceGreet.class) ? 1 : 0;
}
} public class Introduction { // 模拟引入新接口 public static void main(String[] args) throws InterruptedException {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Greet.class);
enhancer.setInterfaces(new Class[]{FranceGreet.class}); // 扩展新接口
enhancer.setCallbacks(new Callback[]{ NoOp.INSTANCE, new FranceGreeting()}); // 实现新接口
enhancer.setCallbackFilter(new FranceGreetingMatcher()); // 关联接口与实现
Greet greet = (Greet) enhancer.create();
System.out.println(greet.hello()); // 原方法不受影响
System.out.println(greet.hi());
FranceGreet franceGreet = (FranceGreet) greet;
System.out.println(franceGreet.bonjour()); // 新接口方法正常调用
TimeUnit.HOURS.sleep(1);
}
}

原理简析

 从前面的案例可以看到,CGLib 使用的方式很简单,大致可以分为两步:

  1. 配置 Enhancer
  • 设置需要代理的目标类与接口
  • 通过 Callback 设置需要增强的功能
  • 通过 CallbackFilter 将方法匹配到具体的 Callback
  1. 创建代理对象
  • 通过 CallbackFilter 获取方法与 Callback 的关联关系
  • 继承目标类并重写override方法,在调用代码中嵌入 Callback
  • 编译动态生成的字节码生成代理类
  • 通过反射调用构造函数生成代理对象

Callback 分类

 此外,CGLib 支持多种 Callback,这里简单介绍几种:

  • NoOp 不使用动态代理,匹配到的方法不会被重写
  • FixedValue 返回固定值,被代理方法的返回值被忽略
  • Dispatcher 指定上下文,将代理方法调用委托给特定对象
  • MethodInterceptor 调用拦截器,用于实现环绕通知around advice

 其中 MethodInterceptor 最为常用,可以实现多种丰富的代理特性。

 但这类 Callback 也是其中最重的,会导致生成更多的动态类,具体原因后续介绍。

字节码生成过程

 底层通过 Enhancer.generateClass() 生成代理类,其具体过程不作深究,可以简单概括为:

  1. 通过ClassVisitor获取目标类信息
  2. 通过ClassEmitter调用 asm 库注入增强方法,并生成byte[] 形式的字节码
  3. 通过反射调用ClassLoader.defineClass()byte[]转换为Class对象
  4. 将生成完成的代理类缓存至LoadingCache,避免重复生成

生成的类结构

 通过 arthas 的 jad 命令可以观察到,案例 Weaving 中实际生成了以下类:

  • 目标类:buttercup.test.Greet
  • 代理类:buttercup.test.Greet$$EnhancerByCGLIB(省略后缀)
  • 目标类 FastClass:buttercup.test.Greet$$FastClassByCGLIB(省略后缀)
  • 代理类 FastClass:buttercup.test.Greet$$EnhancerByCGLIB$$FastClassByCGLIB(省略后缀)

代理类

 代理类就是 Ehancer.create() 中为了创建代理对象动态生成的类,该类不但继承了目标类,并且还重写了需要被代理的方法。其命名规则为:目标类 + $$EnhancerByCGLIB

 在案例一中,我们分别给 Greet.hello()Greet.hi() 分别添加了拦截器Weaving.adviceBeforeWeaving.adviceAfter,下面我们分析代理类是如何完成这一功能的:

public class Greet$$EnhancerByCGLIB extends Greet implements Factory {

  private static final Object[] CGLIB$emptyArgs = new Object[0]; // 默认空参数
private static final Callback[] CGLIB$STATIC_CALLBACKS; // 静态 Callback(忽略)
private static final ThreadLocal CGLIB$THREAD_CALLBACKS; // 用于给构造函数传递 Callback // 通过 MethodProxy 代理 Greet.hell() 方法
private static final Method CGLIB$hello$0$Method;
private static final MethodProxy CGLIB$hello$0$Proxy; // 通过 MethodProxy 代理 Greet.hi() 方法
private static final Method CGLIB$hi$1$Method;
private static final MethodProxy CGLIB$hi$1$Proxy; private boolean CGLIB$BOUND; // 判断 Callback 是否已经初始化
private NoOp CGLIB$CALLBACK_0; // 默认不拦截,直接调用目标类方法
private MethodInterceptor CGLIB$CALLBACK_1; // Weaving.adviceBefore(增加 before: 前缀)
private MethodInterceptor CGLIB$CALLBACK_2; // Weaving.adviceAfter(增加 :after 后缀) static {
Greet$$EnhancerByCGLIB.CGLIB$STATICHOOK1();
} static void CGLIB$STATICHOOK1() { // 静态初始化
CGLIB$THREAD_CALLBACKS = new ThreadLocal();
Class<?> clazz = Class.forName("buttercup.test.Greet");
Class<?> clazz2 = Class.forName("buttercup.test.Greet$$EnhancerByCGLIB");
Method[] methodArray = ReflectUtils.findMethods(new String[]{"hello", "()Ljava/lang/String;", "hi", "()Ljava/lang/String;"}, clazz.getDeclaredMethods());
CGLIB$hello$0$Method = methodArray[0];
CGLIB$hello$0$Proxy = MethodProxy.create(clazz, clazz2, "()Ljava/lang/String;", "hello", "CGLIB$hello$0");
CGLIB$hi$1$Method = methodArray[1];
CGLIB$hi$1$Proxy = MethodProxy.create(clazz, clazz2, "()Ljava/lang/String;", "hi", "CGLIB$hi$1");
} // 通过 ThreadLocal 传参
public static void CGLIB$SET_THREAD_CALLBACKS(Callback[] callbackArray) {
CGLIB$THREAD_CALLBACKS.set(callbackArray);
} // 工厂方法,创建增强后的对象
public Object newInstance(Callback[] callbackArray) {
Greet$$EnhancerByCGLIB.CGLIB$SET_THREAD_CALLBACKS(callbackArray);
Greet$$EnhancerByCGLIB Greet$$EnhancerByCGLIB = new Greet$$EnhancerByCGLIB();
Greet$$EnhancerByCGLIB.CGLIB$SET_THREAD_CALLBACKS(null);
return Greet$$EnhancerByCGLIB;
} // 重写 hello() 方法通知 CALLBACK_1
public final String hello() {
MethodInterceptor methodInterceptor = this.CGLIB$CALLBACK_1;
if (methodInterceptor == null) { // 初始化 CALLBACK_1
Greet$$EnhancerByCGLIB.CGLIB$BIND_CALLBACKS(this);
methodInterceptor = this.CGLIB$CALLBACK_1;
}
if (methodInterceptor != null) { // 调用拦截器 Weaving.adviceBefore
return (String)methodInterceptor.intercept(this, CGLIB$hello$0$Method, CGLIB$emptyArgs, CGLIB$hello$0$Proxy);
}
return super.hello();
} // 重写 hi() 方法通知 CALLBACK_2
public final String hi() {
MethodInterceptor methodInterceptor = this.CGLIB$CALLBACK_2;
if (methodInterceptor == null) { // 初始化 CALLBACK_2
Greet$$EnhancerByCGLIB.CGLIB$BIND_CALLBACKS(this);
methodInterceptor = this.CGLIB$CALLBACK_2;
}
if (methodInterceptor != null) { // 调用拦截器 Weaving.adviceAfter
return (String)methodInterceptor.intercept(this, CGLIB$hi$1$Method, CGLIB$emptyArgs, CGLIB$hi$1$Proxy);
}
return super.hi();
} // 直接调用目标类的 hello()
final String CGLIB$hello$0() {
return super.hello();
} // 直接调用目标类的 hi()
final String CGLIB$hi$1() {
return super.hi();
} public Greet$$EnhancerByCGLIB() {
Greet$$EnhancerByCGLIB.CGLIB$BIND_CALLBACKS(this);
} private static final void CGLIB$BIND_CALLBACKS(Object object) {
block2: {
Object object2;
Greet$$EnhancerByCGLIB Greet$$EnhancerByCGLIB;
block3: {
Greet$$EnhancerByCGLIB = (Greet$$EnhancerByCGLIB) object;
// 如果已经初始化过,则直接返回
if (Greet$$EnhancerByCGLIB.CGLIB$BOUND) break block2;
Greet$$EnhancerByCGLIB.CGLIB$BOUND = true;
if (object2 = CGLIB$THREAD_CALLBACKS.get()) != null) break block3; // 从 ThreadLocal 获取 Callback 参数
if ((object2 = CGLIB$STATIC_CALLBACKS) == null) break block2;
}
Callback[] callbackArray = (Callback[])object2; // 初始化 Callback 参数
Greet$$EnhancerByCGLIB greet$$EnhancerByCGLIB = Greet$$EnhancerByCGLIB;
greet$$EnhancerByCGLIB.CGLIB$CALLBACK_2 = (MethodInterceptor)callbackArray[2];
greet$$EnhancerByCGLIB.CGLIB$CALLBACK_1 = (MethodInterceptor)callbackArray[1];
greet$$EnhancerByCGLIB.CGLIB$CALLBACK_0 = (NoOp)callbackArray[0];
}
}
}

 在动态生成的类中,可以看到 CGLib 为每个被代理的方法创建了 MethodProxy 对象。

 该对象替代了 Method.invoke() 功能,是实现高效方法调用的的关键。下面我们以 Greet.hello() 为例对该类进行分析:

public class MethodProxy {

  private Signature sig1; // 目标类方法签名:hello()Ljava/lang/String;
private Signature sig2; // 代理类方法签名:CGLIB$hello$0()Ljava/lang/String; private MethodProxy.CreateInfo createInfo; /* 省略初始化过程 */ private static class CreateInfo {
Class c1; // 目标类 buttercup.test.Greet
Class c2; // 代理类 buttercup.test.Greet$$EnhancerByCGLIB
} private final Object initLock = new Object();
private volatile MethodProxy.FastClassInfo fastClassInfo; private static class FastClassInfo {
FastClass f1; // 目标类 FastClass :
FastClass f2; // 代理类 FastClass :
int i1; // 方法在 f1 中对应的索引
int i2; // 方法在 f2 中对应的索引
} // 只在 MethodProxy 被调用时加载 FastClass,减少不必要的类生成(lazy-init)
private void init() {
if (fastClassInfo == null) {
synchronized (initLock) {
if (fastClassInfo == null) {
MethodProxy.FastClassInfo fci = new MethodProxy.FastClassInfo();
fci.f1 = helper(ci, ci.c1); //
fci.f2 = helper(ci, ci.c2); //
fci.i1 = fci.f1.getIndex(this.sig1);
fci.i2 = fci.f2.getIndex(this.sig2);
fastClassInfo = new FastClassInfo(); }
}
}
} // 根据 Class 对象生成 FastClass
private static FastClass helper(MethodProxy.CreateInfo ci, Class type) {
Generator g = new Generator();
g.setType(type);
return g.create();
} // 调用 buttercup.test.Greet.hello()
// 但实际上会调用代理类的 EnhancerByCGLIB.hello() 实现
public Object invoke(Object obj, Object[] args) throws Throwable {
init();
return fastClassInfo.f1.invoke(fci.i1, obj, args);
} // 调用 buttercup.test.Greet$$EnhancerByCGLIB.CGLIB$hello$0()
// 通过 super.hello() 调用目标类的 Greet.hello() 实现
public Object invokeSuper(Object obj, Object[] args) throws Throwable {
init();
return fastClassInfo.f2.invoke(fci.i2, obj, args);
}
}

 可以看到 MethodProxy 的调用实际是通过 FastClass 完成的,这是 CGLib 实现高性能反射调用的秘诀,下面来解析这个类的细节。

目标类 FastClass

 为了规避反射带来的性能消耗,cglib 定义了 FastClass 来实现高效的方法调用,其主要职责有两个

  1. 方法映射:解析 Class 对象并为每个 Constructor 与 Method 指定一个整数索引值 index
  2. 方法调用:通过 switch(index) 的方式,将反射调用转化为硬编码调用
abstract public class FastClass {

    // 映射:根据方法名称与参数类型,获取其对应的 index
public abstract int getIndex(String methodName, Class[] argClass); // 调用:根据 index 找到指定的方法,并进行调用
public abstract Object invoke(int index, Object obj, Object[] args) throws InvocationTargetException; }

其命名规则为:目标类 + $$FastClassByCGLIB。下面具体分析一下对目标类 Greet 对应的 FastClass

public class Greet$$FastClassByCGLIB extends FastClass {

    public Greet$$FastClassByCGLIB(Class clazz) {
super(clazz);
} // 获取 index 的最大值
public int getMaxIndex() {
return 4; // 当前 FastClass 总共支持 5 个方法
//索引值分别为 0:hello(), 1:hi(), 2:equals(), 3:hasCode(), 4:toString()
} // 根据方法名称以及参数类型,获取到指定方法对应的 index
public int getIndex(String methodName, Class[] argClass) {
switch (methodName.hashCode()) {
case 3329: {
if (!methodName.equals("hi")) break;
switch (argClass.length) {
case 0: { return 1; } // hi() 对应 index 为 1
}
break;
}
case 99162322: {
if (!methodName.equals("hello")) break;
switch (argClass.length) {
case 0: { return 0; } // hello() 对应 index 为 0
}
break;
}
/* 忽略 Object 方法 */
}
return -1;
} // 根据方法签名,获取到指定方法对应的 index
public int getIndex(Signature signature) {
String sig = ((Object)signature).toString();
switch (sig.hashCode()) {
case 397774237: {
if (!sig.equals("hello()Ljava/lang/String;")) break;
return 0; // hello() 对应 index 为 0
}
case 1155503180: {
if (!sig.equals("hi()Ljava/lang/String;")) break;
return 1; // hi() 对应 index 为 1
}
/* 忽略 Object 方法 */
}
return -1;
} // 方法调用(硬编码调用)
public Object invoke(int n, Object obj, Object[] args) throws InvocationTargetException {
Greet greet = (Greet) obj;
switch (n) { // 通过 index 指定目标函数
case 0: { return greet.hello(); } // 通过索引 0 调用 hello()
case 1: { return greet.hi(); } // 通过索引 1 调用 hi()
/* 忽略 Object 方法 */
}
throw new IllegalArgumentException("Cannot find matching method/constructor");
} // 构造函数(硬编码调用)
public Object newInstance(int n, Object[] argClass) throws InvocationTargetException {
switch (n) {
case 0: { return new Greet(); }
}
throw new IllegalArgumentException("Cannot find matching method/constructor");
}
}

代理类 FastClass

 之前提及过:使用 MethodInterceptor 会比其他 Callback 生成更多的动态类,这是因为需要支持 MethodProxy.invokeSuper() 调用:

public interface MethodInterceptor extends Callback {

    // 所有生成的代理方法都调用此方法
// 大多数情况需要通过 MethodProxy.invokeSuper() 来实现目标类的调用
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable;
}

 MethodProxy.invokeSuper() 通过调用代理类中带 $CGLIB$ 前缀的方法,绕过被重写的代理方法,避免出现无限递归。

 为了保证调用效率,需要对代理类也生成 FastClass

public class Greet$$EnhancerByCGLIB$$FastClassByCGLIB extends FastClass {

    public Object invoke(int n, Object obj, Object[] args) throws InvocationTargetException {
Greet$$EnhancerByCGLIB greet$$EnhancerByCGLIB = (Greet$$EnhancerByCGLIB)obj;
switch (n) {
case 9: { // 调用目标类的原始 Greet.hello() 方法
return greet$$EnhancerByCGLIB.CGLIB$hello$0();
}
case 10: { // 调用目标类的原始 Greet.hi() 方法
return greet$$EnhancerByCGLIB.CGLIB$hi$1();
}
/* 忽略其他 Enhancer 方法 */
}
throw new IllegalArgumentException("Cannot find matching method/constructor");
} public int getIndex(String methodName, Class[] argClass) {
switch (methodName.hashCode()) {
case 1837078673: {
if (!methodName.equals("CGLIB$hi$1")) break;
switch (argClass.length) {
case 0: { return 10; }
}
break;
}
case 1891304123: {
if (!methodName.equals("CGLIB$hello$0")) break;
switch (argClass.length) {
case 0: { return 9; }
}
break;
}
/* 忽略其他 Enhancer 方法 */
}
return -1;
} public int getIndex(Signature signature) {
String sig = ((Object)signature).toString();
switch (sig.hashCode()) {
case -1632605946: {
if (!sig.equals("CGLIB$hello$0()Ljava/lang/String;")) break;
return 9;
}
case 540391388: {
if (!sig.equals("CGLIB$hi$1()Ljava/lang/String;")) break;
return 10;
}
/* 忽略其他 Enhancer 方法 */
}
return -1;
} }

补充

 案例 Introduction 中仅使用了 Dispatcher,因此只生成了代理类,未使用到 FastClass

public class Greet$$EnhancerByCGLIB extends Greet implements FranceGreet, Factory {

    private NoOp CGLIB$CALLBACK_0;
private Dispatcher CGLIB$CALLBACK_1; public final String bonjour() {
Dispatcher dispatcher = this.CGLIB$CALLBACK_1;
if (dispatcher == null) {
Greet$$EnhancerByCGLIB.CGLIB$BIND_CALLBACKS(this);
dispatcher = this.CGLIB$CALLBACK_1;
}
return ((FranceGreet)dispatcher.loadObject()).bonjour();
} /* 忽略多余的属性与方法 */
}

 本文案例仅涉及 MethodInterceptorDispatcher,这两个 Callback 也是 Spring AOP 实现的关键,后续将继续分析相关的源码实现。

附录

JIT 编译优化

CGLib 简析的更多相关文章

  1. 简析.NET Core 以及与 .NET Framework的关系

    简析.NET Core 以及与 .NET Framework的关系 一 .NET 的 Framework 们 二 .NET Core的到来 1. Runtime 2. Unified BCL 3. W ...

  2. 简析 .NET Core 构成体系

    简析 .NET Core 构成体系 Roslyn 编译器 RyuJIT 编译器 CoreCLR & CoreRT CoreFX(.NET Core Libraries) .NET Core 代 ...

  3. RecycleView + CardView 控件简析

    今天使用了V7包加入的RecycleView 和 CardView,写篇简析. 先上效果图: 原理图: 这是RecycleView的工作原理: 1.LayoutManager用来处理RecycleVi ...

  4. Java Android 注解(Annotation) 及几个常用开源项目注解原理简析

    不少开源库(ButterKnife.Retrofit.ActiveAndroid等等)都用到了注解的方式来简化代码提高开发效率. 本文简单介绍下 Annotation 示例.概念及作用.分类.自定义. ...

  5. PHP的错误报错级别设置原理简析

    原理简析 摘录php.ini文件的默认配置(php5.4): ; Common Values: ; E_ALL (Show all errors, warnings and notices inclu ...

  6. Android 启动过程简析

    首先我们先来看android构架图: android系统是构建在linux系统上面的. 所以android设备启动经历3个过程. Boot Loader,Linux Kernel & Andr ...

  7. Android RecycleView + CardView 控件简析

    今天使用了V7包加入的RecycleView 和 CardView,写篇简析. 先上效果图: 原理图: 这是RecycleView的工作原理: 1.LayoutManager用来处理RecycleVi ...

  8. Java Annotation 及几个常用开源项目注解原理简析

    PDF 版: Java Annotation.pdf, PPT 版:Java Annotation.pptx, Keynote 版:Java Annotation.key 一.Annotation 示 ...

  9. 【ACM/ICPC2013】POJ基础图论题简析(一)

    前言:昨天contest4的惨败经历让我懂得要想在ACM领域拿到好成绩,必须要真正的下苦功夫,不能再浪了!暑假还有一半,还有时间!今天找了POJ的分类题库,做了简单题目类型中的图论专题,还剩下二分图和 ...

随机推荐

  1. pointnet.pytorch代码解析

    pointnet.pytorch代码解析 代码运行 Training cd utils python train_classification.py --dataset <dataset pat ...

  2. IOS自动化测试环境搭建(Python & Java)

         一.前言 IOS的App自动化测试与Android的一样,也可以用appium来进行.但是IOS自动化依赖苹果的osx系统.Xcode构建等,且封闭的系统需要苹果开发者账号才可以驱动真机.A ...

  3. 「Leetcode-算法_Easy461」通过「简单」题目学习位运算

    Easy 461.汉明距离 因为原题目翻译效果不佳,这里是笔者自己的理解. 输入两个二进制数 x.y, 输出将 y 变为 x 所需改变的二进制位数,成为汉明距离. 注意: 0 ≤ x, y < ...

  4. 小马哥的 Java 项目实战营学习笔记(1)

    小马哥的 Java 项目实战营 小马哥的 Java 项目实战营 第二节:数据存储之 JDBC JDBC 核心 API 数据源 接口 - javax.sql.DataSource获取方式 1.普通对象初 ...

  5. vue页面初始化

    HTML: <div id="app"> <input type="" class="app" v-model=" ...

  6. git config 配置简写命令

    在多人协作开发时,一般用git来进行代码管理. git有一些命令如:git pull . git push等等,这些命令可以设置alias,也就是缩写. 如:git pull 是 git pl, gi ...

  7. 使用账号密码来操作github? NO!

    目录 简介 背景介绍 创建令牌 使用令牌 缓存令牌 使用GCM 总结 简介 最近在更新github文件的时候,突然说不让更新了,让我很是困惑,原因是在2021年8月13号之后,github已经不让直接 ...

  8. 【Java】jeesite使用学习

    初始配置环境及软件: 名称 版本 作用 Tomcat 7.0 微小型服务器,版本无所谓,装个Tomcat 9估计也没事 IntelliJ IDEA 2021.1.3 x64 2021.1.3 编译器, ...

  9. 【笔记】偏差方差权衡 Bias Variance Trade off

    偏差方差权衡 Bias Variance Trade off 什么叫偏差,什么叫方差 根据下图来说 偏差可以看作为左下角的图片,意思就是目标为红点,但是没有一个命中,所有的点都偏离了 方差可以看作为右 ...

  10. uniapp 实现信息推送(App)

    废话不多说直接上代码 以下代码需写在onlaunch生命周期内 onlaunch(){// onlaunch应用级生命周期 :当uni-app 初始化完成时触发(全局只触发一次) //#ifdef A ...