【iOS面試糧食】OC語言—Category(分類)和類擴充套件(extension)、關聯物件
本文章將記錄有關 Category(分類)和類擴充套件(extension)、關聯物件的特性,如有錯誤歡迎指出~
Category(分類)
分類的應用在App的開發中是非常廣泛的,它可以動態地為已有類新增新行為。
我們平常都是使用分類來對系統的類封裝一些小功能,如NSString判空處理等,可以看下 ibireme大神開源的這個庫YYCategories,都是針對系統的類使用分類拓展的小功能,很實用。再來看看業界聞名的空白頁框架DZNEmptyDataSet,它就是通過對 UIScrollView
使用分類功能,非常完美、無侵入的解決了無資料時,避免白屏的尷尬,改善使用者體驗。真的是居家旅行必備之利器,強大~
隨著業務的增長,.m檔案會越來越膨脹,膨脹到你開始撓頭。我們也可以利用分類的特性,將類的實現分開在幾個不同的檔案裡面,好處顯而易見:
- 減少單個檔案的體積
- 把不同的功能分配到不同的分類裡,便於管理
- 可以按需載入想要的分類
通常,我會新增一些 AppDelegate
的分類,比如AppDelegate+Services
,專門用來初始化各種第三方等等。
Category 的結構
在Objective-C
中,所有的類和物件在runtime層都是用struct表示的,category也是如此,它的結構體為category_t
typedef struct category_t {
const char *name; // 類名
classref_t cls; // 類
struct method_list_t *instanceMethods; // 所有給類新增的例項方法的列表
struct method_list_t *classMethods; // 所有新增的類方法的列表
struct protocol_list_t *protocols; // 實現的所有協議的列表
struct property_list_t *instanceProperties; // 新增的所有屬性
} category_t;
複製程式碼
從結構體可以看出,分類能
- 給類新增例項方法 (instanceMethod)
- 給類新增類方法 (classMethod)
- 實現協議 (protocol)
- 新增屬性 (instancePropertie)
但是不能新增例項變數,即無法自動生成例項變數的setter和getter方法。當然,我們可以通過關聯物件來實現分類對例項變數的新增,請看後面章節~
Category的方法會“覆蓋”掉原來類的同名方法?
這個問題的分析,可以看下美圖技術團隊的這篇文章,很詳細。這裡只記錄一下總結:
-
Category的方法沒有“完全替換掉”原來類已經有的方法,也就是說如果Category和原來類都有methodA
-
Category的方法被放到了新方法列表的前面,而原來類的方法被放到了新方法列表的後面,這也就是我們平常所說的Category的方法會“覆蓋”掉原來類的同名方法,這是因為執行時在查詢方法的時候是順著方法列表的順序查詢的,它只要一找到對應名字的方法,很開心的返回了,不會在理會後面的同名方法。
-
同名方法的呼叫,是根據編譯順序決定的,對於“覆蓋”掉的方法,會先找到最後一個編譯的category裡的對應方法。可檢視專案的 Build Phases -> Compile Sources,位置越往後,越晚編譯。
類擴充套件(extension)
extension的別名有很多,擴充套件、延展、匿名分類。它就是類的一部分,在編譯期和標頭檔案裡的@interface
以及實現檔案裡的@implement
一起形成一個完整的類,它伴隨類的產生而產生,亦隨之一起消亡。在viewController.m
檔案中它長這個樣子:
@interface ViewController ()
@end
複製程式碼
是不是特別的熟悉~
沒錯,它看起來很像一個匿名的category。我們一般用來宣告私有方法,私有屬性和私有成員變數。
extension的應用很簡單, 我們基本天天都在用。需要注意的點是它和category的區別:
- extension 在編譯期決議, category在執行期決議。
關聯物件(Associated Object)
如上所述,category是無法為分類新增例項變數的,但是在實際開發中往往需要在分類中新增和物件關聯的值。這時我們可以利用runtime的特性,通過關聯物件來實現為分類新增例項變數的功能。
關聯物件是指某個物件通過一個唯一的key連線到一個類的例項上。
看下runtime提供給我們的3個API方法:
// 關聯物件,傳入 nil 則可以移除已有的關聯物件
void objc_setAssociatedObject(id object,const void *key,id value,objc_AssociationPolicy policy)
// 獲取關聯的物件
id objc_getAssociatedObject(id object,const void *key)
// 移除關聯的物件。這個方法會移除一個物件的所有關聯物件,將該物件恢復成“原始”狀態。這樣做就很有可能把別人新增的關聯物件也一併移除,所以我們通常用不上這個方法。一般的做法是通過給 objc_setAssociatedObject 方法傳入 nil 來移除某個已有的關聯物件。
void objc_removeAssociatedObjects(id object)
複製程式碼
引數說明:
- id object:被關聯的物件
- const void *key:關聯的key,要求唯一。一般用
selector
,使用getter
方法的名稱作為key
值。 - id value:關聯的物件
- objc_AssociationPolicy policy:記憶體管理的策略
objc_AssociationPolicy的列舉值和相關說明:
typedef OBJC_ENUM(uintptr_t,objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0,// 指定一個弱引用相關聯的物件
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,// 指定相關物件的強引用,非原子性
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,// 指定相關的物件被複制,非原子性
OBJC_ASSOCIATION_RETAIN = 01401,// 指定相關物件的強引用,原子性
OBJC_ASSOCIATION_COPY = 01403 // 指定相關的物件被複制,原子性
};
複製程式碼
在絕大多數情況下,我們都會使用 OBJC_ASSOCIATION_RETAIN_NONATOMIC
的關聯策略,這可以保證我們持有關聯物件。
看一下簡單的應用:
為ViewController
的分類 ViewController+AssociatedObjects
新增一個字串型別的成員變數。
@interface ViewController (AssociatedObjects)
@property (strong,nonatomic) NSString *associatedObject;
@end
@implementation ViewController (AssociatedObjects)
- (NSString *)associatedObject {
return objc_getAssociatedObject(self,_cmd);
}
- (void)setassociatedObject:(NSString *)associatedObject {
objc_setAssociatedObject(self,@selector(associatedObject),associatedObject,OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
複製程式碼
如果想了解一下關聯物件底層的實現原理,可以看下雷純峰大神的 Objective-C Associated Objects 的實現原理,這裡只記錄一些總結:
Q : 關聯物件被儲存在什麼地方,是不是存放在被關聯物件本身的記憶體中?
A : 關聯物件與被關聯物件本身的儲存並沒有直接的關係,它是儲存在單獨的雜湊表中的。所有的關聯物件都由AssociationsManager
管理,它是由一個靜態AssociationsHashMap
來儲存所有的關聯物件的。這相當於把所有物件的關聯物件都存在一個全域性map裡面。而map的的key是這個物件的指標地址(任意兩個不同物件的指標地址一定是不同的),而這個map的value又是另外一個AssociationsHashMap
,裡面儲存了關聯物件的key對。
Q : 關聯物件的生命週期是怎樣的,什麼時候被釋放,什麼時候被移除?
A : 關聯物件的釋放時機與移除時機並不總是一致。