OC语言-分类&关联对象&扩展

分类
分类做了什么
  1. 声明私有方法: 把分类的头文件放到类的.m中,就满足了类既能使用这个方法,又对外不暴露
  2. 分解体积庞大的类文件: 按功能进行方法分类
  3. 把Framework的私有方法公开
分类的特点
  1. 在运行时进行决议: 在编写完分类文件之后,并没有把分类中添加的内容附加到类上,而是在运行时通过runtime把分类内容真实的添加到类上
  2. 可以为系统类添加分类: 项目中经常看到UIView的获取坐标的分类方法
分类和扩展的区别
  1. 扩展不是运行时决议
  2. 扩展无法给系统类添加分类
分类中都可以添加哪些内容
  1. 实例方法
  2. 类方法
  3. 协议
  4. 实例属性(只声明了对应的get和set方法,但没有在分类中添加上相应的实例变量)
  5. 可以通过关联对象来为分类添加实例变量
分类结构体

category_t就是创建的分类文件
成员属性name是分类名称
成员变量cls表示宿主类
下面四个结构体表示实例方法列表,类方法列表,协议以及实例属性的列表

加载调用栈

程序启动后,在运行时会调用_objc_init方法,这个方法也就是runtime初始化
然后调用一系列的方法,做了些程序镜像以及内存镜像的相关处理,最终会去加载分类,images方法的命名在这里表示的是镜像,_read_images是读取镜像,就是加载一些可执行文件到内存中进行一些处理
分类的加载内容和逻辑都在remethodizeClass内部

源码分析

这里分析分类添加实例方法的逻辑

static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;
    runtimeLock.assertWriting();
   //1.判断当前类是否为元类对象,这取决于我们添加的是实例方法还是类方法,因为假设添加的是实例方法,isMeta为NO
    isMeta = cls->isMetaClass();

    //2.从类中获取还没有拼接整合的所有分类,
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        if (PrintConnecting) {
            _objc_inform("CLASS: attaching categories to class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
        //如果获取到了,则拼接到宿主类上
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}


###拼接到宿主类的具体实现###

static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    //先对分类判空
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

   /*
     声明三个局部变量,对应的都是二维数组,分别表示方法列表,属性列表和协议列表
     数据结构是数组,数组中的每个元素又是数组,里面数组的元素是method_t这样的结构,method_t代表的就是一个方法
     [[method_t,method_t],[method_t],[method_t,method_t,method_t],...]
     */
    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));

    //下面三个代表方法的参数,属性参数,协议参数
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    //获取宿主类分类的总数,传递进来的分类列表的参数cats,他的最后一个元素是最后参加编译的
    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);
        if (proplist) {
            proplists[propcount++] = proplist;
        }
        //协议列表添加规则
        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }

    //获取宿主类中的rw数据,其中包含宿主类的方法列表信息
    auto rw = cls->data();
    //主要是针对分类中有关于内存管理相关方法情况下的一些特殊处理
    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    //把刚刚处理过的二维数组的数据拼接到宿主类的方法列表上面,rw代表类,methods代表类的方法列表,attachLists是将含有mcount个元素的mlist元素拼接到rw的methods上
    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);
}



###拼接列表的具体实现###
 /* addedLists是传来的二维数组
       [[method_t,method_t],[method_t],[method_t,method_t,method_t],...]
       --------------------  --------   --------------------------
        分类A中的方法列表A        B               C
    
        addedCount = 3
    */
    void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;//先判空

        if (hasArray()) {
            //列表中原有元素总数 oldCount = 2
            uint32_t oldCount = array()->count;
            //拼接之后的元素总数
            uint32_t newCount = oldCount + addedCount;
            //根据新的总数重新分配内存
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            //重新设置元素总数为5
            array()->count = newCount;
            /*
             内存移动
             [[],[],[],[原有的第一个元素],[原有的第二个元素]]
             */
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            /*
             内存拷贝
             [
             A ---> [addedLists中的第一个元素],
             B ---> [addedLists中的第二个元素],
             C ---> [addedLists中的第三个元素],
             [原有的第一个元素],
             [原有的第二个元素]
             ]
             
             这就是分类方法会覆盖宿主类方法的原因,宿主类的重名方法仍然存在,但是在我们消息发送过程中,是根据选择器名称来查找的,一旦查找到对应的实现就会返回,由于分类方法位于数组靠前的位置,若分类和宿主类方法重名,会先查找到分类的方法
             */
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }

根据上面的代码可以得出下面两图


倒序遍历分类,添加到数组中

将分类数组拼接到宿主类的方法列表上
***总结

