Category的实现原理及其使用

616 阅读18分钟

经过一个多月的奋战(颓废),手里工作暂时告一段落,一个多月没有更新了,有时候真的很懒,不想动手动脑;话不多说,开始继续Category和Extension的学习和深入理解吧!对每个iOS开发者来说Category和Extension都是必须掌握的基础技术,本文将从基础定义开始,一步步深入了解掌握Category和Extension。

一、 Category、Extension定义与之间关系

(一)、Category 定义及用途

1、什么是Category?
  • Category是objective-C 2.0之后添加的语言特性,别人口中的分类、类别其实都是指Category。Category的主要作用是为已经存在的类添加方法,除此之外,Apple还推荐了Category的另外两个使用场景。
2、Category用途
  • 1.可以在不修改原来类的基础上,为一个类扩展方法。最主要的用法:给系统自带的类扩展方法。
    • 1.1 分类中只能添加“方法”,不能增加成员变量,因为分类的结构体指针中,没有属性列表,只有方法列表;
    • 1.2 分类中的可以写@property,只会生成这个属性的set、get方法声明,但不会生成setter/getter方法的实现以及私有的成员变量(编译时会报警告);
    • 1.3 分类中可以访问原来类中的成员变量,但是只能访问@protect和@public形式的变量。如果想要访问本类中的私有变量,分类和子类一样,只能通过方法来间接访问;
    • 1.4 如果分类中有和原有类同名的方法,会优先调用分类中的方法,就是说会忽略原有类的方法。所以同名方法调用的优先级为 分类>本类>父类。因此在开发中尽量不要覆盖原有类的方法;
    • 1.5 如果多个分类中都有和原有类中同名的方法,那么调用该方法的时候执行谁由编译器决定;编译器会执行最后一个参与编译的分类中的方法;
  • 2.可以将类的实现代码分散到多个不同的文件或框架中。这样做的好处:
    • 2.1 可以减少单个文件的体积
    • 2.2 可以把不同的功能组织到不同的Category里
    • 2.3 可以由多个开发者共同完成一个类
    • 2.4 可以按需加载想要的Category
    • 2.5 声明私有方法
  • 3.创建对私有方法的前向引用,把framework的私有方法公开。
    • Cocoa没有任何真正的私有方法。只要知道对象支持的某个方法的名称,即使该对象所在的类的接口中没有该方法的声明,你也可以调用该方法。不过这么做编译器会报错,但是只要新建一个该类的类别,在类别.h文件中写上原始类该方法的声明,类别.m文件中什么也不写,就可以正常调用私有方法了。这就是传说中的私有方法前向引用。 所以说cocoa没有真正的私有方法。
    1. 模拟多继承(另外可以模拟多继承的还有protocol,组合、消息转发)。

(二)、Extension 定义及用途

1、什么是Extension?
  • Extension是Category的一个特例,被开发者称为扩展、延展、匿名分类。类扩展声明与分类声明相比只少了分类的名称,所以称之为“匿名分类”。但是Extension和Category几乎完全是两个东西。和Category不同的是Extension不但可以声明方法,还可以声明属性、成员变量。Extension一般用于声明私有方法,私有属性,私有成员变量。且Category有自己的.m实现文件,而Extension声明的方法直接在类的实现文件内实现;对类的要求也不一样,Category不要求一定要有类的实现源码,而Extension必须要知道源码;
2、Extension 用途
  • 1、 可以给类添加属性,但是是私有属性,同时自动生成相应的成员变量(_iva),也可以直接添加成员变量;
  • 2、 可以给类添加方法,也是是私有方法;
  • 3、 编译时决议,是类的一部分,在编译时和头文件的@interface和实现文件里的@implement一起形成了一个完整的类。
  • 4、 伴随着类的产生而产生,也随着类的消失而消失。
  • 5、 Extension一般用来隐藏类的私有方法,你必须有一个类的源码才能添加一个类的Extension,所以对于系统一些类,如NSString,就无法添加类扩展;

(三)、Category、Extension两者之间的联系和区别

Category、Extension之间的联系
  • 1、 Extension是Category的一个特例。类扩展声明与分类声明相比只少了分类的名称,所以称之为“匿名分类”。
  • 2、 都可以为类添加方法;
  • 3、 都可以为类添加属性;
