类扩展 与 关联对象的底层原理探索

类扩展、分类的区别

1. Category(分类或者类别)

  • 专门给主类添加新的方法

  • 不能给分类添加成员变量(添加了也获取不到)

  • 可以给分类添加属性,但是通过 @property 定义的属性,只会生成变量的 settergetter 方法声明,不会生成方法实现(可以通过 rentime 来重写 settergetter 的实现,即 关联对象)以及带下划线的成员变量

2. Extension(类扩展)

  • 可以当作是一个特殊的分类,也称作 匿名分类

  • 可以给主类添加方法、属性、成员变量(都是私有的,即自己可见,外界不能调用)

类扩展的底层原理

类的扩展有两种创建方式

  • 直接在主类中添加:永远都是在主类的声明之后、实现之前(.m 文件中添加)

  • 通过 commond + N 新建 -> Objective-C File -> Extension

单独创建好的类扩展文件如下

类扩展的本质

有两种方法可以探究:clang源码

通过 clang 底层编译

下面我们通过 clang 底层编译,查看类扩展的底层实现

  • 类扩展代码如下
@interface LGPerson : NSObject

- (void)instanceMethod;
+ (void)classMethod;

@end

@interface LGPerson ()

@property (nonatomic, copy) NSString *lg_name;

- (void)ext_instanceMethod;
+ (void)ext_classMethod;

@end

@implementation LGPerson

- (void)instanceMethod {
    
}

+ (void)classMethod {
    
}

- (void)ext_instanceMethod {
    
}

+ (void)ext_classMethod {
    
}

@end
  • 通过 clang -rewrite-objc main.m -o main.cpp 命令生成 cpp 文件,打开 cpp 文件,搜索 lg_name
  • 再查看 LGPerson 的类扩展方法,在 编译过程 中,方法就直接添加到了 methodlist 中,作为类的一部分,即 编译时期直接添加到本类里面
通过 objc 源码探索
  • 创建 LGPerson 类,并创建 LGPerson_Ext.h 主类的类扩展,声明两个方法
#import "LGPerson.h"

@interface LGPerson ()

@property (nonatomic, copy) NSString *ext_name;

- (void)ext_intanceMethod3;
- (void)ext_intanceMethod4;

@end
  • LGPerson 中实现上面两个方法
/* ------ LGPerson.h ------*/
@interface LGPerson : NSObject

- (void)lc_intanceMethod1;

- (void)lc_intanceMethod2;

- (void)lc_intanceMethod3;

- (void)lc_intanceMethod4;

@end

/* ------ LGPerson.m ------*/
@implementation LGPerson

+ (void)load {
    
}

- (void)lc_intanceMethod1 {
    NSLog(@"%s", __func__);
}

- (void)lc_intanceMethod2 {
    NSLog(@"%s", __func__);
}

- (void)lc_intanceMethod3 {
    NSLog(@"%s", __func__);
}

- (void)lc_intanceMethod4 {
    NSLog(@"%s", __func__);
}

- (void)ext_intanceMethod {
    NSLog(@"%s", __func__);
}

- (void)ext_classMethod {
    NSLog(@"%s", __func__);
}

@end
  • 运行 objc 源码,在 readClass 处下个断点,查看此时的 ro 情况

总结

  • 类扩展在编译时期会作为类的一部分,和主类一起编译进来

  • 类扩展只是声明,它需要依赖当前主类,它没有 .m 文件

关联对象的底层原理

主要分为两部分:

  • 通过 objc_setAssociatedObject 设值

  • 通过 objc_getAssociatedObject 取值

在分类中添加属性 cate_name,通过 runtime 的关联属性方法重写它的 set、get 方法,并在 main 函数中调用如下

@interface LGPerson : NSObject

@end

@implementation LGPerson

@end

@interface LGPerson (LG)

@property (nonatomic, copy) NSString *cate_name;

@end

@implementation LGPerson (LG)

- (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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        LGPerson *p = [[LGPerson alloc] init];
        p.cate_name = @"lg_lc";
        NSLog(@"%@", p.cate_name);
        
    }
    return 0;
}

关联对象的设值流程

  1. main 函数中 cate_name 赋值处和分类的 setCate_name 方法中打个断点,运行程序
  1. 继续往下运行

其中 objc_setAssociatedObject 方法有四个参数:

  • 参数1:要关联的对象,即给谁添加关联属性
  • 参数2:标识符,方便下次查找
  • 参数3:设置的value
  • 参数4:属性策略,即 nonatomic、atomic、assign 等,枚举如下
/**
 * Policies related to associative references.
 * These are options to objc_setAssociatedObject()
 */
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};
  1. 进入 objc_setAssociatedObject 的源码实现
void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
    _object_set_associative_reference(object, key, value, policy);
}
_object_set_associative_reference

