ios 分类(Category)的理解

1,分类可以干什么:声明私有方法,分解体积庞大的类文件,把Framework的私有方法公开

2,特点:运行时决议(意思是编译的时候并没有把我们添加的方法加入到我们宿主类上,而是在运行的时候)

3,分类可以添加哪些内容:实例方法,类方法,协议,属性(只添加了set和get方法,这个属性的添加并没有给我们添加成员变量,如果不能区分成员变量和属性,可以上网上查一下)

4,我们来看一下分类的结构体

struct category_t{

constchar*name;// 类名

classref_t  cls;// 分类所属的类

structmethod_list_t*instanceMethods;// 实例方法列表

structmethod_list_t*classMethods;// 类方法列表

structprotocol_list_t*protocols;// 遵循的协议列表

structproperty_list_t*instanceProperties;// 属性列表

// 如果是元类,就返回类方法列表;否则返回实例方法列表

method_list_t*methodsForMeta(bool isMeta) {

if(isMeta) {

return classMethods;

}else{

return instanceMethods;

        }

    }

// 如果是元类,就返回 nil,因为元类没有属性;否则返回实例属性列表,但是...实例属性

property_list_t*propertiesForMeta(bool isMeta) {

if(isMeta) {

return nil;// classProperties;

}else{

return instanceProperties;

        }

    }

从类别的结构体我们可以看到,分类可以添加属性,不能添加成员变量,要记住成员变量和属性的区别,别面试官问你能添加属性吗,你说不能,这就错了,

5,下面是分类加载步骤

_objc_init(初始化runtime,进行了一些初始化操作,注册了镜像状态改变时的回调函数)---->map_2_images()(主要是加锁并调用 map_images_nolock)---->map_2_images_nolock(完成所有 class 的注册、fixup等工作,还有初始化自动释放池、初始化 side table 等工作并在函数后端调用了 _read_images)---->_read_images(读取镜像,加载可执行文件,比如加载类、Protocol、Category)---->remethodizeClass

6,remethodizeClass方法实现就是分类的内部实现,先面是实现代码,我们只为分类添加方法,我们看一下,代码有注释

static void remethodizeClass(class_t*cls){

   category_list *cats;//分类的列表,你可能为一个类添加好几个分类

    BOOL isMeta;

    rwlock_assert_writing(&runtimeLock);

    isMeta = isMetaClass(cls);//判断当前的类是否是元类,如果是元类,添加的就是类方法,如果不是,就是实例方法

   if((cats = unattachedCategoriesForClass(cls))) {//判断一个类是否有分类,而且是未拼接的分类,大概意思就是这些分类,还没有和这个宿主类关联

                if(PrintConnecting) {

                            _objc_inform("CLASS: attaching categories to class '%s' %s", 

                            getName(cls), isMeta ?"(meta)":"");

                }

              attachCategoryMethods(cls, cats, true);//这个方法下面有注释,这个方法是用来拼接分类到所属的宿主类

                free(cats);

    }

}

static void attachCategoryMethods(class_t *cls, category_list *cats,BOOL *inoutVtablesAffected){

    if (!cats)return;//判断是否为空

    if (PrintReplacedMethods) printReplacements(cls, cats);

    BOOL isMeta = isMetaClass(cls); //是否是元类

     method_list_t **mlists = (method_list_t **) _malloc_internal(cats->count *sizeof(*mlists));//mlists是一个二维数组,大概是这样的[[method_t,method_t...],[method_t...],[method_t,method_t,method_t....]]

    int mcount =0;//方法的参数

    int i = cats->count;

    BOOL fromBundle =NO;//宿主分类的总数

    while (i--) { //这里是倒序遍历,最先访问最后编译的分类

             method_list_t *mlist =  cat_method_list(cats->list[i].cat, isMeta);//获取一个分类方法列表

            if (mlist) { 

                 mlists[mcount++] = mlist; //最后编译的分类,里面的方法最先添加到数组列表

                 fromBundle |= cats->list[i].fromBundle; 

              }

     } 

    auto rw = cls->data();//获取宿主类当中的rw,包含宿主类的方法列表信息

    rw->methods.attachLists(mlists,mcount);//把刚才获取到分类的方法,添加到宿主类上面

    free(mlists);

}

void attachLists(List*const* addedLists,uint32_t addedCount){

//addedLists 传递过来的二维数组[[method_t,method_t...](分类A的方法),[method_t...](分类B的方法),[method_t,method_t,method_t....](分类C的方法)]

//addedCount = 3,三个分类,A,B,C

        if(addedCount ==0)return;//判空

        if(hasArray()) {

                // many lists -> many lists

                uint32_toldCount =array()->count;//宿主类原有元素个数

                uint32_tnewCount = oldCount + addedCount;   //拼接之后的元素总数

                 setArray((array_t*)realloc(array(),array_t::byteSize(newCount)));//根据新总数分配内存

                 array()->count = newCount;//重新设置元素总数

                 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_toldCount = oldList ?1:0;

                     uint32_tnewCount = 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])); 

                 }

 }

