假期学习--对象底层结构和继承链

news2024/9/23 11:19:34

OC本质底层实现转化其实都是C/C++代码。

OC对象的本质就是结构体。

NSObject底层是struct objc_object结构体 ;

在这里插入图片描述

struct objc_class : objc_object {

…省略无关代码

// Class ISA; //ISA(从objc_object继承过来的)

Class superclass; //指向其父类

cache_t cache; //缓存

class_data_bits_t bits; //类的数据

…省略无关代码

}

objc_class是继承objc_object的。

  • 每个类的底层都会有一个Class类的isa指针。
  • Class底层是struct objc_class *类型,NSObject底层是struct objc_object结构体,id底层是struct objc_object *类型。

NSObject底层实现的结构体里只有一个成员变量isa,又因为Class底层是struct objc_class *类型,所以 ***\*NSObject\*****的本质是****\*objc_class\****
可以看到gettersetter里是通过****首地址指针+对应成员变量的地址值指针的偏移量*的方式取和存的,最终通过(*(NSString **)还原为string类型。取值的过程就是:*先拿到当前成员变量的地址,再去取这个地址里面所存的值****。

objc_class 和 objc_object 有什么关系?

objc_object

objc_object是一个结构体,它代表了一个Objective-C中的对象实例。这个结构体中最重要的成员是isa指针,它指向对象所属的类(objc_class类型的指针)。isa的存在使得运行时系统能够识别对象的类型,并调用正确的类方法或实例方法。

objc_class

objc_class也是一个结构体,它描述了一个类的特性,包括其父类、方法列表、属性列表、协议列表、成员变量列表等。每个类都有一个对应的objc_class实例,这个实例包含了该类的全部元数据和行为信息。

objc_class继承自objc_object,也就是说objc_class本身也是objc_object的一种,具有isa指针。这意味着类本身也可以被视为对象,能够接收消息。此外,objc_classisa指针指向的是它的元类(Meta-Class),而元类负责存储类方法。

关系总结

  • objc_object是Objective-C中所有对象的基础结构,它允许运行时系统识别和操作对象。
  • objc_class描述了一个类的结构和行为,它本身也是objc_object的一种特殊形式,意味着类也可以被当作对象来对待。
  • 每个objc_object实例都有一个isa指针,指向其所属的objc_class
  • 每个objc_class实例也有一个isa指针,但它指向的是该类的元类,元类中包含了类方法。

通过这种方式,Objective-C实现了动态类型和动态绑定,允许在运行时根据对象的实际类型调用适当的方法。这种机制是Objective-C动态特性的基石,也是其与静态类型语言的主要区别之一。

补充:

objc_class继承自objc_objectobjc_object有isa属性,所以对象都有一个isa,isa 表示指向,来自于当前的objc_object

objc_object与对象的关系:
  • 所有对象都是以objc_object为模板继承过来的。
  • 所有对象都来自于NSObject,但是其底层是一个objc_object的结构体类型,所以objc_object与对象的关系是继承关系。

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=%2FUsers%2Fzhujingye%2FLibrary%2FContainers%2Fcom.yinxiang.Mac%2FData%2Ftmp%2Fcom.yinxiang.Mac%2FWebKitDnD.d2mNGh%2F7603BF5A-554A-4E80-B39C-E62C39943ECF.png&pos_id=img-yQ5SpFXm-172482600141

类结构

在这里插入图片描述

上面是runtime中的类结构关系图 ;

类是一个结构体,大致如下:

struct objc_class : objc_object {
    ...省略无关代码
    // Class ISA;  //ISA(从objc_object继承过来的)
    Class superclass;  //指向其父类
    cache_t cache;  //缓存
    class_data_bits_t bits;  //类的数据
    
	class_rw_t *data() const {
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
    ...省略无关代码
}  

3234234

继承自objc_object的isa指针,不仅实例对象中有,类对象中也有,占8字节。

isa指针的指向

对于每个类创建出来的对象都会默认有一个isa属性,保存类对象的地址,也就是class,通过class就可以查询到这个对象的属性和方法,协议等;

对于类来说,它的类对象就是其元类 ;

isa分两种类型(isa指针是什么含义的时候):

  • 指针型isa:64位的0或者1的整体内容代表所指向的Class的地址,也就是可以通过isa的内容来获得类对象的地址。
    • 非指针型isa:isa的值的部分代表Class的地址,之所以这样是因为我们在寻址过程中,只有三四十位数就可以保证我们寻找到所有Class地址了,多出来的位可以用来存储其他相关内容,来达到节省内存的目的。

isa的数据结构

每个OC对象都含有一个isa指针,__arm64__之前,isa仅仅是一个指针,保存着对象或类对象内存地址,在__arm64__架构之后,apple对isa进行了优化,变成了一个共用体(union)结构,同时使用位域来存储更多的信息。

union isa_t 
{
    Class cls;
    uintptr_t bits;
    struct {
         uintptr_t nonpointer        : 1;//->表示使用优化的isa指针
         uintptr_t has_assoc         : 1;//->是否包含关联对象
         uintptr_t has_cxx_dtor      : 1;//->是否设置了析构函数,如果没有,释放对象更快
         uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000 ->类的指针
         uintptr_t magic             : 6;//->固定值,用于判断是否完成初始化
         uintptr_t weakly_referenced : 1;//->对象是否被弱引用
         uintptr_t deallocating      : 1;//->对象是否正在销毁
         uintptr_t has_sidetable_rc  : 1;//1->在extra_rc存储引用计数将要溢出的时候,借助Sidetable(散列表)存储引用计数,has_sidetable_rc设置成1
        uintptr_t extra_rc          : 19;  //->存储引用计数,实际的引用计数减一,存储的是其对象以外的引用计数
    };
};

初始化isa(其实就是上面两种不同的isa指针初始化也不一样)

  • 通过cls初始化:
    nonpointer,存储着ClassMeta-Class对象的内存地址信息。
  • 通过bits初始化:
    nonpointer,进行一系列的初始化操作。

superclass

superclass指向该对象或该类的父类

cache

45345345

引入了bucket_t(散列表数组),cache_t哈希表结构,哈希表内部存储的bucket_tbucket_t中存储的是SELIMP的键值对。

(这里就是消息传递刚开始查找方法所在的缓存)

img

bits

class_data_bits_t作为属性bits的类型,也是个结构体

struct class_data_bits_t {
    friend objc_class;//这里声明objc_class为class_data_bits_t的友元类,使得objc_class可以访问class_data_bits_t中的私有方法
    uintptr_t bits;
    ......

结构体中的属性bits是无符号整数类型,能够存储指针。这通常意味着它与指针的大小相同。

这里面其实利用位域存储数据,简单来说因为这个bits跟指针大小相同,在iOS中指针大小是8个字节,也就是64位,但是通常单一数据无法用满这64位的,会有很多空位,造成空间浪费。通过位运算,把不同的数据按不同的位置存进这64位内存空间的不同位置里,这样就可以提高内存利用率。这里的bits其实就是这样的,他不只存储class_rw_t结构体指针,还存储其他的信息。所以这里的bits并不是结构体,而是一个无符号整数类型的指针 ;

对于类的方法列表,成员列表的等信息则存放在bits更深一层的class_rw_t指针里面

class_rw_t

4234234

protocols类分类中的协议

properties类分类中的属性

methods类分类中的方法

这三个数据结构是个二维数组

假如我们三个分类A、B、C,编译顺序A->B->C。这时会逆序遍历并打包成分类数组,分类C中的所有方法都在第一列竖列表中,存在二维数组的第1项。分类B中的所有方法都在第二列竖列表中,存在二维数组的第2项。分类A中的所有方法都在第三列竖列表中,存在二维数组的第3项。

总结:这个二维数组的每一项对应一个category所增加的方法 ;结合之后了解到的,也可以知道,这个二微数组中的方法都是在运行时动态添加的

下面是有关class_rw_t的源码:

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint16_t witness;
#if SUPPORT_INDEXED_ISA
    uint16_t index;
#endif
    explicit_atomic<uintptr_t> ro_or_rw_ext;
    Class firstSubclass;
    Class nextSiblingClass;
private:
    using ro_or_rw_ext_t = objc::PointerUnion<const class_ro_t *, class_rw_ext_t *>;
    const ro_or_rw_ext_t get_ro_or_rwe() const {
        return ro_or_rw_ext_t{ro_or_rw_ext};
    }
    void set_ro_or_rwe(const class_ro_t *ro) {
        ro_or_rw_ext_t{ro}.storeAt(ro_or_rw_ext, memory_order_relaxed);
    }
    void set_ro_or_rwe(class_rw_ext_t *rwe, const class_ro_t *ro) {
        // the release barrier is so that the class_rw_ext_t::ro initialization
        // is visible to lockless readers
        rwe->ro = ro;
        ro_or_rw_ext_t{rwe}.storeAt(ro_or_rw_ext, memory_order_release);
    }
    class_rw_ext_t *extAlloc(const class_ro_t *ro, bool deep = false);
public:
    void setFlags(uint32_t set)
    {
        __c11_atomic_fetch_or((_Atomic(uint32_t) *)&flags, set, __ATOMIC_RELAXED);
    }
    void clearFlags(uint32_t clear) 
    {
        __c11_atomic_fetch_and((_Atomic(uint32_t) *)&flags, ~clear, __ATOMIC_RELAXED);
    }
    // set and clear must not overlap
    void changeFlags(uint32_t set, uint32_t clear) 
    {
        ASSERT((set & clear) == 0);
        uint32_t oldf, newf;
        do {
            oldf = flags;
            newf = (oldf | set) & ~clear;
        } while (!OSAtomicCompareAndSwap32Barrier(oldf, newf, (volatile int32_t *)&flags));
    }
    class_rw_ext_t *ext() const {
        return get_ro_or_rwe().dyn_cast<class_rw_ext_t *>();
    }
    class_rw_ext_t *extAllocIfNeeded() {
        auto v = get_ro_or_rwe();
        if (fastpath(v.is<class_rw_ext_t *>())) {
            return v.get<class_rw_ext_t *>();
        } else {
            return extAlloc(v.get<const class_ro_t *>());
        }
    }
    class_rw_ext_t *deepCopy(const class_ro_t *ro) {
        return extAlloc(ro, true);
    }
    const class_ro_t *ro() const {
        auto v = get_ro_or_rwe();
        if (slowpath(v.is<class_rw_ext_t *>())) {
            return v.get<class_rw_ext_t *>()->ro;
        }
        return v.get<const class_ro_t *>();
    }
    void set_ro(const class_ro_t *ro) {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            v.get<class_rw_ext_t *>()->ro = ro;
        } else {
            set_ro_or_rwe(ro);
        }
    }
  ###
  ###
  
    const method_array_t methods() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>()->methods;
        } else {
            return method_array_t{v.get<const class_ro_t *>()->baseMethods()};
        }
    }
    const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>()->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>()->baseProperties};
        }
    }
    const protocol_array_t protocols() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>()->protocols;
        } else {
            return protocol_array_t{v.get<const class_ro_t *>()->baseProtocols};
        }
    }
};

