Spring AOP (三) CGLIB 動態(tài)代理

在深入理解之前,我們先來看一個簡單的例子。

首先,導(dǎo)入 CGLIB 的 Maven 依賴。

<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.2.11</version>
</dependency>

Spring AOPorg.springframework.cglib 包中包含了 CGLIB 的相關(guān)代碼(和 CGLIB Maven 依賴中代碼的一樣,版本可能不同),所以也可以選擇導(dǎo)入 Spring AOP 的 Maven 依賴。

<!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aop</artifactId>
  <version>5.1.6.RELEASE</version>
</dependency>

這里選擇導(dǎo)入 CGLIB 依賴(因為 Spring 框架中的 cglib 不能下載源碼)。

然后,定義一個 Service 類,其有兩個方法并且其中一個方法用 final 來修飾。

public class Service {

    /**
     *  final 方法不能被子類覆蓋
     */
    public final void finalMethod() {
        System.out.println("Service.finalMethod 執(zhí)行了");
    }

    public void publicMethod() {
        System.out.println("Service.publicMethod 執(zhí)行了");
    }
}

接下來,定義一個 MethodInterceptor 的實現(xiàn)類。

public class CglibDynamicProxy implements MethodInterceptor {

    /**
     * 目標(biāo)對象(也被稱為被代理對象)
     */
    private Object target;

    public CglibDynamicProxy(Object target) {
        this.target = target;
    }
    /**
     *
     * @param obj       CGLIB 生成的代理對象
     * @param method    被代理對象方法
     * @param args      方法入?yún)?     * @param proxy     方法代理
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("CglibDynamicProxy intercept 方法執(zhí)行前-------------------------------");

        System.out.println("obj = " + obj.getClass());
        System.out.println("method = " + method);
        System.out.println("proxy = " + proxy);

        Object object = proxy.invoke(target, args);
        System.out.println("CglibDynamicProxy intercept 方法執(zhí)行后-------------------------------");
        return object;
    }

    /**
     * 獲取被代理接口實例對象
     * 
     * 通過 enhancer.create 可以獲得一個代理對象,它繼承了 target.getClass() 類
     *
     * @param <T>
     * @return
     */
    public <T> T getProxy() {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(target.getClass());
        enhancer.setCallback(this);
        return (T) enhancer.create();
    }
}

通過 Client 組合上述代碼,進行測試。

public class Client {
    public static void main(String[] args) {
        // 1. 構(gòu)造目標(biāo)對象
        Service target = new Service();

        // 2. 根據(jù)目標(biāo)對象生成代理對象
        CglibDynamicProxy proxy = new CglibDynamicProxy(target);

        // 獲取 CGLIB 代理類
        Service proxyObject = proxy.getProxy();

        // 調(diào)用代理對象的方法
        proxyObject.finalMethod();
        proxyObject.publicMethod();
    }
}

測試結(jié)果如下所示:

測試結(jié)果.jpg

源碼解析

上文中的是通過 enhancer.create 方法調(diào)用獲取的代理對象,以此為入口深入探究一下 CGLIB 動態(tài)代理的實現(xiàn)原理。

// 生成代理類名稱是用到了 SOURCE 字段
private static final Source SOURCE = new Source(Enhancer.class.getName());

public Enhancer() {
    super(SOURCE);
}

public Object create() {
  return createHelper();
}

private Object createHelper() {
  Object key = ...;
  Object result = super.create(key);
  return result;
}

private static volatile Map<ClassLoader, ClassLoaderData> CACHE = new WeakHashMap<ClassLoader, ClassLoaderData>();

protected Object create(Object key) {
    Map<ClassLoader, ClassLoaderData> cache = CACHE;
    ClassLoaderData data = cache.get(loader);
    
    // 也就是 ClassLoaderData 的 get 方法
    Object obj = data.get(this, getUseCache());
    if (obj instanceof Class) {
      return firstInstance((Class) obj);
    }
    return nextInstance(obj);
}

接下來我們關(guān)注 ClassLoaderDataget 方法的邏輯。

