Category理解笔记

先自我想想下面关于 Category 思考题:

  1. Category能否添加成员变量?如果可以,如何给Category添加成员变量?
  2. Category的实现原理,以及Category为什么只能加方法不能加属性。
  3. Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
  4. load、initialize的区别,以及它们在category重写的时候的调用的次序。

第一题回答:

不能直接添加成员变量,但是可以添加属性(@property),然后通过runtime的方式实现实现get、set方法,从而达到间接添加成员变量的效果。

直接添加成员变量编译器就已经报错


直接添加成员变量

在Category添加的属性,实际上只是在.h文件声明setter和getter方法


在.h文件声明get,set方法
编译器提示需要在.m文件实现get、set方法

现在我们来实现get、set方法

- (void)setAge:(int)age
{
    
}

- (int)age
{
    return 0;
}
Person *person = [[Person alloc] init];
person.age = 18;

系统运行上面代码已经没有任何问题了,对外也实现了直接赋值Person类的age属性;但是现在我们发现从外部传值进来的age没有付给任何属性,这样也就无法保存实例变量。到这一步我们就要思考如何来接收、传递变量数据?

方法一:通过使用静态全局变量

static int _age;

@implementation Person (Age)

- (void)setAge:(int)age
{
    _age = age;
}

- (int)age
{
    return _age;
}

@end
Person *person = [[Person alloc] init];
//调用set方法
person.age = 18;
//调用get方法
NSLog(@"person1.age = %d", person.age);


//调用person2 get方法
NSLog(@"person2.age = %d", [Person new].age);

/**
运行结果如下:
2018-06-12 10:20:33.094612+0800 TestCategory[9951:373561] person1.age = 18
2018-06-12 10:20:33.094908+0800 TestCategory[9951:373561] person2.age = 18
*/

person2.age不是预期的0,是第一次赋值的18。这是_age是静态全局变量,只要程序在运行_age变量就存在;一次赋值,所有person实例都会受影响,哪怕后来创建的实例也不例外。
static声明的变量与类没有关联,并不是真正意义上的成员对象。这里也不推荐大家使用,只是为了解编程原理。

方法二:使用RunTime动态接收、传递
由于OC是动态语言,方法真正的实现是通过runtime完成的,runtime提供了动态添加属性和获得属性的方法。

#import <objc/runtime.h>

static NSString *kPersonAgeKey = @"kPersonAgeKey";

@implementation Person (Age)

- (void)setAge:(int)age
{
    objc_setAssociatedObject(self, &kPersonAgeKey, [NSNumber numberWithInt:age], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (int)age
{
    NSNumber *number = objc_getAssociatedObject(self, &kPersonAgeKey);
    if (number) {
        return [number intValue];
    }
    
    return 0;
}

@end

/**
运行结果如下:
2018-06-12 10:34:50.181881+0800 TestCategory[10247:426647] person1.age = 18
2018-06-12 10:34:50.182092+0800 TestCategory[10247:426647] person2.age = 0
*/

这里运行结果和预期一致,实现添加成员变量的效果。此时已经成功给Person添加age属性,并且Person对象可以通过点语法为属性赋值,age属性与Person实例对象相关联。

objc_setAssociatedObject

/** 
 * Sets an associated value for a given object using a given key and association policy.(使用给定的键和关联策略为给定的对象设置关联值。)
 * 
 * @param object The source object for the association.(关联的源对象,这里要给自己添加属性,用self。)
 * @param key The key for the association.(关联的key,在get方法的objc_getAssociatedObject中通过次key获得属性的值并返回。)
 * @param value The value to associate with the key key for object. Pass nil to clear an existing association.(与对象的键相关联的值)
 * @param policy The policy for the association. For possible values, see “Associative Object Behaviors.”(策略,属性以什么形式保存。)
 * 
 * @see objc_setAssociatedObject
 * @see objc_removeAssociatedObjects
 */
OBJC_EXPORT void
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

关联策略objc_AssociationPolicy有以下几种

/**
 * 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. 
                                            (关联对象弱引用,一般用于Delegate对象关联)*/
    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.
                                             关联对象被复制,原子性*/
};

objc_getAssociatedObject比较简单,请仔细阅读理解

/** 
 * Returns the value associated with a given object for a given key.
 * 
 * @param object The source object for the association.
 * @param key The key for the association.
 * 
 * @return The value associated with the key \e key for \e object.
 * 
 * @see objc_setAssociatedObject
 */
OBJC_EXPORT id _Nullable
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)

