dubbo源码分析之八-dubbo的spi机制

dubbo源码分析之八-dubbo的spi机制本片博文以 spi 机制为切入点 首先了解 spi 机制 介绍了 java 自己的 SPI 实现方式和源码分析 并通过其缺点引出了 Dubbo 的 SPI 机制 通过对其使用示例 流程 源码分析等详细分析了其相关流程 其中对 ExtensionLoa Adaptive Activate SPI 注解进行使用介绍 希望这边文章能让读者很好的了解 javaSPI 和 Dubbo 的 SPI 机制

一、前言

​ 在之前的dubbo源码分析我们以及了解了dubbo相关的架构、用法和原理,但是提到dubbo我们就不得不提其中spi机制,dubbo源码中使用了大量的spi机制,其所有的核心组件都做成了基于spi的实现方式,比如Protocol层=>DubboProtocol,Cluster层=>FailoverCluster等,spi这种机制可以实现这个组件的插拔式使用(一个接口多种实现,可以随时调整实现方式)同时支持我们进行定制化扩展。

下图为dubbo使用spi形式引入的内部组件

在这里插入图片描述

下面就让我们一起来探究一下dubbo的spi相关机制。

二、java SPI

​ 在介dubbo的spi之前我们需要先了解一下什么是spi以及jdk默认支持的spi机制。

2.1、什么是SPI

​ SPI的全名为Service Provider Interface,模块之间相互调用基于接口编程,需要为某个接口寻找服务实现将装配的控制权移到程序之外的机制。

要使用Java SPI,需要遵循如下约定:

  • 1、当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
  • 2、接口实现类所在的jar包放在主程序的classpath中;
  • 3、主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
  • 4、SPI的实现类必须携带一个不带参数的构造方法;

像我们熟悉的java的jdbc,jdk定义了一套接口规范,不同的数据库厂商提供不同的实现。

mysql数据厂商实现:在这里插入图片描述

2.2、java SPI 示范例

项目结构
在这里插入图片描述

spi-api: 提供接口规范

public interface SpiService { 
    void say(); } 

spi-impl1、spi-impl2: 提供不同实现

public class SpiServiceImplOne implements SpiService { 
    public void say() { 
    System.out.println("i am SpiServiceImplOne"); } } 

其他实现类类似

spi配置 不同实现都有相似配置

在这里插入图片描述
在这里插入图片描述

@Test public void testSpi(){ 
    ServiceLoader<SpiService> serviceLoader = ServiceLoader.load(SpiService.class); for (SpiService o : serviceLoader) { 
    o.say(); } } 

测试结果

在这里插入图片描述

介绍java提供的spi机制、实例、源码分析,优缺点 引申出dubbo的spi机制

2.3、java spi源码解析

public static <S> ServiceLoader<S> load(Class<S> service) { 
    //获取classLoader ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); } public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) { 
    return new ServiceLoader<>(service, loader); } private ServiceLoader(Class<S> svc, ClassLoader cl) { 
    //获取接口类 service = Objects.requireNonNull(svc, "Service interface cannot be null"); //获取类记载器 loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; //安全管理 acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; //重新家在 reload(); } public void reload() { 
    //清空缓存中的接口对应的实例对象 providers.clear(); //创建LazyIterator 该对象是用来循环遍历实例化接口实现类的 lookupIterator = new LazyIterator(service, loader); } 

从测试类ServiceLoader的load()开始溯源,最终其使用两个参数对象 服务接口的class和类加载器创建LazyIterator对象,该对象是实际进行服务实例化的,其实现了Iterator迭代器hasNext()=>hasNextService()next()=>nextService().

private boolean hasNextService() { 
    if (nextName != null) { 
    return true; } if (configs == null) { 
    try { 
    //获取spi配置文件路径 classPath /META-INF/service/${接口全限定类名} String fullName = PREFIX + service.getName(); //根据配置文件路径记载配置文件到configs if (loader == null) configs = ClassLoader.getSystemResources(fullName); else configs = loader.getResources(fullName); } catch (IOException x) { 
    fail(service, "Error locating configuration files", x); } } //读取并解析配置文件获取其中的配置信息(配置信息可以是多个实现类是一个列表)则pending为接口实现名的列表迭代对象 while ((pending == null) || !pending.hasNext()) { 
    if (!configs.hasMoreElements()) { 
    return false; } pending = parse(service, configs.nextElement()); } //从迭代器获取一个接口实现 使用nextName接收 nextName = pending.next(); return true; } 

hasNextSerive实现判断是否还有需要实例化的接口实现,其中有三个熟悉关注

  1. nextName: 需要进行实例化的接口实现类全限定类名 从pending迭代器中获取到
  2. pending:从配置文件中解析出来的所有实现接口全限定类名列表集合(迭代器形式)
  3. configs: 根据classPath /META-INF/service/${接口全限定类名}夹在的URL对象
//获取服务实例对象 private S nextService() { 
    //省略相关校验 try { 
    //使用实现类的空构造器创建服务接口对象 S p = service.cast(c.newInstance()); //放入缓存中 providers.put(cn, p); return p; } catch (Throwable x) { 
    fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); // This cannot happen } 