Category、Extension之间的区别
  • 1、 声明时的格式不同,Category有自己的实现,Extension没有自己的实现;
分类格式:
@interface 类名称(分类的名称)

@end
@implementation 类名称(分类的名称)

@end

类扩展格式:
@interfaceXXX()
//私有属性
//私有方法(如果不实现,编译时会报警,Method definition for 'XXX' not found)
@end
  • 2、 分类只能添加“方法”,不能增加成员变量,可以添加属性,但是要通过runtime解决无setter/getter的实现的问题,进而添加属性;类扩展不仅可以增加方法,还可以增加实例变量(或者属性),只是该实例变量默认是@private类型的;
  • 3、 Extension类扩展中添加的新方法,一定要实现,没被实现,编译器会报警;Category中没有这种限制,类别中的方法没被实现编译器是不会有任何警告的。这是因为类扩展是在编译阶段被添加到类中,而分类是在运行时添加到类中。
  • 4、 通常来讲,分类定义在.h文件中,但也可以定义.m文件中,此时分类的方法就变成私有方法;类扩展可以定义在.m文件中,这种扩展方式中定义的变量都是私有的,也可以定义在.h文件中,这样定义的代码就是共有的,类扩展在.m文件中声明私有方法是非常好的方式。
  • 5、 类扩展不能像类别那样拥有独立的实现部分(@implementation部分),也就是说,类扩展所声明的方法必须依托对应类的实现部分来实现。

(四)、Category 为什么不能添加实例变量?

  • 1、Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针。它的定义如下:
    typedef struct objc_class *Class;
    
    
  • 2、objc_class结构体的定义如下:
    struct objc_class {
        Class isa  OBJC_ISA_AVAILABILITY;
    #if !__OBJC2__
        Class super_class                       OBJC2_UNAVAILABLE;  // 父类
        const char *name                        OBJC2_UNAVAILABLE;  // 类名
        long version                            OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0
        long info                               OBJC2_UNAVAILABLE;  // 类信息,供运行期使用的一些位标识
        long instance_size                      OBJC2_UNAVAILABLE;  // 该类的实例变量大小
        struct objc_ivar_list *ivars            OBJC2_UNAVAILABLE;  // 该类的成员变量链表
        struct objc_method_list **methodLists   OBJC2_UNAVAILABLE;  // 方法定义的链表
        struct objc_cache *cache                OBJC2_UNAVAILABLE;  // 方法缓存
        struct objc_protocol_list *protocols    OBJC2_UNAVAILABLE;  // 协议链表
    #endif
    } OBJC2_UNAVAILABLE;
    
  • 3、在上面的objc_class结构体中,ivars是objc_ivar_list成员变量列表的指针;methodLists是指向objc_method_list指针的指针。在Runtime中,objc_class结构体大小是固定的,不可能往这个结构体中添加数据,只能修改。所以ivars指向的是一个固定区域,只能修改成员变量值,不能增加成员变量个数。methodList是一个二维数组,所以可以修改*methodLists的值来增加成员方法,虽没办法扩展methodLists指向的内存区域,却可以改变这个内存区域的值(存储的是指针)。因此,可以动态添加方法,不能添加成员变量。

(五)、Category中能添加属性吗?

  • 1、Category不能添加成员变量(instance variables),那到底能不能添加属性(property)呢? 这个我们要从Category的结构体开始分析:
    typedef struct category_t {
        const char *name;  //类的名字
        classref_t cls;  //类
        struct method_list_t *instanceMethods;  //category中所有给类添加的实例方法的列表
        struct method_list_t *classMethods;  //category中所有添加的类方法的列表
        struct protocol_list_t *protocols;  //category实现的所有协议的列表
        struct property_list_t *instanceProperties;  //category中添加的所有属性
    } category_t;
    

从Category的定义也可以看出Category的可为(可以添加实例方法,类方法,甚至可以实现协议,添加属性)和不可为(无法添加成员变量)。

  • 2、不能直接给Category添加成员变量,但是可以添加属性(实质是添加setter/getter方法),且并不能自动生成对应的成员变量;我们可以使用以下两个runtime的API,动态添加属性:
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy);
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key);

二、Category 源码分析

