iOS 分类、+load、+initialize 方法

2,981 阅读9分钟

分类
分类和类扩展区别、swift中的类扩展(Extensions)
load 和 initialize 方法

一、分类

1. 作用

  1. 增强已有类的功能,在不破坏原有类的结构的前提下,进行模块化的扩展。
  2. 为继承自某类的所有子类添加公有的方法(因为分类中的方法可以被子类继承)。
  3. 用于把一个比较庞大的类分割开来,具有相同功能的方法放到一个分类中,避免类过于庞大。

2. 局限性

  1. 无法直接添加实例变量(分类没有位置容纳实例变量);
  2. 方法名称冲突。分类中的方法名称与类中方法名称重名,分类具有更高的优先级,调用的时候执行分类中的方法。

3. 分类的结构、编译和初始化

struct _category_t {
    const char *name;//类名
    struct _class_t *cls;
    const struct _method_list_t *instance_methods;  // 实例方法
    const struct _method_list_t *class_methods;     // 类方法
    const struct _protocol_list_t *protocols;       // 协议信息
    const struct _prop_list_t *properties;          // 属性
};

上面是编译后的分类结构。编译的时候我们添加的方法都会存储在上面的结构体之中,在这个阶段类和分类里面的信息还是分开的,也就是说所有的分类的信息在编译完之后都是存放在一个叫 struct _category_t 的结构体实例里面的。
RunTime 初始化时加载某个类的所有分类数据。把所有分类的数据(方法 属性 协议)合并到一个大的数组中,后面参与编译的分类数据会在数组的前面。最后将合并后的分类数据(方法 属性 协议) 插入到类原来数据的前面。

4. 分类和类中的方法的调用顺序

一个类有多个分类的时候,调用最后一个编译的分类的方法。

从runtime的源码分析为什么会按照上述规则进行调用:

// cls 本类
// cats_list 存储分类数组
// cats_count 存储分类的数量
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
    if (slowpath(PrintReplacedMethods)) {
        printReplacements(cls, cats_list, cats_count);
    }
    if (slowpath(PrintConnecting)) {
        _objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
                     cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
                     cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
    }

    /*
     * Only a few classes have more than 64 categories during launch.
       在启动期间只有少数类拥有超过64个分类
     * This uses a little stack, and avoids malloc.
     * 使用少量栈避免分类
     * Categories must be added in the proper order, which is back
       分类肯定会按照合适的顺序被添加,这个顺序就是从后向前
     * to front. To do that with the chunking, we iterate cats_list
       为了做这个组块,我们迭代分类数组从前到后
     * from front to back, build up the local buffers backwards,
       建造一个向后的本地缓冲
     * and call attachLists on the chunks. attachLists prepends the
       调用附加数组在多个组块上
     * lists, so the final result is in the expected order.
       附加数组频道list上,所以最终的结果就是按照期待的顺序
     */
    constexpr uint32_t ATTACH_BUFSIZ = 64;
    method_list_t   *mlists[ATTACH_BUFSIZ];      // 类方法数组
    property_list_t *proplists[ATTACH_BUFSIZ];   // 对象方法数组
    protocol_list_t *protolists[ATTACH_BUFSIZ];  // 协议数组

    uint32_t mcount = 0;
    uint32_t propcount = 0;
    uint32_t protocount = 0;
    bool fromBundle = NO;
    bool isMeta = (flags & ATTACH_METACLASS); // 是否是元类
    auto rwe = cls->data()->extAllocIfNeeded(); // 找到本类的rw 并且判断是否需要额外分配
    // 遍历分类数组
    for (uint32_t i = 0; i < cats_count; i++) {
        auto& entry = cats_list[i]; // 具体的分类
        // 取出类方法
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            if (mcount == ATTACH_BUFSIZ) {
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
                // 添加到本来的方法列表中
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
        // 取出对象方法
        property_list_t *proplist =
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            if (propcount == ATTACH_BUFSIZ) {
                rwe->properties.attachLists(proplists, propcount);
                propcount = 0;
            }
            proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
        }
        // 取出协议
        protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
        if (protolist) {
            if (protocount == ATTACH_BUFSIZ) {
                rwe->protocols.attachLists(protolists, protocount);
                protocount = 0;
            }
            protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
        }
    }

    if (mcount > 0) {
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount, NO, fromBundle);
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) flushCaches(cls);
    }

    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);

    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

决定方法调用顺序就是下面源码:

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]));
    }
}

5. 分类添加属性 -> 关联对象

分类不可以直接添加成员变量,需要自己实现getter和setter方法,通过关联对象添加成员变量。 objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy)objc_getAssociatedObject(id object, const void * key)

object: 目标对象。
key: 一个对象可以关联多个新对象,我们需要一个标志来区分他们。key 就起这样的作用。这里的需要key的地址,不关心它指向谁。
value: 表示要添加的属性, objc_AssociationPolicy:策略,包括 OBJC_ASSOCIATION_ASSIGN、OBJC_ASSOCIATION_RETAIN、OBJC_ASSOCIATION_COPY,分别对应我们在声明属性时的assign、retain、copy。