该方法简单主要是使用接口实现类的空构造器(所以java SPI接口实现类需要遵循提供一个空构造器约束)

2.4、java SPI的优劣

​ 在使用和源码分析后,我们能大概总结出其优缺点

优点:

使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件,从而很好的支持可插拔和变更扩展。

缺点:

ServiceLoader只能完全遍历并全部进行实例化,不能很好的按需加载(遇到接口实现类实例化特别耗费资源情况)会有性能损失和不灵活

多个并发多线程使用ServiceLoader类的实例是不安全的。

所以dubbo并没有完全采用javaSPI(只是做了兼容),而是自己实现类一套SPI机制,下面我们来看看Dubbo的spi机制。

三、dubbo SPI

​ dubbo在原有的spi基础上主要有以下的改变,①配置文件采用键值对配置的方式,使用起来更加灵活和简单通过@SPI实现按需加载 增强了原本SPI的功能,使得SPI具备ioc和aop的功能,这在原本的java中spi是不支持的。dubbo的spi是通过ExtensionLoader来解析的,通过ExtensionLoader来加载指定的实现类,

配置文件的路径在META-INF/dubbo路径下

我们先来看一下 Dubbo 对配置文件目录的约定,不同于 Java SPI ,Dubbo 分为了三类目录。

  1. META-INF/services/ 目录:该目录下的 SPI 配置文件是为了用来兼容 Java SPI 。
  2. META-INF/dubbo/ 目录:该目录存放用户自定义的 SPI 配置文件。
  3. META-INF/dubbo/internal/ 目录:该目录存放 Dubbo 内部使用的 SPI 配置文件。

3.1、示例

​ Dubbo的SPI例子的和Java的项目架构相似,接口以及实现类似只是需要在接口中使用dubbo的SPI相关注解

  1. 接口
@SPI("two") public interface SpiDemo { 
    //动态自适应扩展注解 @Adaptive void getSpi(URL url); } 
  1. 配置
    在这里插入图片描述

  2. 测试
@Test public void testSpi(){ 
    ExtensionLoader<SpiDemo> extensionLoader = ExtensionLoader.getExtensionLoader(SpiDemo.class); URL url = URL.valueOf("test://123.5.5.5/test"); //普通扩展 //从全部的实现类中根据spi名字获取一个 SpiDemo extension = extensionLoader.getExtension("two"); extension.getSpi(url); //输出SpiDemoImpl two //自适应扩展 //默认获取 @SPI中的value属性作为参数 SpiDemo adaptiveExtension = extensionLoader.getAdaptiveExtension(); adaptiveExtension.getSpi(url); //输出SpiDemoImpl two //@SPI中参数为two,url中设置的参数为one 则使用one对应的SpiDemoImplOne url = URL.valueOf("test://123.5.5.5/test?spi.demo=one"); adaptiveExtension = extensionLoader.getAdaptiveExtension(); adaptiveExtension.getSpi(url); //输出SpiDemoImpl one //@Activate 会获取满足条件注解中条件的一组接口实现 一般在Dubbo的过滤器中使用较多 List<SpiDemo> myWorks = extensionLoader.getActivateExtension(url, "", "myWork"); for(SpiDemo spiDemo:myWorks){ 
    spiDemo.getSpi(url); } } 
