想深入理解Objective-C這門動態(tài)語言就不得不深入理解下它的“動態(tài)”是如何實現(xiàn)的。早先拜讀過《Effective Objective-C 2.0》就讓我更深入的窺探到OC運行時特別之處,本文當(dāng)中也有部分內(nèi)容借鑒自這本經(jīng)典著作键耕。第四屆互聯(lián)網(wǎng)大會的項目也完成了,年底閑來無事整理寫些總結(jié)纺阔。
動態(tài)語言是相對于靜態(tài)語言如C語言區(qū)別而言的玻靡。C語言在編譯期就能決定了運行時應(yīng)該調(diào)用的函數(shù)绣否,函數(shù)地址實際上是硬編碼在指令之中的。而OC在編譯期甚至不知道對象的類型奴紧,需要在運行時處理唐含,當(dāng)然它的底層也都是轉(zhuǎn)化為C函數(shù)調(diào)用。運行時實際上決定了OC最終的編程實現(xiàn)沫浆,即什么類的對象執(zhí)行什么函數(shù)捷枯,而且這個執(zhí)行調(diào)用是可以修改的,這也是運行時吸引人的地方专执。
運行時的調(diào)用有3種方式
* 第一種是系統(tǒng)底層封裝實現(xiàn)的淮捆,所有OC的代碼就會調(diào)用,那就是消息傳遞
機(jī)制本股。
id value = [someObj methodName:parameter];
// 編譯期OC轉(zhuǎn)化為標(biāo)準(zhǔn)C函數(shù)
id value = objc_msgSend(someObj,@selector(methodName:),parameter);
objc_msgSend
是消息傳遞
機(jī)制中的核心函數(shù)(實際上是四種objc_msgSend
攀痊, objc_msgSend_stret
, objc_msgSendSuper
拄显, objc_msgSendSuper_stret
苟径,其他三種在處理一些“邊界情況”的時候會用到,可查閱《Effective Objective-C 2.0》第45頁躬审,這篇文章也有提及 )棘街,它會根據(jù)對象即someObj
和它的方法名來調(diào)用合適的方法完成完整的函數(shù)調(diào)用實現(xiàn)蟆盐。在查詢方法名時,它會首先在someObj
的“方法列表”中查找蹬碧,找不到就沿著它的繼承體系向上找舱禽,如果都沒有那就會看到調(diào)試時控制臺提示的錯誤包含一句[__ClassName methodName] unrecognized selector sent to instance xxxx
,someObj
所屬的__ClassName
類找不到methodName
這個對象方法恩沽,否則就可以正常運行了誊稚。如此看來,方法調(diào)用似乎每次都需要查表效率很低罗心,其實不然里伯,objc_msgSend
會將匹配結(jié)果緩存到“快速映射表”(fast map)里,每個類都有這樣一塊緩存渤闷,下次再調(diào)用方法就直接可在映射表里找了疾瓮。
- 第二種是NSObjec這個基類特有的幾個調(diào)用方法,能做類型判斷或者查看是否有響應(yīng)函數(shù)的這些方法都是運行時機(jī)制的方法飒箭。
-class方法返回對象的類狼电;
-isKindOfClass: 和 -isMemberOfClass: 方法檢查對象是否存在于指定的類的繼承體系中(是否是其子類或者父類或者當(dāng)前類的成員變量);
-respondsToSelector: 檢查對象能否響應(yīng)指定的消息弦蹂;
-conformsToProtocol:檢查對象是否實現(xiàn)了指定協(xié)議類的方法肩碟;
-methodForSelector: 返回指定方法實現(xiàn)的地址。
- 第三種就是直接調(diào)用Runtime函數(shù)庫了凸椿,稍后在實際應(yīng)用中會介紹到削祈。
runtime可以做什么
- 動態(tài)方法添加
如上所述,在開發(fā)中偶爾會有在消息轉(zhuǎn)發(fā)過程中找不到調(diào)用方法而導(dǎo)致程序閃退脑漫,為了用戶體驗髓抑,閃退是不能允許的,所以我們需要利用運行時來杜絕因這個問題而導(dǎo)致的閃退优幸,而轉(zhuǎn)化為彈出其他報錯提示吨拍,并把日志記錄到后臺中方便我們做進(jìn)一步的程序完善。
[__ClassName methodName] unrecognized selector sent to instance xxxx
這段異常信息是由NSObject
的doesNotRecognizeSelector:
方法所拋出的网杆。但并不是攔截這個方法做處理防止閃退密末,因為這個方法只是幫助打印提示信息的。
消息轉(zhuǎn)發(fā)分為兩個階段跛璧,第一階段是沿著繼承體系查找是否能動態(tài)添加方法,以處理當(dāng)前這個未知的方法新啼,叫“動態(tài)方法解析”追城,第二階段涉及“完整的消息轉(zhuǎn)發(fā)機(jī)制”,如果第一階段運行完燥撞,那方法接收者(如上邊例子中的someObj
)就無法再動態(tài)添加方法來響應(yīng)這個找不到的方法了座柱。此時運行時系統(tǒng)會請求接收者用其他手段來處理與消息有關(guān)的方法調(diào)用迷帜,這里又細(xì)分為2小步。首先請接收者看看有沒有其他對象能處理這條消息色洞,如果有戏锹,那么一切如常。若沒有火诸,則會啟動完整的消息轉(zhuǎn)發(fā)機(jī)制锦针,運行時系統(tǒng)會把與消息有關(guān)的全部細(xì)節(jié)都封裝到NSInvocation對象中(NSInvocation的使用),再給接收者最后一次機(jī)會置蜀,讓它來設(shè)法解決這條消息奈搜。
動態(tài)方法解析:
在對象收到無法解讀的消息后,首先將調(diào)用其所屬類的下列類方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel
該方法的參數(shù)就是那個未知的方法盯荤,其返回值Boolean
類型馋吗,表示這個類是否能新增一個實例方法類處理這個方法。如果未知的方法不是對象方法而是類方法秋秤,那么調(diào)用的就是+ (BOOL)resolveClassMethod:(SEL)sel
這個方法了宏粤。
例如:
someObj
調(diào)用了未實現(xiàn)的實例方法callMethod
,此時我們可以通過重載+ (BOOL)resolveInstanceMethod:(SEL)sel
來處理這個未知方法。
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == NSSelectorFromString(@"callMethod")) {
/*
* IMP 是編譯期生成的函數(shù)指針
* class_addMethod 函數(shù)完成向特定類添加特定方法實現(xiàn)的操作
*/
class_addMethod(self,sel,(IMP)callMethodTest,"chart");
return YES;
}
return [super resolveClassMethod:sel];
}
void callMethodTest (id self ,SEL _cmd){
NSLog(@"---callMethodTest----");
}
這種處理方式也常用來處理@dynamic
修飾的屬性灼卢,因為使用@dynamic
就是告訴編譯器绍哎,不要自動創(chuàng)建實現(xiàn)屬性所用的實例變量,也不要為其創(chuàng)建存取方法芥玉,我們會為這個屬性動態(tài)提供存取方法蛇摸。
注意:我們并不能重載+ (BOOL)resolveInstanceMethod:(SEL)sel
使返回值直接為YES
,這樣會讓我們不知道哪里出了問題,因為我們不能通過SEL
來獲取方法信息灿巧。
- 2.動態(tài)添加屬性和判斷屬性類型
動態(tài)添加屬性:
一般來說分類(category)中是不支持添加屬性的赶袄,但有時候確實需要添加,那么就可以通過 objc/runtime.h
庫中的一些函數(shù)來實現(xiàn)抠藕。在AFNetworking
饿肺、Masonry
、SDWebImage
等常用框架中都大量用到了這種方式盾似。
栗子:
#import <Foundation/Foundation.h>
@interface NSObject (ExchangeMethod)
@property (strong, nonatomic) NSString *name;
@end
#import "NSObject+ExchangeMethod.h"
#import <objc/runtime.h>
#define NameKey @"nameKey"
@implementation NSObject (ExchangeMethod)
- (void)setName:(NSString *)name{
// 將屬性同對象關(guān)聯(lián)
objc_setAssociatedObject(self, NameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name{
// 取出 對應(yīng)Key關(guān)聯(lián)的對象屬性
return objc_getAssociatedObject(self, NameKey);
}
@end
屬性類型判斷:
類型判斷常見的使用場景就是數(shù)據(jù)解析--字典轉(zhuǎn)模型敬辣。
獲取屬性列表的方式有兩種:
// 第一種
unsigned int count;
objc_property_t *properties = class_copyPropertyList(self.class, &count);
NSMutableArray *array = [NSMutableArray array];
for (int i =0; i< count ; i++) {
objc_property_t pro = properties[I];
const char *name = property_getName(pro);
const char *attributes = property_getAttributes(pro);
NSString *property = [[NSString alloc] initWithUTF8String:name];
[array addObject:property];
NSLog(@"attributes : %s, name: %s",attributes,name);
}
// 第二種
unsigned int count;
/*
*參數(shù)1:類名
*參數(shù)2:傳入無符號整型的內(nèi)存地址,當(dāng)讀取到成員變量的數(shù)量時零院,會給這個值賦值
*返回值:Ivar * :是一個指針類型溉跃,相當(dāng)于數(shù)組,里邊裝著Ivar
*/
Ivar *ivars = class_copyIvarList([UIView class],&count);
for (int i=0; i < count; i++) {
Ivar ivar = ivars[I];
// 獲取屬性名字告抄,調(diào)用函數(shù)ivar_getName(ivar)獲取
NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 獲取屬性類型撰茎,調(diào)用函數(shù)ivar_getTypeEncoding(ivar)獲取
NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
NSLog(@"type : %@, name: %@",type,name);
}
/// An opaque type that represents an instance variable.
/*
Ivar 是表示成員變量的類型
*/
typedef struct objc_ivar *![Ivar.png](http://upload-images.jianshu.io/upload_images/308319-1ad920412e90db1d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
;
/// An opaque type that represents an Objective-C declared property.
/*
objc_property_t 是表示一個Objective-C聲明的屬性
*/
typedef struct objc_property * objc_property_t;
兩者都可以獲取屬性名稱和類型,信息詳細(xì)程度不一樣打洼。
objc_property_t
打印的屬性的特性字符串說明龄糊,通過property_getAttributes(objc_property_t _Nonnull property)
獲取查看
//特性
typedef struct {
const char *name; //特性名稱
const char *value; //特性的值
} objc_property_attribute_t;
特性編碼 具體含義
R readonly
C copy
& retain
N nonatomic
G(name) getter=(name)
S(name) setter=(name)
D @dynamic
W weak
P 用于垃圾回收機(jī)制
詳細(xì)參見
一般獲取屬性信息用第一種逆粹,YYModel和MJExtension 框架中都有用到。
- 3.方法交換
OC對象在收到消息后炫惩,究竟調(diào)用哪種方法是在運行時才能解析決定的僻弹。而在運行時我們還可以新增、修改或者交換執(zhí)行方法他嚷,也叫“方法調(diào)配”即method swizzling
蹋绽。
類的“方法列表”中會把方法名映射到相關(guān)的方法實現(xiàn)上,通過“動態(tài)消息派發(fā)系統(tǒng)”找到對應(yīng)的調(diào)用方法爸舒。這些方法均已函數(shù)指針的形式來表示蟋字,即IMP
。比如:someObj
對象可以響應(yīng)makeName
扭勉、makeHeight
鹊奖、makeSex
等方法,這張表中的每個方法都映射到不同的IMP上涂炎,如下圖忠聚。
OC在運行時系統(tǒng)提供的幾個API能用來操作這張表。
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
可以用來做方法交換唱捣。
+(void)load{
Method m1 = class_getInstanceMethod(self, NSSelectorFromString(@"makeName"));
Method m2 = class_getInstanceMethod(self, @selector(testMakeName));
method_exchangeImplementations(m1, m2);
}
不過两蟀,在實際開發(fā)中,直接交換方法的意義并不大震缭,每一個方法都應(yīng)該對應(yīng)自己的實現(xiàn)赂毯。但是,為既有方法添加新功能是比較實用的拣宰。
栗子:
NSString
的獲取小寫字符串方法lowercaseString
党涕,我們要打印信息,那我們可以這樣寫
+(void)load{
Method m1 = class_getInstanceMethod(self, NSSelectorFromString(@"lowercaseString"));
Method m2 = class_getInstanceMethod(self, @selector(mcLowercaseString));
method_exchangeImplementations(m1, m2);
}
- (NSString *)mcLowercaseString{
NSString *lowercase = [self mcLowercaseString];
NSLog(@"%@ => %@",self,lowercase);
return lowercase;
}
這段代碼看似會死循環(huán)巡社,其實不然膛堤,因為兩個方法名指向了對方的函數(shù)指針I(yè)MP,所以[self mcLowercaseString];
實際上是調(diào)用的lowercaseString
晌该。通過這種方式肥荔,我們可以為那些系統(tǒng)黑盒方法增加日志打印功能,非常有助于調(diào)試使用朝群。一般很少有人用這個特性永久修改某各類的功能燕耿,而且若濫用的話,反而會讓代碼不易讀難于維護(hù)姜胖。
關(guān)于分類category美團(tuán)技術(shù)博客也有一篇點擊查看缸棵。
喜歡就點個贊唄!
歡迎大家提出更好的改進(jìn)意見和建議,從搬磚到設(shè)計建筑的路上,你我同行堵第!