Objective-C通過聯合存儲爲類增加屬性及原理解析

聯合存儲實現方式及底層原理解析
作者:wangzz
轉載請註明出處
如果覺得文章對你有所幫助,請通過留言或關注微信公衆帳號wangzzstrive來支持我,謝謝!

動態語言的最大好處,就是靈活性,對於Objective-C來說,能在運行時動態地爲類增加方法和實例變量是很多其它語言羨慕不已的能力。現在說說爲類增加實例變量用到的技術:聯合存儲。

一、聯合存儲的實現方式
下面這段代碼實現了爲Duck類增加color屬性:
Duck+associative.h文件
#import "Duck.h"

@interface Duck (associative)

@property (nonatomic, retain) NSString *color;

@end
Duck+associative.m文件
#import "Duck+associative.h"
#import <objc/runtime.h>

@implementation Duck (associative)

static char colorKey = NULL;

- (NSString *)color {
    return objc_getAssociatedObject(self, &colorKey);
}

- (void)setColor:(NSString *)aColor {
    objc_setAssociatedObject(self, &colorKey,
                             aColor,
                             OBJC_ASSOCIATION_RETAIN);
}
調用舉例:
Duck    *smallDuck = [[Duck alloc] init];
smallDuck.color = @"red color";
NSLog(@"duck color:%@",smallDuck.color);
[smallDuck release];
輸出結果:
2013-07-18 19:09:26.578 ObjcRunTime[429:403] duck color:red color
至此,我們已經成功的爲Duck類增加了一個color屬性。

二、爲類動態增加屬性用到的技術
主要用到了三種設計模式:
1、訪問器(accessor)
訪問器模式是Cocoa設計模式中很重要的一個,使用它的目的是通過少量的方法(通常時get和set方法)來訪問類中每個實例的引用。通過該技術,儘管Duck類沒有color實例變量,但是通過聯合存儲,依然可以實現同訪問實例變量完全一樣的效果。這些對於類的使用者來說,屏蔽了實現細節。
可以說,訪問器模式是通過聯合存儲實現爲類增加屬性的必要前提。
2、類別(category)
類別可以在運行時爲類動態的增加方法,這是可以利用訪問器模式實現爲類增加屬性的基礎。
3、聯合存儲(associative storage)
通過類別和訪問器,再結合聯合存儲技術,我們完成了爲類增加屬性的功能。這一切讓用戶覺得好像真的增加新的實例變量了,但是實際上我們只是通過訪問器模擬出來了一個,而不是真正的增加了。

三、聯合存儲的實現原理
從上面的例子可以看出,爲類增加屬性看起來是so easy的事情,主要是調了objc_setAssociatedObject和objc_getAssociatedObject兩個方法。我的疑問是爲類增加的屬性對應的對象值存儲在哪了呢?下面通過這兩個方法的實現部分來尋找答案:
1、objc_setAssociatedObject方法的實現部分:
void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy) {
    if (UseGC) {
        //這部分是有垃圾回收機制的實現,我們不用管
        if ((policy & OBJC_ASSOCIATION_COPY_NONATOMIC) == OBJC_ASSOCIATION_COPY_NONATOMIC) {
            value = objc_msgSend(value, @selector(copy));
        }
        auto_zone_set_associative_ref(gc_zone, object, key, value);
    } else {
        //這是引用計數機制部分的實現
        // Note, creates a retained reference in non-GC.
        _object_set_associative_reference(object, key, value, policy);
    }
}
從上述方法中可以看出,objc_setAssociatedObject實際上調用的是:
2、_object_set_associative_reference方法的實現部分:
__private_extern__ void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // retain the new value (if any) outside the lock.
    uintptr_t old_policy = 0;
    // NOTE:  old_policy is always assigned to when old_value is non-nil.
    id new_value = value ? acquireValue(value, policy) : nil, old_value = nil;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        if (new_value) {
            //如果new_value不爲空,開始遍歷associations指向的map,查找object對象是否存在保存聯合存儲數據的ObjectAssociationMap對象
            // break any existing association.
            AssociationsHashMap::iterator i = associations.find(object);
            if (i != associations.end()) {
                //object對象在associations指向的map中存在一個ObjectAssociationMap對象refs
                //檢查refs中是否使用key保存過value
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    //使用過該key保存value,用新的value和policy替換掉原來的值
                    ObjcAssociation &old_entry = j->second;
                    old_policy = old_entry.policy;
                    old_value = old_entry.value;
                    old_entry.policy = policy;
                    old_entry.value = new_value;
                } else {
                    //沒用使用過該key保存value,將value和policy保存到key映射的map中
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // object對象在associations指向的map中不存在一個ObjectAssociationMap對象
                // 則新建一個ObjectAssociationMap對象,並將new_value通過key的地址映射到該map中保存
                // create the new association (first time).
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                _class_assertInstancesHaveAssociatedObjects(object->isa);//通知object對象,綁定了一個新的值
            }
        } else {
            //new_value爲空,如果使用過該key保存過value,則解除使用該key保存的value值
            // setting the association to nil breaks the association.
            AssociationsHashMap::iterator i = associations.find(object);
            if (i != associations.end()) {
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    ObjcAssociation &old_entry = j->second;
                    old_policy = old_entry.policy;
                    old_value = (id) old_entry.value;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_value) releaseValue(old_value, old_policy);
}
3、通過這個方法的實現部分可以清楚的看出,在runtime系統中:
有一個單例的AssociationsHashMap實例
該實例的生成方式如下:
AssociationsHashMap &associations() {
        if (_map == NULL)
            _map = new(::_malloc_internal(sizeof(AssociationsHashMap))) AssociationsHashMap();
        return *_map;
    }
AssociationsHashMap實例用於保存一個個的ObjectAssociationMap對象
每個類都擁有一個ObjectAssociationMap實例,每個類通過聯合存儲模式保存的鍵值對也都保存在ObjectAssociationMap實例中
④Key對應的值無所謂,我們需要的是key的地址,因此定義key時通常的寫法是:
static char colorKey = NULL;
也就是說,說有的數據其實還是保存在AssociationsHashMap實例中,現在似乎一切都豁然開朗了!

四、聯合存儲的優缺點
1、優點
聯合存儲的最大的優點,在於它能通過靈活的方式實現爲類增加屬性。
2、缺點
效率低,使用單條機器指令就可以訪問真正的實例變量,但是訪問存儲在映射表中的值需要多個函數調用,效率問題還是需要注意的。

事實上,目前許多Cocoa類,像NSAttributedString、NSFileManager、NSNotification、NSProcessInfo等都廣泛地使用了聯合存儲。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章