public Object get(AbstractClassGenerator gen, boolean useCache) {
  if (!useCache) {
    return gen.generate(ClassLoaderData.this);
  } else {
    Object cachedValue = generatedClasses.get(gen);
    return gen.unwrapCachedValue(cachedValue);
  }
}

AbstractClassGenerator 類中的 generate 方法邏輯如下所示:

// 代理類字節(jié)碼的默認(rèn)生成策略
private GeneratorStrategy strategy = DefaultGeneratorStrategy.INSTANCE;

// 代理類的默認(rèn)命名策略
private NamingPolicy namingPolicy = DefaultNamingPolicy.INSTANCE;

protected Class generate(ClassLoaderData data) {
  Class gen;
  try {
    ClassLoader classLoader = data.getClassLoader();
    if (classLoader == null) {
      throw new IllegalStateException("ClassLoader is null while trying to define class " +
          getClassName() + ". It seems that the loader has been expired from a weak reference somehow. " +
          "Please file an issue at cglib's issue tracker.");
    }
    synchronized (classLoader) {
      // 1. 生成代理類的類名
      String name = generateClassName(data.getUniqueNamePredicate());              
      data.reserveName(name);
      this.setClassName(name);
    }
    // 2. 生成代理類字節(jié)碼的二進制數(shù)組
    byte[] b = strategy.generate(this);
    String className = ClassNameReader.getClassName(new ClassReader(b));
    ProtectionDomain protectionDomain = getProtectionDomain();
    synchronized (classLoader) { // just in case
      if (protectionDomain == null) {
        gen = ReflectUtils.defineClass(className, b, classLoader);
      } else {
        gen = ReflectUtils.defineClass(className, b, classLoader, protectionDomain);
      }
    }
    return gen;
}

private String generateClassName(Predicate nameTestPredicate) {
    return namingPolicy.getClassName(namePrefix, source.name, key, nameTestPredicate);
}

CGLIB 通過 DefaultNamingPolicy 類中的 getClassName 方法獲取到代理類的名稱,邏輯如下所示。

/**
 *  為代理類選擇一個類名
 * @param prefix  被代理類的全路徑名稱,比如這里的 com.wilimm.proxy.cglib.Service
 * @param source  生成代理類的全限定名(如 "net.sf.cglib.Enhance", "net.sf.cglib.reflect.FastClass")
 * @param key    表示參數(shù)狀態(tài)的關(guān)鍵對象; 要使緩存正常工作,相等的鍵應(yīng)該生成相同的生成類名。默認(rèn)策略將 key.hashCode() 合并到類名中。
 * @param names  如果給定的類名已經(jīng)在同一個 ClassLoader 中使用,則返回 true 的謂詞。
 * @return 全限定的代理類類名
 */
public String getClassName(String prefix, String source, Object key, Predicate names) {
  String base =
    prefix + "$$" + 
    source.substring(source.lastIndexOf('.') + 1) +
    getTag() + "$$" +
    Integer.toHexString(STRESS_HASH_CODE ? 0 : key.hashCode());
  String attempt = base;
  int index = 2;
  
  // 檢測生成的代理類名是否已經(jīng)生成,如果已經(jīng)生成,則需要添加后綴
  while (names.evaluate(attempt))
    attempt = base + "_" + index++;
  return attempt;
}

protected String getTag() {
    return "ByCGLIB";
}

我們簡單看一下 DefaultGeneratorStrategy 類中的 generate 方法,這是真正生成代理類的地方。

public byte[] generate(ClassGenerator cg) throws Exception {
  DebuggingClassWriter cw = getClassVisitor();
  // 生成代理類的地方,代碼邏輯過于復(fù)雜,暫時忽略
  transform(cg).generateClass(cw);
  return transform(cw.toByteArray());
}

最后,我們重點關(guān)注下 DebuggingClassWriter 類中的 toByteArray 方法,在這個方法中我們可以看到如何保存 CGLIB 代理類 Class 到文件中。

public static final String DEBUG_LOCATION_PROPERTY = "cglib.debugLocation";
    
private static String debugLocation;

static {
  debugLocation = System.getProperty(DEBUG_LOCATION_PROPERTY);
  if (debugLocation != null) {
    System.err.println("CGLIB debugging enabled, writing to '" + debugLocation + "'");
  }
}

public byte[] toByteArray() {
  return (byte[]) java.security.AccessController.doPrivileged(
  new java.security.PrivilegedAction() {
    public Object run() {
      byte[] b = ((ClassWriter) DebuggingClassWriter.super.cv).toByteArray();
      
      // 如果 DebuggingClassWriter.DEBUG_LOCATION_PROPERTY 系統(tǒng)屬性被設(shè)置,則輸出代理類到指定目錄
      if (debugLocation != null) {
        String dirs = className.replace('.', File.separatorChar);
        try {
          new File(debugLocation + File.separatorChar + dirs).getParentFile().mkdirs();
          
          File file = new File(new File(debugLocation), dirs + ".class");
          OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
          try {
            out.write(b);
          } finally {
            out.close();
          }
        } catch (Exception e) {
          throw new CodeGenerationException(e);
        }
      }
      return b;
     }  
    });
  }
}

