Runtime源码解析-类中bits

news2024/11/16 1:46:10
  • Runtime源码解析-类中bits
    • class_rw_t
      • ro_or_rw_ext_t
        • 成员变量
        • 方法
          • 初始化方法
          • 存取方法
          • 类型判断
      • 公有方法
        • 获取class_rw_ext_t
        • 获取/设置class_ro_t
        • 方法、属性、协议列表
    • class_rw_ext_t
    • class_ro_t
    • 总结
      • 1. 为什么ro_or_rw_ext 会有两种类型,class_rw_ext_t或者class_ro_t类型?
      • 2. 数据区别?
    • 获取列表
      • list_array_tt
        • array_t
        • iterator
      • entsize_list_tt
      • 方法列表
      • 属性列表
      • 协议列表
    • 总结

Runtime源码解析-类中bits

  • 首先我们再看一眼objc_class类的定义,本篇文章研究bits到底存储了哪些信息
struct objc_class : objc_object {
 	// 初始化方法
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    // 其他方法
}
  • 然后进入到class_data_bits_t结构中
struct class_data_bits_t {
    friend objc_class;

    uintptr_t bits;
	// 省略方法
}
  • 发现该结构只存了一个8字节长的数据,然后通过查阅它的public方法
class_rw_t* data() const {
    return (class_rw_t *)(bits & FAST_DATA_MASK);
}
void setData(class_rw_t *newData)
{
    ASSERT(!data()  ||  (newData->flags & (RW_REALIZING | RW_FUTURE)));
    // Set during realization or construction only. No locking needed.
    // Use a store-release fence because there may be concurrent
    // readers of data and data's contents.
    uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
    atomic_thread_fence(memory_order_release);
    bits = newBits;
}
  • 发现它提供了一个获取data()的方法,这里面应该存储了某些数据,进入返回的类型class_rw_t

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
	
    // ro_or_rw_ext 会有两种情况:
    // 1. 编译时值是 class_ro_t *
    // 2. class_rw_ext_t *,编译时的 class_ro_t * 作为 class_rw_ext_t 的 const class_ro_t *ro 成员变量保存
    explicit_atomic<uintptr_t> ro_or_rw_ext;

    Class firstSubclass;
    Class nextSiblingClass;

private:
    // 省略私有方法

public:
    // 省略部分方法
    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 *>(&ro_or_rw_ext)->methods;
        } else {
            return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->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 *>(&ro_or_rw_ext)->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->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 *>(&ro_or_rw_ext)->protocols;
        } else {
            return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
        }
    }
};
  • 发现class_rw_t是一个结构体,提供了获取属性列表方法列表协议列表的方法。
  • 我们先看一下private里面的内容:
private:
    using ro_or_rw_ext_t = objc::PointerUnion<const class_ro_t, class_rw_ext_t, PTRAUTH_STR("class_ro_t"), PTRAUTH_STR("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, &ro_or_rw_ext}.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, &ro_or_rw_ext}.storeAt(ro_or_rw_ext, memory_order_release);
    }

    class_rw_ext_t *extAlloc(const class_ro_t *ro, bool deep = false);

  • 使用 using 关键字声明一个 ro_or_rw_ext_t 类型为: objc::PointerUnion<**const** class_ro_t, class_rw_ext_t)>;

  • 这里可以简单理解为一个指针联合体,系统只为其分配一个指针的内存空间,一次只能保存 class_ro_t 指针或者 class_rw_ext_t 指针

  • 接着我们看一下public里面的主要方法,它大致分为下面几类

    1. 获取class_rw_ext_t或者class_ro_t
    2. 获取方法列表属性列表协议列表

ro_or_rw_ext_t

  • 这里使用using关键字申明了ro_or_rw_ext_t类型,它的具体类型是一个PointerUnion模版类
template <class T1, class T2, typename Auth1, typename Auth2>
class PointerUnion {
    // 仅有一个成员变量 _value
    uintptr_t _value;

    static_assert(alignof(T1) >= 2, "alignment requirement");
    static_assert(alignof(T2) >= 2, "alignment requirement");

    struct IsPT1 {
      static const uintptr_t Num = 0;
    };
    struct IsPT2 {
      static const uintptr_t Num = 1;
    };
    template <typename T> struct UNION_DOESNT_CONTAIN_TYPE {};