上面几段代码就是分类的内部实现

下面我们来看一下,怎么为分类添加成员变量

分类添加成员变量是通过关联对象添加的,我们来看一下什么是关联对象

下面是关联对象的几个函数

id objc_getAssociatedObject(id object,constvoid*key)

void objc_setAssociatedObject(id object,constvoid*key, id value, objc_AssociationPolicy policy)

void objc_removeAssociatedObjects(id object)

关联对象是由AssociationsManager管理,并在AssociationsHashMap储存,并不是我们想象的,在我们宿主类上面,AssociationsManager创建的是一个全局的对象,工程中任何一个分类添加的成员变量都在这个manager的AssociationsHashMap中,到底是怎么储存的呢,我们来分析一下

先看我们传的参数(id object,const void*key, id value, objc_AssociationPolicy policy)。

第一步,先把我们传进来的value和policy关联起来生成一个ObjcAssociation类型的数据结构,policy是关联策略,是通过copy还是retain还是assign,就是通过它来设置,如果通过retain就传OBJC_ASSOCIATION_RETAIN_NONATOMIC。

第二步,拿到我们刚才生成的ObjcAssociation,把它里面的value和key形成映射关系,映射到一个叫ObjcAssociationMap中去,形成一个ObjcAssociationMap结构

第三步,我们传进来的object是被关联对象的指针值,然他和刚才形成的ObjcAssociationMap形成映射关系,放到AssociationsHashMap中,以后我们就可以通过object指针找到,被关联对象的属性了

我们看一下源码,只看set的源码

void objc_setAssociatedObject(id object,const void*key,idvalue, 

                         objc_AssociationPolicy policy) 

{

    objc_setAssociatedObject_non_gc(object, key, value, policy);

}

void objc_setAssociatedObject_non_gc(id object,const void *key,idvalue,objc_AssociationPolicy policy) {

    _object_set_associative_reference(object, (void*)key, value, policy);

}

void _object_set_associative_reference(id object,void*key,id value,uintptr_t policy) {

    // 参数object被关联的对象,key要关联的key就是我们创建的成员变量名,value要关联的值,就是给成员变量赋什么值,policy关联策略,是通过retain copy assign

    ObjcAssociation old_association(0,nil);

    id new_value = value  ? acquireValue(value, policy)  : nil;//根据policy生成一个新数据,这个是我们上面刚才说的第一步

    {

        AssociationsManager manager;//关联对象管理类

        AssociationsHashMap&associations(manager.associations());//获取维护一个Hashmap,可以看成我们oc的字典,是一个全局容器

        disguised_ptr_t disguised_object = DISGUISE(object);//对指针地址按位取反,用来做一个对象的key和AssociationsHashMap(value)做对应

        if(new_value) {//被关联的值

            // break any existing association.

            AssociationsHashMap::iterator i = associations.find(disguised_object);//根据对象指针查找这个被关联对象所对应的AssociationsHashMap的map

            if(i != associations.end()) {//找到了这个map

                // secondary table exists

                ObjectAssociationMap*refs = i->second;//获取ObjectAssociationMap

                ObjectAssociationMap::iterator j = refs->find(key);//根绝我门传进来的key进行查找

                if(j != refs->end()) {//如果找到,我们就把value换成最新的

                    old_association = j->second;

                    j->second = ObjcAssociation(policy, new_value);

                }else{//如果没有找到我们根据new_value和policy生成新的ObjcAssociation结构体,放到ObjectAssociationMap中

                    (*refs)[key] = ObjcAssociation(policy, new_value);

                }

            }else{//没找到这个map

                // create the new association (first time).

                ObjectAssociationMap *refs = new ObjectAssociationMap;//因为没有找到,需要新创建一个map

                associations[disguised_object] = refs;//把被关联对象指针反转后的值disguised_object当作key,新创建的ObjectAssociationMap refs当作value映射起来,放到AssociationsHashMap,这一步是我们上面说的第三步

                (*refs)[key] = ObjcAssociation(policy, new_value);//ObjcAssociation(policy, new_value)就是我们刚才说的第二步,用policy和new_value生成一个ObjcAssociation数据结构,然后拿到我们传进来的key和生成的ObjcAssociation做映射放到ObjectAssociationMap中

                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()) {//把以前的值移除,如果传进来的是nil

                    old_association = j->second;

                    refs->erase(j);

                }

            }

        }

    }

    // release the old value (outside of the lock).

    if(old_association.hasValue())ReleaseValue()(old_association);

}

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

推荐阅读更多精彩内容