在iOS開發的過程中經常會提及一個東西叫做RunTime,並且在面試中RunTime也是經常被考到的問題。那麼本文就來探討下RunTime到底是什麼,他如何來使用。
環境信息
Mac OS X 10.10.2
xcode 6
iOS 8.1
一、RunTime是什麼
首先Objective-C是C語言的擴展,並加入了面向對象特性和Smalltalk式的消息傳遞機制。而這個擴展的核心就是一個用C和彙編語言寫的RunTime庫,這個庫所做的事情就是加載類信息,進行方法的分發和轉發,正是這個庫賦予了Objective-C的動態特性。
運行時很小卻很強大,並且Objc Runtime是開源的,蘋果也允許你使用RunTime裏面的東西(不會導致上不了架)。
雖然這些特性在開發中使用的較少,但是理解Objective-C的Runtime機制可以幫我們更好的瞭解這個語言,合理的運用還能在系統層面上解決一些技術問題。
二、消息機制
比如我們初始化一個NSObject對象:
NSObject *object = [[NSObject alloc] init];
事實上,調用方法其實也是在給這個對象發送一個消息,在編譯時這句話會翻譯成一個C的函數調用,即:
objc_msgSend(objc_msgSend([NSObject class],@selector(alloc)),@selector(init));
那麼,這不是把OC代碼轉換成C代碼了麼,他的動態特性體現在哪裏?對於C語言,函數的調用在編譯的時候就會去決定調用哪個函數。而OC是一種動態語言,它會儘可能的把代碼執行的決策從編譯和鏈接的時候,推遲到運行時。
給一個對象發送的一個消息並不會立即執行,而是在運行的時候再去尋找他對應的實現。那麼你就可以把消息轉發給你想要的對象,或者隨意交換一個方法的實現之類的。
那麼這個消息機制到底是怎麼運作的呢,我們就需要來了解下Objective-C的對象模型。
三、Objective-C對象模型
我們打開<objc/objc.h>文件可以看到如下對NSObject的定義:
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;}
Objective-C是一門面向對象的語言,每一個對象都是一個類的實例,在Objective-C的內部,每個對象都有一個isa的指針,指向該對象的類。每個類描述了一系列它的實例的特點,包括成員變量的列表,成員函數的列表等,每個對象都可以接收消息,而對象可以接收的消息列表保存在它對應的類中。
我們再來看看class的定義
typedef struct objc_class *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; // 類的版本信息,默認爲0
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 *` */
發現他裏面也有一個isa指針,因爲在Objective-C語言中,每個類實際上也是一個對象,每個類也可以接收消息,例如[NSObject alloc]。那麼既然一個類也是對象,所以它也必須是另一個類的實例,這個類就是元類(metaclass)。元類保存了類方法的列表,當一個類被調用時,元類會首先查找本身是否有該類方法的實現,如果沒有,則該元類會向它的父類查找該方法。
元類也是一個對象,爲了設計上的完整,所以元類的isa指針都會指向一個根元類(root metaclass)。根元類本身的isa指針指向自己,這樣就形成了一個閉環。
所以使用objc_msgSend函數,會執行以下步驟
- 通過對象(類)的isa指針去找到他的class
- 在class的method list 找到該消息的實現
- 如果class中沒有改消息的實現,就繼續到它的super_class中去找
- 一旦找到這個這個消息的實現,那麼就去執行他的IMP
這樣的話每發送一個消息就要方法列表objc_method_list進行一次遍歷,爲了提高效率,使用了objc_cache對經常調用的函數進行緩存,再次調用時就先到objc_cache中去查找函數實現。
四、RunTime的具體應用
1.動態創建一個類
#import <objc/runtime.h>
// 自定義一個方法
void reportFunction (id self, SEL _cmd) {
NSLog(@"This object is %p", self);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 1.動態創建對象 創建一個Person 繼承自 NSObject類
Class newClass = objc_allocateClassPair([NSObject class], “Person”, 0);
// 爲該類增加名爲Report的方法
class_addMethod(newClass, @selector(report), (IMP)reportFunction, @"v@:");
// 註冊該類
objc_registerClassPair(newClass);
// 創建一個 Student 類的實例
instantOfNewClass = [[newClass alloc] init];
// 調用方法
[instantOfNewClass report];
}
return 0;
}
這裏用到的Selector事實上是一個C的結構體,表示一個消息,類似於C的方法調用:
typedef struct objc_selector *SEL;
IMP (Method Implementations),就是一個函數指針,當你發起一個ObjC消息之後,最終它會執行的那個代碼,就是由這個函數指針指定的。
typedef id (*IMP)(id self,SEL _cmd,...);
2.關聯對象
對象在內存中的排布可以看成是一個結構體,該結構體的大小並不能動態的變化,所以無法在運行時動態的給對象增加成員變量,但是我們可以通過關聯對象的方法變相的給對象增加一個成員變量。
比如,我們想給NSObject新增一個關聯對象:
創建一個NSObject的類目AssociatedObject,在.h文件裏面聲明一個屬性
@interface NSObject (AssociatedObject)
@property (nonatomic, strong) id associatedObject;
@end
在NSObject+AssociatedObject.m文件裏面進行關聯
#import "NSObject+AssociatedObject.h"
#import <objc/runtime.h>
@implementation NSObject (AssociatedObject)
@dynamic associatedObject;
- (void)setAssociatedObject:(id)object {
// 設置關聯對象
objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)associatedObject {
// 得到關聯對象
return objc_getAssociatedObject(self, @selector(associatedObject));
}
@end
這樣就給NSObject新增了一個屬性,可是,這有什麼用呢?通常,我們會利用關聯對象給UIAlertView新增一個block回調,方便使用。
3.利用RunTime進行模型歸檔
對於一個有很多屬性的Person類,遵守了NSCoding協議之後,我們可以利用RunTime遍歷模型對象的所有屬性進行歸檔,關鍵代碼如下:
// 利用runtime機制進行屬性的歸檔接檔
- (void)encodeWithCoder:(NSCoder *)aCoder {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([Person class], &count);
for (int i = 0; i<count; i++) {
// 取出i位置對應的成員變量
Ivar ivar = ivars[i];
// 查看成員變量
const char *name = ivar_getName(ivar);
// 歸檔
NSString *key = [NSString stringWithUTF8String:name]; id value = [self valueForKey:key];
[aCoder encodeObject:value forKey:key];
}
free(ivars);
}
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([Person class], &count);
for (int i = 0; i<count; i++) {
// 取出i位置對應的成員變量
Ivar ivar = ivars[i];
// 查看成員變量
const char *name = ivar_getName(ivar);
// 歸檔
NSString *key = [NSString stringWithUTF8String:name];
id value = [aDecoder decodeObjectForKey:key];
// 設置到成員變量身上
[self setValue:value forKey:key];
}
free(ivars);
}
return self;
}
五、小結
利用RunTime我們還可以完成字典和對象模型之間的轉換,例如MJExtension,還可以自己實現KVO,封裝框架(修改系統實現),實現客戶端根據後臺動態更改邏輯等等,光說無用,還是需要自己多練才行。
六、參考資料
http://tech.glowing.com/cn/objective-c-runtime/
http://www.justinyan.me/post/1624
http://limboy.me/ios/2013/08/03/dynamic-tips-and-tricks-with-objective-c.html
http://nshipster.cn/associated-objects/
http://blog.devtang.com/blog/2013/10/15/objective-c-object-model/
http://honglu.me/2014/12/29/%E6%B5%85%E8%B0%88OC%E8%BF%90%E8%A1%8C%E6%97%B6-RunTime/