    uintptr_t getPointer() const {
        return _value & ~1;
    }
    uintptr_t getTag() const {
        return _value & 1;
    }

public:
    explicit PointerUnion(const std::atomic<uintptr_t> &raw)
    : _value(raw.load(std::memory_order_relaxed))
    { }
    PointerUnion(T1 *t, const void *address) {
        _value = (uintptr_t)Auth1::sign(t, address);
    }
    PointerUnion(T2 *t, const void *address) {
        _value = (uintptr_t)Auth2::sign(t, address) | 1;
    }

    void storeAt(std::atomic<uintptr_t> &raw, std::memory_order order) const {
        raw.store(_value, order);
    }

    template <typename T>
    bool is() const {
        using Ty = typename PointerUnionTypeSelector<T1 *, T, IsPT1,
            PointerUnionTypeSelector<T2 *, T, IsPT2,
            UNION_DOESNT_CONTAIN_TYPE<T>>>::Return;
        return getTag() == Ty::Num;
    }

    template <typename T> T get(const void *address) const {
        ASSERT(is<T>() && "Invalid accessor called");
        using AuthT = typename PointerUnionTypeSelector<T1 *, T, Auth1,
            PointerUnionTypeSelector<T2 *, T, Auth2,
            UNION_DOESNT_CONTAIN_TYPE<T>>>::Return;

        return AuthT::auth((T)getPointer(), address);
    }

    template <typename T> T dyn_cast(const void *address) const {
      if (is<T>())
        return get<T>(address);
      return T();
    }
};
  • 在定义时using ro_or_rw_ext_t = objc::PointerUnion<const class_ro_t, class_rw_ext_t>;对应到模版类中T1:const class_ro_t类型,T2:class_rw_ext_t类型

成员变量

  • 只有有一个成员变量 _value
  • 这里可以理解为只能保存 const class_ro_tclass_rw_ext_t

方法

初始化方法
explicit PointerUnion(const std::atomic<uintptr_t> &raw)
    : _value(raw.load(std::memory_order_relaxed))
    { }

// 初始化T1
PointerUnion(T1 *t, const void *address) {
    _value = (uintptr_t)Auth1::sign(t, address);
}
// 初始化T2,把_value最后一位设置为1
PointerUnion(T2 *t, const void *address) {
    _value = (uintptr_t)Auth2::sign(t, address) | 1;
}
  • 这里初始化时,不同类型,通过最后一位来区分
存取方法
// 根据指定的 order 以原子方式把 raw 保存到 _value 中
void storeAt(std::atomic<uintptr_t> &raw, std::memory_order order) const {
    raw.store(_value, order);
}


// 获取指针 class_ro_t 或者 class_rw_ext_t 指针
template <typename T> T get(const void *address) const {
    ASSERT(is<T>() && "Invalid accessor called");
    using AuthT = typename PointerUnionTypeSelector<T1 *, T, Auth1,
    PointerUnionTypeSelector<T2 *, T, Auth2,
    UNION_DOESNT_CONTAIN_TYPE<T>>>::Return;

    return AuthT::auth((T)getPointer(), address);
}

// get 函数中如果当前 _value 类型和 T 不匹配的话,强制转换会返回错误类型的指针
// dyn_cast 则始终都返回 T 类型的指针
template <typename T> T dyn_cast(const void *address) const {
    if (is<T>())
        return get<T>(address);
    return T();
}
类型判断
// 定义结构体 IsPT1,内部仅有一个静态不可变 uintptr_t 类型的值为 0 的 Num。
//(用于 _value 的类型判断, 表示此时是 class_ro_t *)
struct IsPT1 {
    static const uintptr_t Num = 0;
};

// 定义结构体 IsPT2,内部仅有一个静态不可变 uintptr_t 类型的值为 1 的 Num。
//(用于 _value 的类型判断,表示此时是 class_rw_ext_t *)
struct IsPT2 {
    static const uintptr_t Num = 1;
};

// 来判断_value类型
template <typename T>
bool is() const {
    using Ty = typename PointerUnionTypeSelector<T1 *, T, IsPT1,
    PointerUnionTypeSelector<T2 *, T, IsPT2,
    UNION_DOESNT_CONTAIN_TYPE<T>>>::Return;
    return getTag() == Ty::Num;
}
  • 通过is()方法来判断,ro_or_rw_ext 当前是 class_rw_ext_t 还是 class_ro_t