进入 _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
    DisguisedPtr<objc_object> disguised{(objc_object *)object};//相当于包装了一下 对象object,便于使用
    // 包装一下 policy - value
    ObjcAssociation association{policy, value};

    // retain the new value (if any) outside the lock.
    association.acquireValue();//根据策略类型进行处理
    //局部作用域空间
    {
        //初始化manager变量,相当于自动调用AssociationsManager的析构函数进行初始化
        AssociationsManager manager;//并不是全场唯一,构造函数中加锁只是为了避免重复创建,在这里是可以初始化多个AssociationsManager变量的
    
        AssociationsHashMap &associations(manager.get());//AssociationsHashMap 全场唯一

        if (value) {
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});//返回的结果是一个类对
            if (refs_result.second) {//判断第二个存不存在,即bool值是否为true
                /* it's the first association we make 第一次建立关联*/
                object->setHasAssociatedObjects();//nonpointerIsa ,标记位true
            }

            /* establish or replace the association 建立或者替换关联*/
            auto &refs = refs_result.first->second; //得到一个空的桶子,找到引用对象类型,即第一个元素的second值
            auto result = refs.try_emplace(key, std::move(association));//查找当前的key是否有association关联对象
            if (!result.second) {//如果结果不存在
                association.swap(result.first->second);
            }
        } else {//如果传的是空值,则移除关联,相当于移除
            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();//释放
}

通过以上源码可知,_object_set_associative_reference 方法主要分为以下几部分:

    1. 创建一个 AssociationsManager 管理类
    1. 获取全局唯一的静态哈希表:AssociationsHashMap
    1. 判断插入的 value 值是否存在
    • 3.1:存在走第四步

    • 3.2:不存在则 移除关联对象

    1. 创建一个空的 ObjectAssociationMap,通过 try_emplace 方法去查询并获取键值对(如果发现没有这个 key 就插入一个 空的 BucketT 进去并返回 true
    1. 如果没有这个 key,就标记为第一次创建
    1. 用当前 policyvalue 组成了一个 ObjcAssociation 替换原来的 BucketT 或者创建新的
    1. 如果是第一次创建的,通过 setHasAssociatedObjects 方法标记对象存在关联对象,即设置 isa 指针的 has_assoc 属性为 true
源码调试流程
  • 定义 AssociationsManager 变量,从源码可以得知,就是自动调用 AssociationsManager 的构造函数和析构函数
AssociationsManager()   { AssociationsManagerLock.lock(); }
~AssociationsManager()  { AssociationsManagerLock.unlock(); }

加锁并不代表唯一,只是为了避免多线程重复创建,可以在外层定义多个 AssociationsManager 变量的

  • 定义 AssociationsHashMap 类型的哈希 map
// 定义变量
AssociationsHashMap &associations(manager.get());

进入 manager.get() 源码,如下

typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;

// class AssociationsManager manages a lock / hash table singleton pair.
// Allocating an instance acquires the lock

class AssociationsManager {
    using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
    static Storage _mapStorage; // 静态变量

public:
    AssociationsManager()   { AssociationsManagerLock.lock(); } // 构造函数
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); } // 析构函数

    AssociationsHashMap &get() {
        return _mapStorage.get(); // 从上面定义的静态变量中获取,全场唯一
    }

    static void init() {
        _mapStorage.init();
    }
};

从源码中我们可以得知,_mapStorage 是一个静态变量,静态变量又是全局唯一的,AssociationsHashMap 是从静态变量 _mapStorage 中获取的,所以 AssociationsHashMap 是全局唯一的

  • 查看目前的数据结构

运行项目,走到断点处,打印变量的数据结构

  • disguised : 其中的 value 是来自 object 还原出来的
  • association : 包装的策略类型处理
  • manager : AssociationsManager 管理类
  • associations : 目前的 associations0x0,表示还没有查找到相应的递归查找域中

继续向下执行,此时传入的 value 有值,所以走 if 流程(如果传入的 value 为空,就会走 移除关联函数 流程,即 else 流程),查看 refs_result 的数据结构,类型很长,可以进行拆解

// pair 表示有键值对
(std::pair<
 objc::DenseMapIterator<DisguisedPtr<objc_object>,
 
 objc::DenseMap<const void *, objc::ObjcAssociation, objc::DenseMapValueInfo<objc::ObjcAssociation>, objc::DenseMapInfo<const void *>, objc::detail::DenseMapPair<const void *, objc::ObjcAssociation> >,
 
 objc::DenseMapValueInfo<objc::DenseMap<const void *, objc::ObjcAssociation, objc::DenseMapValueInfo<objc::ObjcAssociation>, objc::DenseMapInfo<const void *>, objc::detail::DenseMapPair<const void *, objc::ObjcAssociation> > >,
 
 objc::DenseMapInfo<DisguisedPtr<objc_object> >,
 
 objc::detail::DenseMapPair<DisguisedPtr<objc_object>, objc::DenseMap<const void *, objc::ObjcAssociation, objc::DenseMapValueInfo<objc::ObjcAssociation>, objc::DenseMapInfo<const void *>, objc::detail::DenseMapPair<const void *, objc::ObjcAssociation> > >,
 
 false>,
 
 bool>)
 
 👇
 
 // 简写
 (std::pair<
 
 objc,
 
 bool>)
