Category相关探究

1. Category的作用

  • 可以减少单个文件的体积
  • 可以把不同的功能组织到不同的Category中
  • 可以按需加载
  • 声明私有方法
  • 把framework的私有方法公开
    Tips:比如一个类的方法,没有在.h文件中暴露出来,我们可以给这个类创建一个分类,在分类的.h文件中,去声明这个私有方法。

Extension 扩展

  • 类的一部分
  • 编译时期就确定的
  • 隐藏类的私有属性

Category

  • 运行时期确定的
  • 不能添加变量(编译时期,对象的内存布局已经确定了,添加变量会破坏类的内存布局)

2. Category 底层原理探索

Category 的数据结构

struct category_t {
    const char *name; // 所属类的名称
    classref_t cls; // 要扩展的类对象(编译时期是没有值的)
    struct method_list_t *instanceMethods; // 实例方法
    struct method_list_t *classMethods; // 类方法
    struct protocol_list_t *protocols;    // 协议
    struct property_list_t *instanceProperties; // 实例属性
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties; // 类属性

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

从上面的Category的数据结构,我们就可以看出category的可为(可以添加实例方法,类方法,甚至可以实现协议,添加属性)和不可为(无法添加实例变量)。

2.1 代码调用流程分析
  • 2.1.1 编译时期

可以通过clang 来编译一下:

clang -rewrite-objc DDPerson+Test.m

DDPerson+Test.m就是我随便创建的一个分类的 .m文件,使用上述命令编译完成后,会在该文件同级目录下生成一个DDPerson+Test.cpp文件。

Tips:编译过后的东西,都会存在Mach-O的可执行文件当中。

我们可以打开该文件找到分类的结构体定义 _category_t:
再向下可以看到这个分类的结构体到底存放了什么内容:

static struct _category_t _OBJC_$_CATEGORY_DDPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = 
// 这个分类它存放在Mach-O文件__objc_const的字段中
{
    "DDPerson",  //  当前类的名称
    0, // &OBJC_CLASS_$_DDPerson, 指向类对象的指针,运行时依赖 当前类的名称进行绑定
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_DDPerson_$_Test, // 当前分类里面的实例方法 
    0,
    0,
    0,
};

我们可以看到第三个 &OBJC$CATEGORY_INSTANCE_METHODS_DDPerson... ,是编译器生成的当前分类的实例方法列表。
因为我们只是给分类添加了一个方法,所以其余的类方法列表,协议列表,属性列表,都是0,也就是空的。

static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_DDPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
  // 下面三个参数分别是 方法编号 方法签名 真实的函数地址
    {{(struct objc_selector *)"dd_privateTest", "v16@0:8", (void *)_I_DDPerson_Test_dd_privateTest}}
};

这一步就是生成分类的方法列表,用这个方法列表,初始化分类本身。

到现在为止分类在编译时期做的事情差不多已经完成了,
总结来说就是把当前的分类编译成结构体,然后把对应的字段填充上相应的数据。

但是,编译最后的分类数据放在哪里呢?我们继续向下看:

static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
    &_OBJC_$_CATEGORY_DDPerson_$_Test,
};

上面是非常关键的,所有的分类都会存在 __DATA 数据段下的的 __objc_catlist section当中。

到目前为止,在编译时期对于分类的操作都已经完成了,运行时期,App就会从 __objc_catlist section中去读取所有的分类了。

  • 2.1.2 运行时期

运行时期就要开始加载编译时期存储的所有分类了。

那么Category是如何被加载的呢?

  • dyld 是苹果的动态加载器,用来加载 image (不是指图片,而是指 Mach-O 格式的二进制文件)
  • 当程序启动的时候,系统内核首先会加载dyld,而dyld会将我们APP所以来的各种库加载到内存空间中,其中就包括libobjc库(OC和runtime),这些工作,是在APP的main函数执行前完成的。
  • _objc_init 是 OC runtime 的入口函数,在这里面主要功能是读取Mach-O 文件 OC对应的 Segment section,并根据其中的数据代码信息,完成为OC的内存布局,以及初始化runtime相关的数据结构。

