Categary

310 阅读5分钟

分类的本质

  分类的本质是一个结构体,代码被编译之后(可以在终端使用命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Cat+Eat.m将分类.m文件转换为cpp文件查看),分类结构体的结构为:

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;// 属性列表
};

//
static struct _category_t _OBJC_$_CATEGORY_Cat_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "Cat",
    0, // &OBJC_CLASS_$_Cat,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Cat_$_Eat,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Cat_$_Eat,
    (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Cat_$_Eat,
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Cat_$_Eat,
};

  此时分类中的实例方法、类方法等还没合并到类对象以及元类对象中,这个阶段一个类如果有多个分类,那每一个分类此时都是一个独立的结构体。从结构中可以看出分类的结构体中并没有成员变量列表,所以分类不支持新增成员变量

分类中的方法、属性、协议附加到类对象的过程

  系统在运行时将各个分类中的方法、属性、协议附加到类对象以及元类对象中,源码中的重要环节实现如下:

attachLists.png   从源码中可知调用了realloc函数扩容,先将编译时期就有的方法(属性、协议)后移addedCount位,将分类中的方法(属性、协议)添加在原本的位置(这样的话就可以解释分类申明了同一个方法后,会优先调用分类的这个方法的实现,因为分类覆盖了这个方法)。

分类(category)和扩展(Extention)的区别

  1. Extention在编译的时候,他的数据就已经包含在类中。
  2. Category是在运行时的时候才会将数据合并到类中。
  3. Extention可以申明成员变量,属性并会自动生成setter、getter方法。
  4. Category可以申明属性,但不会生成_开头的成员变量,也不会自动生成setter、getter方法。

类和Category的 load方法的调用顺序

/***********************************************************************
* call_load_methods
* Call all pending class and category +load methods.
* Class +load methods are called superclass-first. 
* 只有类的+load方法被调用完才会去调用分类的+load方法
* Category +load methods are not called until after the parent class's +load.
* 
* This method must be RE-ENTRANT, because a +load could trigger 
* more image mapping. In addition, the superclass-first ordering 
* must be preserved in the face of re-entrant calls. Therefore, 
* only the OUTERMOST call of this function will do anything, and 
* that call will handle all loadable classes, even those generated 
* while it was running.
*
* The sequence below preserves +load ordering in the face of 
* image loading during a +load, and make sure that no 
* +load method is forgotten because it was added during 
* a +load call.
* Sequence:
* 1. Repeatedly call class +loads until there aren't any more
* 2. Call category +loads ONCE. //分类的+load方法只会调用一次
* 3. Run more +loads if:
*    (a) there are more classes to load, OR
*    (b) there are some potential category +loads that have 
*        still never been attempted.
* Category +loads are only run once to ensure "parent class first" 
* ordering, even if a category +load triggers a new loadable class 
* and a new loadable category attached to that class. 
*
* Locking: loadMethodLock must be held by the caller 
*   All other locks must not be held.
**********************************************************************/

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();
        }
        // 只有类的+load方法被调用完才会去调用分类的+load方法
        // 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;
}

// loadable_class结构体
struct loadable_class {
    Class cls;  // may be nil
    IMP method;
};

// loadable_category结构体
struct loadable_category {
    Category cat;  // may be nil
    IMP method;
};

/***********************************************************************
* prepare_load_methods
* Schedule +load for classes in this image, any un-+load-ed 
* superclasses in other images, and any categories in this image.
**********************************************************************/

// Recursively schedule +load for cls and any un-+load-ed superclasses.
// cls must already be connected.
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);
    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

/***********************************************************************
* call_class_loads
* Call all pending class +load methods.
* If new classes become loadable, +load is NOT called for them.
*
* Called only by call_load_methods().
**********************************************************************/

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方法实现IMP
        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方法
        (*load_method)(cls, @selector(load));
    }
    // Destroy the detached list.
    if (classes) free(classes);
}
  1. 类的+load方法会优先调用
  • 按照编译顺序调用(先编译,先调用)。
  • 调用子类的+load方法之前会先调用父类的+load方法。
  • 调用+load方法是找到了类的+load方法实现IMP然后直接调用,并不是runtime的消息发送objc_msgSend。
  1. 所有类的+load方法调用完毕之后才会去调用分类的+load方法
  • 按照编译顺序调用(先编译,先调用)。

类和Category的 initialize方法的调用时机和顺序

  initialize方法会在类第一次接受消息的时候调用。会先调用父类的initialize方法再调用子类的initialize方法。

  • lookUpImpOrForward函数中判断当前类有没有调用过initialize方法,如果没有调用的话就直接调用类的initialize方法。
  • 在initializeNonMetaClass函数中会判断父类是否存在,是否没调用过initialize,如果成立会先调用父类的initialize。
  • 如果子类没有实现initialize方法,会调用父类的initialize方法,所以父类的initialize方法可能会被多次调用。
  • initialize方法的调用是通过objc_msgSend这种消息发送机制,所以如果分类实现了initialize方法,则会覆盖类的initialize方法。

Category - 关联对象

  上文已知Category可以申明属性,但不会生成_开头的成员变量,也不会自动生成setter、getter方法。所以我们可以实现属性的setter、getter方法,在方法内部通过运行时关联一个对象从而达到分类间接实现新增成员变量的效果,具体代码实现如下:

@interface Person (file)
// 在分类.h文件中声明一个属性。
@property (nonatomic, copy) NSString *name;
@end

@implementation Person (file)

- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name {
    return objc_getAssociatedObject(self, @"name");
}
@end
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,  // 指定一个弱引用相关联的对象
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相关对象的强引用,非原子性
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,  // 指定相关的对象被复制,非原子性
    OBJC_ASSOCIATION_RETAIN = 01401,  // 指定相关对象的强引用,原子性
    OBJC_ASSOCIATION_COPY = 01403     // 指定相关的对象被复制,原子性   
};

关联对象实现原理

  源码中实现关联对象的几个核心对象:

  1. AssociationsManager
  2. AssociationsHashMap
  3. ObjectAssociationMap
  4. ObjcAssociation
关联对象小结
  1. 关联对象并不是存储在被关联对象本身内存中。
  2. 关联对象存储在全局的统一的一个AssociationHashManager中。
    • AssociationHashManager中key为object,value为一个ObjectAssociationMap
    • ObjectAssociationMap中存储着这个object所有的关联对象,其中key为关联对象的key,value为一个ObjcAssociation
    • ObjcAssociation中存储着关联对象的value以及策略。
  3. 设置关联对象为nil就是移除关联对象。