对于最后三个方法,我们可以从名称大致猜到他们和保存的方法列表,属性列表和协议列表有关 ;

const method_array_t methods()为例,获取类的实例方法也是通过调用methods()来获取的。比如说runtime函数class_copyMethodList,其代码如下:

Method *
class_copyMethodList(Class cls, unsigned int *outCount)
{
    unsigned int count = 0;
    Method *result = nil;

    if (!cls) {
        if (outCount) *outCount = 0;
        return nil;
    }

    mutex_locker_t lock(runtimeLock);
    const auto methods = cls->data()->methods(); /
    
    ASSERT(cls->isRealized());

    count = methods.count();

    if (count > 0) {
        result = (Method *)malloc((count + 1) * sizeof(Method));
        
        count = 0;
        for (auto& meth : methods) {
            result[count++] = &meth;
        }
        result[count] = nil;
    }

    if (outCount) *outCount = count;
    return result;
}

对于上面的const auto methods = cls->data()->methods();,这里调用了methods()方法获取了方法列表 ,也就是class_rw_t中的const method_array_t methods() 方法 ;但我们在观察class_rw_t时却在其结构中找不到存储方法列表的属性,从const method_array_t methods() 方法 中我们可以发现对方法列表的读取是在class_rw_ext_t和class_ro_t指针中进行的 ;