我们来看下_objc_init方法

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();

    // 我们主要关注下面这个方法,这个方法注册了三个回调函数:
    // dyld将image加载入内存的时候(将image映射到内存空间的时候)会执行 map_images 
    // dyld初始化image模块完成的时候调用 load_images(load方法也会在这个时候调用)
    // dyld将image移出内存的时候 unmap_image函数会被调用
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

将分类加载到当前的类的操作是在 map_images 回调函数中完成的,下面我们先去看下这个 回调函数:
我们忽略不必要的代码走到下面这个函数中:

void 
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                  const struct mach_header * const mhdrs[])

在这个函数底部,我们可以看到 _read_images的函数:

if (hCount > 0) {
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }

_read_images 最主要的作用是: 读取OC相关的 Section段(Mach-O文件中的数据段),并进行初始化。

我们继续看 _read_images这个函数,直接到分类相关的部分:

// EACH_HEADER 就是遍历头文件
// Discover categories. 
    for (EACH_HEADER) {
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        for (i = 0; i < count; i++) {
            category_t *cat = catlist[I];
            Class cls = remapClass(cat->cls);

            if (!cls) {
                // Category's target class is missing (probably weak-linked).
                // Disavow any knowledge of this category.
                catlist[i] = nil;
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class", 
                                 cat->name, cat);
                }
                continue;
            }
      ...

我们拿到的 catlist 就是上节讲到的编译器为我们生成的category_t数组,它是通过
_getObjc2ClassList 这个方法拿到的,我们来关注下 _getObjc2ClassList 这个方法,点进去可以看到:


TSECT(_getObjc2NonlazyClassList,    classref_t,      "__objc_nlclslist");
// 看这个
GETSECT(_getObjc2CategoryList,        category_t *,    "__objc_catlist");
GETSECT(_getObjc2NonlazyCategoryList, category_t *,    "__objc_nlcatlist");
GETSECT(_getObjc2ProtocolList,        protocol_t *,    "__objc_protolist");

在编译时期,我们可以知道,所有的分类都是存放在__objc_catlist 这个section当中(或者说对应这个key值),所以这里就是读取这个 __objc_catlist 对应的数据。

读取完成之后,我们继续往下看,下面这段代码:

// 处理这个Category
            // 首先,将Category注册到它的目标类。
            // 然后,如果实现了类,则重构类的方法列表(等等)。
            bool classExists = NO;
           // 把category的实例方法、协议以及属性添加到类上
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {// 把分类注册到它的目标类建立一个关联
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    // 重构类的方法列表
                    remethodizeClass(cls);
                    classExists = YES;
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category -%s(%s) %s", 
                                 cls->nameForLogging(), cat->name, 
                                 classExists ? "on existing class" : "");
                }
            }

           // 把category的类方法和协议添加到类的metaclass上
            if (cat->classMethods  ||  cat->protocols  
                ||  (hasClassProperties && cat->_classProperties)) 
            {
                addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                if (cls->ISA()->isRealized()) {
                    remethodizeClass(cls->ISA());
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category +%s(%s)", 
                                 cls->nameForLogging(), cat->name);
                }
            }

上面这段代码,主要的功能如下:
a. 把category的实例方法、协议以及属性添加到类上
b. 把category的类方法和协议添加到类的metaclass上

在上述的代码片段里,addUnattachedCategoryForClass只是把类和category做一个关联映射,而remethodizeClass才是真正去处理添加事宜的功臣。

那好,我们再去分析 remethodizeClass 这个关键方法:

// 在runtime源码中,我们翻译一下这个函数的注释,就能知道这个函数的作用了
// 1.将当前分类附加到现有类。
// 2.重新排列cls的方法列表、协议列表和属性列表。
// 3.更新cls及其子类的方法缓存。
static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;

    runtimeLock.assertLocked();

    isMeta = cls->isMetaClass();

    // Re-methodizing: check for more categories
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        if (PrintConnecting) {
            _objc_inform("CLASS: attaching categories to class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}

我们看到上面的方法,主要是调用 attachCategories 这个方法,
那么我们继续,在 attachCategories 函数中看到 prepareMethodList函数:

...
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
...

这个函数的关键内容如下,它只是把所有category的实例方法列表拼成了一个大的实例方法列表:

...
  // 添加方法列表到数组
  // 重新分配方法列表。
    // The new methods are PREPENDED to the method list array.

    for (int i = 0; i < addedCount; i++) {
        method_list_t *mlist = addedLists[I];
        assert(mlist);

        // Fixup selectors if necessary
        if (!mlist->isFixedUp()) {
          // 
            fixupMethodList(mlist, methodsFromBundle, true/*sort*/);
        }

        // Scan for method implementations tracked by the class's flags
        if (scanForCustomRR  &&  methodListImplementsRR(mlist)) {
            cls->setHasCustomRR();
            scanForCustomRR = false;
        }
        if (scanForCustomAWZ  &&  methodListImplementsAWZ(mlist)) {
            cls->setHasCustomAWZ();
            scanForCustomAWZ = false;
        }
    }
...

准备好了这个方法列表之后之后,我们继续看

...
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
...

主要是 rw->methods.attachLists(mlists, mcount); 这个方法

void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) {
            // many lists -> many lists
            // 旧的大小,指的是原先方法列表的大小
            uint32_t oldCount = array()->count;
           // 新的就是 旧的 + 分类中要添加的
            uint32_t newCount = oldCount + addedCount;
           // 然后就是把新的方法添加到旧的方法列表
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
        } 
        else {
            // 1 list -> many lists
            List* oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount;
            if (oldList) array()->lists[addedCount] = oldList;
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }

这里面主要是使用 memmove memcpy 两个方法
都是用来做拷贝的操作,用来移动内存空间,为将要添加的方法腾出内存空间:

void    *memcpy(void *__dst, const void *__src, size_t __n);
void    *memmove(void *__dst, const void *__src, size_t __len);
// 把当前 src 指向的位置,拷贝len的长度,放到dst的位置

比如:1 2 3 4 5 6 7
memmove(1的位置,4的位置,长度3)
完成之后就是 4 5 6 4 5 6 7
就是把 456 从 4 的位置,拷贝到 1 的位置
这种场景下,使用memcpy 也可以。

如果是另一个场景:将 1 的位置 长度为 3 拷贝到 3 的位置,
那么**有可能**会出现 12121567 这种情况

所以这种内存重叠的情况下,memcpy 函数 有可能成功,有可能失败。

然后我们分析下 代码中

memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));

他的作用:
如果原有方法有10个,新添加的方法有3个,一共有13个,这句代码的作用就是将原有的10个方法,放到13个方法的后10个
刚开始是
旧 旧 旧 旧 旧 旧 旧 旧 旧 旧 空 空 空
变成
空 空 空 旧 旧 旧 旧 旧 旧 旧 旧 旧 旧
这样,前面三个方法就空出来了,因为这个场景刚好是上面介绍 memmove memcpy函数时的第二个场景,有可能会发生内存重叠,所以这里使用的是memmove。

前面3个方法空出来了,此时我们就要把需要添加的三个方法放到这里了,这里不会发生内存重叠,所以我们使用memcpy

memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));

它就是拷贝要添加的方法 addedLists 地址,长度为要添加的长度 addedCount 放到lists中。

从attachLists函数中我们可以看到,分类的方法覆盖掉了主类的方法,分类方法会添加到主类方法的前面,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休,殊不知后面可能还有一样名字的方法:

分类方法 分类方法 主类方法 主类方法。
我们可以打印出类的方法,然后通过代码可以调用到主类的原有的方法。

我们现在可以总结一下map_images中对分类做的事情了:

  1. _getObjc2CategoryList方法调用__objc_catlist section 段记录的所有Category,存放到category_t*数组
  2. 依次读取category_t*数组
  3. cat和cls(主类)进行映射
  4. remethodizeClass (修改method_list的结构(memmove memcpy))

3.关联对象探索

我们在开发中经常使用关联对象,在分类中添加一个属性:

@interface DDPerson (Test)

@property (nonatomic, copy) NSString *name;

@end

我们知道在主类中的属性其实包含三个东西:getter setter ivar, 这种在分类中声明的属性是没有ivar的,只是生成了一个存取方法,以及存储当前value的容器(ObjcAssociation 下面会讲到),因为上面也提到过,分类是不能添加成员变量的。

3.1 设置关联对象

在给分类关联对象的时候,我们会调用这个方法:

objc_setAssociatedObject(关联到哪个对象, 存储的key, 要关联的对象, 关联策略)

