分类的加载原理补充及类扩展 , 关联对象介绍

类的加载原理:
iOS 类的加载原理上
iOS 类的加载原理中
iOS 类的加载原理下
分类的加载原理补充及类扩展 , 关联对象介绍

分类加载的补充

method_list 数据结构

首先通过源码可以看到 method_list_t 继承于 entsize_list_tt,包含 method_t , method_list_t , 0xffff0003 这三个元素。

我们在 attachCategories 函数中输出打印可以看到 method_list 是一个指针类型,也可以看到 method_list 的数据结构,我们通过 get(0) 可以获取到 method_t

我们查看 entsize_list_tt 也可以看到有一个 get 方法,并且 method_list_t 里面存储的是指针而不是 method_t 相应的数据结构。

主类没有实现 load 方法,分类实现了 load 方法,数据是如何加载的

前面我们在 iOS 类的加载原理下 中分析了分类加载的几种情况,这里再补充一种,当主类不实现 load 方法,分类大于 1 个,且都实现 load 方法的时候最后会执行 attachCategories 方法。这里我们来分析一下具体的执行流程。

  1. load_images

首先不一样的点是在 load_images 函数中没有走 loadAllCategories 方法,而是执行了 prepare_load_methods

  1. prepare_load_methods

这里会迫使主类执行 realizeClassWithoutSwift 流程。

  1. methodizeClass

  2. attachToClass

这里会执行到 else 里面,因为从 realizeClassWithoutSwift 方法来到这里,就可以确定要么是元类,要么是本类。

  1. attachCategories

这里就会进行方法的处理,跟正常方法的加载处理逻辑一样。这里需要补充一点,当一个分类分类里面没有任何方法,这里 cats_count 不会计入。

分类的加载是否需要排序

当分类的加载方法列表是否需要排序,这里需要分两种情况,同样我们在 iOS 类的加载原理下 中讲的,当类与分类都实现 load 方法的时候 methodList 中存放的是方法数组指针,分类 A 方法数组指针 + 分类 B 方法数组指针 + 主类方法数组指针(这里分类的先后跟加载顺序有关),通过前面讲的,我们也知道,这里会经过排序。

在这种情况下当分类与主类都实现一个方法 saySomething 的时候,我们来看一下源码的查找流程。

getMethodNoSuper_nolock 函数中我们输出 methods.beginLists() 可以看到这里取出的是分类的方法数组指针,也可以看出 methods 存放的是方法数组指针,这里会循环遍历 methods,然后取出 mlists,然后查找 mlists 中是否有对应的 method_t,有的话就返回。

上面讲了分类与主类都实现 load 方法的情况,其他情况下分类与主类的方法会一次通过 data() 方法获取到,method_list_t 中存放的不再是数组指针,而是 method_t 指针,分类与主类的方法都会存放在 method_list_t 中。

template
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
    ASSERT(list);

    auto first = list->begin();
    auto base = first;
    decltype(first) probe;

    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    // base相当于low地址,count是max地址,probe是middle地址
    for (count = list->count; count != 0; count >>= 1) {
        // 指针平移至中间位置
        // 从首地址 + 下标 --> 移动到中间位置(count >> 1)
        probe = base + (count >> 1);
        // 获取该位置的sel名称
        uintptr_t probeValue = (uintptr_t)getName(probe);
        // 如果查找的key的keyvalue等于中间位置(probe)的probeValue,则直接返回中间位置
        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            // 分类方法同名- while 平移 -- 向前在查找,判断是否存在相同的方法,保证调用的是分类的
            while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
                probe--;
            }
            return &*probe;
        }
        // 如果keyValue 大于 probeValue,就往probe即中间位置的右边查找,即中间位置再右移
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

当方法查找的时候会在 findMethodInSortedMethodList 函数中会通过二分查找的方式进行查找,保证找到 method_list_t 中最前面的一个方法。

class_ro_t 数据结构

我们前面讲的在 realizeClassWithoutSwift 中执行 auto ro = (const class_ro_t *)cls->data() 这句代码就能得到 class_ro_t 这样的数据结构,并对 class_ro_t 中的属性进行赋值。那么这里是如何赋值的呢?