- (NSString*)title {
    return objc_getAssociatedObject(self, &titleKey);
} 
- (void)setTitle:(NSString*)title {
    objc_setAssociatedObject(self, &titleKey, title, OBJC_ASSOCIATION_COPY);
}
- (BOOL)af_downloadProgressAnimated {
    return [(NSNumber *)objc_getAssociatedObject(self, @selector(af_downloadProgressAnimated)) boolValue];

}
- (void)af_setDownloadProgressAnimated:(BOOL)animated {
    objc_setAssociatedObject(self, @selector(af_downloadProgressAnimated), @(animated), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

关联对象内部设计实现: AssociationsManager.png 关联对象不存储在对象本身的内存中,而是存储在一个全局统一的AssociationsManager中。如果设置关联对象为nil就相当于移除关联对象。

6. 分类和类中的方法关系

  1. 分为重写的方法、未重写的方法。
  2. 引用Category中未重写的拓展方法,必须引入Category的声明文件。
    引用Category中的重写方法,不用引入Category声明文件,系统会自动调用Category的重写方法。
  3. 类引用自己的Category时,只能在.m实现文件中引用(如果在.h声明文件中引用自己的分类,会因为头文件原因造成死循环),子类引用父类的分类,在.h或.m文件中引用皆可。
  4. Category中如果重写了A类从父类继承来的方法s,理论上不会影响同级类(比如B类,A、B继承了同一个父类)中的方法s。
  5. 子类会不会继承父类的Category:Category中的重写方法会对子类造成影响,但是子类不会主动继承父类的Category中的非重写拓展方法。但是在子类中引入父类Category的声明文件后,子类就会继承Category里的非重写拓展方法。注意,是继承。而继承的具体表现就是:当子类里的方法和父类Category中的方法完全相同(这里的相同只除了方法体外的其他地方相同)时,那么子类里的方法会覆盖掉父类Category,因为这相当于子类重写了继承自父类的方法。
  6. 类别的方法中,不可以调用super方法。--类别的局限

二、其他

分类(Category)和类扩展(Extension)的区别

  1. 分类只能扩充方法,不能扩展属性和成员变量(如果包含成员变量会直接报错);如果分类中声明了一个属性,那分类只会生成这个属性的 setter/getter 方法声明,不会生成 setter/getter 方法,也就是不会有实现。
  2. 类扩展一般只针对自定义的类,对于系统类增加类扩展(无法去直接改变系统文件里的代码,也就是属性无 setter/getter,方法也不能实现)没什么意义。
  3. 分类是有名称的,类扩展没有名称

swift中的类扩展(Extensions)

swift中没有分类这种写法,swif中只有扩展。swif中的扩展就是向一个已有的类、结构体、枚举类型或者协议类型添加新功能。这包括在没有权限获取原始源代码的情况下扩展类型的能力(即逆向建模)。扩展和OC中的分类类似,不过更加强大,与OC不同的是 swift 的扩展没有名字。

  • swift中扩展的优点:
    1. 代码模块化:把功能性相同的代码放到一个扩展中。如把VC中关于网络请求的、表相关的代理方法、其他视图控件的代理方法、私有方法、公开方法、视图的创建处理等分别放到各自的扩展中,使控制器的代码层次更清晰。
    2. 与 OC 的扩展不同,Swift 的类扩展里的方法、属性在外部都可以使用,而且支持被继承。
    3. 注意扩展中不能有存储类型的属性,只可以添加计算型实例属性和计算型类型属性。

三、load 和 initialize

load 和 initialize 方法,不要显示的调用 super 的对应方法。

load 方法的调用顺序

load 是类或分类是在类加载(runtime初始化)的时候被调用的,在 main 函数前,每个类的 load 函数只会自动调用一次。

load 函数调用特点:

  1. 分类 load 方法不会覆盖本类的 load 方法。当父类和子类都实现 load 方法时,二者的 load 方法都会被调用,父类的 load 方法执行顺序要优先于子类。
  2. 当子类未实现 load 方法时,在加载该子类时,不会去调用其父类 load 方法。
  3. 类中的 load 方法执行顺序要优先于类别(Category)。
  4. 多个类别(Category)都实现了 load 方法, load 方法都会执行,执行顺序与编译顺序一致。
  5. 当有多个不同的类的时候,每个类 load 执行顺序与编译顺序一致。

initialize 方法的调用顺序

initialize 是在类或其子类收到第一条消息之前调用,并且只调用一次。这里的消息包括实例方法和类方法的调用。也就是说 initialize 方法是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的 initialize 方法是永远不会被调用的。

  1. 父类的 initialize 方法会比子类先执行。
  2. 当子类未实现 initialize 方法时,在该子类收到第一条消息之前,会调用父类 initialize 方法,子类实现 initialize 方法时,则会调用子类的 initialize 方法,不会调用父类的。
  3. 当有多个 category 都实现了 initialize 方法,会执行最后被被编译的 category 的 initialize 方法。

其他

  • 给类发送了一个未实现的方法会执行initialize方法么?
    会。