號(hào)外 : 一些關(guān)于runtime的小demo在我的下一篇文章iOS-Runtime-實(shí)踐篇中
我們都知道Objective-C是一門(mén)動(dòng)態(tài)語(yǔ)言, 動(dòng)態(tài)之處體現(xiàn)在它將許多靜態(tài)語(yǔ)言編譯鏈接時(shí)要做的事通通放到運(yùn)行時(shí)去做, 這大大增加了我們編程的靈活性.
毫不過(guò)分地說(shuō), Runtime就是OC的靈魂.
接下來(lái)我就要撥開(kāi)OC最外層的外衣, 帶大家看看OC的真面目(C/C++).
目錄
- 類和對(duì)象
- 消息發(fā)送和轉(zhuǎn)發(fā)
- KVO原理
類和對(duì)象
@interface Person : NSObject {
NSString *_name;
int _age;
}
- (void)study;
+ (void)study;
@end
@implementation Person
- (void)study
{
NSLog(@"instance - study");
}
+ (void)study
{
NSLog(@"class - study");
}
@end
為了更好地說(shuō)明類在底層的表現(xiàn)形式是怎樣, 我們將上面代碼利用clang -rewrite-objc Person.m
指令將其用C/C++重寫(xiě), 一窺究竟.
把不必要的刪除, 整理后為下面
struct _class_t {
struct _class_t *isa; // isa指針
struct _class_t *superclass; // 父類
void *cache;
void *vtable;
struct _class_ro_t *ro; // class的其他信息
};
// class包含的信息
struct _class_ro_t {
unsigned int flags;
unsigned int instanceStart;
unsigned int instanceSize;
unsigned int reserved;
const unsigned char *ivarLayout;
const char *name; // 類名
const struct _method_list_t *baseMethods; // 方法列表
const struct _objc_protocol_list *baseProtocols; // 協(xié)議列表
const struct _ivar_list_t *ivars; // ivar列表
const unsigned char *weakIvarLayout;
const struct _prop_list_t *properties; // 屬性列表
};
// Person(class)
struct _class_t OBJC_CLASS_$_Person = {
.isa = &OBJC_METACLASS_$_Person, // 指向Person-metaclass
.superclass = &OBJC_CLASS_$_NSObject, // 指向NSObject-class
.cache = &_objc_empty_cache,
0, // unused, was (void *)&_objc_empty_vtable,
&_OBJC_CLASS_RO_$_Person, // 包含了實(shí)例方法, ivar信息等
};
// Person(metaclass)
struct _class_t OBJC_METACLASS_$_Person = {
.isa = &OBJC_METACLASS_$_NSObject, // 指向NSObject-metaclass
.superclass = &OBJC_METACLASS_$_NSObject, // 指向NSObject-metaclass
.cache = &_objc_empty_cache,
0, // unused, was (void *)&_objc_empty_vtable,
&_OBJC_METACLASS_RO_$_Person, // 包含了類方法
};
原來(lái)(顯然), 我們的類其實(shí)就是一個(gè)結(jié)構(gòu)體!!! 類跟我們的對(duì)象一樣, 都有一個(gè)isa指針, 所以類其實(shí)也是對(duì)象的一種.
isa指針
isa指針?lè)浅V匾? 對(duì)象需要通過(guò)isa指針找到它的類, 類需要通過(guò)isa找到它的元類. 這在調(diào)用實(shí)例方法和類方法的時(shí)候起到重要的作用.
實(shí)例對(duì)象在調(diào)用方法時(shí), 首先通過(guò)isa指針找到它所屬的類, 然后在類的緩存(cache)里找該方法的IMP, 如果沒(méi)有, 則去類的方法列表中查找, 然后找到則調(diào)用該方法, 找不到則報(bào)錯(cuò).
類對(duì)象調(diào)用方法則如出一轍, 通過(guò)isa指針找到元類, 然后就跟上述一致了. 這里涉及的發(fā)送消息機(jī)制下面會(huì)詳細(xì)講..
下面展示一些運(yùn)行時(shí)動(dòng)態(tài)獲取對(duì)象和類的屬性的C語(yǔ)言方法
類和類名 :
// 返回對(duì)象的類
Class object_getClass ( id obj );
// 設(shè)置對(duì)象的類
Class object_setClass ( id obj, Class cls );
// 獲取類的父類
Class class_getSuperclass ( Class cls );
// 創(chuàng)建一個(gè)新類和元類
Class objc_allocateClassPair ( Class superclass, const char *name, size_t extraBytes );
// 在應(yīng)用中注冊(cè)由objc_allocateClassPair創(chuàng)建的類
void objc_registerClassPair ( Class cls );
// 銷(xiāo)毀一個(gè)類及其相關(guān)聯(lián)的類
void objc_disposeClassPair ( Class cls );
// 獲取類的類名
const char * class_getName ( Class cls );
// 返回給定對(duì)象的類名
const char * object_getClassName ( id obj );
ivar和屬性 :
// 添加成員變量
BOOL class_addIvar ( Class cls, const char *name, size_t size, uint8_t alignment, const char *types );
// 添加屬性
BOOL class_addProperty ( Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount );
// 返回類的某一ivar
Ivar class_getInstanceVariable(__unsafe_unretained Class cls, const char *name)
// 返回對(duì)象中實(shí)例變量的值
id object_getIvar ( id obj, Ivar ivar );
// 設(shè)置對(duì)象中實(shí)例變量的值
void object_setIvar ( id obj, Ivar ivar, id value );
// 獲取整個(gè)成員變量列表
Ivar * class_copyIvarList ( Class cls, unsigned int *outCount );
// 獲取屬性列表
objc_property_t * class_copyPropertyList ( Class cls, unsigned int *outCount );
方法 :
// 添加方法
BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types );
// 獲取實(shí)例方法
Method class_getInstanceMethod ( Class cls, SEL name );
// 獲取類方法
Method class_getClassMethod ( Class cls, SEL name );
// 獲取所有方法的數(shù)組
Method * class_copyMethodList ( Class cls, unsigned int *outCount );
// 替代方法的實(shí)現(xiàn)
IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types );
// 交換兩個(gè)方法的實(shí)現(xiàn)(Method Swizzling)
void method_exchangeImplementations(Method m1, Method m2);
這里說(shuō)個(gè)注意點(diǎn) : addIvar
并不能為一個(gè)已經(jīng)存在的類添加成員變量, 只能為那些運(yùn)行時(shí)動(dòng)態(tài)添加的類, 并且只能在objc_allocateClassPair
與objc_registerClassPair
這兩個(gè)方法之間才能添加Ivar.
消息發(fā)送和轉(zhuǎn)發(fā)機(jī)制
在OC中, 如果向某對(duì)象發(fā)送消息, 那就會(huì)使用動(dòng)態(tài)綁定機(jī)制來(lái)決定需要調(diào)用的方法. OC的方法在底層都是普通的C語(yǔ)言函數(shù), 所以對(duì)象收到消息后究竟要調(diào)用什么函數(shù)完全由運(yùn)行時(shí)決定, 甚至可以在運(yùn)行時(shí)改變執(zhí)行的方法.
[person read:book];
會(huì)被編譯成
objc_msgSend(person, @selector(read:), book);
objc_msgSend的具體流程如下
1. 通過(guò)isa指針找到所屬類
2. 查找類的cache列表, 如果沒(méi)有則下一步
3. 查找類的"方法列表"
4. 如果能找到與選擇子名稱相符的方法, 就跳至其實(shí)現(xiàn)代碼
5. 找不到, 就沿著繼承體系繼續(xù)向上查找
6. 如果能找到與選擇子名稱相符的方法, 就跳至其實(shí)現(xiàn)代碼
7. 找不到, 執(zhí)行"消息轉(zhuǎn)發(fā)".
消息轉(zhuǎn)發(fā)
上面我們提到, 如果到最后都找不到, 就會(huì)來(lái)到消息轉(zhuǎn)發(fā)
- 動(dòng)態(tài)方法解析 : 先問(wèn)接收者所屬的類, 你看能不能動(dòng)態(tài)添加個(gè)方法來(lái)處理這個(gè)"未知的選擇子"? 如果能, 則消息轉(zhuǎn)發(fā)結(jié)束.
- 備胎(后備接收者) : 請(qǐng)接收者看看有沒(méi)有其他對(duì)象能處理這條消息? 如果有, 則把消息轉(zhuǎn)給那個(gè)對(duì)象, 消息轉(zhuǎn)發(fā)結(jié)束.
- 消息簽名 : 這里會(huì)要求你返回一個(gè)消息簽名, 如果返回nil, 則消息轉(zhuǎn)發(fā)結(jié)束.
- 完整的消息轉(zhuǎn)發(fā) : 備胎都搞不定了, 那就只能把該消息相關(guān)的所有細(xì)節(jié)都封裝到一個(gè)NSInvocation對(duì)象, 再問(wèn)接收者一次, 快想辦法把這個(gè)搞定了. 到了這個(gè)地步如果還無(wú)法處理, 消息轉(zhuǎn)發(fā)機(jī)制也無(wú)能為力了.
動(dòng)態(tài)方法解析 :
對(duì)象在收到無(wú)法解讀的消息后, 首先調(diào)用其所屬類的這個(gè)類方法 :
+ (BOOL)resolveInstanceMethod:(SEL)selector
// selector : 那個(gè)未知的選擇子
// 返回YES則結(jié)束消息轉(zhuǎn)發(fā)
// 返回NO則進(jìn)入備胎
假如尚未實(shí)現(xiàn)的方法不是實(shí)例方法而是類方法, 則會(huì)調(diào)用另一個(gè)方法resolveClassMethod:
備胎 :
動(dòng)態(tài)方法解析失敗, 則調(diào)用這個(gè)方法
- (id)forwardingTargetForSelector:(SEL)selector
// selector : 那個(gè)未知的選擇子
// 返回一個(gè)能響應(yīng)該未知選擇子的備胎對(duì)象
通過(guò)備胎這個(gè)方法, 可以用"組合"來(lái)模擬出"多重繼承".
消息簽名 :
備胎搞不定, 這個(gè)方法就準(zhǔn)備要被包裝成一個(gè)NSInvocation對(duì)象, 在這里要先返回一個(gè)方法簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
// NSMethodSignature : 該selector對(duì)應(yīng)的方法簽名
完整的消息轉(zhuǎn)發(fā) :
給接收者最后一次機(jī)會(huì)把這個(gè)方法處理了, 搞不定就直接程序崩潰!
- (void)forwardInvocation:(NSInvocation *)invocation
// invocation : 封裝了與那條尚未處理的消息相關(guān)的所有細(xì)節(jié)的對(duì)象
在這里能做的比較現(xiàn)實(shí)的事就是 : 在觸發(fā)消息前, 先以某種方式改變消息內(nèi)容, 比如追加另外一個(gè)參數(shù), 或是改變選擇子等等. 實(shí)現(xiàn)此方法時(shí), 如果發(fā)現(xiàn)某調(diào)用操作不應(yīng)該由本類處理, 可以調(diào)用超類的同名方法. 則繼承體系中的每個(gè)類都有機(jī)會(huì)處理該請(qǐng)求, 直到NSObject. 如果NSObject搞不定, 則還會(huì)調(diào)用doesNotRecognizeSelector:來(lái)拋出異常, 此時(shí)你就會(huì)在控制臺(tái)看到那熟悉的unrecognized selector sent to instance..
上面這4個(gè)方法均是模板方法氓润,開(kāi)發(fā)者可以override,由runtime來(lái)調(diào)用。最常見(jiàn)的實(shí)現(xiàn)消息轉(zhuǎn)發(fā)周偎,就是重寫(xiě)方法3和4埋涧,忽略這個(gè)消息或者代理給其他對(duì)象.
Method Swizzling
被稱為黑魔法的一個(gè)方法, 可以把兩個(gè)方法的實(shí)現(xiàn)互換.
如上文所述, 類的方法列表會(huì)把選擇子的名稱映射到相關(guān)的方法實(shí)現(xiàn)上, 使得"動(dòng)態(tài)消息派發(fā)系統(tǒng)"能夠據(jù)此找到應(yīng)該調(diào)用的方法. 這些方法均以函數(shù)指針的形式來(lái)表示, 這種指針叫做IMP,
<pre> id (*IMP)(id, SEL, ...)</pre>
OC運(yùn)行時(shí)系統(tǒng)提供了幾個(gè)方法能夠用來(lái)操作這張表, 動(dòng)態(tài)增加, 刪除, 改變選擇子對(duì)應(yīng)的方法實(shí)現(xiàn), 甚至交換兩個(gè)選擇子所映射到的指針. 如,
如何交換兩個(gè)已經(jīng)寫(xiě)好的方法實(shí)現(xiàn)?
// 取得方法
Method class_getInstanceMethod(Class aClass, SEL aSelector)
// 交換實(shí)現(xiàn)
void method_exchangeImplementations(Method m1, Method m2)
通過(guò)Method Swizzling可以為一些完全不知道其具體實(shí)現(xiàn)的黑盒方法增加日志記錄功能, 利于我們調(diào)試程序. 并且我們可以將某些系統(tǒng)類的具體實(shí)現(xiàn)換成我們自己寫(xiě)的方法, 以達(dá)到某些目的. (例如, 修改主題, 修改字體等等)
KVO原理
KVO的實(shí)現(xiàn)也依賴Runtime. Apple文檔曾簡(jiǎn)單提到過(guò)KVO的實(shí)現(xiàn)原理 :
Automatic key-value observing is implemented using a technique called isa-swizzling... When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class ...
Apple的文檔提得不多, 但是大神Mike Ash在很早很早以前就已經(jīng)做過(guò)研究, 摘下了KVO神秘的面紗了, 有興趣的可以去查下, 這里不多深究, 只是簡(jiǎn)單闡述下原理.
原來(lái)當(dāng)你對(duì)一個(gè)對(duì)象進(jìn)行觀察時(shí), 系統(tǒng)會(huì)自動(dòng)新建一個(gè)類繼承自原類, 然后重寫(xiě)被觀察屬性的setter
方法. 然后重寫(xiě)的setter
方法會(huì)負(fù)責(zé)在調(diào)用原setter
方法前后通知觀察者. 然后把原對(duì)象的isa指針指向這個(gè)新類, 我們知道, 對(duì)象是通過(guò)isa指針去查找自己是屬于哪個(gè)類, 并去所在類的方法列表中查找方法的, 所以這個(gè)時(shí)候這個(gè)對(duì)象就自然地變成了新類的實(shí)例對(duì)象.
不僅如此, Apple還重寫(xiě)了原類的- class
方法, 視圖欺騙我們, 這個(gè)類沒(méi)有變, 還是原來(lái)的那個(gè)類. 只要我們懂得Runtime的原理, 這一切都只是掩耳盜鈴罷了.
后記
這只是我的Runtime文章的第一篇, 之后還會(huì)有Runtime實(shí)踐篇以及利用Runtime解決實(shí)際問(wèn)題的幾個(gè)demo, 感興趣的話還請(qǐng)大家關(guān)注關(guān)注_