Category

199 阅读6分钟

1.Category底层结构是什么?

Category的实现原理

  • category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息。
  • 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象,元类对象中)

  • Category加载过程: 1.通过Runtime加载某个类的所有Category数据。 2.把所有Category的方法、属性、协议数据,合并到一个大数组中,后面参与编译的Category数据,会在数组的前面。 3.将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面。(Build Phases --> Compile Sources控制编译顺序,最上面的先参与编译)

    4.怎样合并到类信息的大数组中呢?是先扩容,然后通过 memmove和memcopy 两个C函数拷贝过去,memcopy如果被拷贝的和目标地址有重合的地方,可能会造成不正确。但是效果高些。在此不扩展深入。

  • 所以,当一个分类和这个类有相同的方法,会覆盖原来的方法。但是这个覆盖不是真正的覆盖,只不过在他前面调用,后面的不会被调用。

  • 分类也可以遵从协议,也可以增加属性。

和类 Extension的不同

Class Extension(作用和.m里的匿名类别作用一致,其实叫匿名类别不正确,和Category有本质的区别)在编译的时候,它的数据就已经包含在类信息中, Category是在运行时,才会将数据合并到信息中。

2. +load 方法

  • +load方法是runtime把类,分类载入内存的时候调用。就算不显示的代码调用,也会被调用。

  • +load 方法是直接调用,而不是消息发送机制。
    源码: runtime入口函数:

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

load_images --> call_load_methods --> call_class_loads

call_class_loadscall_category_loads

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方法的内存地址调用,不是消息发送机制。

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方法会在runtime加载类、分类时调用。
  • 每个类、分类的+load,在程序运行过程中只调用一次。
  • 调用顺序:
    1. 先调用类的 +load(所有需要加载的类)

      1. 按照编译先后顺序(先编译,先调用)
      2. 调用子类的+load之前会先调用父类的+load
    2. 再调用分类的+load

      按照编译先后顺序调用(先编译,先调用)

    3. 注意不是 类1+load -> 分类1+load --> 类2+load --> 分类2+load. 而是 类1+load -> 类2+load -> 分类1+load -> 分类2+load.

阅读源码:

源码

  • 手动调用的话:[MyStudent load],就是消息发送机制。所以load方法可以继承。

3.+initialize方法(类初始化)

  • +initialize方法会在类第一次接收到消息时调用

  • 调用+initialize是消息发送机制

  • 调用顺序

    1. 先调用父类的+initialize,再调用子类的+initialize(调用子类的时候源码里主动先调用了父类的+initialize)
    2. 先初始化分类,再初始化子类,每个类只会初始化1次,
  • +load和+initialize的最大区别是,+initialize是通过objc_megSend进行调用的,所以有以下特点:

    1. 如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被多次调用) 2.如果分类实现了+initialize,就会覆盖本身的+initialize调用。

源码解读过程:

  • 第一次发送消息,寻找方法的时候,会判断该类有没有初始化,就去判断父类有没有初始化,如果没有就先 调用父类的initialize,然后调用自身的initialize.initialize也是发送消息。如果子类已经初始化了,就无需判断父类是否已经初始化,里面逻辑都不会走了。

4.关联对象

属性

  • 对类增加一个属性,做了三件事:生成一个成员变量,生成setter,getter方法声明,生成setter,getter方法实现。
  • 对分类增加一个属性,只会增加setter,getter方法声明。(没有成员变量和实现)
  • 分类不可以增加成员变量。struct category_t 结构里没有成员变量列表。

但可以通过关联对象来间接的实现给分类添加成员变量。

关联对象提供了以下API

  • 添加关联对象

    void objc_setAssociatedObject(id object,const void *key,id value, objc_AssociatePolicy policy)

  • 获取关联对象

    id objc_getAssociatedObject(id object,objc_AssociatePolicy policy)

  • 移除所有关联对象
    void objc_removeAssociatedObjects(id object)

四种key的方案

  1. static void *MyKey = &MyKey;

objc_setAssociatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC) objc_getAssociatedObject(obj, MyKey)

  1. static char MyKey;

objc_setAssociatedObject(obj, &MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC) objc_getAssociatedObject(obj, &MyKey)

  1. 使用属性名作为key

objc_setAssociatedObject(obj, @"property", value, OBJC_ASSOCIATION_RETAIN_NONATOMIC); objc_getAssociatedObject(obj, @"property");

思考:@"name" 放在MachO文件的哪个段?

  1. 使用get方法的@selecor作为key

objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC) objc_getAssociatedObject(obj, @selector(getter))

可以用_cmd代替。每个对象方法都有两个隐式参数 self 和 _cmd.

例如:

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

- (NSString *)name
{
    // 隐式参数
    // _cmd == @selector(name)
    return objc_getAssociatedObject(self, _cmd);
}

objc_AssociatePolicy

源码与原理

  1. 关联对象存储在全局唯一的一个 AssociationManager中。

  2. AssociationManager 有一个Map. key为对象地址,value为这个对象的关联对象Map. ObjectAssociationMap.

  3. ObjectAssociationMap中key为设置的key,value为ObjectAssociation.

  4. ObjectAssociation里有value和policy.

5.设置关联对象为nil,就相当于移除关联对象。

MJPerson *person2 = [[MJPerson alloc] init];
person2.name = nil;

可以查看源码,ObjectAssociationMap的key对应的value为nil的时候,会有个擦除操作,会把这个key-value移除。

其他

  1. 也可以给类对象设置关联对象达到添加成员变量的目的。但是因为类对象只有一个,会有冲突。
objc_setAssociatedObject([MyPerson class], @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
  1. 添加关联对象不会对原来实例对象的结构和类对象的结构造成改变。

MJPerson *person = [[MJPerson alloc] init];
        person.age = 10;
        person.name = @"jack";
        person.weight = 30;
        
        
        {
            MJPerson *temp = [[MJPerson alloc] init];


            objc_setAssociatedObject(person, @"temp", temp, OBJC_ASSOCIATION_ASSIGN);
        }

        NSLog(@"%@", objc_getAssociatedObject(person, @"temp"));

1.大括号里设置ObjectAssociationMap里一个key为 @“temp”的内存地址,value为temp对象。但temp的作用域只限于大括号,出了大括号后,temp就销毁了,这时候value里依然存储这这块内存地址,所以取的时候就会发生 坏内存访问,就会崩溃。说明 id value对temp是强引用。如果是弱引用,temp销毁后就会把value置空,value = nil.
2. objc_setAssociatedObject(person, @"temp", temp, OBJC_ASSOCIATION_ASSIGN); 对person不存在引用。