下面,我们就以这个方法为切入点,从源码中看一下:

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

实际上是调用了 _object_set_associative_reference 这个方法,继续看:

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    ObjcAssociation old_association(0, nil);
    // 取了一个新的value,根据策略设置
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        // 关联对象的管理者
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        // 当前要关联到的对象的地址按位取反(作为当前hashmap当中的key)
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // c++ 语法,获取一个迭代器,找到 disguised_object 作为key时对应的value,里面的 j->first 指的是key j->second 指的是key对应的value
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) { // 如果disguised_object 对应的value有值,说明该对象有过关联
                // 拿到 disguised_object 作为key 对应的value,也是一个map
                ObjectAssociationMap *refs = i->second;
                // 再以key值获取一个新的迭代器,这个key是我们传进来的,是我们所定义的
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) { // 如果用户传进的key有对应的value
                    // 获取到旧的value,赋给old_association,最后会被释放掉
                    old_association = j->second;
                    // 赋值新的value,ObjcAssociation 来存储我们的关联属性
                    j->second = ObjcAssociation(policy, new_value);
                } else { // 如果用户传进的key没有对应的value
                    // 直接赋值新的value
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // 如果这个对象是第一次关联,会走到这里,先创建一个Map,以disguised_object 作为key存进manager管理的hashmap,再将new_value 和 policy生成的 ObjcAssociation 对象,以key存入 Map
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                // 设置当前object的isa指针,添加一个标识,说明这个对象有关联对象,在这个对象销毁的时候,去销毁这些关联对象
                object->setHasAssociatedObjects();
            }
        } else {
            // 如果是nil,则断开关联
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }
    // 释放 old_association
    if (old_association.hasValue()) ReleaseValue()(old_association);
}

代码很长,我们可以从上到下一点一点的分析:

a.先进行内存管理

// 这个方法是判断value有值的话,就去调用acquireValue 生成一个新的对象,进行内存管理
 id new_value = value ? acquireValue(value, policy) : nil;
// 看下源码
 acquireValue(value, policy) =>
// 清晰明了,根据关联策略,对value进行操作, objc_retain ((id(*)(id, SEL))objc_msgSend)(value, SEL_copy); 这两个操作就很熟悉了,strong copy关键字的内部实现。
static id acquireValue(id value, uintptr_t policy) {
    switch (policy & 0xFF) {
    case OBJC_ASSOCIATION_SETTER_RETAIN:
        return objc_retain(value);
    case OBJC_ASSOCIATION_SETTER_COPY:
        return ((id(*)(id, SEL))objc_msgSend)(value, SEL_copy);
    }
    return value;
}

b. AssociationsManager 初始化其管理的hashmap

 // 管理关联对象,其内部有一个static的 hashmap,用来管理关联对象的
 AssociationsManager manager;
// 这个是初始化hashMap, 下面代码中有这个方法的实现
 AssociationsHashMap &associations(manager.associations());
=> 点进去看下 AssociationsManager:
// 从下面的注释中我们也可以看到 这个hashMap 以对象的指针作为key的,value仍然是一个hashMap ,我们先了解下 AssociationsManager 的作用就是维护了一个hashmap.
class AssociationsManager {
    // associative references: object pointer -> PtrPtrHashMap.
    static AssociationsHashMap *_map;
public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }
    
    AssociationsHashMap &associations() {
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    }
};

存的过程,可以通过下面这张图来理解,就会非常的清楚:


屏幕快照 2019-09-08 下午12.07.33.png
3.2 取关联对象

理解了怎么存的也就明白如何取的了,贴上源码:

id _object_get_associative_reference(id object, void *key) {
    id value = nil;
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            ObjectAssociationMap *refs = i->second;
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                ObjcAssociation &entry = j->second;
                value = entry.value();
                policy = entry.policy();
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
                    objc_retain(value);
                }
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        objc_autorelease(value);
    }
    return value;
}

3.3 关联对象如何释放呢?

是在关联到的对象释放的时候,去释放它的关联对象。
我们可以从dealloc 方法入手去探究一下,这个隐藏的很深:

- (void)dealloc
↓
_objc_rootDealloc(self);
obj->rootDealloc();
object_dispose((id)this);
objc_destructInstance(obj);

