引言
Objective-C是在C語言和匯編的基礎上加入了面向對象特性和消息轉發機制的動態語言,這意味著它不僅需要一個編譯器,還需要Runtime系統來動態創建類和對象,進行消息發送和轉發。Objc 從三種不同的層級上與 Runtime 系統進行交互,分別是通過 Objective-C 源代碼,通過 Foundation 框架的NSObject類定義的方法,通過對 runtime 函數的直接調用:
Objective-C源代碼
大部分情況下你就只管寫你的Objc代碼就行,runtime 系統自動在幕后辛勤勞作著。
還記得引言中舉的例子吧,消息的執行會使用到一些編譯器為實現動態語言特性而創建的數據結構和函數,Objc中的類、方法和協議等在 runtime 中都由一些數據結構來定義,這些內容在后面會講到。(比如objc_msgSend
函數及其參數列表中的id和SEL都是啥)NSObject的方法
Cocoa 中大多數類都繼承于NSObject類,也就自然繼承了它的方法。最特殊的例外是NSProxy
,它是個抽象超類,它實現了一些消息轉發有關的方法,可以通過繼承它來實現一個其他類的替身類或是虛擬出一個不存在的類,說白了就是領導把自己展現給大家風光無限,但是把活兒都交給幕后小弟去干。
有的NSObject中的方法起到了抽象接口的作用,比如description
方法需要你重載它并為你定義的類提供描述內容。NSObject還有些方法能在運行時獲得類的信息,并檢查一些特性,比如class返回對象的類;isKindOfClass:
和isMemberOfClass:
則檢查對象是否在指定的類繼承體系中;respondsToSelector:
檢查對象能否響應指定的消息;conformsToProtocol:
檢查對象是否實現了指定協議類的方法;methodForSelector:
則返回指定方法實現的地址。Runtime的函數
Runtime 系統是一個由一系列函數和數據結構組成,具有公共接口的動態共享庫。頭文件存放于/usr/include/objc
目錄下。許多函數允許你用純C代碼來重復實現 Objc 中同樣的功能。雖然有一些方法構成了NSObject類的基礎,但是你在寫 Objc 代碼時一般不會直接用到這些函數的,除非是寫一些 Objc 與其他語言的橋接或是底層的debug工作。在Objective-C Runtime Reference中有對 Runtime 函數的詳細文檔。
下面通過分析Apple開源的Runtime代碼來深入理解Objective-C的Runtime機制。
Runtime術語
在講這塊內容之前,我們看一下編譯器如何將OC代碼轉化為運行時的代碼,首先進入.m文件所在的文件路徑,通過終端命令編譯.m文件:clang -rewrite-objc xxx.m
可以看到編譯后的xxx.cpp(C++文件)。
比如我們創建了一個對象[[NSObject alloc] init]
#import "Maker.h"
@implementation Maker
- (void)dosome {
id objc = [[NSObject alloc] init];
NSLog(@"wshhsw");
}
int main(int argc, char * argv[]) {
@autoreleasepool {
id objc = [[NSObject alloc] init];
return 0;
}
}
@end
最終被轉換為幾萬行代碼,截取最關鍵的一句可以看到底層是通過runtime創建的對象
// - (void)dosome;
/* @end */
// @implementation Maker
static void _I_Maker_dosome(Maker * self, SEL _cmd) {
id objc = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_4k_hh7kg7252wl_5zvbpzsx4nfc0000gn_T_Maker_df949a_mi_0);
}
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
id objc = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
return 0;
}
}
// @end
刪除掉一些強制轉換語句,可以看到調用方法本質就是發消息,[[NSObject alloc] init]
語句發了兩次消息,第一次發了alloc 消息,第二次發送init 消息。當我們在使用objc_msgSend()
、 sel_registerName()
方法時需要導入頭文件<objc/message.h>
, 設置build settings中的 ENABLE_STRICT_OBJC_MSGSEND
為NO,如果你的項目是默認ARC模式的話請將相應的文件設置為-fno-objc-arc
,下面是我的代碼例子:
#import "SecondViewController.h"
#import <objc/message.h>
@interface SecondViewController ()
@end
@implementation SecondViewController
- (void)viewDidLoad {
[super viewDidLoad];
objc_msgSend(self , @selector(test));
}
- (void)test {
NSLog(@"wwwwsssshhhh");
}
上面介紹了編譯后和手動調用的情況,下面我們接著分析一下[receiver message]
內部的一些東西,其實我們在使用[receiver message]
語法并不會馬上執行receiver對象的message方法的代碼,而是向receiver發送一條message消息,如果接受者找到對應的selector,那么就相當于直接執行了這個對象的特定方法;否則,消息要么被轉發,或者臨時向接受者動態添加這個selector對應的實現內容;要么就干脆玩崩潰。其實[receiver message]
被編譯器轉化為:
id objc_msgSend ( id self, SEL op, ... );
下面以上面提到的兩個數據結構id
和SEL
來逐步分析和理解Runtime中哪些重要的Runtime術語:
SEL
查看到SEL數據結構如下:
typedef struct objc_selector *SEL;
其實它就是映射到方法的C字符串,你可以通過Objc編譯器命令@selector()
或者Runtime
系統的sel_registerName
函數來獲取一個SEL
類型的方法選擇器。
如果你知道selector
對應的方法名是什么,可以通過NSString* NSStringFromSelector(SEL aSelector)
方法將SEL
轉化為字符串,再用NSLog
打印。
id
接下來看objc_msgSend第一個參數的數據類型id,id是通用類型指針,能夠表示任何對象。按下面路徑打開objc.h文件
查看到id數據結構如下:
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
#endif
id其實就是一個指向objc_object結構體指針,它包含一個Class isa成員,根據isa指針就可以順藤摸瓜找到對象所屬的類。
注意:根據Apple的官方文檔Key-Value Observing Implementation Details提及,
key-value observing
是使用isa-swizzling
的技術實現的,isa
指針在運行時被修改,指向一個中間類而不是真正的類。所以,你不應該使用isa指針來確定類的關系,而是使用class
方法來確定實例對象的類。
Class
isa指針的數據類型是Class,Class表示對象所屬的類,按下面路徑打開objc.h文件
typedef struct objc_class *Class;
可以查看到Class其實就是一個objc_class結構體指針,但這個頭文件找不到它的定義,需要在runtime.h
才能找到objc_class
結構體的定義。
按下面路徑打開runtime.h
文件
查看到objc_class
結構體定義如下:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
注意:
OBJC2_UNAVAILABLE
是一個Apple對Objc系統運行版本進行約束的宏定義,主要為了兼容非Objective-C 2.0的遺留版本,但我們仍能從中獲取一些有用信息。
讓我們分析一些重要的成員變量表示什么意思和對應使用哪些數據結構。
-
isa表示一個Class對象的Class,也就是Meta Class。在面向對象設計中,一切都是對象,Class在設計中本身也是一個對象。
其實Meta Class
也是一個Class
,它也跟其他Class一樣有自己的isa
和super_class
指針,關系如下:
Class isa and superclass relationship from Google
上圖形象版的關系圖
有幾個關鍵點需要解釋以下: 當我們對一個實例發送消息時(-開頭的方法),會在該 instance 對應的類的 methodLists 里查找。
當我們對一個類發送消息時(+開頭的方法),會在該類的 MetaClass 的 methodLists 里查找。
每個 Class 都有一個 isa 指針指向一個唯一的 Meta Class
每一個 Meta Class 的 isa 指針都指向最上層的 Meta Class,即 NSObject 的 MetaClass,而最上層的 MetaClass 的 isa 指針又指向自己
super_class
表示實例對象對應的父類name
表示類名ivars
表示多個成員變量,它指向objc_ivar_list
結構體。在runtime.h
可以看到它的定義:
struct objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
}
objc_ivar_list
其實就是一個鏈表,存儲多個objc_ivar
,而objc_ivar
結構體存儲類的單個成員變量信息。
- methodLists表示方法列表,它指向
objc_method_list
結構體的二級指針,可以動態修改*methodLists的值來添加成員方法,也是Category實現原理,同樣也解釋Category不能添加實例變量的原因。在runtime.h
可以看到它的定義:
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
同理,objc_method_list
也是一個鏈表,存儲多個objc_method
,而objc_method
結構體存儲類的某個方法的信息。
-
cache
用來緩存經常訪問的方法,它指向objc_cache
結構體,后面會重點講到。 -
protocols
表示類遵循哪些協議
Method
Method表示類中的某個方法,在runtime.h文件中找到它的定義:
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
其實Method就是一個指向objc_method結構體指針,它存儲了方法名(method_name)、方法類型(method_types)和方法實現(method_imp)等信息。而method_imp的數據類型是IMP,它是一個函數指針,后面會重點提及。
舉個例子:
- (void)getMethodTest {
unsigned int count;
Method *methods = class_copyMethodList([ViewController class], &count);
//Method *methods = class_copyMethodList([ViewController class]->isa, &count); //這樣就可以獲取類方法了
for (int i = 0; i < count; i++) {
Method method = methods[i];
SEL selector = method_getName(method);
NSString *name = NSStringFromSelector(selector);
NSLog(@"方法名字 === %@",name);
}
free(methods);
}
Ivar
Ivar表示類中的實例變量,在runtime.h文件中找到它的定義:
/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar;
struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE;
char *ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}
Ivar其實就是一個指向objc_ivar結構體指針,它包含了變量名(ivar_name)、變量類型(ivar_type)等信息。
舉個例子:
- (void)getIvarsTest {
unsigned int count;
Ivar *ivars = class_copyIvarList([ViewController class], &count);
for (unsigned int i = 0; i < count; i++) {
Ivar ivar = ivars[i];
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
ivarName = [ivarName substringFromIndex:1];
//屬性類型
NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
NSLog(@"成員變量名字 === %@ 類型名字 %@",ivarName,ivarType);
}
free(ivars);
}
IMP
在上面講Method時就說過,IMP本質上就是一個函數指針,指向方法的實現,在objc.h找到它的定義:
/// A pointer to the function of a method implementation.
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id (*IMP)(id, SEL, ...);
#endif
當你向某個對象發送一條信息,可以由這個函數指針來指定方法的實現,它最終就會執行那段代碼,這樣可以繞開消息傳遞階段而去執行另一個方法實現。
Cache
顧名思義,Cache主要用來緩存,那它緩存什么呢?我們先在runtime.h文件看看它的定義:
typedef struct objc_cache *Cache OBJC2_UNAVAILABLE;
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
Cache其實就是一個存儲Method的鏈表,主要是為了優化方法調用的性能。當對象receiver調用方法message時,首先根據對象receiver的isa指針查找到它對應的類,然后在類的methodLists中搜索方法,如果沒有找到,就使用super_class指針到父類中的methodLists查找,一旦找到就調用方法。如果沒有找到,有可能消息轉發,也可能忽略它。但這樣查找方式效率太低,因為往往一個類大概只有20%的方法經常被調用,占總調用次數的80%。所以使用Cache來緩存經常調用的方法,當調用方法時,優先在Cache查找,如果沒有找到,再到methodLists查找。
Property
@property標記了類中的屬性,這個不必多說大家都很熟悉,它是一個指向objc_property結構體的指針:
/// An opaque type that represents an Objective-C declared property.
typedef struct objc_property *objc_property_t;
可以通過class_copyPropertyList
和 protocol_copyPropertyList
方法來獲取類和協議中的屬性:
OBJC_EXPORT objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount);
OBJC_EXPORT objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount);
返回類型為指向數組的指針,因為屬性列表是個數組,每個元素內容都是一個objc_property_t指針,而這兩個函數返回的值是指向這個數組的指針。
舉個例子:
- (void)getAllProperties {
u_int count;
//使用class_copyPropertyList及property_getName獲取類的屬性列表及每個屬性的名稱
objc_property_t *properties = class_copyPropertyList([ViewController class], &count);
for (int i = 0; i < count; i++) {
const char* propertyName = property_getName(properties[i]);
NSLog(@"屬性 === %@",[NSString stringWithUTF8String:propertyName]);
}
free(properties);
}
消息發送
前面從objc_msgSend
作為入口,逐步深入分析Runtime的數據結構,了解每個數據結構的作用和它們之間關系后,我們正式轉入消息發送這個正題。
objc_msgSend函數
在前面已經演示過,當某個對象使用語法[receiver message]
來調用某個方法時,其實[receiver message]
被編譯器轉化為:
id objc_msgSend ( id self, SEL op, ... );
現在讓我們看一下objc_msgSend
它具體是如何發送消息:
- 首先根據
receiver
對象的isa
指針獲取它對應的class
,如果找不到對應的,也不會Crash,會被忽略掉,因為ObjC 的特性是允許對一個 nil 對象執行任何一個方法的。 - 優先在
class
的cache
查找message
方法,如果找不到,再到methodLists
查找。 - 如果沒有在
class
找到,再到super_class
查找,一直找,直到找到NSObject類為止,一旦找到message
這個方法,就執行它實現的IMP
。 -
如果還找不到就要開始進入動態方法解析了,后面會提到。
Objc Message.jpeg
self與super
為了讓大家更好地理解self和super,借用sunnyxx博客的ios程序員6級考試一道題目:下面的代碼分別輸出什么?
@implementation Son : Father
- (id)init
{
self = [super init];
if (self)
{
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
self表示當前這個類的對象,而super是一個編譯器標示符,和self指向同一個消息接受者。在本例中,無論是[self class]
還是[super class]
,接受消息者都是Son對象,但super與self不同的是,self調用class方法時,是在子類Son中查找方法,而super調用class方法時,是在父類Father中查找方法。
當調用[self class]
方法時,會轉化為objc_msgSend
函數,這個函數定義如下:
id objc_msgSend(id self, SEL op, ...)
這時會從當前Son類的方法列表中查找,如果沒有,就到Father類查找,還是沒有,最后在NSObject類查找到。我們可以從NSObject.mm
文件中看到- (Class)class
的實現:
- (Class)class {
return object_getClass(self);
}
所以NSLog(@"%@", NSStringFromClass([self class]))
;會輸出Son
。
當調用[super class]
方法時,會轉化為objc_msgSendSuper
,這個函數定義如下:
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
objc_msgSendSuper
函數第一個參數super的數據類型是一個指向objc_super
的結構體,從message.h
文件中查看它的定義:
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained Class class;
#else
__unsafe_unretained Class super_class;
#endif
/* super_class is the first class to search */
};
#endif
結構體包含兩個成員,第一個是receiver
,表示某個類的實例。第二個是super_class
表示當前類的父類。
這時首先會構造出objc_super結構體,這個結構體第一個成員是self,第二個成員是(id)class_getSuperclass(objc_getClass("Son"))
,實際上該函數會輸出Father。然后在Father類查找class方法,查找不到,最后在NSObject查到。此時,內部使用objc_msgSend(objc_super->receiver, @selector(class))
去調用,與[self class]調用相同,所以結果還是Son。
方法中隱藏的參數self和_cmd
當[receiver message]
調用方法時,系統會在運行時偷偷地動態傳入兩個隱藏參數self
和_cmd
,之所以稱它們為隱藏參數,是因為在源代碼中沒有聲明和定義這兩個參數。它們是在代碼被編譯時被插入實現中的。盡管這些參數沒有被明確聲明,在源代碼中我們仍然可以引用它們。至于對于self
的描述,上面已經解釋非常清楚了,下面我們重點講解_cmd
。
_cmd
表示當前調用方法,其實它就是一個方法選擇器SEL。一般用于判斷方法名或在Associated Objects
中唯一標識鍵名,后面在Associated Objects
會講到。
動態方法解析和消息轉發
[receiver message]
調用方法時,如果在message方法在receiver對象的類繼承體系中沒有找到方法,那怎么辦?一般情況下,程序在運行時就會Crash掉,拋出 unrecognized selector sent to …
類似這樣的異常信息。但在拋出異常之前,還有三次機會按以下順序讓你拯救程序。
- Method Resolution
- Fast Forwarding
- Normal Forwarding
Method Resolution
首先Objective-C在運行時調用 + resolveInstanceMethod:
或 + resolveClassMethod:
方法,讓你添加方法的實現。如果你添加方法并返回YES,那系統在運行時就會重新啟動一次消息發送的過程。
現在舉個簡單的例子,定義一個類MethodForwardTest,它主要定義一個方法test,下面就是它的設計與實現:
@interface MethodForwardTest : NSObject
- (void)test;
@end
但是我在MethodForwardTest.m
沒有實現這個方法,此時我在別的地方調用這個累的方法,程序就會直接崩潰,我們在resolveInstanceMethod
來實現這個方法。
id dynamicMethod(id self, SEL _cmd) {
NSLog(@"首先嘗試動態添加方法");
return @"首先嘗試動態添加方法";
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selectorString = NSStringFromSelector(sel);
if ([selectorString isEqualToString:@"test"]) {
class_addMethod(self, sel, (IMP)dynamicMethod, "@@:");
}
return YES;
}
注意到上面代碼有這樣一個字符串"v@*,它表示方法的參數和返回值,詳情請參考Type Encodings
如果resolveInstanceMethod方法也沒有實現,運行時就跳轉到下一步:消息轉發(Message Forwarding)
Fast Forwarding
繼續上面MethodForwardTest類的例子,將test和resolveInstanceMethod方法注釋掉,然后添加forwardingTargetForSelector方法的實現:
- (id)forwardingTargetForSelector:(SEL)aSelector {
return [[Replacement alloc] init];
}
此時還缺一個轉發消息的類Replacement,這個類的設計與實現如下:
@interface Replacement : NSObject
- (void)test;
@end
@implementation Replacement
- (void)test {
NSLog(@"備用實現者提供了test方法的實現");
}
@end
這里叫Fast,是因為這一步不會創建NSInvocation對象,但Normal Forwarding會創建它,所以相對于更快點。
Normal Forwarding
如果沒有使用Fast Forwarding
來消息轉發,最后只有使用Normal Forwarding
來進行消息轉發。它首先調用methodSignatureForSelector:
方法來獲取函數的參數和返回值,如果返回為nil,程序會Crash掉,并拋出unrecognized selector sent to instance
異常信息。如果返回一個函數簽名,系統就會創建一個NSInvocation
對象并調用-forwardInvocation:
方法。
繼續前面的例子,將forwardingTargetForSelector方法注釋掉,添加methodSignatureForSelector和forwardInvocation方法的實現:
- (void)anotherTest {
NSLog(@"另一個test方法");
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature) { // 如果不能處理這個方法
if ([self respondsToSelector:@selector(anotherTest)]) {
// 返回另一個函數的方法簽名,這個函數不一定要定義在本類中
signature = [MethodForwardTest instanceMethodSignatureForSelector:@selector(anotherTest)];
}
}
return signature;
}
/**
* 這個函數中可以修改很多信息,比如可以替換選方法的處理者,替換選擇子,修改參數等等
*
* @param anInvocation 被轉發的選擇子
*/
- (void)forwardInvocation:(NSInvocation *)anInvocation {
[anInvocation setSelector:@selector(anotherTest)]; // 設置需要調用的選擇子
[anInvocation invokeWithTarget:self]; // 設置消息的接收者,不一定必須是self
}
三種方法的選擇
Runtime提供三種方式來將原來的方法實現代替掉,那該怎樣選擇它們呢?
- Method Resolution:由于Method Resolution不能像消息轉發那樣可以交給其他對象來處理,所以只適用于在原來的類中代替掉。
- Fast Forwarding:它可以將消息處理轉發給其他對象,使用范圍更廣,不只是限于原來的對象。
- Normal Forwarding:它跟Fast Forwarding一樣可以消息轉發,但它能通過NSInvocation對象獲取更多消息發送的信息,例如:target、selector、arguments和返回值等信息。
Associated Objects
當想使用Category對已存在的類進行擴展時,一般只能添加實例方法或類方法,而不適合添加額外的屬性。雖然可以在Category頭文件中聲明property屬性,但在實現文件中編譯器是無法synthesize任何實例變量和屬性訪問方法。這時需要自定義屬性訪問方法并且使用Associated Objects來給已存在的類Category添加自定義的屬性。Associated Objects提供三個API來向對象添加、獲取和刪除關聯值:
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 )
其中objc_AssociationPolicy是個枚舉類型,它可以指定Objc內存管理的引用計數機制。
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */
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. */
};
下面有個關于 UIButton+NMCategory Category
添加屬性 dragEnable
和 adsorbEnable
的示例代碼:
@interface UIButton (NMCategory)
@property(nonatomic,assign,getter = isDragEnable) BOOL dragEnable;
@property(nonatomic,assign,getter = isAdsorbEnable) BOOL adsorbEnable;
@end
static void *DragEnableKey = &DragEnableKey;
static void *AdsorbEnableKey = &AdsorbEnableKey;
@implementation UIButton (NMCategory)
- (void)setDragEnable:(BOOL)dragEnable {
objc_setAssociatedObject(self, DragEnableKey,@(dragEnable), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)isDragEnable {
return [objc_getAssociatedObject(self, DragEnableKey) boolValue];
}
- (void)setAdsorbEnable:(BOOL)adsorbEnable {
objc_setAssociatedObject(self, AdsorbEnableKey,@(adsorbEnable), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)isAdsorbEnable {
return [objc_getAssociatedObject(self, AdsorbEnableKey) boolValue];
}
@end
Method Swizzling
Method Swizzling就是在運行時將一個方法的實現代替為另一個方法的實現。如果能夠利用好這個技巧,可以寫出簡潔、有效且維護性更好的代碼。下面看一個例子:
- (void)testMethodSwizzling {
NSLog(@"未進行Method Swizzling之前:");
NSLog(@"小寫字符串:%@", [mixedString lowercaseString]);
NSLog(@"大寫字符串:%@", [mixedString myLowercaseString]);
[self startMethodSwizzling];
NSLog(@"進行Method Swizzling之后:");
NSLog(@"小寫字符串:%@", [mixedString lowercaseString]);
NSLog(@"大寫字符串:%@", [mixedString myLowercaseString]);
}
- (void)startMethodSwizzling {
Method originalLowercaseStringMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method newLowercaseStringMethod = class_getInstanceMethod([NSString class], @selector(myLowercaseString));
method_exchangeImplementations(originalLowercaseStringMethod, newLowercaseStringMethod);
}
應用案例
危險性
Method Swizzling就像一把瑞士小刀,如果使用得當,它會有效地解決問題。但使用不當,將帶來很多麻煩。在stackoverflow上有人已經提出這樣一個問題:What are the Dangers of Method Swizzling in Objective C?,它的危險性主要體現以下幾個方面:
- Method swizzling is not atomic (Method swizzling不是原子的)
- Changes behavior of un-owned code (更改代碼所有權的行為)
- Possible naming conflicts (可能的命名沖突)
- Swizzling changes the method's arguments (Swizzling改變方法的參數)
- The order of swizzles matters (swizzles的順序很重要)
- Difficult to understand (looks recursive) (很難理解)
- Difficult to debug (難以調試)
總結
我們之所以讓自己的類繼承NSObject不僅僅因為蘋果幫我們完成了復雜的內存分配問題,更是因為這使得我們能夠用上 Runtime 系統帶來的便利。可能我們平時寫代碼時可能很少會考慮一句簡單的[receiver
message]背后發生了什么,而只是當做方法或函數調用,但當你閱讀一些iOS開源項目時,你就會發現很多時候都會用到。所以深入理解 Runtime 系統的細節有利于我們更容易地閱讀和學習開源項目,最終寫出功能更強大的代碼。