公有方法

获取class_rw_ext_t

// 从 ro_or_rw_ext 中取得 class_rw_ext_t 指针
class_rw_ext_t *ext() const {
    return get_ro_or_rwe().dyn_cast<class_rw_ext_t *>(&ro_or_rw_ext);
}

/*
由 class_ro_t 构建一个 class_rw_ext_t。
	如果 ro_or_rw_ext 已经是 class_rw_ext_t 指针了,则直接返回,
	如果 ro_or_rw_ext 是 class_ro_t 指针的话,根据 class_ro_t 的值构建 class_rw_ext_t 并把它的地址赋值给 class_rw_t 的 ro_or_rw_ext,
*/
class_rw_ext_t *extAllocIfNeeded() {
    auto v = get_ro_or_rwe();
    if (fastpath(v.is<class_rw_ext_t *>())) {
        // 直接返回 class_rw_ext_t 指针
        return v.get<class_rw_ext_t *>(&ro_or_rw_ext);
    } else {
        // 构建 class_rw_ext_t 
        return extAlloc(v.get<const class_ro_t *>(&ro_or_rw_ext));
    }
}

class_rw_ext_t *deepCopy(const class_ro_t *ro) {
    return extAlloc(ro, true);
}

获取/设置class_ro_t

// 从 ro_or_rw_ext 中取得 class_ro_t 指针,
const class_ro_t *ro() const {
    auto v = get_ro_or_rwe();
    if (slowpath(v.is<class_rw_ext_t *>())) {
        // 如果此时是 class_rw_ext_t 指针,则返回它的 ro
        return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro;
    }
    
    // 如果此时是 class_ro_t,则直接返回
    return v.get<const class_ro_t *>(&ro_or_rw_ext);
}

// 设置ro
void set_ro(const class_ro_t *ro) {
    auto v = get_ro_or_rwe();
    if (v.is<class_rw_ext_t *>()) {
        // 如果 ro_or_rw_ext 中保存的是 class_rw_ext_t 指针,则把 ro 赋值给 class_rw_ext_t 的 const class_ro_t *ro。
        v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro = ro;
    } else {
       // 如果 ro_or_rw_ext 中保存的是 class_ro_t *ro 的话,直接入参 ro 保存到 ro_or_rw_ext 中。
        set_ro_or_rwe(ro);
    }
}

方法、属性、协议列表

/*
方法列表获取
	1. class_rw_ext_t 的 method_array_t methods
	2. class_ro_t 的 method_list_t * baseMethodList 构建的 method_array_t
*/
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 *>(&ro_or_rw_ext)->methods;
    } else {
        return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->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 *>(&ro_or_rw_ext)->properties;
    } else {
        return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->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 *>(&ro_or_rw_ext)->protocols;
    } else {
        return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
    }
}

class_rw_ext_t

  • 前面我们一直说class_rw_t结构中有一个ro_or_rw_ext变量,它可以是class_rw_ext_t类型指针。我们具体看一下它的结构
struct class_rw_ext_t {
    DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
    // 指向class_ro_t的指针
    class_ro_t_authed_ptr<const class_ro_t> ro;
    // 方法列表
    method_array_t methods;
    // 属性列表
    property_array_t properties;
    // 协议列表
    protocol_array_t protocols;
    // 所属的类名
    char *demangledName;
    // 版本号
    uint32_t version;
};
  • 可以看到该类中存储了方法、属性、协议列表,是可以动态扩展的列表。
  • 以及指向class_ro_t的指针

class_ro_t

  • 查看一下class_ro_t结构
struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    union {
        const uint8_t * ivarLayout;
        Class nonMetaclass;
    };
	
    // 类名
    explicit_atomic<const char *> name;
    // 实例方法列表
    WrappedPtr<method_list_t, method_list_t::Ptrauth> baseMethods;
    // 协议列表
    protocol_list_t * baseProtocols;
    // 成员变量列表
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    // 属性列表
    property_list_t *baseProperties;

    // 省略方法
};
  • 它有基础方法列表、基础协议列表、成员变量列表、基础属性列表,这些内容都是编译器确定的。它是可读的,不可修改。
  • 至于分类所加载的数据、已经关联对象都存储在class_rw_ext_t

