hello category

事由:今年去面试,然后面试官问了我一些关于runtime的用法,我有说到Method Swizzling。通过在categoryload中去修改我们调用的方法,来达到全局修改的目的。随后面试官问到关于category的实现,哇! 尴尬,我好像从来没有想过这个问题。现在有时间就给整理一下,水平有限,肯定会有很多不足。希望大家多多指点!多谢 zzz

</br>

categoryObjective-C 2.0之后添加的语言特性. 一般我们使用它有以下两种场景

  • 给系统类添加方法和属性
  • 通过组合的设计模式把类的实现分开成多个category在几个不同的文件里面
    • 可以减少单个文件的体积
    • 可以把不同的功能组织到不同的category
    • 可以由多个开发者共同完成一个类
    • 可以只加载自己想要的category,达到业务分离

</br>

关于问题(因为确实不知道该从什么地方开始看起,所以强迫试的给自己定了几个问题。让自己去弄明白)

  • category是什么东西
  • category是怎样加载的
  • category方法为什么可以覆盖宿主类的方法
  • category的属性跟方法是怎么添加到宿主类的
  • category为什么可以添加属性,方法,协议。缺不能添加成员变量

</br>

category是什么东西

objc所有类和对象都是c结构体,category也一样。我们可以通过clang去看一下

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

_category_t里面有名字、宿主类的对象、实例方法列表、类方法列表、协议方法列表、属列表性。可以看到是没有成员变量列表的,因为category是依赖runtime的,而在运行时对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的。这就是为什么我们没有在_category_t里面找到成员变量列表和category不可以添加成员变量的原因

</br>

category是怎样加载的

上面我们提到过category是依赖runtime的。那我们来看一下runtime的加载过程。下面用到的runtime源码都来自于点这! 我下载的是723最新的版本

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();
    lock_init();
    exception_init();
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

</br>

开始是一些初始化方法

map_images方法表示将文件中的二进制文件映射到内存,category被添加到宿主类就发生在这个方法里面。我们来看一下这个方法实现

void
map_images(unsigned count, const char * const paths[],
           const struct mach_header * const mhdrs[])
{
    // 加锁操作 保证在映射到dyld过程调用ABI是安全的
    rwlock_writer_t lock(runtimeLock);
    //函数在加锁后就转向了 map_images_nolock 函数
    return map_images_nolock(count, paths, mhdrs);
}

map_images_nolock方法代码太长,我就不粘过来了。它主要做的操作是在函数中,它检查传入的每个 image,如果 image有需要的信息,就将它记录在hList中,并将hCount 加一,最终判断 hCount>0来调用_read_images读取 image 中的数据 。

</br>

我们再来看_read_images,方法有点长。我给跟category相关代码截取出来了

for (EACH_HEADER) {
        // 获取 镜像中的所有分类
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        for (i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls);

            if (!cls) {
                // Category's target class is missing (probably weak-linked).
                // Disavow any knowledge of this category.
                catlist[i] = nil;
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class", 
                                 cat->name, cat);
                }
                continue;
            }

            // Process this category. 
            // First, register the category with its target class. 
            // Then, rebuild the class's method lists (etc) if 
            // the class is realized. 
            bool classExists = NO;
            // 从这开始,正式对category开始处理
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {
                //为类添加未依附的分类,把Category和类关联起来
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    remethodizeClass(cls);
                    classExists = YES;
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category -%s(%s) %s", 
                                 cls->nameForLogging(), cat->name, 
                                 classExists ? "on existing class" : "");
                }
            }

            if (cat->classMethods  ||  cat->protocols  
                ||  (hasClassProperties && cat->_classProperties)) 
            {
                //为类添加未依附的分类,把Category和metaClass关联起来。因为类方法是存在元类中的
                addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                if (cls->ISA()->isRealized()) {
                    remethodizeClass(cls->ISA());
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category +%s(%s)", 
                                 cls->nameForLogging(), cat->name);
                }
            }
        }
    }

