我們知道,在 Objective-C 中可以通過 Category 給一個現有的類添加屬性,但是卻不能添加實例變數,這似乎成為了 Objective-C 的一個明顯短板。然而值得慶幸的是,我們可以通過 Associated Objects 來彌補這一不足。本文將結合 runtime 源碼深入探究 O ...
我們知道,在 Objective-C 中可以通過 Category 給一個現有的類添加屬性,但是卻不能添加實例變數,這似乎成為了 Objective-C 的一個明顯短板。然而值得慶幸的是,我們可以通過 Associated Objects 來彌補這一不足。本文將結合 runtime 源碼深入探究 Objective-C 中 Associated Objects 的實現原理。
在閱讀本文的過程中,讀者需要著重關註以下三個問題:
- 關聯對象被存儲在什麼地方,是不是存放在被關聯對象本身的記憶體中?
- 關聯對象的五種關聯策略有什麼區別,有什麼坑?
- 關聯對象的生命周期是怎樣的,什麼時候被釋放,什麼時候被移除?
這是我寫這篇文章的初衷,也是本文的價值所在。
使用場景
按照 Mattt Thompson 大神的文章 Associated Objects 中的說法,Associated Objects 主要有以下三個使用場景:
- 為現有的類添加私有變數以幫助實現細節;
- 為現有的類添加公有屬性;
- 為
KVO
創建一個關聯的觀察者。
從本質上看,第 1
、2
個場景其實是一個意思,唯一的區別就在於新添加的這個屬性是公有的還是私有的而已。就目前來說,我在實際工作中使用得最多的是第 2
個場景,而第 3
個場景我還沒有使用過。
相關函數
與 Associated Objects 相關的函數主要有三個,我們可以在 runtime 源碼的 runtime.h 文件中找到它們的聲明:
1
2
3
|
|
這三個函數的命名對程式員非常友好,可以讓我們一眼就看出函數的作用:
objc_setAssociatedObject
用於給對象添加關聯對象,傳入nil
則可以移除已有的關聯對象;objc_getAssociatedObject
用於獲取關聯對象;objc_removeAssociatedObjects
用於移除一個對象的所有關聯對象。
註:objc_removeAssociatedObjects
函數我們一般是用不上的,因為這個函數會移除一個對象的所有關聯對象,將該對象恢覆成“原始”狀態。這樣做就很有可能把別人添加的關聯對象也一併移除,這並不是我們所希望的。所以一般的做法是通過給 objc_setAssociatedObject
函數傳入 nil
來移除某個已有的關聯對象。
key 值
關於前兩個函數中的 key
值是我們需要重點關註的一個點,這個 key
值必須保證是一個對象級別(為什麼是對象級別?看完下麵的章節你就會明白了)的唯一常量。一般來說,有以下三種推薦的 key
值:
- 聲明
static char kAssociatedObjectKey;
,使用&kAssociatedObjectKey
作為key
值; - 聲明
static void *kAssociatedObjectKey = &kAssociatedObjectKey;
,使用kAssociatedObjectKey
作為key
值; - 用
selector
,使用getter
方法的名稱作為key
值。
我個人最喜歡的(沒有之一)是第 3
種方式,因為它省掉了一個變數名,非常優雅地解決了計算科學中的兩大世界難題之一(命名)。
關聯策略
在給一個對象添加關聯對象時有五種關聯策略可供選擇:
關聯策略 | 等價屬性 | 說明 |
---|---|---|
OBJC_ASSOCIATION_ASSIGN | @property (assign) or @property (unsafe_unretained) | 弱引用關聯對象 |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | @property (strong, nonatomic) | 強引用關聯對象,且為非原子操作 |
OBJC_ASSOCIATION_COPY_NONATOMIC | @property (copy, nonatomic) | 複製關聯對象,且為非原子操作 |
OBJC_ASSOCIATION_RETAIN | @property (strong, atomic) | 強引用關聯對象,且為原子操作 |
OBJC_ASSOCIATION_COPY | @property (copy, atomic) | 複製關聯對象,且為原子操作 |
其中,第 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
26
27
28
29
30
31
32
33
34
35
|
|
在 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
20
21
22
23
24
25
|
|
在 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
中看到如下的類似輸出:
點擊繼續運行按鈕,有一個觀察點將被命中。我們先查看 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
,進一步驗證一下這個結論。
接下來,我們點擊 ViewController
導航欄左上角的按鈕,返回前一個界面,此時,又將有一個觀察點被命中。同理,我們可以知道這個觀察點是 string_weak_retain
。我們查看左側的調用棧,將會發現一個非常敏感的函數調用 _object_remove_assocations
,調用這個函數後 ViewController
的所有關聯對象被全部移除。最終,self.associatedObject_retain
指向的對象被釋放。
點擊繼續運行按鈕,最後一個觀察點 string_weak_copy
被命中。同理,self.associatedObject_copy
指向的對象也由於關聯對象的移除被最終釋放。
結論
由這個實驗,我們可以得出以下結論:
- 關聯對象的釋放時機與被移除的時機並不總是一致的,比如上面的
self.associatedObject_assign
所指向的對象在ViewController
出現後就被釋放了,但是self.associatedObject_assign
仍然有值,還是保存的原對象的地址。如果之後再使用self.associatedObject_assign
就會造成 Crash ,所以我們在使用弱引用的關聯對象時要非常小心; - 一個對象的所有關聯對象是在這個對象被釋放時調用的
_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
|
|