Category 看了又看 😭

关于 category 的面试总的概括为以下几点:

  • category 的实现原理
  • +load 和 +initialize 的区别以及调用顺序
  • 关联对象

实现原理

从两个方面来说明,一个是编译时,一个是运行时

1. 编译时的 category

每个 category 在编译后会得到一个 _category_t 的结构体. 多个 category 编译后得到多个 _category_t, 即使是同一个类不同的 category, 也会得到不同的 _category_t .

创建一个 DemoClass 类的分类

@interface DemoClass (Cate) <NSObject>
@property (nonatomic, strong) NSDictionary *dic;
@end

@implementation DemoClass (Cate)
- (void)demoCalssInstanceFuc {
    NSLog(@"%#", __func__);
}

+ (void)demoCalssClassFuc {
    NSLog(@"%#", __func__);
}

转成 C++ 文件, 可清晰的看到 _category_t 存储的信息有分类的实例方法、类方法、协议信息及属性信息等,但没有成员变量信息,从 _category_t 的结构体也可知为何 category 没法创建属性。

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_DemoClass_$_Cate __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    // 赋值顺序与上面结构体顺序一一对应滴
    "DemoClass", // 类名赋值
    0, // &OBJC_CLASS_$_DemoClass,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_DemoClass_$_Cate, //实例方法数组赋值
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_DemoClass_$_Cate, //类方法数组赋值
    (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_DemoClass_$_Cate, //协议数组赋值
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_DemoClass_$_Cate, //属性数组赋值
};
2. 运行时的 category

运行时, runtime 会将分类里的实例方法合并到类 rw_t 的方法列表中, 将类方法合并到元类对象的方法列表中, 可读 runtime 的源码
阅读顺序... 入口为 objc-os.mm 文件

  1. 找到void _objc_init(void) 方法, 找到&map_images
  2. 进入 &map_images 方法
  3. 进入 map_images_nolock 方法, 找到 _read_images 方法
  4. 进入 _read_images, 找到 category_t ... 在比较下面的位置, 2555行... 找到 remethodizeClass(cls)
  5. remethodizeClass(cls) 中再进入 attachCategories 方法 🤣
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
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;
    bool fromBundle = NO;

    // 遍历得到所有分类的方法 协议 属性, 合并到相应数组中
    while (i--) { // i-- 意味着数组后面的对象会保存新数组前面
        auto& entry = cats->list[i];

        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }

    auto rw = cls->data();
   
    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    // 分类的方法附加到原来 rw 的方法列表中
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);

    rw->properties.attachLists(proplists, propcount);
    free(proplists);

    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}

// 附加的方法
    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;
            // 将原方法列表后移 addedCount 位
            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]));
        }
    }
注意:

由分类方法合并到原方法列表的代码可知,合并操作是先将原方法列表的方法后移,再将分类的方法拷贝到原来方法列表的位置。由此可知,合并之后的方法列表,是分类的方法在前而原有方法在后。按找到方法后便将地址返回的查找顺序,若分类的方法与原方法名一致时,最终执行的是分类的方法。所以若方法名一致,分类的方法将覆盖原来的方法。

遍历分类方法列表时, 是采用倒序 while (i--), 所以编译在后的分类, 该分类方法会存在方法列表前面的位置. 所以,若多个分类方法名一致,则执行后编译的分类的方法。

  • 分类的方法名和类的方法名一样的话, 会调用分类的方法.
  • 有两个分类的方法名一样, 看编译顺序, 会调用后编译的

ps: 在 Build Phases -> Compile Sources 中排前面的文件先编译

+load 和 +initialize

1. +load 和 +initialize的区别
  1. 调用方式上的区别
  • +load 是通过函数指针调用的
  • +initialize 是通过 objc_msgSend() 的来调用的
  1. 调用时刻上的区别
  • +load 在程序启动时进行调用
  • +initialize 是在类第一次收到消息的时候被调用
2. +load 的调用
  • 先调用类再调用分类的。
  • 类和分类的调用顺序按照编译顺序。
  • 每一个类和分类的都会被调用且只会被调用一次
3. +initialize 的调用
  • 先调用父类再调用子类
  • 原则上来说每个类只会初始化一次。
  • 由于是通过 objc_msgSend() 调用,如果子类没有实现 +initialize 方法,则会通过 isa 指针找到父类的 +initialize 方法进行调用,所以父类的可能会被调用多次。
  • 依然是由于由于是通过 objc_msgSend() 调用,若多个分类均实现了 +initialize 方法,则最终只执行最后一个编译的分类的 +initialize 方法。原因在上面的原理部分已经交代清楚了
