IOS-Category实现原理

分类(Categroy)

你用分类都做了那些事?

  • 声明私有方法
  • 分解体积庞大的类文件
  • 把Framework的私有方法公开化

特点

  • 运行时决议

    在编译后的Categroy类时,是没有将方法添加到宿主类的,是通过程序在运行时,通过Runtime动态添加到宿主类上面的

  • 可以为系统类添加分类

    类似UIView获取Left Top等分类

  • 分类是用于给原有类添加方法的,因为在分类的结构体指针中,没有属性列表,只有方法列表。原则上分类只能添加方法,不能添加属性,实际上可以通过'关联对象'给分类添加属性

  • 分类中是可以写@property,但是不会生成setter/getter方法,也不会生成实现以及私有的成员变量,会编译通过,但是引用变量就会报错

  • 如果分类中和原类中有同名的方法,会优先调用分类中的方法,就是说会忽略原有的类方法(不是替换掉原有的方法,因为分类的方法是后添加到方法列表中的,所有会优先调用)

    同名方法调用的优先级:分类 > 本类 > 父类

  • 如果多个分类中都有和原有的类中有同名方法,那么调用该方法的时候,执行谁室友编译器决定的;编译器会执行最后一个参与编译的分类中的方法

分类中都可以添加哪些内容

  • 实例方法
  • 类方法
  • 协议
  • 属性(关联对象)

实现原理

  • 将category中的方法,属性,协议数据放在category_t结构体中,然后通过remethodizeClass方法重新整理类的数据
  • category中的实例方法和属性被整合到主类中
  • category中的类方法被整合到元类中
  • category中对协议助理的比较特殊,同时被整合到主类和元类中

调用加载栈

调用加载栈

实现原理

category源码分析(objc-756.2)

分类结构体
struct category_t {
    const char *name;                   //分类名称
    classref_t cls;                     //所属宿主类
    struct method_list_t *instanceMethods;          //实例方法列表
    struct method_list_t *classMethods;                 //类方法列表
    struct protocol_list_t *protocols;                  //协议列表
    struct property_list_t *instanceProperties; //实例属性列表
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

remethodizeClass
static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;

    runtimeLock.assertLocked();

    isMeta = cls->isMetaClass();

    // Re-methodizing: check for more categories
    // 获取为拼接的分类列表
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        if (PrintConnecting) {
            _objc_inform("CLASS: attaching categories to class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
        //将分类cats拼接到cls上
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}
attachCategories
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_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--) {//这里是倒叙,所以谁最后编译,最先访问谁
        //获取一个分类
        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;
        }
    }
        //获取宿主类当中的rw数据,其中包含数组类的方法列表信息
    auto rw = cls->data();
        // 主要针对 分类中有关内存管理的相关方法,做一些特殊处理
    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
  
    /*
        rw代表宿主类
        methods代表宿主类的方法列表
        attachLists方法是 将含有mcount个元素的二维数组拼接到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);
}
attachLists
/*
    addedLists 上面传递过来的二维数组
    [[method_t,method_t,...], [method_t], [method_t,method_t,method_t],...]
     -----------------------  ----------  ---------------------------
        分类A的方法列表                           B                                C
     addedCount 二维数组长度 3
*/
void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) {
            // 类表中原有的元素总数
            uint32_t oldCount = array()->count;
            // 拼接之后的元素总数
            uint32_t newCount = 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]));
            /*
                内存拷贝
                [
                    A           --->        [addedLists中的第一个元素],
                    B           --->        [addedLists中的第二个元素],
                    C           --->        [addedLists中的第三个元素],
                    [原有第一个元素],
                    [原有第二元素]
                ]
                这就是为什么有相同的方法名时,会先调用最后编译的分类方法的原因
            */
            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]));
        }
    }

分类添属性(成员变量)(关联对象)

@interface UIView (KTestCategroy)
@property (nonatomic,copy) NSString *name;
@end
@implementation UIView (KTestCategroy)
-(void)setName:(NSString *)name{
    objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
-(NSString*)name{
    return objc_getAssociatedObject(self, "name");
}
@end
{
  "0x847547475":{
    "@selector(name)":{
      "value":"KsView",
      "policy":"copy"
    }
  }
}
关联对象实现
  • 关联对象由AssociationsManager管理,并存放在AssociationsHashMap
  • 所有对象的关联内容都在同一个<font color=FF0000>全局容易中存储</font>中
objc_setAssociatedObject源码分析
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // This code used to work when nil was passed for object and key. Some code
    // probably relies on that to not crash. Check and handle it explicitly.
    // rdar://problem/44094390
    if (!object && !value) return;
    
    assert(object);
    
    if (object->getIsa()->forbidsAssociatedObjects())
        _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
    
    // retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        //关联对象管理类
        AssociationsManager manager;
        // 获取其维护的一个Hashmap
        // 一个全局的容器
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // 根据对象指针查找对应的一个AssociationsHashMap结构的map
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                //如果找到这个Key,替换新值
                if (j != refs->end()) {
                    old_association = j->second;
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    //设置新值
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // 第一次为这个对象添加关联对象
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
          
            // 传过来的新值是nil,则清空原有的值
            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);
}

总结

  1. category为什么不能添加属性?

    category可以添加属性,但是不会生成实例变量和set/get方法,因为category_t结构体中不存在成员变量,成员变量是存放在实例对象中的,在编译好的那一刻就决定好了。而分类是在运行时才去加载的,那么就无法在程序运行时将分类的成员变量添加到实例对象的结构体重,所以说分类中不可以添加成员变量。实例变量没有set/get方法,也没有自己的isa指针,所以,添加属性时系统没有报错,但是也不能用。

  2. category中有load方法吗?load方法调用时机以及load方法能继承吗?

    category中有load方法;

    category中的load方法在程序装载类信息的时候就会调用;

    load方法可以继承,会先调用父类的load方法

    调用顺序:先调用类的+load方法,再调用分类的+load,先编译,先调用的原则,调用子类的+load方法前会先调用父类的+load

  3. load、initialize的区别,以及它们在category重写的时候的调用顺序

    区别就在于调用方式和调用时刻;

    调用方式:load是根据函数地址直接调用;initialize是通过objc_msgSend调用

    调用时刻:load是在runtime加载类、分类的时候调用(只调用一次);initialize是类第一次接收到消息的时候调用,每个类只会initialize一次(父类的initialize方法可能会被调用多次)

  1. 如何清空一个关联对象?

    传nil则清空

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

推荐阅读更多精彩内容