可以看出关联对象的使用非常简单,主要就是两个方法。上面理解好了足以应对初级iOS面试,中高级iOS开发还需要理解关联对象的底层原理,CoreFoundation框架是如何设计Category的?

关联对象原理

实现关联对象技术的核心对象有

  1. AssociationsManager

  2. AssociationsHashMap

  3. ObjectAssociationMap

  4. ObjcAssociation

其中Map同我们平时使用的字典类似, 通过key-value一一对应存值。

Objective-C Runtime源码是开源的,下载地址为: http://opensource.apple.com/tarballs/objc4/
需要下载一个新一点的代码包,我下的是 objc4-723.tar.gz

对关联对象技术的核心对象有了一个大概的意识,我们通过源码来探寻这些对象的存在形式以及其作用

objc_setAssociatedObject函数

来到runtime源码,首先找到objc_setAssociatedObject函数,看一下其实现

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
    _object_set_associative_reference(object, (void *)key, value, policy);
}

objc_setAssociatedObject函数其实内部调用的是_object_set_associative_reference函数,我们来到_object_set_associative_reference函数,这就是runtime为Category动态添加成员变量的核心,读懂这段函数也意味着理解RunTime动态关联对象的原理。

_object_set_associative_reference函数

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                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 {
                // create the new association (first time).
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                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()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}

我们依次解释上述代码的关键信息:

1.转换对象

根据policy调用对应的方法,比如retain或者copy,将value转为new_value。

static id acquireValue(id value, uintptr_t policy) {
    switch (policy & 0xFF) {
    case OBJC_ASSOCIATION_SETTER_RETAIN:
        return objc_retain(value);
    case OBJC_ASSOCIATION_SETTER_COPY:
        return ((id(*)(id, SEL))objc_msgSend)(value, SEL_copy);
    }
    return value;
}

2.获取全局的AssociationsHashMap和object的对象地址

AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);

先获取AssociationsManager单例,构造函数中会进行加锁;接着通过associations()方法获取静态AssociationsHashMap;再把object的地址取反,得到一个unsigned long类型的变量,用作AssociationsHashMap的键。

AssociationsManager内容不多,看一下源码也就理解了。

// class AssociationsManager manages a lock / hash table singleton pair.
// Allocating an instance acquires the lock, and calling its assocations()
// method lazily allocates the hash table.

spinlock_t AssociationsManagerLock;

class AssociationsManager {
    // associative references: object pointer -> PtrPtrHashMap.
    static AssociationsHashMap *_map;
public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }
    
    AssociationsHashMap &associations() {
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    }
};

在看一下AssociationsHashMap源码,内容也很少,主要就是Key-Value的Map表,其中Key为disguised_ptr_t ,Value为ObjectAssociationMap

class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator> {
public:
    void *operator new(size_t n) { return ::malloc(n); }
    void operator delete(void *ptr) { ::free(ptr); }
};

3.设置全局的AssociationsHashMap和object的对象地址管理关系

            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                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 {
                // create the new association (first time).
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }

这里是函数的核心,先解释两个概念ObjectAssociationMapObjcAssociation,有助于理解代码思想。
ObjectAssociationMap看名称也能猜到该类的作用,用来存储ObjcAssociation的Map表,其中Key为_object_set_associative_reference方法入口的key参数。
看一下ObjectAssociationMap源码,前面两个参数(void *, ObjcAssociation)分别对应了Key和Value

class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator> {
public:
    void *operator new(size_t n) { return ::malloc(n); }
    void operator delete(void *ptr) { ::free(ptr); }
};