首先 realizeClassWithoutSwift_read_images 函数中执行,所以在编译阶段就能读取到这些数据,所以 class_ro_t 不是在 objc 源码中,而是在 LLVM 源码中(gitHub 上可以下载到,建议用 VSCode 打开)。

LLVM 源码中我们可以看到 class_ro_t 的数据结构跟在 objc 源码中看到的类似,只是多了 m_ 的前缀。


然后我们来到 Read 方法,这里会先读取当前的 addr,然后经过一些包装处理得到 extractor,然后分别执行 extractor .GetU32_unchecked(&cursor) 对结构体的属性进行赋值。在 objc 源码中是通过类型强转进行接收的。

Read_class_row 方法中可以看到在这里进行了 Red 方法的调用。其实 method_list_tobjc_class 的读取也是类似。

类扩展分析

类扩展与分类的区别。

  1. category: 类别,分类
  • 专门用来给类添加新的方法
  • 不能给类添加成员属性,添加了成员变量,也无法取到(注意:其实可以通过 runtime 给分类添加属性)
  • 分类中用 property 定义变量,只会生成变量的 getter, setter 方法的声明,不能生成方法实现和带下划线的成员变量。
  1. extension: 类扩展
  • 可以说成是特殊的分类,也称作匿名分类
  • 可以给类添加成员属性,但是是私有变量
  • 可以给类添加方法,也是私有方法

类扩展底层代码实现

首先我们在 main.m 文件中定义一个 LGPerson 类,并在类扩展中添加属性与方法。然后通过 clang -rewrite-objc main.m 生成 cpp 文件来看一下底层 c++ 代码的实现。

@interface LGStudent : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;

- (void)instanceMethod;
+ (void)classMethod;

@end

@interface LGStudent ()

@property (nonatomic, copy) NSString *ext_name;
@property (nonatomic, assign) int ext_age;
- (void)ext_instanceMethod;
+ (void)ext_classMethod;
@end

@implementation LGStudent
- (void)instanceMethod{
    NSLog(@"%s",__func__);
}

+ (void)classMethod{
    NSLog(@"%s",__func__);
}


- (void)ext_instanceMethod{
    NSLog(@"%s",__func__);
}

+ (void)ext_classMethod{
    NSLog(@"%s",__func__);
}
@end



int main(int argc, const char * argv[]) {
    @autoreleasepool {

        LGPerson * person = [LGPerson alloc];
        [person saySomething];
    }
    return 0;
}

生成 cpp 文件之后我们可以看到在类扩展里面添加的方法 ext_instanceMethod 一样被加载到 method_list 中来了。

类扩展是否影响类的加载和编译

前面我们讲分类的时候知道分类会影响类的加载和编译,那么类扩展是否也会影响呢?这里我们为 LGPerson 类添加类扩展并添加方法,然后执行源码并在 realizeClassWithoutSwift 函数中打印 ro 来看一下。

@interface LGPerson ()

- (void)ext_instanceMethod;

+ (void)ext_classMethod;
@end

@implementation LGPerson

+ (void)load{}

- (void)saySomething{
    NSLog(@"%s",__func__);
}

+ (void)sayHappy{
    NSLog(@"LGPerson say : Happy!!!");
}



- (void)sayHello1{
    NSLog(@"sayHello1 %s", __func__);
}

+ (void)say6661{
    NSLog(@"say6661 %s", __func__);
}


- (void)ext_instanceMethod{
    NSLog(@"%s",__func__);
}

+ (void)ext_classMethod{
    NSLog(@"%s",__func__);
}

@end

我们在这里不断 p $1.get(n).big(),当 p $1.get(2).big() 的时候可以看到输出了类扩展中添加的方法 ext_instanceMethod,可以看出类扩展中数据会作为类的一部分被一块加载。

关联对象