@Adaptive注解根据Dubbo 的URL相关参数动态的选择具体的实现 通过该getAdaptiveExtension获取 该项目默认参数设置 spi.demo(接口名SpiDemo的驼峰逆转为spi.demo) 如果没有设置则默认参数为@Spi注解的value值即 spi.demo=two 即使用SpiDemoImplOne实现类 如果设置protocol://host:port/name?spi.demo=one 即使用SpiDemoImplTwo实现类 
Activate注解表示一个扩展是否被激活(使用),可以放在类定义和方法(本文不讲)上,dubbo将它标注在spi的扩展类上,表示这个扩展实现激活条件和时机。它有两个设置过滤条件的字段,group,value 都是字符数组。 用来指定这个扩展类在什么条件下激活 

有关DUBBO相关SPI使用可参考:https://www.jianshu.com/p/dcce98

3.2、dubbo原理解析

dubbo扩展的核心代码如下:

1.ExtensionLoader.getExtensionLoader(xxx.class).getExtension(name);

2.ExtensionLoader.getExtensionLoader(xxx.class).getAdaptiveExtension();

3.ExtensionLoader.getExtensionLoader(xxx.class).getActivateExtension(url, key);

getExtensionLoader方法

Dubbo SPI为每一个SPI接口都创建一个ExtensionLoader并放入对应的缓存中,每次获取都先从缓存中获取对应的Loader 没有则创建一个新的并放入缓存中,其中获取扩展有三种类型

  1. 一种是普通类型的扩展实现类获取,直接通过class实例化
  2. 一种是自适应的扩展实现类获取,主要是通过DubboURL中的参数动态获取实现类,对应的类或者方法会使用@Adaptive注解修饰,使用该注解修饰的类dubbo编译过程中会生成XXXx$adaptive代理类。
  3. 一种是选择符合dub boUrl的某种条件的一组接口实现类,这些类使用@Adative注解修饰,主要在dubbo的过滤器中使用较多。

3.2.1、getExtension方法

和getExtensionLoader类似 先从缓存中获取@SPI注解value对应的接口实现,没有则调用createExtension()创建

createExtension方法

private T createExtension(String name) { 
    //从我们上面说的三个目录中加载接口实现类的Class 并按照名字获取 //获取class属性比较复杂此处额外讲解 Class<?> clazz = getExtensionClasses().get(name); //找不到抛出异常 if (clazz == null) { 
    throw findException(name); } try { 
    //从缓存中获取实例对象 没有反射创建并放入缓存中 T instance = (T) EXTENSION_INSTANCES.get(clazz); if (instance == null) { 
    EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance()); instance = (T) EXTENSION_INSTANCES.get(clazz); } //使用IOC的形式为该是实例通过setXXX()形式进行依赖注入 injectExtension(instance); //如果有包装类(以Wrapper结尾)获取进行对该实力进行包装(一些通用的逻辑可以放在包装类中 因为包装类都会持有 // 原始对象执行完后会继续调用原始对象) Set<Class<?>> wrapperClasses = cachedWrapperClasses; if (wrapperClasses != null && !wrapperClasses.isEmpty()) { 
    for (Class<?> wrapperClass : wrapperClasses) { 
    //对包装类进行实例化和依赖注入 instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance)); } } return instance; } catch (Throwable t) { 
    //异常处理 } } 

该方法中包含了获取接口实例的主要流程。包括class实例获取缓存获取,反射实例化,依赖注入,包装类包装整体流程会在源码分析完成后整体属性。下面我们来关注一下如何获取到符合条件的class的getExtensionClasses,这个是我们进行接口实例化的基础。

instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance)); 

WrapperClass – AOP

包装类是因为一个扩展接口可能有多个扩展实现类,而这些扩展实现类会有一个相同的或者公共的逻辑,如果每个实现类都写一遍代码就重复了,此处dubbo设置出来了包装类统一的通用逻辑在此处实现类似于spring的AOP思想。