CGLIB 生成的代理類剖析

由上文可知,把 DebuggingClassWriter.DEBUG_LOCATION_PROPERTY(也就是 cglib.debugLocation)系統(tǒng)屬性設(shè)置為當(dāng)前項目的根目錄,即可保存 CGLIB 生成的代理類到當(dāng)前項目根目錄下。

// 獲取當(dāng)前項目的根目錄
String userDir = System.getProperty("user.dir");
//System.setProperty("cglib.debugLocation", userDir);
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, userDir);

運行前文中 Clientmain 方法,即可在當(dāng)前項目根目錄下看到 CGLIB 生成的代理類 Class 文件,如下圖所示。

CGLIB 代理類.jpg

Service$$EnhancerByCGLIB$$bdabbd96 就是 CGLIB 生成的代理類,它繼承了 Service 類。

我們來簡單分析下 Service$$EnhancerByCGLIB$$bdabbd96 中的代碼結(jié)構(gòu)。

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.wilimm.proxy.cglib;

import java.lang.reflect.Method;
import net.sf.cglib.core.ReflectUtils;
import net.sf.cglib.core.Signature;
import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.Factory;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class Service$$EnhancerByCGLIB$$bdabbd96 extends Service implements Factory {
    private MethodInterceptor CGLIB$CALLBACK_0; // 攔截器
    private static final Method CGLIB$publicMethod$0$Method; // 被代理方法
    private static final MethodProxy CGLIB$publicMethod$0$Proxy; // 代理方法

    static void CGLIB$STATICHOOK1() {
        // 代理類
        Class var0 = Class.forName("com.wilimm.proxy.cglib.Service$$EnhancerByCGLIB$$bdabbd96");
        // 被代理類
        Class var1;
        CGLIB$publicMethod$0$Method = ReflectUtils.findMethods(new String[]{"publicMethod", "()V"}, (var1 = Class.forName("com.wilimm.proxy.cglib.Service")).getDeclaredMethods())[0];
        CGLIB$publicMethod$0$Proxy = MethodProxy.create(var1, var0, "()V", "publicMethod", "CGLIB$publicMethod$0");
    }
    
    // 代理方法(methodProxy.invokeSuper會調(diào)用)
    final void CGLIB$publicMethod$0() {
        super.publicMethod();
    }

    // 被代理方法(methodProxy.invoke 會調(diào)用,這就是為什么在攔截器中調(diào)用 methodProxy.invoke 會死循環(huán),一直在調(diào)用攔截器)
    public final void publicMethod() {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (var10000 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }

        if (var10000 != null) {
            // 調(diào)用攔截器
            var10000.intercept(this, CGLIB$publicMethod$0$Method, CGLIB$emptyArgs, CGLIB$publicMethod$0$Proxy);
        } else {
            super.publicMethod();
        }
    }
    
    static {
        CGLIB$STATICHOOK1();
    }
}

MethodProxy 類非常關(guān)鍵,我們分析一下它的內(nèi)部邏輯。