- (void)setCate_name:(NSString *)cate_name{
    objc_setAssociatedObject(self, "cate_name", cate_name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

我们通常通过 runtimeobjc_setAssociatedObject 方法来添加分类的关联对象,那么我们来看一下 objc_setAssociatedObject 方法的底层是如何实现的(这里是 779 版本)。

void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
    _object_set_associative_reference(object, key, value, policy);
}

通过底层源码可以看到这里会调用 _object_set_associative_reference 方法,不同版本的源码这里实现会有不同,但是最上层都是调用 objc_setAssociatedObject 方法,这里就提现了苹果的分层思想,也是值得我们学习的,这里就保证了最上层 api 的稳定性。

接着我们继续分析 _object_set_associative_reference 方法。


首先我们先看下 _object_set_associative_reference 方法的简单注释及这张结构体图。通过结构图我们可以看到,ObjcAssociationObjcAssociation 会被包装成 ObjcAssociation 的形式,然后通过键值匹配的形式被存储到 ObjectAssociationMap,每个对象会对应一张 ObjectAssociationMap 表,最后每个对象跟对应的ObjectAssociationMap 又会以键值的形式存到 AssociationsHashMap,这里是双层 hashMap 结构。AssociationsHashMap 是一张全局唯一的表。具体原因我们可以看下 AssociationsManager

class AssociationsManager {
    using Storage = ExplicitInitDenseMap, ObjectAssociationMap>;
    // _mapStorage 是一个静态变量,写到这里代表只有 AssociationsManager 才能调起 _mapStorage
    // 所以不同的 AssociationsManager 这里调用的都是同一个 _mapStorage
    static Storage _mapStorage;

public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }

    // 所以得到的 AssociationsHashMap 是唯一的
    AssociationsHashMap &get() {
        return _mapStorage.get();
    }

    static void init() {
        _mapStorage.init();
    }
};

通过打印我们也可以看到 AssociationsHashMaprefs_result 的数据结构。这里我们大致了解了关联对象的数据结构,那么 key , value 具体是如何存储的呢?我们接着继续往下看。

if (value) {
            // 第一次调用的时候 try_emplace 会创建一个空的 TheBucket
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) {
                /* it's the first association we make */
                isFirstAssociation = true;
            }

            /* establish or replace the association */
            auto &refs = refs_result.first->second;
            // 第二次再执行的时候会对 TheBucket 的 value 赋值为 association
            auto result = refs.try_emplace(key, std::move(association));
            if (!result.second) {
                association.swap(result.first->second);
            }
        } else {
            // 如果 value 为空这里就会清空
            auto refs_it = associations.find(disguised);
            if (refs_it != associations.end()) {
                auto &refs = refs_it->second;
                auto it = refs.find(key);
                if (it != refs.end()) {
                    association.swap(it->second);
                    refs.erase(it);
                    if (refs.size() == 0) {
                        associations.erase(refs_it);

                    }
                }
            }
        }
  template 
  std::pair try_emplace(const KeyT &Key, Ts &&... Args) {
    // 这里会先创建一个空的 BucketT
    BucketT *TheBucket;
    // 这里 key 是包装的对象,被关联对象,会判断能不能找到对应的 TheBucket
    if (LookupBucketFor(Key, TheBucket))
      return std::make_pair(
               makeIterator(TheBucket, getBucketsEnd(), true),
               false); // Already in map.

    // 找不到就会来到这里会插入一个空的 TheBucket
    TheBucket = InsertIntoBucket(TheBucket, Key, std::forward(Args)...);
    return std::make_pair(
             makeIterator(TheBucket, getBucketsEnd(), true),
             true);
  }

最后总结一下关联对象的设值流程:

关联对象:设值流程
1:创建一个AssociationsManager管理类
2:获取唯一的全局静态哈希Map
3:判断是否插入的关联值是否存在:
3.1: 存在走第4步
3.2:不存在就走:关联对象插入空流程
4:创建一个空的ObjectAssociationMap去取查询的键值对
5:如果发现没有这个key就插入一个空的BucketT进去返回
6:标记对象存在关联对象
7:用当前修饰策略和值组成了一个ObjcAssociation 替换原来BucketT中的空
8:标记一下ObjectAssociationMap的第一次为 false

关联对象插入空流程
1:根据DisguisedPtr找到AssociationsHashMap中的iterator迭代查询器
2:清理迭代器
3:其实如果插入空置相当于清除

你可能感兴趣的:(分类的加载原理补充及类扩展 , 关联对象介绍)