injectExtension -IOC

该方法主要用于将实例化的接口实现类的相关依赖给注入进来,类似于spring的IOC。

private T injectExtension(T instance) { 
    try { 
    if (objectFactory != null) { 
    for (Method method : instance.getClass().getMethods()) { 
    //针对实现类的使用公有的setXXX(xxx)方法注入相关依赖 if (method.getName().startsWith("set") && method.getParameterTypes().length == 1 && Modifier.isPublic(method.getModifiers())) { 
    Class<?> pt = method.getParameterTypes()[0]; try { 
    //通过setXXX 获取对应的属性名xxx String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : ""; //调用Objecttfactory去获取属性值(objectFactory获取也使用了SPI机制) Object object = objectFactory.getExtension(pt, property); if (object != null) { 
    //反射调用填充数据 method.invoke(instance, object); } } catch (Exception e) { 
    //省略错误异常处理 } } } } } catch (Exception e) { 
    //省略错误异常处理 } return instance; } 

injectExtension 方法将属性通过set方法注入,获取属性值的方式是使用Objectfactory.getExtension() 如果和spring整合则会使用实现类SpringExtensionFactory对象从spring容器中获取对象

public <T> T getExtension(Class<T> type, String name) { 
    for (ApplicationContext context : contexts) { 
    if (context.containsBean(name)) { 
    Object bean = context.getBean(name); if (type.isInstance(bean)) { 
    return (T) bean; } } } return null; } 

getExtensionClasses方法

套路一样缓存中获取没有则调用loadExtensionClasses()方法加载,该方法加载会调用loadDirectory()方法从我们上面说的三个目录分别加载对应目录下的配置信息并通过loadResource()方法进行全部配置文件加载最终通过loadClass()方法加载具体的class

loadClass()

/ * 加载所有使用@SPI注解修饰在配置文件中声明的接口实现class * @param extensionClasses 解析出来的扩展类缓存集合 * @param resourceURL 对应某个spi配置文件(在该方法中没作用只是用来日志打印) * @param clazz 扩展类实现 * @param name 扩展类的名字 key(name)=value(clazz的名字) */ private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) { 
    if (!type.isAssignableFrom(clazz)) { 
    throw new IllegalStateException("Error when load extension class(interface: " + type + ", class line: " + clazz.getName() + "), class " + clazz.getName() + "is not subtype of interface."); } //如果该类被Adaptive注解修饰,则将该类存放cachedAdaptiveClass中 // 这种机制应该是@Adaptive注解只能修饰一个接口类型实现类 if (clazz.isAnnotationPresent(Adaptive.class)) { 
    if (cachedAdaptiveClass == null) { 
    cachedAdaptiveClass = clazz; } else if (!cachedAdaptiveClass.equals(clazz)) { 
    throw new IllegalStateException("More than 1 adaptive class found: " + cachedAdaptiveClass.getClass().getName() + ", " + clazz.getClass().getName()); } //如果是包装类则wrappers中加入 } else if (isWrapperClass(clazz)) { 
    Set<Class<?>> wrappers = cachedWrapperClasses; if (wrappers == null) { 
    cachedWrapperClasses = new ConcurrentHashSet<Class<?>>(); wrappers = cachedWrapperClasses; } wrappers.add(clazz); } else { 
    //普通类 clazz.getConstructor(); //获取名字,没有生成(怎么可能没有名字,不都是键值对吗?) if (name == null || name.length() == 0) { 
    name = findAnnotationName(clazz); if (name == null || name.length() == 0) { 
    if (clazz.getSimpleName().length() > type.getSimpleName().length() && clazz.getSimpleName().endsWith(type.getSimpleName())) { 
    name = clazz.getSimpleName().substring(0, clazz.getSimpleName().length() - type.getSimpleName().length()).toLowerCase(); } else { 
    throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL); } } } String[] names = NAME_SEPARATOR.split(name); if (names != null && names.length > 0) { 
    //如果类上使用@Activate注解修饰则将该类页也放入cachedActivates缓存中 //之后将name 和class作为key-value存放到extensionClasses集合中 Activate activate = clazz.getAnnotation(Activate.class); if (activate != null) { 
    cachedActivates.put(names[0], activate); } for (String n : names) { 
    if (!cachedNames.containsKey(clazz)) { 
    cachedNames.put(clazz, n); } Class<?> c = extensionClasses.get(n); if (c == null) { 
    extensionClasses.put(n, clazz); } else if (c != clazz) { 
    //省略异常 } } } } } private boolean isWrapperClass(Class<?> clazz) { 
    try { 
    //type 是对应的接口类对象class clazz的属性中包含接口对象则说明该类是一个包装类 //在dubbo中所有的Wrapper类都会持有一个接口类对象 clazz.getConstructor(type); return true; } catch (NoSuchMethodException e) { 
    return false; } } 

该方法中主要针对class的不同类型将其放入不同的缓存对象中,有四种缓存class类型。

  1. 对于使用@Adaptive注解修饰的类放入cachedAdaptiveClass对象中,每一个接口类只能有一个实现类使用@Adaptive注解修饰,使用该注解修饰getAdaptiveExtensions则不会根据dubbo URL中参数选择实现类,而是使用该注解修饰的类实现。
  2. 对于包装类将其放入cachedWrapperClasses列表中
  3. 普通类存放到extensionClasses中,如果该类也被@Activate注解修饰也会将其放入cachedActivates map集合中

3.2.2、getAdaptiveExtension()方法

​ 与getExtension()方法类似先从其对应的缓存中获取,没有调用createAdaptiveExtension()方法

return injectExtension((T) getAdaptiveExtensionClass().newInstance()); private Class<?> getAdaptiveExtensionClass() { 
    getExtensionClasses(); if (cachedAdaptiveClass != null) { 
    return cachedAdaptiveClass; } return cachedAdaptiveClass = createAdaptiveExtensionClass(); } 

getAdaptiveExtensionClass() 获取对应的class类,获取的类有两种方式一种是之前使用@Adaptive注解修饰在类上 通过getExtensionClasses()方法存放到cachedAdaptiveClass中的class,另一种是使用使用@Adaptive注解修饰在方法上,这里Dubbo框架会自动生成XXXX#Adaptive代理类 如下:

public class SpiDemo$Adaptive implements com.xiu.dubbo.service.SpiDemo { 
    public void getSpi(com.alibaba.dubbo.common.URL arg0) { 
    if (arg0 == null) throw new IllegalArgumentException("url == null"); com.alibaba.dubbo.common.URL url = arg0; //根据dubboURL中的参数test://123.5.5.5/test?spi.demo=two 生成一个名字 根据参数类型动态获取自适应的扩展实现 String extName = url.getParameter("spi.demo", "two"); if(extName == null) { 
    throw new IllegalStateException("Fail to get extension(com.xiu.dubbo.service.SpiDemo)" + " name from url(" + url.toString() + ") use keys([spi.demo])"); } com.xiu.dubbo.service.SpiDemo extension = (com.xiu.dubbo.service.SpiDemo)ExtensionLoader.getExtensionLoader( com.xiu.dubbo.service.SpiDemo.class).getExtension(extName); extension.getSpi(arg0); } } 

这里就体现出了dubbo提供的自适应扩展,它获取实现类可以通过Dubbo 的URL中的参数动态选择实现类,从而更加灵活。后面的流程也和getExtension类似,根据class空构造器创建实例对象,injectExtension()spring形式注入相关依赖对象。

3.2.2、getActivateExtension()方法

​ 获取使用@Activate注解修饰的符合条件的一组接口实现。

/ * 根据dubboURl的参数和分组信息 筛选使用@Activate注解修饰的一组接口实现 * @param url dubboURL * @param values 参数对应的值列表 * @param group 分组信息 * @return 符合条件的一组接口实现列表 */ public List<T> getActivateExtension(URL url, String[] values, String group) { 
    List<T> exts = new ArrayList<T>(); //将DUBBO的URL中查询条件的值作为names列表用于查找符合的实现 List<String> names = values == null ? new ArrayList<String>(0) : Arrays.asList(values); //先根据分组获取符合条件的(且不在nams中的防止重复获取) if (!names.contains(Constants.REMOVE_VALUE_PREFIX + Constants.DEFAULT_KEY)) { 
    //所有使用注解@Activate修饰的类会缓存到cachedActivates中 getExtensionClasses(); for (Map.Entry<String, Activate> entry : cachedActivates.entrySet()) { 
    String name = entry.getKey(); Activate activate = entry.getValue(); //符合分组且不在names中 if (isMatchGroup(group, activate.group())) { 
    T ext = getExtension(name); if (!names.contains(name) && !names.contains(Constants.REMOVE_VALUE_PREFIX + name) && isActive(activate, url)) { 
    exts.add(ext); } } } //排序 Collections.sort(exts, ActivateComparator.COMPARATOR); } List<T> usrs = new ArrayList<T>(); //查找符合names属性的实现 for (int i = 0; i < names.size(); i++) { 
    String name = names.get(i); if (!name.startsWith(Constants.REMOVE_VALUE_PREFIX) && !names.contains(Constants.REMOVE_VALUE_PREFIX + name)) { 
    if (Constants.DEFAULT_KEY.equals(name)) { 
    if (!usrs.isEmpty()) { 
    exts.addAll(0, usrs); usrs.clear(); } } else { 
    T ext = getExtension(name); usrs.add(ext); } } } //汇总到一起返回 if (!usrs.isEmpty()) { 
    exts.addAll(usrs); } return exts; } 