public class MethodProxy {
    private Signature sig1;
    private Signature sig2;
    private CreateInfo createInfo;
    
    private final Object initLock = new Object();
    private volatile FastClassInfo fastClassInfo;
    
    /**
     *
     * @param c1  被代理對象 Class
     * @param c2  CGLIB 代理對象 Class
     * @param desc 入?yún)㈩愋?     * @param name1 有攔截器邏輯的方法名 publicMethod
     * @param name2 直接調(diào)用父類方法的方法名 CGLIB$publicMethod$0
     * @return
     */
    public static MethodProxy create(Class c1, Class c2, String desc, String name1, String name2) {
        MethodProxy proxy = new MethodProxy();
        proxy.sig1 = new Signature(name1, desc);
        proxy.sig2 = new Signature(name2, desc);
        proxy.createInfo = new CreateInfo(c1, c2);
        return proxy;
    }
    
    private void init() {
          CreateInfo ci = createInfo;
          FastClassInfo fci = new FastClassInfo();
          // 1. 生成被代理類 FastClass,在這里是 Service$$FastClassByCGLIB$$fdf36b96
          fci.f1 = helper(ci, ci.c1);
          // 2. 生成代理類 FastClass,在這里是 Service$$EnhancerByCGLIB$$bdabbd96$$FastClassByCGLIB$$a112cdb2
          fci.f2 = helper(ci, ci.c2);
          // 3. 得到被代理類 FastClass 中的 publicMethod 方法簽名
          fci.i1 = fci.f1.getIndex(sig1);
          // 4. 得到代理類 FastClass 中的 CGLIB$publicMethod$0 方法簽名
          fci.i2 = fci.f2.getIndex(sig2);
          fastClassInfo = fci;
          createInfo = null;
    }
    
    private static class FastClassInfo {
        FastClass f1; //被代理類 FastClass,在這里是 Service$$FastClassByCGLIB$$fdf36b96
        FastClass f2; //代理類 FastClass,在這里是 Service$$EnhancerByCGLIB$$bdabbd96$$FastClassByCGLIB$$a112cdb2
        int i1; // 被代理類 FastClass 中的 publicMethod方法簽名(index)
        int i2; // 代理類 FastClass 中的 CGLIB$publicMethod$0 的方法簽名
    }
    
    /**
     * 在相同類型的不同對象上調(diào)用原始方法。如果傳入的是代理對象,則調(diào)用的是 CGLIB 代理類重寫的方法     
     * @param obj 方法調(diào)用的對象,如果使用 MethodInterceptor 的 intercept 方法的第一個參數(shù),將導(dǎo)致遞歸
     * @param args 參數(shù)列表
     */
    public Object invoke(Object obj, Object[] args) throws Throwable {
        try {
            init();
            FastClassInfo fci = fastClassInfo;
            // 調(diào)用 Service$$FastClassByCGLIB$$fdf36b96 類中的 publicMethod 方法
            return fci.f1.invoke(fci.i1, obj, args);
        } catch (InvocationTargetException e) {
            throw e.getTargetException();
        } catch (IllegalArgumentException e) {
            if (fastClassInfo.i1 < 0)
                throw new IllegalArgumentException("Protected method: " + sig1);
            throw e;
        }
    }

    /**
     * 在代理對象上調(diào)用父類的原始方法
     * @param obj CGLIB 生成的代理對象
     * @param args 參數(shù)列表
     */
    public Object invokeSuper(Object obj, Object[] args) throws Throwable {
        try {
            init();
            FastClassInfo fci = fastClassInfo;
            // 調(diào)用 Service$$EnhancerByCGLIB$$bdabbd96$$FastClassByCGLIB$$a112cdb2 類中的 CGLIB$publicMethod$0 方法
            return fci.f2.invoke(fci.i2, obj, args);
        } catch (InvocationTargetException e) {
            throw e.getTargetException();
        }
    }
}