实例对象的isa指针指向类对象,类对象的isa指针指向元类对象,当实例对象调用对象方法时,通过实例对象的isa指针找到类对象,然后在类对象中查找对象方法,如果没有找到,就通过类对象的superclass指针找到父类对象,接着去寻找对象方法。

  • 1、那么当调用分类的方法时,步骤是否和调用对象方法一样呢?

分类中的对象方法依然是存储在类对象中的,同本类对象方法在同一个地方,调用步骤也同调用对象方法一样。如果是类方法的话,也同样是存储在元类对象中。

  • 2、那么分类方法是如何存储在类对象中的,我们来通过源码看一下分类的底层结构。
    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);
    };
    

从源码基本可以看出我们平时使用categroy的方式,对象方法,类方法,协议,和属性都可以找到对应的存储方式。并且我们发现分类结构体中是不存在成员变量的,因此分类中是不允许添加成员变量的。分类中添加的属性并不会帮助我们自动生成成员变量,只会生成get set方法的声明,需要我们自己去实现。通过源码我们发现,分类源码中确实是将我们定义的对象方法,类方法,属性等都存放在catagory_t结构体中。

  • 3、接下来我们到runtime源码查看catagory_t存储的方法,属性,协议等是如何存储在类对象中的。

    • 3.1、先从runtime中的初始化函数开始;
    /***********************************************************************
    * _objc_init
    * Bootstrap initialization. Registers our image notifier with dyld.
    * Called by libSystem BEFORE library initialization time
    **********************************************************************/
    
    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_objc_notify_register(&map_images, load_images, unmap_image);
    }
    
    

    在源码中跟踪查看最后一句中的&map_images读取模块(images这里代表模块),来到map_images_nolock函数中找到_read_images函数,在_read_images函数中我们找到分类相关代码;

    // 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;
                }
    
                // Process this category. 处理分类
                // First, register the category with its target class. 用他的目标类注册类别
                // Then, rebuild the class's method lists (etc) if the class is realized. 如果类实现了,则重新构建类的方法列表(等)
                bool classExists = NO;
                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" : "");
                    }
                }
    
                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);
                    }
                }
            }
        }
    

    从上述代码中我们可以知道这段代码是用来查找有没有分类的。通过_getObjc2CategoryList函数获取到分类列表之后,进行遍历,获取其中的方法,协议,属性等。可以看到最终都调用了remethodizeClass(cls)函数。我们来到remethodizeClass(cls)函数内部查看。

    /***********************************************************************
    * remethodizeClass
    * Attach outstanding categories to an existing class.将未完成(附加)的类别附加到现有类
    * Fixes up cls's method list, protocol list, and property list.修复cls的方法列表、协议列表和属性列表
    * Updates method caches for cls and its subclasses.更新cls及其子类的方法缓存
    * Locking: runtimeLock must be held by the caller 锁定:runtimeLock必须由调用方持有
    **********************************************************************/
    static void remethodizeClass(Class cls)
    {
        category_list *cats;
        bool isMeta;
        runtimeLock.assertWriting();
        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(cls, cats, true /*flush caches*/)函数接收了类对象cls和分类数组cats,一个类可以有多个分类。之前我们说到分类信息存储在category_t结构体中,那么多个分类则保存在category_list中。

    • 3.2、我们来到attachCategories函数内部。
    // Attach method lists and properties and protocols from categories to a class.将方法列表、属性和协议从类别附加到类
    // Assumes the categories in cats are all loaded and sorted by load order,oldest categories first.假设cats中的类别都是按加载顺序加载和排序的,最先加载的类别优先
    static void attachCategories(Class cls, category_list *cats, bool flush_caches)
    {
        if (!cats) return;
        if (PrintReplacedMethods) printReplacements(cls, cats);
    
        bool isMeta = cls->isMetaClass();
    
        // fixme rearrange to remove these intermediate allocations 根据分类列表中方法列表,属性列表,协议类表分配内存
        method_list_t **mlists = (method_list_t **)
            malloc(cats->count * sizeof(*mlists));
        property_list_t **proplists = (property_list_t **)
            malloc(cats->count * sizeof(*proplists));
        protocol_list_t **protolists = (protocol_list_t **)
            malloc(cats->count * sizeof(*protolists));
    
        // Count backwards through cats to get newest categories first 通过对cats(分类列表)的倒数得到最新的类别
        int mcount = 0;
        int propcount = 0;
        int protocount = 0;
        int i = cats->count;
        bool fromBundle = NO;
        while (i--) {
            auto& entry = cats->list[i];//拿到的每一个分类
            //将所有分类中的所有方法存入到mlists二重数组中
            method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
            if (mlist) {
                mlists[mcount++] = mlist;
                fromBundle |= entry.hi->isBundle();
            }
            ///将所有分类中的所有属性存入到proplists二重数组中
            property_list_t *proplist = 
                entry.cat->propertiesForMeta(isMeta, entry.hi);
            if (proplist) {
                proplists[propcount++] = proplist;
            }
            ///将所有分类中的所有协议方法存入到protolists二重数组中
            protocol_list_t *protolist = entry.cat->protocols;
            if (protolist) {
                protolists[protocount++] = protolist;
            }
        }
        /// rw : class_rw_t结构体,class结构体中用来存储对象方法,属性,协议方法的结构体
        auto rw = cls->data();
        /// 调用rw的methods中的函数attachLists,将mlists和其数量传入,即将所有分类中的方法列表,传入attachLists函数,然后释放mlists
        prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
        rw->methods.attachLists(mlists, mcount);
        free(mlists);
        if (flush_caches  &&  mcount > 0) flushCaches(cls);
        /// 调用rw的properties中的函数attachLists,将proplists和其数量传入,即将所有分类中的属性列表,传入attachLists函数,然后释放proplists        rw->properties.attachLists(proplists, propcount);
        free(proplists);
        /// 调用rw的protocols中的函数attachLists,将protolists和其数量传入,即将所有分类中的协议列表,传入attachLists函数,然后释放protolists        rw->protocols.attachLists(protolists, protocount);
        free(protolists);
    }
    

    上述源码中可以看出,首先根据方法列表,属性列表,协议列表,malloc分配内存,根据多少个分类以及每一块方法需要多少内存来分配相应的内存地址。之后从分类数组里面往三个数组里面存放分类数组里面存放的分类方法,属性以及协议放入对应mlist、proplists、protolosts数组中,这三个数组放着所有分类的方法,属性和协议。

    通过类对象的data()方法,拿到类对象的class_rw_t结构体rw,在class结构中class_rw_t中存放着类对象的方法,属性和协议等数据,rw结构体通过类对象的data方法获取,所以rw里面存放着类对象里面的数据。分别通过rw调用方法列表、属性列表、协议列表的attachList函数,将所有的分类的方法、属性、协议列表数组传进去,我们大致可以猜想到在attachList方法内部将分类和本类相应的对象方法,属性,和协议进行了合并。

    • 3.3、我们来看一下attachLists函数内部。
        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]));
            }
        }
    
    • 上述源代码中有两个重要的数组:
      • array()->lists: 类对象原来的方法列表,属性列表,协议列表。
      • addedLists:传入所有分类的方法列表,属性列表,协议列表。

    attachLists函数中最重要的两个方法为memmove内存移动和memcpy内存拷贝。

    • 3.4、我们先来分别看一下这两个函数
    // memmove :内存移动。
    /*  __dst : 移动内存的目的地
    *   __src : 被移动的内存首地址
    *   __len : 被移动的内存长度
    *   将__src的内存移动__len块内存到__dst中
    */
    void    *memmove(void *__dst, const void *__src, size_t __len);
    // memcpy :内存拷贝。
    /*  __dst : 拷贝内存的拷贝目的地
    *   __src : 被拷贝的内存首地址
    *   __n : 被移动的内存长度
    *   将__src的内存移动__n块内存到__dst中
    */
    void    *memcpy(void *__dst, const void *__src, size_t __n);
    
    • 下面我们图示经过memmove和memcpy方法过后的内存变化。
      • 未经过内存移动和拷贝时 未经过内存移动和拷贝时
      • 经过memmove方法之后,内存变化为
      // addedCount 分类数组长度
      // oldCount * sizeof(array()->lists[0]) 原来数组占据的空间
      memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0]));
      
      memmove方法之后内存变化 经过memmove方法之后,我们发现,虽然本类的方法,属性,协议列表会分别后移,但是本类的对应数组的指针依然指向原始位置。
      • memcpy方法之后,内存变化
      // array()->lists 原来方法、属性、协议列表数组
      // addedLists 分类方法、属性、协议列表数组
      // addedCount * sizeof(array()->lists[0]) 原来数组占据的空间
      memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0]));
      
      memmove方法之后,内存变化

    我们发现原来指针并没有改变,至始至终指向开头的位置。并且经过memmove和memcpy方法之后,分类的方法,属性,协议列表被放在了类对象中原本存储的方法,属性,协议列表前面。

    那么为什么要将分类方法的列表追加到本来的对象方法前面呢,这样做的目的是为了保证分类方法优先调用,我们知道当分类重写本类的方法时,会覆盖本类的方法。 其实经过上面的分析我们知道本质上并不是覆盖,而是优先调用。本类的方法依然在内存中的。我们可以通过打印所有类的所有方法名来查看

    - (void)printMethodNamesOfClass:(Class)cls
    {
        unsigned int count;
        // 获得方法数组
        Method *methodList = class_copyMethodList(cls, &count);
        // 存储方法名
        NSMutableString *methodNames = [NSMutableString string];
        // 遍历所有的方法
        for (int i = 0; i < count; i++) {
            // 获得方法
            Method method = methodList[i];
            // 获得方法名
            NSString *methodName = NSStringFromSelector(method_getName(method));
            // 拼接方法名
            [methodNames appendString:methodName];
            [methodNames appendString:@", "];
        }
        // 释放
        free(methodList);
        // 打印方法名
        NSLog(@"%@ - %@", cls, methodNames);
    }
    