3.3、DUBBO spi执行流程

![请添加图片描述](https://img-blog.csdnimg.cn/ee8a33f3bd13433cafa301eebf60c6d3.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBAbGl1c2hhbmd6YWliZWlqaW5n,size_20,color_FFFFFF,t_70,g_se,x_16)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/203831.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月19日 下午9:29
下一篇 2026年3月19日 下午9:30


相关推荐

  • 深入了解Vue.js组件笔记

    深入了解Vue.js组件笔记

    2021年6月12日
    107
  • vue双向绑定失效_vue热更新失效

    vue双向绑定失效_vue热更新失效为什么会失效呢首先vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的实现方式是get和set方法然后是通过Object.defineProperty()来实现数据劫持的。然后呢要是,实现数据的双向绑定,首先要对数据进行劫持监听,因为写的代码没有被监听到,所以只能手动setthis.$set(obj,key,value)查找的资料:1.实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。2.实现一个订阅者Watche..

    2025年11月14日
    4
  • 汪滔留任,收购Moltbook,Meta的AI未来仍然是个迷

    汪滔留任,收购Moltbook,Meta的AI未来仍然是个迷

    2026年3月15日
    1
  • QListWidget的使用

    QListWidget的使用QListWidgetQListWidget类提供了一个基于item的列表小部件。QListWidget是一个方便的类,它提供了类似于QlistView所具有的列表视图,但是具有增加和删除的功能。QListWidget使用内部模型来管理列表中的每个QListWidgetItem。想要有更灵活的列表视图,请使用具有标准模型的QListView类。QlistWidget有两种方法追加数据,一种

    2022年5月3日
    79
  • 零拷贝是什么_file.copy()

    零拷贝是什么_file.copy()一、DMAio读写有两种方式:中断 DMA用户进程发起数据读取请求 系统调度为该进程分配cpu cpu向io控制器(ide,scsi)发送io请求 用户进程等待io完成,让出cpu 系统调度cpu执行其他任务 数据写入至io控制器的缓冲寄存器 缓冲寄存器满了向cpu发出中断信号 cpu读取数据至内存通过中断,cpu需要拷贝数据。2、DMA用户进程发起…

    2026年2月7日
    4
  • 什么是句柄

    什么是句柄一、百度百科解释:在文件I/O中,要从一个文件读取数据,应用程序首先要调用操作系统函数并传送文件名,并选一个到该文件的路径来打开文件。该函数取回一个顺序号,即文件句柄(filehandle),该

    2022年7月1日
    22

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号