终于到了objc_destructInstance方法:

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }
    return obj;
}

我们可以看到是判断 assoc 这个变量为真时,去调用 _object_remove_assocations 移除关联对象的方法,获取 assoc 值的方法是
obj->hasAssociatedObjects(); 这个方法中其实就是获取在设置关联对象时,给isa设置的那个标志。

我们继续看 _object_remove_assocations 这个方法:


void _object_remove_assocations(id object) {
    vector< ObjcAssociation,ObjcAllocator > elements;
    {
        // 获取manager
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        // 如果size为0,说明没有关联属性,直接return
        if (associations.size() == 0) return;
        // 对象的地址取反
        disguised_ptr_t disguised_object = DISGUISE(object);
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            
            // 获取到disguised_object 对应的map
            ObjectAssociationMap *refs = i->second;
            // 获取到所有的关联对象,放入elements
            for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
                elements.push_back(j->second);
            }
            // remove the secondary table.
            delete refs;
            associations.erase(i);
        }
    }
    // 释放掉
    for_each(elements.begin(), elements.end(), ReleaseValue());
}

4. load方法探究

调用规则

  • 一个类的load方法在所有父类load 方法调用之后调用
  • 分类的load方法在当前类的load方法调用之后调用
  • 分类load的调用顺序和编译顺序有关

在前面,我们有讲到objc_init是runtime的入口函数

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();

    // 我们主要关注下面这个方法,这个方法注册了三个回调函数:
    // dyld将image加载入内存的时候(将image映射到内存空间的时候)会执行 map_images 
    // dyld初始化image模块完成的时候调用 load_images(load方法也会在这个时候调用)
    // dyld将image移出内存的时候 unmap_image函数会被调用
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

load方法也是在 load_images 中调用的,我们现在就去从load_images方法入手去看一看:

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        mutex_locker_t lock2(runtimeLock);
       // 准备
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant) 调用
    call_load_methods();
}

这个方法分为两块,prepare_load_methods()和 call_load_methods();

4.1 prepare_load_methods

我们这边先分析 prepare_load_methods (),他是先把各个类或分类load方法都准备好:

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, I;

    runtimeLock.assertLocked();

  // 从Mach-o 文件加载类的列表
    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        // 数组[,,] 有顺序
        // 调整类的顺序
        schedule_class_load(remapClass(classlist[i]));
    }

// 针对分类的操作
    category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[I];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        realizeClass(cls);
        assert(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}
4.1.1 针对类的load方法的操作

我们去看下 schedule_class_load 这个方法:

static void schedule_class_load(Class cls)
{
    if (!cls) return;
    assert(cls->isRealized());  // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;

    // Ensure superclass-first ordering
    schedule_class_load(cls->superclass);

    // 根据cls,将cls的load方法添加到一个全局的数组中
    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

这个方法是递归调用,目的就是遵循上面的第一条规则,父类的load方法,在子类的load方法之前调用,也就是将类和类的load方法,这样存入全局的一个容器(可以理解为数组)中:
[<父类,父类load方法>,<父类,父类load方法>,<子类,子类load方法>]

上面说到添加到一个全局的容器中,这一步是如何操作的呢?就是add_class_to_loadable_list:

void add_class_to_loadable_list(Class cls)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = cls->getLoadMethod();
    if (!method) return;  // Don't bother if cls has no +load method
    
    if (PrintLoading) {
        _objc_inform("LOAD: class '%s' scheduled for +load", 
                     cls->nameForLogging());
    }
    
    if (loadable_classes_used == loadable_classes_allocated) {
        // 字节对齐(???)
        loadable_classes_allocated = loadable_classes_allocated*2 + 16;
        loadable_classes = (struct loadable_class *)
            realloc(loadable_classes,
                              loadable_classes_allocated *
                              sizeof(struct loadable_class));
    }
    // loadable_classes 就是一个全局的容器
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}

到现在 schedule_class_load(remapClass(classlist[i]));
这个方法我们就分析完了,它就是调整顺序,先将所有父类的load方法存入全局容器中,再存入当前类的load方法。

4.1.2 针对分类load方法的操作

接下来,就到了对于分类的操作了,还回到prepare_load_methods 这个方法