三、load 和 initialize的调用顺序问题

(一)、分类(category)、类、父类加载(load)顺序

load方法会在程序启动就会调用,当装载类信息的时候就会调用。调用顺序看一下源代码。

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) {
            call_class_loads();
        }
        // 2. Call category +loads ONCE
        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;
}

通过源码我们发现是优先调用类的load方法,之后调用分类的load方法。我们添加YNStudent继承YNPresen类,并添加YNStudent+Eat分类,分别重写只+load方法,其他什么都不做,执行结果: 通过打印发现,确实是优先调用类的load方法之后调用分类的load方法,不过调用类的load方法之前会保证其父类已经调用过load方法。

(二)、initialize方法的调用顺序

现在为YNPreson、YNStudent 、YNStudent+Eat 添加initialize方法。执行结果如下: 我们知道当类第一次接收到消息时,就会调用initialize,相当于第一次使用类的时候就会调用initialize方法。调用子类的initialize之前,会先保证调用父类的initialize方法。如果之前已经调用过initialize,就不会再调用initialize方法了。当分类重写initialize方法时会先调用分类的方法。

(三)、load和initialize的调用机制

load方法不会被覆盖,按照 父类-本类-分类 的顺序调用;而initialize的调用会被覆盖,按照 父类-(分类? :本类) 的顺序调用。

  • 首先我们来看一下load方法的调用源码。
    • 类中的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);
    }
    
    • 分类中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方法还是分类中的load方法都是直接拿到load方法的内存地址直接调用方法,不在是通过消息发送机制调用。因此正如我们之前试验的一样,分类中重写load方法,并不会优先调用分类的load方法,而不调用本类中的load方法了。
  • initialize的调用源码
    void callInitialize(Class cls)
    {
        ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
        asm("");
    }
    

从源码我们发现,initialize是通过消息发送机制调用的,消息发送机制通过isa指针找到对应的方法与实现,因此先找到分类方法中的实现,会优先调用分类方法中的实现。

总结

问:Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗? 答:Category中有load方法,load方法在程序启动装载类信息的时候就会调用。load方法可以继承。调用子类的load方法之前,会先调用父类的load方法

问:load、initialize的区别,以及它们在category重写的时候的调用的次序。 答:区别在于调用方式和调用时刻 调用方式:load是根据函数地址直接调用,initialize是通过objc_msgSend调用 调用时刻:load是runtime加载类、分类的时候调用(只会调用1次),initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)

调用顺序:先调用类的load方法,先编译那个类,就先调用load。在调用load之前会先调用父类的load方法。分类中load方法不会覆盖本类的load方法,先编译的分类优先调用load方法。initialize先初始化父类,之后再初始化子类。如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次),如果分类实现了+initialize,就覆盖类本身的+initialize调用。

由于本人水平有限,文中如有不足之处,望大神指出。
如果你看完后觉得对你有所帮助,勿忘点赞+关注
附本文的Demo,赠人玫瑰,手有余香。