class_rw_ext_t

class_rw_ext_t是个结构体,其数据结构如下:

struct class_rw_ext_t {
    const class_ro_t *ro;
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    char *demangledName;
    uint32_t version;
};

由此可知,结构体class_rw_ext_t中的确有方法列表、属性列表和协议等信息,而且还有一个class_ro_t指针,接下来继续看class_ro_t

class_ro_t

4324234

以下是class_ro_t的结构体:

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif
    const uint8_t * ivarLayout;
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
    // This field exists only when RO_HAS_SWIFT_INITIALIZER is set.
    _objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];
    _objc_swiftMetadataInitializer swiftMetadataInitializer() const {
        if (flags & RO_HAS_SWIFT_INITIALIZER) {
            return _swiftMetadataInitializer_NEVER_USE[0];
        } else {
            return nil;
        }
    }
    method_list_t *baseMethods() const {
        return baseMethodList;
    }
    class_ro_t *duplicate() const {
        if (flags & RO_HAS_SWIFT_INITIALIZER) {
            size_t size = sizeof(*this) + sizeof(_swiftMetadataInitializer_NEVER_USE[0]);
            class_ro_t *ro = (class_ro_t *)memdup(this, size);
            ro->_swiftMetadataInitializer_NEVER_USE[0] = this->_swiftMetadataInitializer_NEVER_USE[0];
            return ro;
        } else {
            size_t size = sizeof(*this);
            class_ro_t *ro = (class_ro_t *)memdup(this, size);
            return ro;
        }
    }
};