void prepare_load_methods(const headerType *mhdr)
{
...
// 针对分类的操作
    category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[I];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        realizeClass(cls);
        assert(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

我们知道分类load的调用顺序是 和编译顺序有关,上面的代码中我们可以看到,当从Mach-o文件中读取到 categorylist 的时候,顺序就已经决定了,谁在前面就会先去调用谁的load方法,所以在下面就会直接调用 add_category_to_loadable_list(cat)方法:

void add_category_to_loadable_list(Category cat)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = _category_getLoadMethod(cat);

    // Don't bother if cat has no +load method
    if (!method) return;

    if (PrintLoading) {
        _objc_inform("LOAD: category '%s(%s)' scheduled for +load", 
                     _category_getClassName(cat), _category_getName(cat));
    }
    
    if (loadable_categories_used == loadable_categories_allocated) {
        loadable_categories_allocated = loadable_categories_allocated*2 + 16;
        loadable_categories = (struct loadable_category *)
            realloc(loadable_categories,
                              loadable_categories_allocated *
                              sizeof(struct loadable_category));
    }

    loadable_categories[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used++;
}

这个方法跟上面对于类的load方法操作一样,初始化了loadable_categories 这么一个容器,存储分类的load方法:
[<分类,load方法>,<分类,load方法>,<分类,load方法>]

到此,prepare_load_methods 的方法也就执行完毕了,它就是准备好了两个全局容器loadable_classes和loadable_category,分别存储类的load方法,分类的load方法。

4.2 call_load_methods

准备好了之后,就要开始调用了:

void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            // 先调用类的load方法
            call_class_loads();
        }

        // 2. Call category +loads ONCE 再调用分类的load方法
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

在上面可以看到,是先 call_class_loads 调用类的load方法,再 call_category_loads 调用分类的load方法。

4.2.1 call_class_loads 调用类的load方法
static void call_class_loads(void)
{
    int I;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, SEL_load);
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}
4.2.1 call_category_loads 调用分类的load方法
static bool call_category_loads(void)
{
    int i, shift;
    bool new_categories_added = NO;
    
    // Detach current loadable list.
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Category cat = cats[i].cat;
        load_method_t load_method = (load_method_t)cats[i].method;
        Class cls;
        if (!cat) continue;

        cls = _category_getClass(cat);
        if (cls  &&  cls->isLoadable()) {
            if (PrintLoading) {
                _objc_inform("LOAD: +[%s(%s) load]\n", 
                             cls->nameForLogging(), 
                             _category_getName(cat));
            }
            (*load_method)(cls, SEL_load);
            cats[i].cat = nil;
        }
    }

    // Compact detached list (order-preserving)
    shift = 0;
    for (i = 0; i < used; i++) {
        if (cats[i].cat) {
            cats[i-shift] = cats[I];
        } else {
            shift++;
        }
    }
    used -= shift;

    // Copy any new +load candidates from the new list to the detached list.
    new_categories_added = (loadable_categories_used > 0);
    for (i = 0; i < loadable_categories_used; i++) {
        if (used == allocated) {
            allocated = allocated*2 + 16;
            cats = (struct loadable_category *)
                realloc(cats, allocated *
                                  sizeof(struct loadable_category));
        }
        cats[used++] = loadable_categories[I];
    }

    // Destroy the new list.
    if (loadable_categories) free(loadable_categories);

    // Reattach the (now augmented) detached list. 
    // But if there's nothing left to load, destroy the list.
    if (used) {
        loadable_categories = cats;
        loadable_categories_used = used;
        loadable_categories_allocated = allocated;
    } else {
        if (cats) free(cats);
        loadable_categories = nil;
        loadable_categories_used = 0;
        loadable_categories_allocated = 0;
    }

    if (PrintLoading) {
        if (loadable_categories_used != 0) {
            _objc_inform("LOAD: %d categories still waiting for +load\n",
                         loadable_categories_used);
        }
    }

    return new_categories_added;
}

现在,我们可以对load方法的调用做下总结了:


屏幕快照 2019-09-08 下午1.11.21.png

调用顺序也就是跟我们上面说的调用规则一样了。

上面关于给分类设置关联对象的时候,我们给当前对象的isa设置了一个标志,这里做一下补充:


屏幕快照 2019-09-08 下午1.16.13.png

你可能感兴趣的:(Category相关探究)