第一部分:基礎(chǔ)篇
什么是運(yùn)行時(shí)?
編程語言有靜態(tài)和動(dòng)態(tài)之分。
靜態(tài)語言:如 Java、C,在編譯階段就確定了成員變量、函數(shù)的內(nèi)存地址。
動(dòng)態(tài)語言:如OC,在運(yùn)行期間才確定成員變量和函數(shù)地址,即使沒有實(shí)現(xiàn)部分同樣能通過編譯。
動(dòng)態(tài)語言具有比較高的靈活性,但是正因?yàn)槿绱?,?dòng)態(tài)語言即使在編譯通過之后,依然會(huì)發(fā)生錯(cuò)誤,程序有著當(dāng)對的不確定性。Objective-C 就是一種動(dòng)態(tài)語言,它為我們提供了 runtime 機(jī)制,是一套純c語言的 api,OC 中代碼最終都會(huì)便編譯器轉(zhuǎn)換成運(yùn)行時(shí)的代碼,通過消息機(jī)制決定使用那個(gè)函數(shù)。
消息機(jī)制
OC 代碼最終都會(huì)轉(zhuǎn)換成運(yùn)行時(shí)的代碼。OC 的方法調(diào)用一般為 [obj method] ,或者是其他衍生的調(diào)用方式,每次方法調(diào)用都是一個(gè)通過運(yùn)行時(shí)發(fā)送消息的過程。[obj method] 在編譯時(shí)期會(huì)被編譯為 objc-msgSend(obj, method, org1, org2, ...)。在運(yùn)行時(shí)期會(huì)變得相對復(fù)雜,如果,obj 能都找到對應(yīng)的 method,則直接執(zhí)行,如果找不到對應(yīng)的 method,消息被轉(zhuǎn)發(fā),或者指定其他接收者完成處理,否則會(huì)拋出異常發(fā)生崩潰。
OC 中最初的根類 NSObject 中的很多的方法就體現(xiàn)出了動(dòng)態(tài)性,例如 -isKindOfClass: 檢測對象是否屬于某一類型、-respondsToSelector: 檢測能否響應(yīng)指定方法、-conformsToProtocol: 檢測是否遵循指定協(xié)議等等,這些類似方法最終都通過 Runtime 進(jìn)行轉(zhuǎn)換實(shí)現(xiàn)。例如拿對象的初始化來講,使用運(yùn)行時(shí)同樣能夠?qū)崿F(xiàn)類似的效果。
Class class = [UIView class];
// 等同于
Class viewClass = objc_getClass("UIView");
UIView *view = [UIView alloc];
// 等同于
UIView *view = ((id (*)(id, SEL))(void *)objc_msgSend)((id)viewClass, sel_registerName("alloc"));
Runtime 數(shù)據(jù)結(jié)構(gòu)
接下來主要來看下 Class 在 Runtime 的結(jié)構(gòu),文件地址 objc/runtime.h 。
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
參數(shù)解析:
- isa 指針
objc_object 結(jié)構(gòu)中同樣也有一個(gè) isa 指針,指向該對象的定義,而類本身也可以看成一個(gè)對象,稱之為類對象,也就是元類 meta,元類的 isa 指針指向最終的 Root class 類,Root class 的 isa 指向自己。
舉個(gè)例子來說,Person 的實(shí)例就是圖中的 Instance of Subclass , 它的 isa 指針指向 Person 定義,而 Person 中的 isa 指針指向 Person.Class 元類。
- super_class
super_class 指向 objc_class 類所繼承的類,如上圖所示,如果當(dāng)前類已經(jīng)是最頂層的類,則 super_class 值為 nil。
- ivars
ivars 是類所存放成員變量的,它也是一個(gè)結(jié)構(gòu)體。
- methodLists
和 ivars 類似,methodLists 則是存放方法的地方,例如成員變量的存取方法。
- cache
存放了 method 響應(yīng)的記錄,下次消息過來是,優(yōu)先會(huì)在 cache 中查找,效率會(huì)提高。
- protocols
存放類所有的協(xié)議信息。
其他更多的運(yùn)行時(shí)類型,如 objc_method 表示 方法,objc_ivar 表示 成員變量,等等。
除了這下定義,該文件中還有非常多的操作 objc_class 的函數(shù),可以自行查看,后面的示例中也會(huì)介紹到相關(guān)函數(shù)的使用。
進(jìn)一步認(rèn)識(shí)消息機(jī)制
前面介紹到運(yùn)行時(shí)的消息發(fā)送機(jī)制相對復(fù)雜,可能順利執(zhí)行也可能拋出異常,這一節(jié)中我們來詳細(xì)了解一下完整的消息發(fā)送與轉(zhuǎn)發(fā)機(jī)制。
OC 對象調(diào)用方法會(huì)被編譯為下面的形式。
返回值 objc_msgSend(接收者, 方法, 參數(shù)1, 參數(shù)2, ...)
id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
消息發(fā)送會(huì)經(jīng)過以下幾個(gè)步驟:
- 檢測
SEL和target是否為空,有一個(gè)為空時(shí),會(huì)被忽略 - 查找類的
IMP實(shí)現(xiàn),檢測cache,找到則執(zhí)行對應(yīng)函數(shù) -
cache找不到,查找methodLists,找到則執(zhí)行對應(yīng)函數(shù),并添加至cache -
methodLists找不到,就沿著父類鏈向上查找,直到 NSObject 類。 - 仍然找不到,則會(huì)進(jìn)入 動(dòng)態(tài)方法解析 -> 接收者重定向 -> 消息重定向。
動(dòng)態(tài)方法解析:進(jìn)入 -resolveClassMethod: ,如果可以正確解析到函數(shù),返回 YES,否則返回 NO,進(jìn)入下一個(gè)階段。
接收者重定向:進(jìn)入 -forwardingTargetForSelector:,如果查找到能處理該消息的接收者,返回接收者,否則返回 nil,進(jìn)入下一個(gè)階段。
消息重定向:進(jìn)入 -forwardInvocation: ,結(jié)果就兩種,消息成功處理或者拋出異常。
具體看一下。
動(dòng)態(tài)方法解析
當(dāng)類和其父類都無法找到接收者和響應(yīng)函數(shù),那么運(yùn)行時(shí)就會(huì)進(jìn)入動(dòng)態(tài)添加方法,我們可以在此方法中,對消息作出反應(yīng)。
方法有兩種,一種是針對實(shí)例消息,一種是針對類消息。
+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;
示例:定義一個(gè)類,分別調(diào)用它的兩個(gè)只有方法聲明但是沒有實(shí)現(xiàn)的方法。
這里分別是 sayHello 和 run。不出意外的話,肯定會(huì)出錯(cuò),接下來我們演示針對這兩個(gè)方法動(dòng)態(tài)的添加上實(shí)現(xiàn)。
// 實(shí)例的動(dòng)態(tài)方法解析
+(BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(sayHello)) {
// 將實(shí)現(xiàn)部分指向 默認(rèn)的處理
class_addMethod(self.class, sel, class_getMethodImplementation(self.class, @selector(defaultSayHello)), "v@:");
return YES;
}
// 繼續(xù)查找父類是否能夠處理
return [super resolveInstanceMethod:sel];
}
// 默認(rèn)的實(shí)例處理方法
-(void)defaultSayHello{
NSLog(@"%s",__func__);
}
+(BOOL)resolveClassMethod:(SEL)sel{
if(sel == @selector(run)){
class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(defaultRun)), "v@:");
return YES;
}
return [class_getSuperclass(self) resolveClassMethod:sel];
}
+(void)defaultRun{
NSLog(@"%s",__func__);
}
其中
// 當(dāng)前類、傳遞的消息、實(shí)現(xiàn)IMP、IMP對應(yīng)的返回值和參數(shù)類型
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types)
更多的 types 類型,可以查詢 官網(wǎng)資料。
接收者重定向
在動(dòng)態(tài)方法解析無法處理該消息時(shí),該消息就會(huì)進(jìn)入到轉(zhuǎn)發(fā)階段。該階段可以指定其他接收者,以保障程序的執(zhí)行。
-(id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(sayHello)) {
//返回能夠處理這個(gè)sel的實(shí)例對象
return [NSClassFromString(@"Student") new];
}
return [super forwardingTargetForSelector:aSelector];
}
如果是類方法,請使用 + 的類方法,以及返回 Class 類,而不是實(shí)例。
消息重定向
如果在經(jīng)過動(dòng)態(tài)方法解析、接收者重定向都無法處理此條消息時(shí),那么就會(huì)進(jìn)入最后的階段,針對這條消息進(jìn)行重定向。運(yùn)行時(shí)會(huì)通過方法 -forwardInvocation: 來通知該對象,給予最后一次處理這條消息的機(jī)會(huì)。
Invocation 是一個(gè)消息對象,包含了調(diào)用者、方法選擇器等信息。要想實(shí)現(xiàn)消息重定向,我們還需要重寫 -methodSignatureForSelector: 為方法 -forwardInvocation: 的參數(shù) anInvocation 提供一個(gè) methodSignature 方法簽名,這個(gè) methodSignature 用來描述方法的返回值,參數(shù)類型,關(guān)于方法的描述可以看 官網(wǎng)資料。
-(void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;
// 判斷某個(gè)對象是否可以響應(yīng)消息,如果可以,該對象響應(yīng)
if ([[NSClassFromString(@"Student") new] respondsToSelector:sel]) {
[anInvocation invokeWithTarget:[NSClassFromString(@"Student") new]];
}
/*檢測其他對象*/
else {
// 如果依然沒有可以響應(yīng)消息,則爆找不到響應(yīng)方法
[self doesNotRecognizeSelector:sel];
}
}
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSMethodSignature* methodSignature = [super methodSignatureForSelector:aSelector];
if (!methodSignature) {
methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return methodSignature;
}
以上就是完成的消息機(jī)制。三個(gè)階段,范圍一步一步的擴(kuò)大:
動(dòng)態(tài)方法解析范圍依舊是在本類,你可以將采用動(dòng)態(tài)添加的方式將消息轉(zhuǎn)交給其他方法。
接收者重定向范圍不限制在本類,你可以選擇一個(gè)其他可以處理該消息的類。
消息重定向范圍也不限制在本類,和接收者重定向比較,你會(huì)發(fā)現(xiàn)消息重定向允許你將消息傳遞給多個(gè)對象,搜索范圍進(jìn)一步擴(kuò)大,你甚至可以在這里實(shí)現(xiàn)類似多重繼承的操作,即當(dāng)前類無法響應(yīng)消息時(shí),尋找其他多個(gè)響應(yīng)者。
第二部分:應(yīng)用篇
在簡單認(rèn)識(shí)了以下運(yùn)行時(shí)關(guān)于消息傳遞知識(shí)后,我們來簡單介紹以下運(yùn)行時(shí)具體給可以幫我們做點(diǎn)什么。
經(jīng)驗(yàn)總結(jié)。
- 方法交換:攔截方法,加入其他任務(wù)。如:AOP方式進(jìn)行日志統(tǒng)計(jì)
- 屬性關(guān)聯(lián):將某內(nèi)存地址關(guān)聯(lián)到屬性。如:實(shí)現(xiàn)分類中的屬性添加
- 解析未知對象:獲取未知對象的成員列表、方法列表、系誒咦列表等信息
- 消息機(jī)制:動(dòng)態(tài)添加方法,解決消息無法響應(yīng)的問題
- 動(dòng)態(tài)操作:動(dòng)態(tài)的創(chuàng)建類、添加方法、添加屬性,從無到有
方法交換
Method Swizzling 是運(yùn)行時(shí)中的黑魔法,實(shí)現(xiàn)的原理是,通過運(yùn)行時(shí)獲取到類中的方法的實(shí)現(xiàn) IMP 地址,將其指向另外一個(gè) IMP ,進(jìn)而能夠動(dòng)態(tài)的交換兩方法,而你則可以無感知的進(jìn)行額外操作。
下面是列舉了兩個(gè)應(yīng)用示例。
- 日志統(tǒng)計(jì)
主要目的是為了監(jiān)聽控制的進(jìn)出情況,以統(tǒng)計(jì)頁面功能相關(guān)的指標(biāo),如收歡迎程度。
核心代碼
//獲取類方法
Method _Nullable class_getClassMethod(Class _Nullable cls, SEL _Nonnull name)
//獲取實(shí)例對象方法
Method _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
//交換兩個(gè)方法的實(shí)現(xiàn)
void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
下面是在控制器的分類中進(jìn)行的方法交換。
// 該方法會(huì)在編譯時(shí)期被調(diào)用
+(void)load {
[self exchangeMethod:@selector(viewDidAppear:)];
[self exchangeMethod:@selector(viewDidDisappear:)];
}
// 交換兩個(gè)方法
+ (void)exchangeMethod:(SEL)originalSelector{
SEL swizzledSelector = NSSelectorFromString([@"ll_" stringByAppendingString:NSStringFromSelector(originalSelector)]);
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
if (class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
- (void)ll_viewDidAppear:(BOOL)animated{
// 做一些統(tǒng)計(jì)操作
[self ll_viewDidAppear:animated]; //這里的方法指向的是系統(tǒng)的`-viewDidAppear:`
}
- (void)ll_viewDidDisappear:(BOOL)animated{
// 做一些統(tǒng)計(jì)操作
[self ll_viewDidDisappear:animated];
}
- 越界操作
這是針對一些危險(xiǎn)操作的,如數(shù)組越界、字符截取等等。下面演示了關(guān)于不可變數(shù)組的操作攔截,對越界的行為進(jìn)行了斷點(diǎn)提示。
#define NSAssertTip(tip) NSAssert(NO, tip)
#import <objc/runtime.h>
@implementation NSObject (Swizzle)
// 通用的方法交換
+ (void)swizzleMethod:(SEL)srcSel tarClass:(NSString *)tarClassName tarSel:(SEL)tarSel{
if (!srcSel||!tarClassName||!tarSel) {
return;
}
Class srcClass = [self class];
Class tarClass = NSClassFromString(tarClassName);
Method srcMethod = class_getInstanceMethod(srcClass,srcSel);
Method tarMethod = class_getInstanceMethod(tarClass,tarSel);
method_exchangeImplementations(srcMethod, tarMethod);
}
@end
#pragma mark ----------------------- 不可變數(shù)組 -----------------------
@implementation NSArray (Safe)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleMethod:@selector(initWithObjects_safe:count:) tarClass:@"__NSPlaceholderArray" tarSel:@selector(initWithObjects:count:)];
[self swizzleMethod:@selector(objectAtIndex_safe:) tarClass:@"__NSArrayI" tarSel:@selector(objectAtIndex:)];
[self swizzleMethod:@selector(arrayByAddingObject_safe:) tarClass:@"__NSArrayI" tarSel:@selector(arrayByAddingObject:)];
});
}
- (instancetype)initWithObjects_safe:(id *)objects count:(NSUInteger)cnt{
NSUInteger newCnt=0;
for (NSUInteger i=0; i<cnt; i++) {
if (!objects[i]) {
NSAssertTip(@"數(shù)組初始化錯(cuò)誤");
break;
}
newCnt++;
}
self = [self initWithObjects_safe:objects count:newCnt];
return self;
}
- (id)objectAtIndex_safe:(NSUInteger)index{
if (index>=self.count) {
NSAssertTip(@"數(shù)組越界");
return nil;
}
return [self objectAtIndex_safe:index];
}
- (NSArray *)arrayByAddingObject_safe:(id)anObject {
if (!anObject) {
NSAssertTip(@"新增的對象錯(cuò)誤");
return self;
}
return [self arrayByAddingObject_safe:anObject];
}
@end
屬性關(guān)聯(lián)
OC 中的分類通常只是為了增加方法,如果你添加了屬性,那么只能生成 setter 和 getter 方法,無法生成成員變量,變相的無法完成屬性的添加。但是有了運(yùn)行時(shí) ,你可以通過其關(guān)聯(lián)對象特性來完成 setter 和 getter 方法,相關(guān)的值則被關(guān)聯(lián)到某一個(gè)對象上。
核心代碼
/**
關(guān)聯(lián)屬性
@param object 需要設(shè)置關(guān)聯(lián)屬性的對象,即給哪個(gè)對象關(guān)聯(lián)屬性
@param key 關(guān)聯(lián)屬性對應(yīng)的key,可通過key獲取這個(gè)屬性,
@param value 給關(guān)聯(lián)屬性設(shè)置的值
@param policy 關(guān)聯(lián)屬性的存儲(chǔ)策略(對應(yīng)Property屬性中的assign,copy,retain等)
OBJC_ASSOCIATION_ASSIGN @property(assign)。
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)。
*/
void objc_setAssociatedObject(id _Nonnull object,
const void * _Nonnull key,
id _Nullable value,
objc_AssociationPolicy policy)
/**
獲取關(guān)聯(lián)的屬性
@param object 從哪個(gè)對象中獲取關(guān)聯(lián)屬性
@param key 關(guān)聯(lián)屬性對應(yīng)的key
@return 返回關(guān)聯(lián)屬性的值
*/
id _Nullable objc_getAssociatedObject(id _Nonnull object,
const void * _Nonnull key)
/**
移除對象所關(guān)聯(lián)的屬性
@param object 移除某個(gè)對象的所有關(guān)聯(lián)屬性
*/
void objc_removeAssociatedObjects(id _Nonnull object)
下面列舉了為 UIButton 對象添加了數(shù)據(jù)綁定。
@interface UIButton (PassValue)
@property (strong ,nonatomic) NSDictionary *paramDic;
@end
// 實(shí)現(xiàn)setter、getter方法
-(NSDictionary *)paramDic{
// _cmd 表示當(dāng)前的方法,該參數(shù)也可以是唯一的全局對象,如字符串
return objc_getAssociatedObject(self, _cmd);
}
-(void)setParamDic:(NSDictionary *)paramDic{
objc_setAssociatedObject(self, @selector(paramDic), paramDic, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
解析未知對象
在 Runtime 數(shù)據(jù)結(jié)構(gòu)中,我們看到了 objc_class 中存放著類的成員變量、方法列表等信息,運(yùn)行時(shí)通用提供了方法,讓能夠進(jìn)一步了解一個(gè)類。有了運(yùn)行時(shí),所有的類在你的面前都是透明的。
[self.textField setValue:UIColor.redColor forKey:@"_placeholderLabel.textColor"];
上面的代碼是使用了私有成員變量來設(shè)置輸入框的占位標(biāo)簽的顏色。那么如何知道 _placeholderLabel 這個(gè)私有屬性的呢?是通過運(yùn)行時(shí)。
unsigned int count;
Ivar *ivarList = class_copyIvarList([UITextField class], &count);
for (int i= 0; i<count; i++) {
Ivar ivar = ivarList[i];
const char *ivarName = ivar_getName(ivar);
NSLog(@"Ivar(%d): %@", i, [NSString stringWithUTF8String:ivarName]);
}
free(ivarList);
上述代碼輸出了 UITextField 所有的成員變量,其中一個(gè)就是占位符標(biāo)簽 _placeholderLabel。
你同樣可以通過運(yùn)行時(shí)獲取到屬性列表、方法列表等信息,對應(yīng)的函數(shù)類似。
注:iOS 11以后,蘋果限制非常多的通過私有api設(shè)值的方式,其中就有 _placeholderLabel , 這個(gè)對象已經(jīng)是 UITextFieldLabel。因此,在開發(fā)過程中最好避免使用私有 api,一方面是很可能被拒絕,另一方面就是未來的某一時(shí)刻,這些私有api都不會(huì)做兼容的替換。
消息機(jī)制
這一節(jié)請參考上一部分的內(nèi)容。
動(dòng)態(tài)的加入類、方法、屬性
更多的應(yīng)用
- 數(shù)據(jù)和模型轉(zhuǎn)換
參考這篇內(nèi)容iOS 數(shù)據(jù)模型轉(zhuǎn)換。
- 自動(dòng)歸檔和解檔
優(yōu)化歸檔和解檔同樣是應(yīng)用了通過運(yùn)行時(shí)獲取類成員變量,然后實(shí)現(xiàn)歸檔和解檔操作。
// 對象解檔
- (instancetype)initWithCoder:(NSCoder *)aDecoder{
if ([super init]) {
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList([self class], &count);
for (int i = 0; i < count; i++) {
Ivar ivar = ivarList[i];
const char *ivarName = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:ivarName];
// 從解碼器中獲取到對象
id value = [aDecoder decodeObjectForKey:key];
// 通過KVC的方式設(shè)值
[self setValue:value forKey:key];
}
free(ivarList);
}
return self;
}
// 對象歸檔
- (void)encodeWithCoder:(NSCoder *)aCoder{
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList([self class], &count);
for (NSInteger i = 0; i < count; i++) {
Ivar ivar = ivarList[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
id value = [self valueForKey:key];
[aCoder encodeObject:value forKey:key];
}
free(ivarList);
}
總結(jié)
OC 運(yùn)行時(shí)的功能十分的強(qiáng)大,上面僅僅介紹了冰上一角,熟練的掌握運(yùn)行時(shí),對我們開發(fā)有著非常大的幫助,也能幫助我們進(jìn)一步了解 OC 底層原理。
和 OC 動(dòng)態(tài)性相比,Swift 語言在對類型的定義上就非常的嚴(yán)厲了,你必須在你編譯期明確類型,官方給出的解釋是 Swift 基于安全穩(wěn)定性考慮,讓錯(cuò)誤發(fā)生在編譯期而非運(yùn)行期,那么 app 整體會(huì)相對健壯。另一方面 Swift 砍掉了 OC 中的運(yùn)行時(shí),取而代之的是 Mirror 對象,但是這個(gè)對象目前而言功能僅限于查看對象的屬性、方法等,和運(yùn)行時(shí)相比簡直就是云泥之別,期待在 Swift 更高的版本中該對象能夠擁有更強(qiáng)大的能力。