</br>

哇! 终于 终于 到了最关键的方法了</br>
</br> 首先。我们调用remethodizeClass来调用category的幕后大佬----attachCategories方法,从名字就可以看出来它的作用,添加category

</br>

plz show me the code !!!

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
    /*
     #warning attachCategories
     
     category的加载顺序是通过编译顺序决定的
     
     这样倒序遍历,保证先将数组内元素(category)的方法从后往前添加到新数组
     
     这样编译在后面的category方法会在数组的前面
     
     */
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;
    bool fromBundle = NO;
    while (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->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);
}

</br>

得到重新组合的数组在调用attachLists方法

</br>

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;
        
        //C 库函数 void *realloc(void *ptr, size_t size) 尝试重新调整之前调用 malloc 或 calloc 所分配的 ptr 所指向的内存块的大小
        setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
        array()->count = newCount;
        /*
         1.memmove
         
         函数原型:void *memmove(void *dest, const void *source, size_t count)
         
         返回值说明:返回指向dest的void *指针
         
         参数说明:dest,source分别为目标串和源串的首地址。count为要移动的字符的个数
         
         函数说明:memmove用于从source拷贝count个字符到dest,如果目标区域和源区域有重叠的话,memmove能够保证源串在被覆盖之前将重叠区域的字节拷贝到目标区域中。
         
         
         
         2.memcpy
         
         
         
         函数原型:void *memcpy(void *dest, const void *source, size_t count);
         
         返回值说明:返回指向dest的void *指针
         
         函数说明:memcpy功能和memmove相同,但是memcpy中dest和source中的区域不能重叠,否则会出现未知结果。
 
         3.两者区别
         
         函数memcpy()   从source  指向的区域向dest指向的区域复制count个字符,如果两数组重叠,不定义该函数的行为。
         而memmove(),如果两函数重叠,赋值仍正确进行。
         
         memcpy函数假设要复制的内存区域不存在重叠,如果你能确保你进行复制操作的的内存区域没有任何重叠,可以直接用memcpy;
         如果你不能保证是否有重叠,为了确保复制的正确性,你必须用memmove。
         */
        
        /*
         这样就完成将category方法列表里面的方法 加到 class的方法列表里面而且是前面。等到我们再去调用class的方法时候,我们通过去遍历class的方法列表去查到SEL,找到就会调用相应方法。由于category的方法在前面---导致所以会覆盖宿主类本来的方法(这就是为什么category方法的优先级高于宿主类方法)
         属性和协议同理!!!!!!!!!!!
         */
        
        //相当于给array()->lists 重新放在起始地址 = array()->lists的起始地址 + addedCount
        memmove(array()->lists + addedCount, array()->lists, 
                oldCount * sizeof(array()->lists[0]));
        
        //相当于给addedLists 这个category新数组加到array()->的起始地址
        memcpy(array()->lists, addedLists, 
               addedCount * sizeof(array()->lists[0]));
        
    }

</br>

以上应该可以回答,之前提的所有问题。如有疑问,可以联系我。





笔者是一个刚入门iOSer

这次关于category的管中窥豹,一定有很多的不足,希望大家不吝赐教!

有任何问题可以留言,或者直接联系QQ:346658618

希望可以相互学习,一起进步!

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,644评论 0 9
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,480评论 33 467
  • 摘要 无论一个类设计的多么完美,在未来的需求演进中,都有可能会碰到一些无法预测的情况。那怎么扩展已有的类呢?一般而...
    癫癫的恋了阅读 1,044评论 0 6
  • 本文载自: http://blog.csdn.net/a316212802/article/details/49...
    MrLuckyluke阅读 2,432评论 1 7
  • 夜晚,被不知哪里来的黑色填充 亘古如此,神奇的白炽灯也不能改变 疲惫的眼睛到了打瞌睡的时候 像往常一样阖上吧 也许...
    滴墨成魂_b88e阅读 153评论 0 0