iOS能夠在Category進行實例變量的一種比較好的實現

在閱讀本文的過程當中,讀者須要着重關注如下三個問題:html

  1. 關聯對象被存儲在什麼地方,是否是存放在被關聯對象自己的內存中?ios

  2. 關聯對象的五種關聯策略有什麼區別,有什麼坑?git

  3. 關聯對象的生命週期是怎樣的,何時被釋放,何時被移除?程序員

這是我寫這篇文章的初衷,也是本文的價值所在。github

使用場景數據結構

按照 Mattt Thompson 大神的文章 Associated Objects 中的說法,Associated Objects 主要有如下三個使用場景:app

  1. 爲現有的類添加私有變量以幫助實現細節;ide

  2. 爲現有的類添加公有屬性;函數

  3. 爲 KVO 建立一個關聯的觀察者。ui

從本質上看,第 1 、2 個場景實際上是一個意思,惟一的區別就在於新添加的這個屬性是公有的仍是私有的而已。就目前來講,我在實際工做中使用得最多的是第 2 個場景,而第 3 個場景我尚未使用過。

相關函數

與 Associated Objects 相關的函數主要有三個,咱們能夠在 runtime 源碼的 runtime.h 文件中找到它們的聲明:

1

2

3

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

id objc_getAssociatedObject(id object, const void *key);

void objc_removeAssociatedObjects(id object);

這三個函數的命名對程序員很是友好,可讓咱們一眼就看出函數的做用:

  1. objc_setAssociatedObject 用於給對象添加關聯對象,傳入 nil 則能夠移除已有的關聯對象;

  2. objc_getAssociatedObject 用於獲取關聯對象;

  3. objc_removeAssociatedObjects 用於移除一個對象的全部關聯對象。

注:objc_removeAssociatedObjects 函數咱們通常是用不上的,由於這個函數會移除一個對象的全部關聯對象,將該對象恢復成「原始」狀態。這樣作就頗有可能把別人添加的關聯對象也一併移除,這並非咱們所但願的。因此通常的作法是經過給 objc_setAssociatedObject 函數傳入 nil 來移除某個已有的關聯對象。

key 值

關於前兩個函數中的 key 值是咱們須要重點關注的一個點,這個 key 值必須保證是一個對象級別(爲何是對象級別?看完下面的章節你就會明白了)的惟一常量。通常來講,有如下三種推薦的 key 值:

  1. 聲明 static char kAssociatedObjectKey; ,使用 &kAssociatedObjectKey 做爲 key 值;

  2. 聲明 static void *kAssociatedObjectKey = &kAssociatedObjectKey; ,使用 kAssociatedObjectKey 做爲 key 值;

  3. 用 selector ,使用 getter 方法的名稱做爲 key 值。

我我的最喜歡的(沒有之一)是第 3 種方式,由於它省掉了一個變量名,很是優雅地解決了計算科學中的兩大世界難題之一(命名)。

關聯策略

在給一個對象添加關聯對象時有五種關聯策略可供選擇:

blob.png

其中,第 2 種與第 4 種、第 3 種與第 5 種關聯策略的惟一差異就在於操做是否具備原子性。因爲操做的原子性不在本文的討論範圍內,因此下面的實驗和討論就之前三種以例進行展開。

實現原理

在探究 Associated Objects 的實現原理前,咱們仍是先來動手作一個小實驗,研究一下關聯對象何時會被釋放。本實驗主要涉及 ViewController 類和它的分類 ViewController+AssociatedObjects 。注:本實驗的完整代碼能夠在這裏 AssociatedObjects 找到,其中關鍵代碼以下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

@interface ViewController (AssociatedObjects)

@property (assign, nonatomic) NSString *associatedObject_assign;

@property (strong, nonatomic) NSString *associatedObject_retain;

@property (copy,   nonatomic) NSString *associatedObject_copy;

@end

@implementation ViewController (AssociatedObjects)

- (NSString *)associatedObject_assign {

    return objc_getAssociatedObject(self, _cmd);

}

- (void)setAssociatedObject_assign:(NSString *)associatedObject_assign {

    objc_setAssociatedObject(self, @selector(associatedObject_assign), associatedObject_assign, OBJC_ASSOCIATION_ASSIGN);

}

- (NSString *)associatedObject_retain {

    return objc_getAssociatedObject(self, _cmd);

}