從上面的代碼分析中,我們可以得知 MethodProxy 類有兩個主要的方法。

  • invoke:調(diào)用代理類中被調(diào)用的方法,即 Service$$EnhancerByCGLIB$$bdabbd96 中的 publicMethod 方法。根據(jù) Java 多態(tài)特性可知:

    • 如果果傳入代理對象,即調(diào)用代理類的增強方法,
    • 如果傳入目標(biāo)對象,調(diào)用未增強的目標(biāo)類的目標(biāo)方法。
  • invokeSuper: 直接在代理類中調(diào)用父類的方法,即 Service$$EnhancerByCGLIB$$bdabbd96 中的 CGLIB$publicMethod$0 方法。

FastClass 機制

還記得之前兩個 FastClass 文件嗎?

  • Service$$FastClassByCGLIB$$fdf36b96 是被代理類的 FastClass。
  • Service$$EnhancerByCGLIB$$bdabbd96$$FastClassByCGLIB$$a112cdb2 是代理類的 FastClass。

CGLIB 動態(tài)代理執(zhí)行代理方法效率之所以比JDK 動態(tài)代理高,是因為 CGLIB 采用了 FastClass 機制。

下面我們先來看一下被代理類的 FastClass Service$$FastClassByCGLIB$$fdf36b96。

package com.wilimm.proxy.cglib;

import java.lang.reflect.InvocationTargetException;
import net.sf.cglib.core.Signature;
import net.sf.cglib.reflect.FastClass;

public class Service$$FastClassByCGLIB$$fdf36b96 extends FastClass {
    public Service$$FastClassByCGLIB$$fdf36b96(Class var1) {
        super(var1);
    }
    
    // 根據(jù)方法簽名,獲取到 FastClass 中的方法 index 值
    public int getIndex(Signature var1) {
        String var10000 = var1.toString();
        switch(var10000.hashCode()) {
        case -560613858:
            if (var10000.equals("finalMethod()V")) {
                return 1;
            }
            break;
        case -433379701:
            if (var10000.equals("publicMethod()V")) {
                return 0;
            }
            break;
        }

        return -1;
    }
    
     /**
     * 調(diào)用 FastClass 中的方法
     * @param var1 方法 index 值
     * @param var2 方法所在的類的對象
     * @param var3 參數(shù)列表
     */
    public Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException {
        // 強制轉(zhuǎn)換 Object 為 Service 類型,從而可以直接調(diào)用 Service 中定義的方法
        Service var10000 = (Service)var2;
        int var10001 = var1;

        try {
            switch(var10001) {
            case 0:
                var10000.publicMethod();
                return null;
            case 1:
                var10000.finalMethod();
                return null;
            case 2:
                return new Boolean(var10000.equals(var3[0]));
            case 3:
                return var10000.toString();
            case 4:
                return new Integer(var10000.hashCode());
            }
        } catch (Throwable var4) {
            throw new InvocationTargetException(var4);
        }

        throw new IllegalArgumentException("Cannot find matching method/constructor");
    }
}

然后我們再來看一下代理類的 FastClass Service$$EnhancerByCGLIB$$bdabbd96$$FastClassByCGLIB$$a112cdb2。

package com.wilimm.proxy.cglib;

import com.wilimm.proxy.cglib.Service..EnhancerByCGLIB..bdabbd96;
import java.lang.reflect.InvocationTargetException;
import net.sf.cglib.core.Signature;
import net.sf.cglib.proxy.Callback;
import net.sf.cglib.reflect.FastClass;

public class Service$$EnhancerByCGLIB$$bdabbd96$$FastClassByCGLIB$$a112cdb2 extends FastClass {
    public Service$$EnhancerByCGLIB$$bdabbd96$$FastClassByCGLIB$$a112cdb2(Class var1) {
        super(var1);
    }
    