总结

  • 看完上面内容是不是有一下疑惑?

    1. 为什么ro_or_rw_ext 会有两种类型,class_rw_ext_t或者class_ro_t类型?
    2. 为什么class_rw_ext_tclass_ro_t都有方法、属性、协议列表,那为什么还需要两个结构存储?有什么区别
  • 下面我们一一解答一下上面问题

1. 为什么ro_or_rw_ext 会有两种类型,class_rw_ext_t或者class_ro_t类型?

  1. 如果没有在运行时,对该类的方法、属性、协议等进行动态添加,则 ro_or_rw_extclass_ro_t指针。class_ro_t是在编译期确定的。

  • class_rw_t中成员变量ro_or_rw_ext直接指向class_ro_t,省去class_rw_ext_t结构体的数据,减少内存消耗。
  1. 如果在运行时,动态的添加了方法、属性、协议等,ro_or_rw_extclass_rw_ext_t指针。

  • class_rw_t中成员变量ro_or_rw_ext直接指向class_rw_ext_tclass_rw_ext_t的成员变量ro指向class_ro_t

2. 数据区别?

  • class_rw_ext_t中存储列表使用是一个继承自list_array_tt的结构,可以理解为一个二维数组,数组中存的是列表。
  • class_ro_t中存储列表使用的是一个普通数组。
  • 为什么会存储结构不同?在运行时动态添加方法时,会直接在list_array_tt类型的二维数组中,把整个分类列表插入进去。

获取列表

  • class_rw_t最主要的作用就是存储了方法、属性、协议列表,以及提供了如何获取相关列表。

  • 下面我们主要研究一下如何获取相关列表,在开始之前,我们需要了解它们的存储方式是如何?

  • 以方法列表为例

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 *>(&ro_or_rw_ext)->methods;
    } else {
        return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
    }
}
  • 该函数返回了一个method_array_t类型结构
class method_array_t : 
    public list_array_tt<method_t, method_list_t, method_list_t_authed_ptr>
  • 发现该结构继承于list_array_tt结构,所以我们首先看看这个结构,它是如何存储的。

list_array_tt

template <typename Element, typename List, template<typename> class Ptr>
class list_array_tt {
    struct array_t {...};
protected:
    class iterator {...};
private:
    union {
        Ptr<List> list;
        uintptr_t arrayAndFlag;
    };

    bool hasArray() const {
        return arrayAndFlag & 1;
    }

    array_t *array() const {
        return (array_t *)(arrayAndFlag & ~1);
    }

    void setArray(array_t *array) {
        arrayAndFlag = (uintptr_t)array | 1;
    }

