Category 加载详解

定义

category 的主要作用是为已经存在的类添加方法。

官方文档

官网提供了其他的优势:

  • 可以把类的实现分开在几个不同的文件里面。
    • 可以减少分开文件的体积
    • 可以把不同的功能组织到不同的category里
    • 可以由多个开发者共同完成一个类
    • 可以按需加载想要的类别等等。
  • 声明专有方法

原理

思考?

一个类的实例方法、类方法默认都是存在这个类的类对象和元类对象中,那么分类的方法存在哪呢?
(也在类方法,合并,运行时合并 利用runtime)

首先我们创建一个类,然后创建一个类的 category 文件,并使用编译命令来生成对应的 c++ 文件。

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+name.m

可以看见我们的分类被编译成了下面这种结构

// 空的分类
static struct _category_t _OBJC_$_CATEGORY_Person_$_name __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "Person",
    0, // &OBJC_CLASS_$_Person,
    0,
    0,
    0,
    0,
};

// 设置了方法 和 属性等 编译后的文件
static struct _category_t _OBJC_$_CATEGORY_Person_$_name __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "Person",
    0, // &OBJC_CLASS_$_Person,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_name,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_name,
    (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_name,
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_name,
};

结构体的类型为 _category_t

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 /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_name __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"myName", "@16@0:8", (void *)_I_Person_name_myName}}
};

类方法的结构体

static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_name __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"myHeight", "i16@0:8", (void *)_C_Person_name_myHeight}}
};

属性的结构体

static struct /*_prop_list_t*/ {
    unsigned int entsize;  // sizeof(struct _prop_t)
    unsigned int count_of_properties;
    struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_Person_$_name __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_prop_t),
    1,
    {{"len","Ti,N"}}   // i  指的是 int 类型
};

协议的结构体

static struct /*_protocol_list_t*/ {
    long protocol_count;  // Note, this is 32/64 bit
    struct _protocol_t *super_protocols[2];
} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_name __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    2,
    &_OBJC_PROTOCOL_NSCopying,
    &_OBJC_PROTOCOL_NSCoding
};

runtime 源码解读

首先找到 runtime 的入口, 在 objc-os.mm 中的 _objc_init 函数

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();
    runtime_init();
    exception_init();
#if __OBJC2__
    cache_t::init();
#endif
    _imp_implementationWithBlock_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

然后在 map_images 中调用 map_images_nolock,然后调用了_read_images
然后里面有下面代码


    // Discover categories. Only do this after the initial category
    // attachment has been done. For categories present at startup,
    // discovery is deferred until the first load_images call after
    // the call to _dyld_objc_notify_register completes. rdar://problem/53119145
    if (didInitialAttachCategories) {
        for (EACH_HEADER) {
            load_categories_nolock(hi);
        }
    }

跟进去之后,可以找到 attachCategories 方法,这部分代码就是合并分类的代码。

// 附加上分类的核心操作
// cls:类对象或者元类对象,cats_list:分类列表
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)" : "");
    }

    // 先分配固定内存空间来存放方法列表、属性列表和协议列表
    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();

    for (uint32_t i = 0; i < cats_count; i++) {
        // 取出某个分类
        auto& entry = cats_list[i];

        // entry.cat就是category_t *cat
        // 根据isMeta属性取出每一个分类的类方法列表或者对象方法列表
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        
        // 如果有方法则添加mlist数组到mlists这个大的方法数组中
        // mlists是一个二维数组:[[method_t, method_t, ....], [method_t, method_t, ....]]
        if (mlist) {
            if (mcount == ATTACH_BUFSIZ) {
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            // 将分类列表里先取出来的分类方法列表放到大数组mlists的最后面(ATTACH_BUFSIZ - ++mcount),所以最后编译的分类方法列表会放在整个方法列表大数组的最前面            
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        // 同上面一样取出的是分类中的属性列表proplist加到大数组proplists中
        // proplists是一个二维数组:[[property_t, property_t, ....], [property_t, property_t, ....]]
        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;
        }

        // 同上面一样取出的是分类中的协议列表protolist加到大数组protolists中
        // protolists是一个二维数组:[[protocol_ref_t, protocol_ref_t, ....], [protocol_ref_t, protocol_ref_t, ....]]
        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, __func__);
        
        // 将分类的所有对象方法或者类方法,都附加到类对象或者元类对象的方法列表中
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) {
            flushCaches(cls, __func__, [](Class c){
                return !c->cache.isConstantOptimizedCache();
            });
        }
    }

    // 将分类的所有属性附加到类对象的属性列表中
    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);

    // 将分类的所有协议附加到类对象的协议列表中
    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

里面有关键性代码 attachLists, 在此处进行了分类方法的加载顺序。

void attachLists(List* const * addedLists, uint32_t addedCount) {
   if (addedCount == 0) return;

   if (hasArray()) {
       // 这里是把类的地址重新分配新的地址加 newcount 也是分类的创建个数,而老的分类信息重新复制一下内存,通过 i-- 倒着取值,所以最先编译的会在最后面,
       // 获取原本的个数
       uint32_t oldCount = array()->count;
       // 最新的个数 = 原本的个数 + 新添加的个数
       uint32_t newCount = oldCount + addedCount;
       
       // 重新分配内存
       array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
       // 将新数组的个数改为最新的个数
       newArray->count = newCount;
       // 将旧数组的个数改为最新的个数
       array()->count = newCount;
       
       // 递减遍历,将旧数组里的元素从后往前的依次放到新数组里
       for (int i = oldCount - 1; i >= 0; i--)
           newArray->lists[i + addedCount] = array()->lists[i];
       
       // 将新增加的元素从前往后的依次放到新数组里
       for (unsigned i = 0; i < addedCount; i++)
           newArray->lists[i] = addedLists[i];
       
       // 释放旧数组数据
       free(array());
       
       // 赋值新数组数据
       setArray(newArray);
       validate();
   }
   else if (!list  &&  addedCount == 1) {
       // 0 lists -> 1 list
       list = addedLists[0];
       validate();
   } 
   else { .... }
}

上面内容就是分类方法的一个合并流程。

参考文档:

http://www.babyitellyou.com/details?id=6045c2fe4da5fa50e14ff4f3
//www.greatytc.com/p/ed8d3be7e8f4

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

推荐阅读更多精彩内容