这里面我们发现class_ro_t不仅有baseMethodList、 baseProtocols、baseProperties等信息,还有成员变量const ivar_list_t * ivars等其他信息。在了解他们的结构之后,我们从结构上简单做一下它们之间关系的分析:

  • 首先class_rw_t有一个指针ro_or_rw_ext,ro_or_rw_ext指向可能是class_rw_ext_t或者是class_ro_t。ro_or_rw_ext指针的作用是,当读取类相关信息时,会优先判断是否指向class_rw_ext_t,如果是class_rw_ext_t就从它读取,没有就是class_ro_t,直接从class_ro_t读取。

    那么在程序运行中到底ro_or_rw_ext什么时候是class_rw_ext_t,什么时候是class_ro_t呢?

class_ro_t & class_rw_t & class_rw_ext_t & Class之间的关系

从字面意思解读class_ro_t中的“ro”代表只读;class_rw_t中的“rw”代表可读可写;class_rw_ext_t中的“rw_ext”代表可读可写的扩展。
干净内存和脏内存:干净内存内存指的是内存一旦加载就不会被改变。反之,脏内存就是在运行时会被修改的。

Class本身是运行时加载的,在运行时会被改变,所以本身Class就是属于脏内存。那么如果想要获取Class的干净内存,也就是编译时确定的数据结构包括方法列表、成员变量等的,该怎么办?
这其实就是class_ro_t的作用。因为class_ro_t是只读,意味着 class_ro_t是从mach-o读取类的数据之后,就不会被改变。那如果我们想在运行时修改类的信息,比如添加方法,比如加载category怎么办呢?那这时候就有一个与之对应的class_rw_t结构,class_rw_t可以在运行时存储类的信息,可读可写的,可以在运行时修改。
说到这里,好像还漏掉一个结构class_rw_ext_t,这个东西又是干什么用的呢?存在的意义是什么?其实还是跟运行时有关。实际上在我们的app运行中,需要运行时修改的类是非常少的,据统计平均大概就10%左右。那也就是说大部分只需要读取class_ro_t中的数据就够了,少部分才需要修改。因此才会有class_rw_ext_t这个扩展的结构体。class_rw_ext_t的作用是这样的:当我们需要修改类结构时,比如添加方法、加载category等时,class_rw_t回去开辟一个额外的空间rwe(class_rw_ext_t),用于存储新的方法和class_ro_t中的方法等信息。这样做的目的有一个好处就是,对于绝大部分类是不需要这个开辟class_rw_ext_t这个结构体,节省内存。