    void validate() {
        for (auto cursor = beginLists(), end = endLists(); cursor != end; cursor++)
            cursor->validate();
    }
}
  • 我们发现list_array_tt是一个模版类
    • typename Element:基础元数据类型(例如: method_t
    • typename List:元数据的列表类型(例如: method_list_t
  • 它内部的成员变量是一个联合体:
    • 一个单独的列表
    • 一个数组,数组中都是指针,每个指针分别指向一个列表
    • 可以理解为一维数组和二维数组的区别。
  • 除此之外,还有两个内部类

array_t

struct array_t {
    uint32_t count; // count 是 lists 数组中 List * 的数量
    Ptr<List> lists[0]; // 指针数组,每个元素指向一个列表

    static size_t byteSize(uint32_t count) {
        return sizeof(array_t) + count*sizeof(lists[0]);
    }
    size_t byteSize() {
        return byteSize(count);
    }
};
  • 这里array_t是一个二维数据结构,里面含有基本的一些数据元素。

iterator

  • 它是一个迭代器,用来遍历array_t结构中的二维数组

entsize_list_tt

  • 还是通过上面获取方法列表的例子
class method_array_t : 
    public list_array_tt<method_t, method_list_t, method_list_t_authed_ptr>
  • 我们发现第二个参数传的是method_list_t类型,进入它的结构
struct method_list_t : entsize_list_tt<method_t, method_list_t, 0xffff0003, method_t::pointer_modifier>
  • 发现它继承于entsize_list_tt结构,我们看一下这个结构有何作用。
template <typename Element, typename List, uint32_t FlagMask, typename PointerModifier = PointerModifierNop>
struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    // entsize_list_tt 的容量
    uint32_t count;

	// 元素的大小(entry 的大小)
    uint32_t entsize() const {
        return entsizeAndFlags & ~FlagMask;
    }
    uint32_t flags() const {
        return entsizeAndFlags & FlagMask;
    }
	
    // 获取对应下标元素
    Element& getOrEnd(uint32_t i) const { 
        ASSERT(i <= count);
        return *PointerModifier::modify(*this, (Element *)((uint8_t *)this + sizeof(*this) + i*entsize()));
    }
    Element& get(uint32_t i) const { 
        ASSERT(i < count);
        return getOrEnd(i);
    }

    size_t byteSize() const {
        return byteSize(entsize(), count);
    }
    
    static size_t byteSize(uint32_t entsize, uint32_t count) {
        return sizeof(entsize_list_tt) + count*entsize;
    }
	
    // 迭代器,用来遍历数组
    struct iterator;
    const iterator begin() const { 
        return iterator(*static_cast<const List*>(this), 0); 
    }
    iterator begin() { 
        return iterator(*static_cast<const List*>(this), 0); 
    }
    const iterator end() const { 
        return iterator(*static_cast<const List*>(this), count); 
    }
    iterator end() { 
        return iterator(*static_cast<const List*>(this), count); 
    }
    
    struct iterator {...};
}
  • entsize_list_tt其实就是一个数组,用来存储编译完成后类的属性,提供了遍历和访问方法。

方法列表

  • 用来存储该类的方法的数据结构,继承list_array_tt
class method_array_t : 
    public list_array_tt<method_t, method_list_t, method_list_t_authed_ptr>
{
    typedef list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> Super;

 public:
    method_array_t() : Super() { }
    method_array_t(method_list_t *l) : Super(l) { }

    const method_list_t_authed_ptr<method_list_t> *beginCategoryMethodLists() const {
        return beginLists();
    }
    
    const method_list_t_authed_ptr<method_list_t> *endCategoryMethodLists(Class cls) const;
};
  • 前面已经说过list_array_tt结构,可能是一维数组或者二维数组,所以method_array_t也是按照如下结构存储。
  • 我们看一下单个方法的存储结构method_t
struct method_t {
    static const uint32_t smallMethodListFlag = 0x80000000;

    method_t(const method_t &other) = delete;

    // The representation of a "big" method. This is the traditional
    // representation of three pointers storing the selector, types
    // and implementation.
    struct big {
        SEL name;
        const char *types;
        MethodListIMP imp;
    };

	// 省略方法
}
  • 我们发现它内部定义了一个big结构体,里面包含了方法的SELimp
  • 我们就可以通过method_t结构拿到big,就可以获取到里面的方法。

属性列表

  • 用来存储该类的属性的数据结构,和上面类似。同样继承list_array_tt
class property_array_t : 
    public list_array_tt<property_t, property_list_t, RawPtr>
{
    typedef list_array_tt<property_t, property_list_t, RawPtr> Super;

 public:
    property_array_t() : Super() { }
    property_array_t(property_list_t *l) : Super(l) { }
};
  • 我们查看一下单个属性结构property_t
struct property_t {
    const char *name;
    const char *attributes;
};
  • 内部非常简单,一个代表变量名,一个代表变量的修饰符(strong、weak等)。

协议列表

  • 用来存储该类的协议的数据结构,和上面类似。同样继承list_array_tt
class protocol_array_t : 
    public list_array_tt<protocol_ref_t, protocol_list_t, RawPtr>
{
    typedef list_array_tt<protocol_ref_t, protocol_list_t, RawPtr> Super;

 public:
    protocol_array_t() : Super() { }
    protocol_array_t(protocol_list_t *l) : Super(l) { }
};
  • 我们查看一下单个协议结构protocol_ref_t
typedef uintptr_t protocol_ref_t;
  • 它就是一个指针,指向对应的协议。

总结

  • bits中存储了属性、方法、协议、成员变量。成员变量只存储在class_ro_t中,它是编译器就决定好的,不可修改。

  • bits中这些信息,是通过class_rw_t结构存储,它有一个ro_or_rw_ext_t变量,它可以是ro类型,也可能是rw_ext类型。

    • 如果该类没有动态添加属性、方法、协议等,就是ro类型
    • 否则就是rw_ext类型,然后它内部会包含ro
    • 之所以会出现两种结构,都是为了优化内存。
  • ro是干净内存,类的加载时类本身的数据存放在ro中,编译期决定。

    • 它存储的方法、属性、协议等列表,是一个一维数组
  • rwe是脏内存,运行时动态创建成员或分类中的成员都在rwe

    • 它存储的方法、属性、协议等列表,是一个二维数据。动态添加的是个数组,编译期决定的是一个数组,方便动态添加对应信息。

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

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

相关文章

Sping Boot 如何实现自动配置

Sping Boot 如何实现自动配置 Spring Boot都需要创建一个mian启动类&#xff0c;而启动类都含有SpringBootApplication注解&#xff0c;从启动类&#xff0c;一步步探索源码。 SpringBootApplication注解 Spring Boot 启动类上都有一个 SpringBootApplication注解&#xff1…

想知道CAD怎么转换为PDF吗?快来收藏这些实用的转换技巧

有一些读建筑类或者电子信息类专业的小伙伴&#xff0c;经常需要使用到CAD软件来设计和修改图纸&#xff0c;并且保存下来的文件一般默认是CAD文件格式。但有的时候&#xff0c;我们将图纸发送给其他人&#xff0c;对方的设备中没有相应的软件&#xff0c;导致无法查看&#xf…

Linux 进程间通信:匿名管道 命名管道 共享内存

进程间通信的必要性 进程间通信&#xff0c;是建立在多进程之上的。如果是单进程&#xff0c;则无法使用并发能力&#xff0c;更加无法进行多进程协同。多进程要想实现多进程协同&#xff08;目的&#xff09;&#xff0c;就必须进行进程间通信&#xff08;手段&#xff09;。…

知识图谱-KGE-语义匹配-双线性模型-2018:SimplE

【paper】 SimplE Embedding for Link Prediction in Knowledge Graphs【简介】 本文是加拿大英属哥伦比亚大学的两位学者发表在 NIPS 2018 上的工作&#xff0c;文章提出了 SimplE&#xff08;Simple Embedding&#xff09;。这篇和前面一篇差不多&#xff0c;也是对 1927 年的…

深度学习 +SLAM:SuperGlue

简介 传统SLAM的流程通常包括如下内容&#xff0c; 特征点提取描述&#xff0c;特征点匹配 异常点去除&#xff0c; 位姿估计。 在以往前人的工作中&#xff0c;SuperPoint和 D2-Net试图解决特征点检测和描述的问题。而检测之后的匹配通常通过最近邻匹配和异常点剔除的方式完…

使用 Arduino 中断 – 硬件、引脚变化和定时器

使用 Arduino 中断 – 硬件、引脚变化和定时器 查看原文 今天我们将学习中断&#xff0c;这是Arduino和其他微控制器的一个非常重要的基本功能。虽然我们将专注于Arduino Uno&#xff0c;但这里介绍的概念与其他板同样有效。 介绍 当我们设计一个项目时&#xff0c;我们通常…

Release notes for VPP 22.10

本次发布新增了212个提交&#xff0c;包括118个修复。关于本次发布的更多信息&#xff0c;请访问&#xff1a;https://gerrit.fd.io/r/gitweb?pvpp.git;ablob;fdocs/aboutvpp/releasenotes/v22.10.rst;h5dfbff5d48e957e83d7e3c2f978820c95c41a2e4;hb07e0c05e698cf5ffd1e2d2de0…

「图文教程」iOS 16测试版如何升级iOS 16正式版?

苹果iOS 16正式版已经更新到iOS 16.1.2了&#xff0c;如果你的iPhone之前为了尝鲜已经下载安装iOS 16测试版&#xff0c;该如何升级iOS 16正式版呢&#xff1f;一起来了解下吧&#xff01; 方法一、移除iOS 16 Beta描述文件 1、进入【设置】-【通用】-【VPN与设备管理】&…

python+django汽车站售票票务管理系统

1.用户需要进行注册才可以登录本系统。 2.用户登录系统后可以在通知公告中获取最新的通知或者搜索需要的通知&#xff1b;可以在车票信息中查询到所需的车票信息并且可以通过站点、票价等选项进行筛选&#xff0c;选定车票后可以直接购票。用户还可以在订单管理中进行退票操作&…

引爆全球的ChatGPT,Java、面试、刷题、双色球它都会?

大家好&#xff0c;我是二哥呀。 这两天&#xff0c;ChatGPT 引爆全球&#xff0c;不管是搞技术的&#xff0c;还是没搞技术的&#xff0c;都在玩&#xff0c;玩的不亦乐乎&#xff0c;仿佛找到了内心真正的伴侣&#xff08;&#x1f602;&#xff09;。 一开始我以为这玩意不…

图像数据的特征工程

一提到特征工程&#xff0c;我们立即想到是表格数据。但是我们也可以得到图像数据的特征&#xff0c;提取图像中最重要的方面。这样做可以更容易地找到数据和目标变量之间的映射。 这样可以使用更少的数据和训练更小的模型。更小的模型可以减少预测所需的时间。这在部署到边缘设…

【 第八章 SQL执行效率,慢日志查询,profile,explain,最左前缀法则,范围查询】

第八章 SQL执行效率&#xff0c;慢日志查询&#xff0c;profile&#xff0c;explain&#xff0c;最左前缀法则&#xff0c;范围查询 1.SQL执行效率&#xff1a; MySQL 客户端连接成功后&#xff0c;通过 show [session|global] status 命令可以提供服务器状态信息。通过如下指…

微服务框架 SpringCloud微服务架构 26 数据聚合 26.1 聚合的分类

微服务框架 【SpringCloudRabbitMQDockerRedis搜索分布式&#xff0c;系统详解springcloud微服务技术栈课程|黑马程序员Java微服务】 SpringCloud微服务架构 文章目录微服务框架SpringCloud微服务架构26 数据聚合26.1 聚合的分类26.1.1 聚合的分类26.1.2 总结26 数据聚合 26…

基础入门 - SpringBoot 自动配置

目录 3、自动配置原理入门 3.1、引导加载自动配置类 1、SpringBootConfiguration 2、ComponentScan 3、EnableAutoConfiguration 1、AutoConfigurationPackage 2、Import(AutoConfigurationImportSelector.class) 3.2、按需开启自动配置项 3.3、修改默认配置 1、自动配…

写作人的福音——obsidian非官方插件之senGener

背景 之前的是转发作者的项目文档&#xff0c;这次应作者要求&#xff0c;写了个试用报告。 刚用这个插件的时候&#xff0c;还磕磕碰碰&#xff0c;总有点小问题&#xff0c;按快捷键不反应&#xff0c;服务器崩溃什么的&#xff0c;并不指望真的用起来&#xff0c;权当个玩具…

【Mysql】当Naviact创建数据库时失败出现1044 -Access denied for user ‘root‘@‘%‘ to database

业务背景&#xff1a;当我用Navicat连接我服务器上的MySQL8&#xff0c;然后点击创建数据库时&#xff0c;创建失败&#xff0c;并报异常 1044 -Access denied for user root% to database解决方案&#xff1a;本问题是因为&#xff0c;账号无权限导致的&#xff0c;因此只需要…

C语言实现一个闪烁的圣诞树(控制台)

下下下周就是圣诞节啦&#xff0c;C语言的圣诞树必须安排起&#xff01;&#xff01;&#xff01; 效果展示&#xff1a; 原理说明&#xff1a; 函数 layer 画出树的层次&#xff0c;根据坐标来输出位置&#xff1b; void layer(int x, int y, int num, int col) 函数 tri…

毕业设计-基于大数据的电影推荐系统-python

目录 前言 课题背景和意义 实现技术思路 实现效果图样例 前言 &#x1f4c5;大四是整个大学期间最忙碌的时光,一边要忙着备考或实习为毕业后面临的就业升学做准备,一边要为毕业设计耗费大量精力。近几年各个学校要求的毕设项目越来越难,有不少课题是研究生级别难度的,对本科…

(七) Docker安装常规软件

Docker安装常规软件一、安装tomcat1、docker hub上面查找tomcat镜像2、从docker hub上拉取tomcat镜像到本地3、docker images查看是否有拉取到的tomcat4、使用tomcat镜像创建容器实例5、访问Tomcat首页6、拉取这个版本不需要去修改Webapps目录&#xff08;免修改版&#xff09;…

在Python中自然语言处理生成词云WordCloud

了解如何在Python中使用WordCloud对自然语言处理执行探索性数据分析。 最近我们被客户要求撰写关于自然语言处理的研究报告&#xff0c;包括一些图形和统计输出。 什么是WordCloud&#xff1f; 很多时候&#xff0c;您可能会看到一片云&#xff0c;上面堆满了许多大小不同的单…