类拓展和关联对象

1. 类拓展和分类

category 类别/分类:

  • 专门用来给类添加新的方法。
  • 不能给类添加成员属性,添加了成员变量,也无法取到。
  • 可通过 runtime给分类添加属性。
  • 分类中用@proprty定义变量,不生成gettersetter方法和带下划线的成员变量。

extension 类拓展:

  • 可以说是特殊的分类,也称作匿名分类。
  • 可以添加属性和方法,但都是私有的。

举个栗子🌰,在main.m中做如下声明

#import <Foundation/Foundation.h>
/**********************主类声明***************************/
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *kc_name;

- (void)kc_instanceMethod1;
- (void)kc_instanceMethod2;
- (void)kc_instanceMethod3;

@end
/**********************类拓展***************************/
/// 只能放这里
@interface LGPerson()
@property (nonatomic, copy) NSString *ex_name;
- (void)kc_exinstanceMethod1;

@end
/**********************主类实现***************************/

@implementation LGPerson
- (void)kc_instanceMethod3{
}

- (void)kc_instanceMethod1{
}

- (void)kc_instanceMethod2{
}

- (void)kc_exinstanceMethod1
{
    
}
@end

/**********************分类声明***************************/
@interface LGPerson (CA)
@property (nonatomic, copy) NSString *cate_name;

- (void)kc_cateMethod1;
- (void)kc_cateMethod2;
- (void)kc_cateMethod3;

@end

/**********************分类实现***************************/
@implementation LGPerson (CA)
- (void)kc_cateMethod1
{
    
}
- (void)kc_cateMethod2
{
    
}
- (void)kc_cateMethod3
{
    
}
@end

/**********************main函数***************************/
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [[LGPerson alloc] init];
//        person.cate_name = @"";
        NSLog(@"");
        // Setup code that might create autoreleased objects goes here.
    }
    return 0;
}

通过下面终端命令进行编译,查看main.cpp文件。

clang -rewrite-objc main.m -o main.cpp

LGPerson编译后结构体.png
LGPerson编译后方法列表.png

编译后的LGPerson属性包括本类声明中的kc_name和扩展中的ex_name,还有这两个属性的gettersetter方法,也包括分类中的kc_exinstanceMethod1方法。

LGPerson (CA)编译后方法和属性列表.png

LGPerson (CA)分类编译后_prop_list_t生成cate_name属性,与_method_list_t方法列表生成kc_cateMethod1/2/3三个方法,并没有生成cate_namegettersetter方法,也就是没法直接访问cate_name属性,一般分类中都是通过objc_setAssociatedObject()方法关联属性给分类添加属性。
main.m中声明的分类都是非懒加载分类,结合上一篇类、分类的加载文章探索的结果可知:LGPerson类会在此类调用的第一个方法时进行类的初始化,属性方法列表在编译时就放入data()中,在methodizeClass方法下断点来验证下。

(lldb) p list
(method_list_t *) $0 = 0x0000000100008038
(lldb) p *$0
(method_list_t) $1 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 24
    count = 12
    first = {
      name = "kc_cateMethod1"
      types = 0x0000000100003f86 "v16@0:8"
      imp = 0x0000000100003cf0 (KCObjc`-[LGPerson(CA) kc_cateMethod1] at main.m:57)
    }
  }
}
(lldb) p $1.get(0)
(method_t) $2 = {
  name = "kc_cateMethod1"
  types = 0x0000000100003f86 "v16@0:8"
  imp = 0x0000000100003cf0 (KCObjc`-[LGPerson(CA) kc_cateMethod1] at main.m:57)
}
(lldb) p $1.get(1)
(method_t) $3 = {
  name = "kc_cateMethod2"
  types = 0x0000000100003f86 "v16@0:8"
  imp = 0x0000000100003d00 (KCObjc`-[LGPerson(CA) kc_cateMethod2] at main.m:61)
}
(lldb)  p $1.get(2)
(method_t) $15 = {
  name = "kc_instanceMethod2"
  types = 0x0000000100003f86 "v16@0:8"
  imp = 0x0000000100003bb0 (KCObjc`-[LGPerson kc_instanceMethod2] at main.m:35)
}
(lldb)  p $1.get(3)
(method_t) $16 = {
  name = "kc_exinstanceMethod1"
  types = 0x0000000100003f86 "v16@0:8"
  imp = 0x0000000100003bc0 (KCObjc`-[LGPerson kc_exinstanceMethod1] at main.m:39)
}

(lldb) p $1.get(11)
(method_t) $4 = {
  name = ".cxx_destruct"
  types = 0x0000000100003f86 "v16@0:8"
  imp = 0x0000000100003cb0 (KCObjc`-[LGPerson .cxx_destruct] at main.m:28)
}

(lldb) p ro
(const class_ro_t *) $5 = 0x00000001000081e0
(lldb) p $5 ->ivars
(const ivar_list_t *const) $6 = 0x0000000100008228
(lldb) p *$6
(const ivar_list_t) $7 = {
  entsize_list_tt<ivar_t, ivar_list_t, 0> = {
    entsizeAndFlags = 32
    count = 2
    first = {
      offset = 0x0000000100008288
      name = 0x0000000100003ea6 "_kc_name"
      type = 0x0000000100003f7a "@\"NSString\""
      alignment_raw = 3
      size = 8
    }
  }
}
(lldb) p $7.get(0)
(ivar_t) $8 = {
  offset = 0x0000000100008288
  name = 0x0000000100003ea6 "_kc_name"
  type = 0x0000000100003f7a "@\"NSString\""
  alignment_raw = 3
  size = 8
}
(lldb) p $7.get(1)
(ivar_t) $9 = {
  offset = 0x0000000100008290
  name = 0x0000000100003eaf "_ex_name"
  type = 0x0000000100003f7a "@\"NSString\""
  alignment_raw = 3
  size = 8
}