try_emplace 源码

进入 try_emplace 源码实现,如下

从源码可以看到,有两个返回:

  • 通过查找桶子,如果 map 中存在,则直接返回,并将 make_pair 的第二个参数置为 false

  • 如果没找到,证明是第一次进入,则通过 InsertIntoBucket 插入 map,然后返回,并将 make_pair 的第二个参数置为 true

** LookupBucketFor 源码**

LookupBucketFor 源码有两个同名方法,区别是第一个方法中第二个参数有 const 修饰,通过 try_emplace 源码可知,调用的是第二个方法(重载函数),而第二个方法内部实现是调用第一个 LookupBucketFor 方法

  • 第一个 LookupBucketFor 源码实现如下
  • 第二个 LookupBucketFor 源码实现如下

继续往下走,运行至 TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...);,查看此时的 TheBucket

可以看到 TheBucket 的类型与 refs_result 中属性的类型是一致的

  • 继续执行,查看 refs 执行 refs.try_emplace 前后变化

** 两次 try_emplace 的区别**

  • 第一次执行 try_emplace 查看 AssociationsHashMap 全局哈希 map 中是否存在该对象的桶子,如果没有,则插入空的桶子

  • 第二次执行 try_emplace 往上面的空桶子中插入 ObjcAssociation (policy, value) 返回 ture

  • result.second 为 NO,证明哈希表中已存在,需要更换最新的

继续执行,断点在 object->setHasAssociatedObjects();,源码如下

由此可知,通过 setHasAssociatedObjectsnonpointerIsahas_assoc 标记为 true,到此就将属性与 value 关联上了

关联对象设值流程图

从上面分析可以得出,关联对象的哈希 map 结构如下

  • AssociationsHashMap 中有很多的关联对象 map,它的 keyDisguisedPtr<objc_object>,是一个包装的对象(例如 LGPerson、LGTeacher 等),它的 value 是也是一个 map(ObjectAssociationMap)
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;
  • ObjectAssociationMap 表中有很多的 key-value 键值对,它的 key 类型为 const void *key,这个 key 就是分类里面传过来的;它的 value 也是一个包装好的对象 ObjcAssociation
typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;
  • ObjcAssociation 是用于包装 policyvalue 的一个类

关联对象的取值流程

  • main.m 添加断点,如下

继续执行下一步,断点来到重写分类的属性 get 方法,进入 objc_getAssociatedObject 源码的实现

_object_get_associative_reference

其源码实现如下:

id
_object_get_associative_reference(id object, const void *key)
{
    ObjcAssociation association{}; // 创建空的关联对象

    {
        AssociationsManager manager; // 创建一个 管理类
        AssociationsHashMap &associations(manager.get()); // 获取全局唯一的静态哈希 map
        AssociationsHashMap::iterator i = associations.find((objc_object *)object); // 根据 object 查找 AssociationsHashMap,即获取 buckets
        if (i != associations.end()) { // 如果迭代器不是最后一个
            ObjectAssociationMap &refs = i->second; // 找到 ObjectAssociationMap 的迭代查询器,获取一个经过属性修饰符修饰的 value
            ObjectAssociationMap::iterator j = refs.find(key); //根据 key 查找 ObjectAssociationMap,即获取 bucket
            if (j != refs.end()) {
                association = j->second; // //获取 ObjcAssociation
                association.retainReturnedValue(); // retain 处理
            }
        }
    }

    return association.autoreleaseReturnedValue(); // 返回 value
}

通过以上源码,可以将取值流程分为以下几部分:

    1. 创建一个 AssociationsManager 管理类变量
    1. 通过管理类变量获取全局唯一的静态哈希表 AssociationsHashMap
    1. 通过 find 方法根据 object 找到 AssociationsHashMap 中的 iterator,迭代查询器
    1. 如果这个迭代器不是最后一个,获取 ObjectAssociationMap
    1. 通过 find 方法根据 key 找到 ObjectAssociationMap 中的迭代查询器获取一个经过属性修饰符修饰的 value
    1. 返回 value
调用流程
  • 进入 find 方法,根据关联对象迭代查找 AssociationsHashMap,即 buckets,源码实现如下
  • 打印 迭代器 i, 以及 i->second 查看它们的类型
  • 再次通过 find 方法,在 buckets 中查找与 key 匹配的 bucket,打印 find 方法执行前后 j 的变化

总结

综上所述,关联对象的底层调用,主要就是两层哈希 map 的处理,即存取时都是两层处理。流程如下图所示

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

推荐阅读更多精彩内容