小结:
  • 为什么优化启动里面都说把 +load 的代码放到 +initialize 里面,因为 +load 是在启动时进行调用的,如果 +load 的代码多且复杂,确实会影响启动时间。

  • 为什么第三方库的分类还是有很多 +load . 当然是因为 +initialize 可能被覆盖啊啊啊!初始化的东西被覆盖了后面分类还怎么玩!?+load 是保证每个类和分类的 +load 都执行一遍的, +initialize 可就不一样了,谁成为最后编译的才执行谁的。

源码如下:

// 调用 load 方法  有些代码给删了... 专注重点... 也是不想篇幅过长 🤣
void call_load_methods(void)
{
    do {
        // 1. Repeatedly call class +loads until there aren't any more
        // 先调用类的 load
        while (loadable_classes_used > 0) {
            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);
}

// 类调用 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 的
        // 所以虽然类的方法名和分类的方法名都叫 load, 但是不会只调用分类的而不调用类的
        (*load_method)(cls, SEL_load);
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}
// objc-initialize.mm
// 进入这个方法之前, 会先查看是否已经初始化, 如果没有初始化才会走这里 
// initialize 的方法  又开始毫无人性的删掉代码了 🤣 请自行看源码
void _class_initialize(Class cls)
{
    assert(!cls->isMetaClass());
    
    Class supercls;
    bool reallyInitialize = NO;
    // 先调父类
    supercls = cls->superclass;
    // 判断是否有初始化, 没有初始化才会调 _class_initialize, 所以只有第一次使用的时候才会调用
    if (supercls  &&  !supercls->isInitialized()) {
        _class_initialize(supercls);
    }

    // Try to atomically set CLS_INITIALIZING.
    {
        monitor_locker_t lock(classInitLock);
        if (!cls->isInitialized() && !cls->isInitializing()) {
            cls->setInitializing();
            reallyInitialize = YES;
        }
    }
   
    if (reallyInitialize) {
        
#if __OBJC2__
        @try
#endif
        {
            // 调用 initialize
            callInitialize(cls);
            
            if (PrintInitializing) {
                _objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",
                             pthread_self(), cls->nameForLogging());
            }
        }
#if __OBJC2__
        @catch (...) {
            if (PrintInitializing) {
                _objc_inform("INITIALIZE: thread %p: +[%s initialize] "
                             "threw an exception",
                             pthread_self(), cls->nameForLogging());
            }
            @throw;
        }
        @finally
#endif
        {
            // Done initializing.
            lockAndFinishInitializing(cls, supercls);
        }
        return;
    }
    
    else {
        // We shouldn't be here.
        _objc_fatal("thread-safe class init in objc runtime is buggy!");
    }
}

void callInitialize(Class cls)
{
    // 消息机制调用 initialize 
    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
    asm("");
}

关联对象

在分类里面写属性, 不会生成成员变量和生成 setter & getter 方法实现, 只会声明 setter & getter. 也不能直接给分类添加成员变量, 但是可以通过关联对象来实现同样效果.

为什么不能给分类添加成员变量...
  • 因为 category_t 结构里面没有存储成员变量的地方
关联对象最终存在哪, 存在类对象里面吗?
  • 不不不, 另外有个关联对象的 manager 来存储管理.

通过 objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy) 方法可以获取关联对象和设置关联对象.

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        // object 经过 DISGUISE() 后作为 AssociationsHashMap 的 key
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // create the new association (first time).
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                // AssociationsHashMap 以 object 作为 key, ObjectAssociationMap 对象作为 value
                associations[disguised_object] = refs;
                // ObjectAssociationMap 以传入的 key 作为 key, ObjcAssociation 对象作为 value
                // ObjcAssociation 保存了传入的 policy 和 value
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            // setting the association to nil breaks the association.
            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);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}

通过设置关联对象的源码, 大概可以整理出实现关联对象需要的几个类

  • AssociationsManager manager
  • AssociationsHashMap associations //类似manager的一个属性
  • ObjectAssociationMap refs //associations里, 以 object 为 key 对应的一个 value
  • ObjcAssociation //refs 里, 以传入的 key 为 key 对应的一个value, 存储着传入的policy 和 value

稍微捋捋几个类之间的关系:

  • 关联对象会保存在一个叫 AssociationsManager 的类里面, 这个对象是全局的, 只有仅此的一份, 保管所有类的关联对象
  • 不同的类会以类名 objectKey(这里并不是那么单纯的以类名为 key, 而是经过一个函数的转换啥的...) 作为 key, ObjectAssociationMap 作为 value, 存在 AssociationsHashMap
  • ObjectAssociationMap 中存着以关联属性的 key 为 key, ObjcAssociation 作为 value 的键值对, 其中 ObjcAssociation 包含了关联属性的 policy 和 value

清晰明了了吗! 没看懂? 看下面! 尽力了... 画图水平就这样了...


以上关系图.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,084评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,623评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,450评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,322评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,370评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,274评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,126评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,980评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,414评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,599评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,773评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,470评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,080评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,713评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,852评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,865评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,689评论 2 354