- (void)setAssociatedObject_retain:(NSString *)associatedObject_retain {

    objc_setAssociatedObject(self, @selector(associatedObject_retain), associatedObject_retain, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}

- (NSString *)associatedObject_copy {

    return objc_getAssociatedObject(self, _cmd);

}

- (void)setAssociatedObject_copy:(NSString *)associatedObject_copy {

    objc_setAssociatedObject(self, @selector(associatedObject_copy), associatedObject_copy, OBJC_ASSOCIATION_COPY_NONATOMIC);

}

@end

在 ViewController+AssociatedObjects.h 中聲明瞭三個屬性,限定符分別爲 assign, nonatomic 、strong, nonatomic 和 copy, nonatomic ,而在 ViewController+AssociatedObjects.m 中相應的分別用 OBJC_ASSOCIATION_ASSIGN 、OBJC_ASSOCIATION_RETAIN_NONATOMIC 、OBJC_ASSOCIATION_COPY_NONATOMIC 三種關聯策略爲這三個屬性添加「實例變量」。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

__weak NSString *string_weak_assign = nil;

__weak NSString *string_weak_retain = nil;

__weak NSString *string_weak_copy   = nil;

@implementation ViewController

- (void)viewDidLoad {

    [super viewDidLoad];

    self.associatedObject_assign = [NSString stringWithFormat:@"leichunfeng1"];

    self.associatedObject_retain = [NSString stringWithFormat:@"leichunfeng2"];

    self.associatedObject_copy   = [NSString stringWithFormat:@"leichunfeng3"];

    string_weak_assign = self.associatedObject_assign;

    string_weak_retain = self.associatedObject_retain;

    string_weak_copy   = self.associatedObject_copy;

}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

//    NSLog(@"self.associatedObject_assign: %@", self.associatedObject_assign); // Will Crash

    NSLog(@"self.associatedObject_retain: %@", self.associatedObject_retain);

    NSLog(@"self.associatedObject_copy:   %@", self.associatedObject_copy);

}

@end

在 ViewController 的 viewDidLoad 方法中,咱們對三個屬性進行了賦值,並聲明瞭三個全局的 __weak 變量來觀察相應對象的釋放時機。此外,咱們重寫了 touchesBegan:withEvent: 方法,在方法中分別打印了這三個屬性的當前值。

在繼續閱讀下面章節前,建議讀者先自行思考一下 self.associatedObject_assign 、self.associatedObject_retain 和 self.associatedObject_copy 指向的對象分別會在何時被釋放,以加深理解。

實驗

咱們先在 viewDidLoad 方法的第 28 行打上斷點,而後運行程序,點擊導航欄右上角的按鈕 Push 到 ViewController 界面,程序將停在斷點處。接着,咱們使用 lldb 的 watchpoint 命令來設置觀察點,觀察全局變量 string_weak_assign 、string_weak_retain 和 string_weak_copy 的值的變化。正確設置好觀察點後,將會在 console 中看到以下的相似輸出:

blob.png

點擊繼續運行按鈕,有一個觀察點將被命中。咱們先查看 console 中的輸出,經過將這一步打印的 old value 和上一步的 new value 進行對比,咱們能夠知道本次命中的觀察點是 string_weak_assign ,string_weak_assign 的值變成了 0x0000000000000000 ,也就是 nil 。換句話說 self.associatedObject_assign 指向的對象已經被釋放了,而經過查看左側調用棧咱們能夠知道,這個對象是因爲其所在的 autoreleasepool 被 drain 而被釋放的,這與我前面的文章《Objective-C Autorelease Pool 的實現原理》中的表述是一致的。提示,待會你也能夠放開 touchesBegan:withEvent: 中第 31 行的註釋,在 ViewController 出現後,點擊一下它的 view ,進一步驗證一下這個結論。

blob.png

接下來,咱們點擊 ViewController 導航欄左上角的按鈕,返回前一個界面,此時,又將有一個觀察點被命中。同理,咱們能夠知道這個觀察點是 string_weak_retain 。咱們查看左側的調用棧,將會發現一個很是敏感的函數調用 _object_remove_assocations ,調用這個函數後 ViewController 的全部關聯對象被所有移除。最終,self.associatedObject_retain 指向的對象被釋放。

blob.png

點擊繼續運行按鈕,最後一個觀察點 string_weak_copy 被命中。同理,self.associatedObject_copy 指向的對象也因爲關聯對象的移除被最終釋放。

blob.png

結論

由這個實驗,咱們能夠得出如下結論:

  1. 關聯對象的釋放時機與被移除的時機並不老是一致的,好比上面的 self.associatedObject_assign 所指向的對象在 ViewController 出現後就被釋放了,可是 self.associatedObject_assign 仍然有值,仍是保存的原對象的地址。若是以後再使用 self.associatedObject_assign 就會形成 Crash ,因此咱們在使用弱引用的關聯對象時要很是當心;

  2. 一個對象的全部關聯對象是在這個對象被釋放時調用的 _object_remove_assocations 函數中被移除的。