rw->methods.attachLists(mlists, mcount);这句代码才将分类的方法真正的添加到宿主类上,这说明了分类是运行时决议

  1. 分类添加的方法可以"覆盖"原类方法,之所以双引号是因为系统方法仍然存在
  2. 若我们添加两个分类,两个分类中都有同名方法,哪个会生效,取决于分类的编译顺序,因为是倒序编译,最后编译的分类同名方法才会最终生效,其他都会被覆盖掉
  3. 名字相同的分类会引起编译报错,因为在生成具体分类的时候,经过runtime在编译过程中会把我们添加的分类名字以下划线方式拼接到宿主类上,如果名字相同,就会类似于我们定义了两个同名变量,会引起编译报错
关联对象
  1. 为分类添加成员变量
/*
添加关联对象
首先设定value值,然后通过key建立和value的对应关系,通过policy策略,将这个对应关系,关联到object对象上
policy是表示value是以何种形式(copy,assign,return)关联到宿主对象上
*/
void objc_setAssociatedObject(id object, const void *key,
                         id value, objc_AssociationPolicy policy)

/*
取出关联对象的值
根据指定的key,到object对象中去获取和key相对应的关联值,然后作为函数的返回值,返回给调用方
*/
id objc_getAssociatedObject(id object, const void *key)

//移除object对象的所有关联对象
void objc_removeAssociatedObjects(id object)
关联对象的本质

关联对象是由系统提供的,由AssociationsManager类来管理,这个类有一个成员变量叫做AssociationsHashMap,我们创建的每一对象的关联对象,都存储在AssociationsHashMap这个容器中
它是通过hash来实现的一个map,它是一个全局容器

关联对象被添加到哪里呢?由上面可以看出,不论我们为哪个对象建立关联值,所有类的关联对象都放在同一个全局容器中

首先,根据传进来的value值和policy策略,封装成ObjcAssociation这样的数据结构
@selector(text)就是我们传递进来的key,key和刚刚的ObjcAssociation建立起一个映射关系,放到ObjectAssociationMap这样的数据结构中
当前被关联对象的指针值,和ObjectAssociationMap建立起一个映射关系,最终放到AssociationsHashMap里

关联进来的对象的数据结构
/*
 object准备被关联的对象,key是我们要g关联的值得标识,value是要关联的值,policy是要通过哪种策略(copy,return或assign)将keyvalue关联到对象上
 */

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    ObjcAssociation old_association(0, nil);
    //1.根据策略对value进行加工
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        //声明一个全局管理者,主要管理关联对象,由C++实现
        AssociationsManager manager;
        //AssociationsHashMap是个全局容器,被manager管理,里面放着所有的关联对象
        AssociationsHashMap &associations(manager.associations());
        //根据传进来的对象,做了一个转换,对指针地址按位取反,来作为全局容器中某一对象的key,
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {//new_value是传进来的准备被关联的值
            //根据对象指针查找对应的一个ObjectAssociationsMap结构的map,object对象在全局容器中,是和ObjectAssociationsMap建立起的映射关系
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {//ObjectAssociationsMap之前被创建过
                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 {////ObjectAssociationsMap之前没有被创建过
                //新创建一个ObjectAssociationMap,作为全局容器当中disguised_object这个key的value
                //再把新关联的值,通过策略和nieValue组装成ObjcAssociationk结构
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            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);
                }
            }
        }
根据代码可以得出:怎样清除某个关联对象被关联的值呢,可以调用objc_setAssociatedObject方法,传nil即可,就会把对应key的值给擦除掉
扩展

在开发时,一般把扩展的声明放到宿主类的.m文件之中

一般用扩展做什么?
  1. 声明私有属性,是可以不对子类暴露的
  2. 声明私有方法,方便阅读
  3. 声明私有成员变量
扩展的特点:
  1. 编译时决议
  2. 只以声明的形式存在,没有具体实现,多数情况寄生于宿主类的.m中,也就是说,它不是独立存在实现的一个文件
    可以把扩展理解为类的一个内部的私有声明
  3. 不能为系统类添加扩展
分类和扩展的区别
  1. 分类是运行时决议,扩展是编译时决议
  2. 分类可以有声明有实现,而扩展只有声明,它的实现是直接写在宿主类当中
  3. 可以为系统类添加分类,但不能为系统类添加扩展
分类实现原理

分类实现原理由运行时决议,不同分类中含有同名分类方法,谁最终生效,取决于谁最后参与编译,假如分类中添加的方法恰好是宿主类中的同名方法,分类方法会"覆盖"同名的宿主类方法(消息传递过程中会优先查找数组靠前的元素,若找到了就会调用,但宿主类的同名方法仍然存在)

能否为分类添加成员变量(也叫实例变量)

可以通过关联对象来添加

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

推荐阅读更多精彩内容