    // 根據(jù)方法簽名,獲取到 FastClass 中的方法 index 值  
    public int getIndex(Signature var1) {
        String var10000 = var1.toString();
        switch(var10000.hashCode()) {
        case -560613858:
            if (var10000.equals("finalMethod()V")) {
                return 21;
            }
            break;
        case -433379701:
            if (var10000.equals("publicMethod()V")) {
                return 9;
            }
            break;
        case 920107676:
            if (var10000.equals("CGLIB$publicMethod$0()V")) {
                return 16;
            }
            break;
        return -1;
    }

   /**
     * 調(diào)用 FastClass 中的方法
     * @param var1 方法 index 值
     * @param var2 方法所在的類的對象
     * @param var3 參數(shù)列表
     */
    public Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException {
        // 強制轉(zhuǎn)換 Object 為 bdabbd96 代理類類型,從而可以直接調(diào)用 代理類中定義的方法
        bdabbd96 var10000 = (bdabbd96)var2;
        int var10001 = var1;

        try {
            switch(var10001) {
            case 0:
                return new Boolean(var10000.equals(var3[0]));
            case 1:
                return var10000.toString();
            case 2:
                return new Integer(var10000.hashCode());
            case 3:
                return var10000.clone();
            case 4:
                return var10000.newInstance((Class[])var3[0], (Object[])var3[1], (Callback[])var3[2]);
            case 5:
                return var10000.newInstance((Callback[])var3[0]);
            case 6:
                return var10000.newInstance((Callback)var3[0]);
            case 7:
                var10000.setCallbacks((Callback[])var3[0]);
                return null;
            case 8:
                var10000.setCallback(((Number)var3[0]).intValue(), (Callback)var3[1]);
                return null;
            case 9:
                var10000.publicMethod();
                return null;
            case 10:
                bdabbd96.CGLIB$SET_THREAD_CALLBACKS((Callback[])var3[0]);
                return null;
            case 11:
                bdabbd96.CGLIB$SET_STATIC_CALLBACKS((Callback[])var3[0]);
                return null;
            case 12:
                return var10000.getCallbacks();
            case 13:
                return var10000.getCallback(((Number)var3[0]).intValue());
            case 14:
                return bdabbd96.CGLIB$findMethodProxy((Signature)var3[0]);
            case 15:
                bdabbd96.CGLIB$STATICHOOK1();
                return null;
            case 16:
                var10000.CGLIB$publicMethod$0();
                return null;
            case 17:
                return var10000.CGLIB$clone$4();
            case 18:
                return new Integer(var10000.CGLIB$hashCode$3());
            case 19:
                return var10000.CGLIB$toString$2();
            case 20:
                return new Boolean(var10000.CGLIB$equals$1(var3[0]));
            case 21:
                var10000.finalMethod();
                return null;
            }
        } catch (Throwable var4) {
            throw new InvocationTargetException(var4);
        }

        throw new IllegalArgumentException("Cannot find matching method/constructor");
    }
}

FastClass 的原理簡單來說就是:為不需要反射 invoke 調(diào)用的原類型生成一個 FastClass 類,然后給原類型的方法分配一個 index,在生成的 FastClass 中的 invoke 方法中,先直接把 Object 強制轉(zhuǎn)換為原類型,然后根據(jù)這個 index,就可以直接定位要調(diào)用的方法直接進行調(diào)用,這樣省去了反射調(diào)用,所以調(diào)用效率比 JDK 動態(tài)代理通過反射調(diào)用高。

至此,我們基本上了解了 CGLIB 動態(tài)代理的實現(xiàn)原理。

CGLIB 和 JDK 動態(tài)代理區(qū)別

說完了 CGLIB 動態(tài)代理和 JDK 動態(tài)代理之后,我們總結(jié)一下兩者的區(qū)別:

  • JDK 動態(tài)代理基于接口,CGLIB 動態(tài)代理基于類。因為 JDK 動態(tài)代理生成的代理類需要繼承 java.lang.reflect.Proxy,而 Java 只支持單繼承,所以只能基于接口。

  • JDK 動態(tài)代理和 CGLIB 動態(tài)代理都是在運行期生成字節(jié)碼,JDK 是直接寫 Class 字節(jié)碼,CGLIB 使用 ASM 框架寫 Class 字節(jié)碼。

  • JDK 通過反射機制調(diào)用方法,CGLIB 通過 FastClass 機制直接調(diào)用方法,所以 CGLIB 執(zhí)行的效率更高。

(正文完)

本文所用代碼地址

擴展閱讀

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容