再来看看ObjcAssociation,该对象也很简单,只有policyvalue,两个值正对应着我们调用objc_setAssociatedObject函数传入的值,也就是说objc_setAssociatedObject函数的value和policy参数最终是存储在ObjcAssociation中的。现在就能理解RunTime为Category设置关联对象时,策略上为什么没有weak和Strong了,Value不支持简单数据类型(int, bool)的原因

class ObjcAssociation {
    uintptr_t _policy;
    id _value;
public:
    ObjcAssociation(uintptr_t policy, id value) : _policy(policy), _value(value) {}
    ObjcAssociation() : _policy(0), _value(nil) {}

    uintptr_t policy() const { return _policy; }
    id value() const { return _value; }
    
    bool hasValue() { return _value != nil; }
};

现在解释代码运行过程:
3.1 先根据object对象地址寻找ObjectAssociationMap,如果能找到ObjectAssociationMap,进入3.2,否则自行创建ObjectAssociationMap,进行对象赋值。
3.2 找到ObjectAssociationMap,则通过key查找ObjcAssociation?如果能找到ObjcAssociation,进入3.3,否则进去3.4.
3.3 找到ObjcAssociation,则将找到的赋给old_association,用于后期对象销毁。
3.4 创建新的ObjcAssociation,设置到ObjectAssociationMap所对应的key上。

如果我们value设置为nil的话,那么会执行下面的代码

// 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()) {
        old_association = j->second;
        refs->erase(j);
    }
}

从上述代码中可以看出,如果我们设置value为nil时,就会将关联对象从ObjectAssociationMap中移除。

最后我们通过一张图可以很清晰的理清楚其中的关系

image

通过上图我们可以总结为:一个实例对象就对应一个ObjectAssociationMap,而ObjectAssociationMap中存储着多个此实例对象的关联对象的key以及ObjcAssociation,为ObjcAssociation中存储着关联对象的value和policy策略。

由此我们可以知道关联对象并不是放在了原来的对象里面,而是自己维护了一个全局的map用来存放每一个对象及其对应关联属性表格。

objc_getAssociatedObject函数

该函数内部实际调用了_object_get_associative_reference,从_object_get_associative_reference函数内部可以看出,向set方法中那样,反向将value一层一层取出最后return出去。

id _object_get_associative_reference(id object, void *key) {
    id value = nil;
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        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()) {
                ObjcAssociation &entry = j->second;
                value = entry.value();
                policy = entry.policy();
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
                    objc_retain(value);
                }
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        objc_autorelease(value);
    }
    return value;
}

objc_removeAssociatedObjects函数

objc_removeAssociatedObjects用来删除所有的关联对象,objc_removeAssociatedObjects函数内部调用的是_object_remove_assocations函数,该函数将object对象向对应的所有关联对象全部删除。

void _object_remove_assocations(id object) {
    vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        if (associations.size() == 0) return;
        disguised_ptr_t disguised_object = DISGUISE(object);
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            // copy all of the associations that need to be removed.
            ObjectAssociationMap *refs = i->second;
            for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
                elements.push_back(j->second);
            }
            // remove the secondary table.
            delete refs;
            associations.erase(i);
        }
    }
    // the calls to releaseValue() happen outside of the lock.
    for_each(elements.begin(), elements.end(), ReleaseValue());
}

为什么没有weak呢?

总过上面对源码的分析我们知道,object经过DISGUISE函数被转化为了unsigned long类型的disguised_object
disguised_ptr_t disguised_object = DISGUISE(object);
而同时我们知道,weak修饰的属性,当没有拥有对象之后就会被销毁,并且指针置位nil,那么在对象销毁之后,虽然在map中既然存在值object对应的AssociationsHashMap,但是因为object地址已经被置位nil,会造成坏地址访问而无法根据object对象的地址转化为disguised_object了。

更多文章:
iOS底层原理总结 - 关联对象实现原理
深入理解Objective-C:Category
objc category的秘密
刨根问底Objective-C Runtime
runtime源码分析之category的实现

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

推荐阅读更多精彩内容