创建时机

*那这几个结构分别是什么时候创建的呢?这里就设计到类的加载流程。首先类在app启动的时候会被映射到内存,这时候会先创建Class(objc_class)结构,然后把类编译时的类数据映射到class_ro_t,class_ro_t结构体指针存储到Class的bits指针中,我们前面提到类的bits中存储的是class_rw_t指针,实际上在类初始化之前这里存储的是class_ro_t,等到类初始化的时候会创建一个class_rw_t结构,然后通过data()从bits中读取class_ro_t,然后class_rw_t通过set_ro(const class_ro_t ro)把指针ro_or_rw_ext指向这个class_ro_t,然后Class通过setData()把class_rw_t指针存储到bits里面。然后在运行时根据需要,根据extAllocIfNeeded或extAlloc创建class_rw_ext_t,然后把class_ro_t关联到class_rw_ext_t

上面的解读也可以说明下面的一些问题了:

category不能添加成员变量的原因

因为category是运行时添加的,他只能操作class_rw_t结构,但是class_rw_t结构没有添加成员变量的入口。成员变量是存储在class_ro_t中的,是无法被修改的,所以category就不能添加成员变量。

类数据的存储结构

4324234

4324234

对于上面的class_rw_t中实际上找不到对应的methods属性,所以更像是调用方法从而直接获取对应的数组,同理其他也应该是这样的 ;

继承者链

示例:

  • 继承自NSObject的类LGPerson
  • 继承自LGPerson的类LGTeacher
  • main中分别用两个定义两个对象:personteacher
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [[LGPerson alloc] init];
        LGTeacher *teacher = [[LGTeacher alloc] init];
        NSLog(@"Hello, World! %@ - %@", person, teacher);
    }
    return 0;
}

  • mainLGTeacher部分加一个断点,运行程序。
  • 开启lldb调试,调试的过程如下图所示。
  • 5345345

0x0100000100008239是person对象的isa指针地址,其&后得到的结果是创建person的类LGPerson。
0x0000000100008210是isa中获取的类信息所指的类的isa的指针地址,即LGPerson类的类的isa指针地址,在Apple中,我们简称LGPerson类的类为元类。
所以,两个打印都是LGPerson的根本原因就是因为元类导致的。

元类是什么

  • 我们都知道对象的isa是指向类,类的其实也是一个对象,可以称为类对象,其isa的位域指向苹果定义的元类。
  • 元类是系统给的,其定义和创建都是由编译器完成,在这个过程中,类的归属来自于元类。
  • 元类是类对象 的类,每个类都有一个独一无二的元类用来存储类方法的相关信息。
  • 元类本身是没有名称的,由于与类相关联,所以使用了同类名一样的名称。

简单的总结一下,元类是没有名称的类(使用了同类名一样的名称),是类对象的isa指针所指向的类 ;

img

对象 --> 类 --> 元类 --> NSobject,NSObject指向自身。

对于NSobject,NSObject(根元类)在内存中永远只存在一份,由于类的信息在内存中永远只存在一份,所以类对象只有一份。

著名的isa走位 & 继承关系图

5345345345

isa走位

实例对象(Instance of Subclass)的isa指向类(class)
类对象(class)的isa指向元类(Meta class)
元类(Meta class)的isa指向根元类(Root metal class)
根元类(Root metal class)的isa指向它自己本身,形成闭环,这里的根元类就是NSObject

superclass走位

类之间的继承关系:

类(subClass)继承自父类(superClass)
父类(superClass)继承自根类(RootClass),此时的根类是指NSObject
根类继承自nil,所以根类即NSObject可以理解为万物起源,即无中生有
元类也存在继承,元类之间的继承关系如下:

子类的元类(metal SubClass)继承自父类的元类(metal SuperClass)
父类的元类(metal SuperClass)继承自根元类(Root metal Class)
根元类(Root metal Class)继承于根类(Root class),此时的根类是指NSObject
【注意】实例对象之间没有继承关系,类之间有继承关系。

对象方法,属性,成员变量,协议信息 存放在class对象中。
类方法,存放在meta-class对象中。
成员变量的具体值,存放在instance对象中。

https://blog.csdn.net/m0_55124878/article/details/125808836

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2085893.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

王伊朵闪耀“快乐阳光”舞台 再夺全国冠军

在近日落幕的“赛克杯”第20届快乐阳光少年儿童歌曲卡拉OK电视大赛全国总决赛中&#xff0c;就读于北京市建华实验学校&#xff0c;年仅11岁的小选手王伊朵以其出色的唱功和卓越的舞台表现&#xff0c;一举夺得全国冠军&#xff0c;成为本次大赛一颗耀眼的小明星。 王伊朵自小对…

基于NNG的六种通信模式

NNG是一个消息传递框架&#xff0c;用来解决常见的消息传递问题。NNG是nanomsg的继任版本&#xff0c;纯c语言开发&#xff0c;工作模式分为一下几种&#xff1a; Pipeline单向管道 此模式可用于解决生产者/消费者问题&#xff0c;包括负载均衡。 消息从推侧流向拉侧。 如果多…

‌U盘闪一下就没了?‌如何有效恢复数据

在日常使用U盘的过程中&#xff0c;‌我们可能会遇到一种突发情况&#xff1a;‌U盘插入电脑后仅仅闪了一下就消失了&#xff0c;‌无法再被识别或访问。‌这种情况下&#xff0c;‌U盘中的数据似乎瞬间变得遥不可及。‌然而&#xff0c;‌不必过于担心&#xff0c;‌因为仍然有…

Kafka消息积压的典型场景及解决方案

Kafka消息积压的典型场景&#xff1a; 1.实时/消费任务挂掉 比如&#xff0c;我们写的实时应用因为某种原因挂掉了&#xff0c;并且这个任务没有被监控程序监控发现通知相关负责人&#xff0c;负责人又没有写自动拉起任务的脚本进行重启。 那么在我们重新启动这个实时应用进行…

【Docker】构建Harbor仓库

下载软件包地址&#xff1a;https://github.com/goharbor/harbor/releases Harbor 是由vmware公司开源的企业级 Docker Registry 项目。 它提供了以下主要功能和特点&#xff1a; 1. 基于角色的访问控制&#xff08;RBAC&#xff09;&#xff1a;可以为不同的用户和用户组分…

告别繁琐!Xinstall地推码,让App安装统计变得更简单!

Xinstall地推码&#xff1a;开启App推广新篇章 在移动互联网时代&#xff0c;App推广的重要性不言而喻。然而&#xff0c;传统的推广方式往往伴随着繁琐的操作和难以精准统计的数据&#xff0c;让推广者头疼不已。幸运的是&#xff0c;Xinstall作为国内专业的App全渠道统计服务…

EmguCV学习笔记 VB.Net 6.6 图像的矩

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 EmguCV是一个基于OpenCV的开源免费的跨平台计算机视觉库,它向C#和VB.NET开发者提供了OpenCV库的大部分功能。 教程VB.net版本请访问…

【保姆级WebStorm安装!!!】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

记录|Form1中嵌套Form2时的频闪问题解决[不同于常见的三部曲]

目录 前言一、常见的解决方案二、自己创建渐变色组件GradientPanel三、最终效果展示更新时间 前言 参考文章&#xff1a; C#画图解决闪烁问题 [解决winform中重绘时控件闪烁的问题](panel1.GetType().GetProperty(“DoubleBuffered”,System.Reflection.BindingFlags.Instance …

试卷擦除,这几款软件擦得干干净净!

在现代教育中&#xff0c;电子化试卷已经成为一种趋势&#xff0c;然而随之而来的问题是如何有效地擦除试卷上的答案&#xff0c;以便进行反复使用试卷。为了解决这个问题&#xff0c;我们介绍了三款试卷答案清除方法&#xff0c;这些方法不仅简单易用&#xff0c;而且效果显著…