接下來,咱們就一塊兒看看 runtime 中的源碼,來驗證下咱們的實驗結論。

objc_setAssociatedObject

咱們能夠在 objc-references.mm 文件中找到 objc_setAssociatedObject 函數最終調用的函數:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

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. AssociationsManager 是頂級的對象,維護了一個從 spinlock_t 鎖到 AssociationsHashMap 哈希表的單例鍵值對映射;

  2. AssociationsHashMap 是一個無序的哈希表,維護了從對象地址到 ObjectAssociationMap 的映射;

  3. ObjectAssociationMap 是一個 C++ 中的 map ,維護了從 key 到 ObjcAssociation 的映射,即關聯記錄;

  4. ObjcAssociation 是一個 C++ 的類,表示一個具體的關聯結構,主要包括兩個實例變量,_policy 表示關聯策略,_value 表示關聯對象。

每個對象地址對應一個 ObjectAssociationMap 對象,而一個 ObjectAssociationMap 對象保存着這個對象的若干個關聯記錄。

弄清楚這些數據結構之間的關係後,再回過頭來看上面的代碼就不難了。咱們發現,在蘋果的底層代碼中通常都會充斥着各類 if else ,可見寫好 if else 後咱們就距離成爲高手不遠了。開個玩笑,咱們來看下面的流程圖,一圖勝千言:

chart.jpg

objc_getAssociatedObject

一樣的,咱們也能夠在 objc-references.mm 文件中找到 objc_getAssociatedObject 函數最終調用的函數:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

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) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);

            }

        }

    }

    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {

        ((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease);

    }

    return value;

}

看懂了 objc_setAssociatedObject 函數後,objc_getAssociatedObject 函數對咱們來講就是小菜一碟了。這個函數先根據對象地址在 AssociationsHashMap 中查找其對應的 ObjectAssociationMap 對象,若是能找到則進一步根據 key 在 ObjectAssociationMap 對象中查找這個 key 所對應的關聯結構 ObjcAssociation ,若是能找到則返回 ObjcAssociation 對象的 value 值,不然返回 nil 。

objc_removeAssociatedObjects

同理,咱們也能夠在 objc-references.mm 文件中找到 objc_removeAssociatedObjects 函數最終調用的函數:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

void _object_remove_assocations(id object) {

    vector< ObjcAssociation,ObjcAllocator > 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());

}

這個函數負責移除一個對象的全部關聯對象,具體實現也是先根據對象的地址獲取其對應的 ObjectAssociationMap 對象,而後將全部的關聯結構保存到一個 vector 中,最終釋放 vector 中保存的全部關聯對象。根據前面的實驗觀察到的狀況,在一個對象被釋放時,也正是調用的這個函數來移除其全部的關聯對象。

給類對象添加關聯對象

看完源代碼後,咱們知道對象地址與 AssociationsHashMap 哈希表是一一對應的。那麼咱們可能就會思考這樣一個問題,是否能夠給類對象添加關聯對象呢?答案是確定的。咱們徹底能夠用一樣的方式給類對象添加關聯對象,只不過咱們通常狀況下不會這樣作,由於更多時候咱們能夠經過 static 變量來實現類級別的變量。我在分類 ViewController+AssociatedObjects 中給 ViewController 類對象添加了一個關聯對象 associatedObject ,讀者能夠親自在 viewDidLoad 方法中調用一下如下兩個方法驗證一下:

1

2

+ (NSString *)associatedObject;

+ (void)setAssociatedObject:(NSString *)associatedObject;

總結

讀到這裏,相信你對開篇的那三個問題已經有了必定的認識,下面咱們再梳理一下:

  1. 關聯對象與被關聯對象自己的存儲並無直接的關係,它是存儲在單獨的哈希表中的;

  2. 關聯對象的五種關聯策略與屬性的限定符很是相似,在絕大多數狀況下,咱們都會使用 OBJC_ASSOCIATION_RETAIN_NONATOMIC 的關聯策略,這能夠保證咱們持有關聯對象;

  3. 關聯對象的釋放時機與移除時機並不老是一致,好比實驗中用關聯策略 OBJC_ASSOCIATION_ASSIGN 進行關聯的對象,很早就已經被釋放了,可是並無被移除,而再使用這個關聯對象時就會形成 Crash 。

在弄懂 Associated Objects 的實現原理後,能夠幫助咱們更好地使用它,在出現問題時也能儘快地定位問題,最後但願本文可以對你有所幫助。

參考連接

相關文章
相關標籤/搜索