(lldb) p proplist
(property_list_t *) $10 = 0x0000000100008160
(lldb) p *$10
(property_list_t) $11 = {
  entsize_list_tt<property_t, property_list_t, 0> = {
    entsizeAndFlags = 16
    count = 3
    first = (name = "cate_name", attributes = "T@\"NSString\",C,N")
  }
}
(lldb) p $11.get(0)
(property_t) $12 = (name = "cate_name", attributes = "T@\"NSString\",C,N")
(lldb) p $11.get(1)
(property_t) $13 = (name = "ex_name", attributes = "T@\"NSString\",C,N,V_ex_name")
(lldb) p $11.get(2)
(property_t) $14 = (name = "kc_name", attributes = "T@\"NSString\",C,N,V_kc_name")

baseMethods()中已经包含kc_exinstanceMethod1类拓展方法,ivars中没有cate_nameproplist中包含。
我们知道类拓展中的方法是不能直接调用的,编译器会报错,那用[person performSelector:@selector(kc_exinstanceMethod1)];底层是objc_msgSend,会能调用到方法吗?方法慢速查找流程中分析过,会从cls->data()->methods()中通过二分查找寻找方法,kc_exinstanceMethod1方法已经在data()->methods()中,来验证下。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [[LGPerson alloc] init];
        [person performSelector:@selector(kc_exinstanceMethod1)];
        NSLog(@"");
    }
    return 0;
}
// LGPerson中实现
- (void)kc_exinstanceMethod1
{
    NSLog(@"%s",__func__);
}

kc_exinstanceMethod1.png

2.关联对象

一般都是通过下面方法实现分类添加属性,这篇文章主要研究下objc_setAssociatedObject是怎么实现关联对象的。

@implementation LGPerson (CA)

- (void)setCate_name:(NSString *)cate_name
{
    objc_setAssociatedObject(self, "cate_name", cate_name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
-(NSString *)cate_name
{
    return objc_getAssociatedObject(self, "cate_name");
}
@end

查看源码:

void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
    SetAssocHook.get()(object, key, value, policy);
}
// SetAssocHook声明
static ChainedHookFunction<objc_hook_setAssociatedObject> SetAssocHook{_base_objc_setAssociatedObject};

SetAssocHook其实就是_base_objc_setAssociatedObject,来验证下。

_base_objc_setAssociatedObject验证.png

_object_set_associative_reference源码:

void
_object_set_associative_reference(id object, const 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;
    // 是否是禁止关联对象的类
    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));
    // 包装object
    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    // 包装policy, value
    ObjcAssociation association{policy, value};
    
    // retain the new value (if any) outside the lock. // 对 OBJC_ASSOCIATION_SETTER_RETAIN 和OBJC_ASSOCIATION_SETTER_COPY策略的关联对象进行处理
    association.acquireValue();

    {
        AssociationsManager manager; // 非全局变量
        
        AssociationsHashMap &associations(manager.get()); // 全局map

        if (value) {
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{}); // 返回的是类对,第二个参数为bool
            if (refs_result.second) {
                /* it's the first association we make */ //标记 isa has_assoc设置为true
                object->setHasAssociatedObjects();
            }

            /* establish or replace the association 建立或者替换关联*/
            auto &refs = refs_result.first->second; //得到空的桶子,找到引用对象类型。
            auto result = refs.try_emplace(key, std::move(association)); //查找当前的key是否有association关联对象
            if (!result.second) {
                association.swap(result.first->second);
            }
        } else { // 如果 value为空则移除关联。
            auto refs_it = associations.find(disguised);
            if (refs_it != associations.end()) {
                auto &refs = refs_it->second;
                auto it = refs.find(key);
                if (it != refs.end()) {
                    association.swap(it->second);
                    refs.erase(it);
                    if (refs.size() == 0) {
                        associations.erase(refs_it);

                    }
                }
            }
        }
    }

    // release the old value (outside of the lock).
    association.releaseHeldValue();
}

主要流程为:

  • 1: 创建一个 AssociationsManager 管理类
  • 2: 获取唯一的全局静态哈希Map
  • 3: 判断是否插入的关联值是否存在:
    3.1: 存在走第4步
    3.2: 不存在就走 : 关联对象插入空流程
  • 4: 创建一个空的 ObjectAssociationMap 去取查询的键值对
  • 5: 如果发现没有这个 key 就插入一个 空的 BucketT进去 返回
  • 6: 标记对象存在关联对象
  • 7: 用当前 修饰策略和值 组成了一个 ObjcAssociation 替换原来 BucketT 中的空
  • 8: 标记一下 ObjectAssociationMap 的第一次为 false

关联对象插入空流程:

  • 1: 根据 DisguisedPtr 找到 AssociationsHashMap 中的 iterator 迭代查询器
  • 2: 清理迭代器
  • 3: 其实如果插入空值相当于清除

取值流程:

  • 1: 创建一个 AssociationsManager 管理类
  • 2: 获取唯一的全局静态哈希Map
  • 3: 根据 DisguisedPtr 找到 AssociationsHashMap 中的 iterator 迭代查询器
  • 4: 如果这个迭代查询器不是最后一个 获取 : ObjectAssociationMap (这里有策略和value) 5: 找到ObjectAssociationMap的迭代查询器获取一个经过属性修饰符修饰的value
  • 6: 返回_value
    objc_setAssociatedObject流程.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容