运维团队如何高效使用监控易

监控易作为一款功能强大的运维监控工具&#xff0c;能够为运维团队提供全面、实时的设备运行状态和性能指标。为了高效使用监控易&#xff0c;运维团队应遵循以下步骤和策略&#xff1a; 一、熟悉监控易界面与功能 深入了解监控易的仪表盘界面&#xff0c;包括设备运行状态、…

APP自动化测试思路整理,跟着步骤快速撸码...

前言 1、开发语言选择 通常用于自动化测试的编程语言有&#xff1a;Python、Java、Javascript、Ruby、C#、PHP等。 一般我们会选择自己熟悉的编程语言来编写自动化脚本&#xff0c;但对于编程基础基本为0的童鞋&#xff08;或者专注于做自动化测试的童鞋&#xff09;&#x…

ICT测试探针市场报告:前五大厂商占有大约26.0%的市场份额

ICT测试探针&#xff0c;即在线测试&#xff08;ICT&#xff09;探针&#xff0c;是专门用于电路板&#xff08;特别是PCBA&#xff09;电气测试的重要工具&#xff0c;通过这些探针接触PCB上的测试点&#xff0c;来检测电路板上元器件的连通性和电气性能&#xff0c;从而保证电…

视频超分辨率重建——AnimeSR网络测试教程(详细图文教程)

&#x1f4aa; 专业从事且热爱图像处理&#xff0c;图像处理专栏更新如下&#x1f447;&#xff1a; &#x1f4dd;《图像去噪》 &#x1f4dd;《超分辨率重建》 &#x1f4dd;《语义分割》 &#x1f4dd;《风格迁移》 &#x1f4dd;《目标检测》 &#x1f4dd;《暗光增强》 &a…

2021CCPC网络赛:G - Function HDU - 7106

题意 定义g(x)为x的各数位之和 求该方程: 在给定x范围内的最小值 思路: 一个x对应一个g(x)&#xff0c;一个g(x)对应多个x 由于g(x)最多取到54,所以可以枚举所有的g(x)所以想到当取到g(x)的时候,只取能使原方程f(x)最小的那几个x 当枚举g(x)的时候,g(x)看作一个常数,此时方程…

干货分享:精选四大Win10录屏工具推荐!

无论你是制作教程视频、游戏实况分享还是日常办公中的演示记录&#xff0c;一款好的录屏软件都能让你事半功倍。Windows 10作为当下最流行的桌面操作系统之一&#xff0c;本文推荐几款优秀的win10录屏工具 福昕录屏大师 链接&#xff1a;www.foxitsoftware.cn/REC/ 福昕录屏…

石油钻杆对直线度测量的需求

关键字&#xff1a;石油钻杆直线度测量仪&#xff0c;钻杆测量仪&#xff0c;钻杆直线度 石油钻杆对直线度测量的需求主要源于其在石油勘探和开发过程中的关键作用以及严苛的工作环境。以下是对这一需求的详细分析&#xff1a; 一、石油钻杆的功能与重要性 石油钻杆是石油钻探…

十五分钟速通Vue

绑值语法( {{}} ) <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title> </head…

热失控传感器在换电站消防安全解决方案中的应用

随着新能源技术的飞速发展&#xff0c;工程车辆行业正逐步向绿色化、电动化转型&#xff0c;作为这一转型过程中的关键设施&#xff0c;换电站的数量日益增多。作为电池集中储存与交换的核心区域&#xff0c;换电站的消防安全问题成为了不容忽视的重大挑战。特别是电池仓&#…

骑行适合戴的耳机?开放式耳机测评

当我们在享受骑行的乐趣时&#xff0c;音乐往往是不可或缺的伴侣。但选择耳机却是一门学问&#xff0c;尤其是在安全和听觉享受之间找到平衡。今天&#xff0c;我就来和大家探讨一下&#xff0c;在骑行时究竟是选择开放式耳机还是封闭式耳机。 骑行时选择耳机的考量因素 1.安全…