runtime這個詞對於iOS程序猿童鞋來講,都是一個「耳熟能詳」的名詞,由於runtime就像面試中的「詛咒」同樣,每當遇到相關面試題,都是幾家歡喜幾家愁。那麼runtime究竟是什麼呢? runtime是 OC底層的一套C語言的API,編譯器最終都會將OC代碼轉化爲運行時代碼。不過蘋果已經將 ObjC runtime 代碼開源了,咱們能夠下面的網址瀏覽源代碼:html
opensource.apple.com/source/objc…面試
那麼咱們就先經過一些經典的面試題來了解一些runtime這個「詛咒」的威力:數組
一、下面代碼輸出結果是什麼?安全
@implementation Son : Father
- (id)init {
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
複製代碼
答案:(1) Son / Son 由於super爲編譯器標示符,向super發送的消息被編譯成objc_msgSendSuper,但仍以self做爲receiver
bash
二、下面代碼輸出結果是什麼?網絡
BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
複製代碼
答案:(2) YES / NO / NO / NO <NSObject>協議有一套類方法的隱藏實現,因此編譯運行正常;因爲NSObject meta class的父類爲NSObject class,因此只有第一句爲YES
(3) 下面的代碼會?Compile Error / Runtime Crash / NSLog…?app
@interface NSObject (Sark)
+ (void)foo;
@end
@implementation NSObject (Sark)
- (void)foo {
NSLog(@"IMP: -[NSObject (Sark) foo]");
}
@end
// 測試代碼
[NSObject foo];
[[NSObject new] foo];
複製代碼
答案(3) 編譯運行正常,兩行代碼都執行-foo。 [NSObject foo]方法查找路線爲 NSObject meta class –super-> NSObject class,和第二題知識點很類似。
(4) 下面的代碼會?Compile Error / Runtime Crash / NSLog…?框架
@interface Sark : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Sark
- (void)speak {
NSLog(@"my name's %@", self.name);
}
@end
@implementation ThirdViewController
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [Sark class];
void *obj = &cls;
[(__bridge id)obj speak];
}
@end
複製代碼
答案 編譯運行正常,輸出ThirdViewController中的self對象。 編譯運行正常,調用了-speak方法,因爲 id cls = [Sark class]; void *obj = &cls; obj已經知足了構成一個objc對象的所有要求(首地址指向ClassObject),因此可以正常走消息機制; 因爲這我的造的對象在棧上,而取self.name的操做本質上是self指針在內存向高位地址偏移(32位下一個指針是4字節),按viewDidLoad執行時各個變量入棧順序從高到底爲(self, _cmd, self.class, self, obj)(前兩個是方法隱含入參,隨後兩個爲super調用的兩個壓棧參數),遂棧低地址的obj+4取到了self。
函數
看到上面的幾道面試題和答案,有些童鞋可能仍是一頭霧水,那下面就一點點的捋一捋runtime的功能:工具
objc_msgSend函數定義以下: id objc_msgSend(id self, SEL op, ...)
打開objc.h文件,看下SEL的定義以下: typedef struct objc_selector *SEL
SEL
是一個指向objc_selector
結構體的指針。而objc_selector
的定義並無在runtime.h中給出定義。咱們能夠嘗試運行以下代碼:
SEL sel = @selector(foo);
NSLog(@"%s", (char *)sel);
NSLog(@"%p", sel);
const char *selName = [@"foo" UTF8String];
SEL sel2 = sel_registerName(selName);
NSLog(@"%s", (char *)sel2);
NSLog(@"%p", sel2);
複製代碼
輸出以下:
2014-11-06 13:46:08.058 Test[15053:1132268] foo
2014-11-06 13:46:08.058 Test[15053:1132268] 0x7fff8fde5114
2014-11-06 13:46:08.058 Test[15053:1132268] foo
2014-11-06 13:46:08.058 Test[15053:1132268] 0x7fff8fde5114
複製代碼
Objective-C在編譯時,會根據方法的名字生成一個用來區分這個方法的惟一的一個ID。只要方法名稱相同,那麼它們的ID就是相同的。 因此兩個類之間,無論它們是父類與子類的關係,仍是之間沒有這種關係,只要方法名相同,那麼它的SEL就是同樣的。每個方法都對應着一個SEL。編譯器會根據每一個方法的方法名爲那個方法生成惟一的SEL。這些SEL組成了一個Set集合,當咱們在這個集合中查找某個方法時,只須要去找這個方法對應的SEL便可。而SEL本質是一個字符串,因此直接比較它們的地址便可。
固然,不一樣的類能夠擁有相同的selector。不一樣類的實例對象執行相同的selector時,會在各自的方法列表中去根據selector去尋找本身對應的IMP
。
IMP
呢?繼續看定義:typedef id (*IMP)(id, SEL, ...);
IMP本質就是一個函數指針,這個被指向的函數包含一個接收消息的對象id,調用方法的SEL,以及一些方法參數,並返回一個id。
所以咱們能夠經過SEL
得到它所對應的IMP
,在取得了函數指針以後,也就意味着咱們取得了須要執行方法的代碼入口,這樣咱們就能夠像普通的C語言函數調用同樣使用這個函數指針。
ivar 在objc中被定義爲: typedef struct objc_ivar *Ivar;
它是一個指向objc_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
} OBJC2_UNAVAILABLE;
複製代碼
類中的Property屬性被編譯器轉換成了Ivar,而且自動添加了咱們熟悉的Set和Get方法。
isa 是一個 objc_class 類型的指針,內存佈局以一個 objc_class 指針爲開始的全部東東均可以當作一個 object 來對待! 這就是說 objc_class 或者說類其實也能夠當作一個 objc_object 對象來對待!這裏要區分清楚兩個名詞:類對象(class object)
與實例對象(instance object)
。ObjC還對類對象
與實例對象
中的 isa 所指向的類結構做了不一樣的命名: 類對象
中的 isa 指向類結構被稱做 metaclass(元類),metaclass 存儲類的static類成員變量與static類成員方法(+開頭的方法); 實例對象
中的 isa 指向類結構稱做 class(普通的),class 結構存儲類的普通成員變量與普通成員方法(-開頭的方法)。
一看就明白,指向該類的父類唄!若是該類已是最頂層的根類(如 NSObject 或 NSProxy),那麼 super_class 就爲 NULL。 好,先中斷一下其餘類結構成員的介紹,讓咱們釐清一下在繼承層次中,子類,父類,根類(這些都是普通 class)以及其對應的 metaclass 的 isa 與 super_class 之間關係:
` 規則一:類的實例對象的 isa 指向該類;該類的 isa 指向該類的 metaclass;
規則二:類的 super_class 指向其父類,若是該類爲根類則值爲 NULL;
規則三:metaclass 的 isa 指向根 metaclass,若是該 metaclass 是根 metaclass 則指向自身;
規則四:metaclass 的 super_class 指向父 metaclass,若是該 metaclass 是根 metaclass 則指向該 metaclass 對應的類; `
class 是 instance object 的類類型。當咱們向實例對象發送消息(實例方法)時,咱們在該實例對象的 class 結構的 methodlists 中去查找響應的函數,若是沒找到匹配的響應函數則在該 class 的父類中的 methodlists 去查找(查找鏈爲上圖的中間那一排)。以下面的代碼中,向str 實例對象發送 lowercaseString 消息,會在 NSString 類結構的 methodlists 中去查找 lowercaseString 的響應函數。
NSString * str;
[str lowercaseString];
複製代碼
metaclass 是 class object 的類類型。當咱們向類對象發送消息(類方法)時,咱們在該類對象的 metaclass 結構的 methodlists 中去查找響應的函數,若是沒有找到匹配的響應函數則在該 metaclass 的父類中的 methodlists 去查找。以下面的代碼中,向 NSString 類對象發送 stringWithString 消息,會在 NSString 的 metaclass 類結構的 methodlists 中去查找 stringWithString 的響應函數。 [NSString stringWithString:@"str"];
category在咱們實際開發過程當中,是一個很是實用的得力助手,由於咱們能夠在不改變原有類的狀況下進行方法拓展,可是有些時候,咱們也須要爲這些category分類添加一些屬性供咱們使用,問題就在這裏,咱們都知道:category分類沒法直接添加屬性,可是咱們開發中又有這樣的需求,這就尷尬啦~~~ 首先咱們先看一下,爲何在category中沒法直接添加屬性呢,category是表示一個指向分類的結構體的指針,其定義以下:
struct objc_category {
char *category_name OBJC2_UNAVAILABLE;// 分類名
char *class_name OBJC2_UNAVAILABLE;// 分類所屬的類名
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE;// 實例方法列表
struct objc_method_list *class_methods OBJC2_UNAVAILABLE;// 類方法列表
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;// 分類所實現的協議列表
}
複製代碼
在這個結構體主要包含了分類定義的實例方法與類方法,其中
instance_methods
列表是objc_class中方法列表的一個子集,
class_methods
列表是元類方法列表的一個子集。
可發現,類別中沒有ivar(ivar表明類中實例變量的類型)
成員變量指針,也就意味着:類別中不可以添加實例變量和屬性,struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;// 該類的成員變量鏈表
簡單的理解就是:在category中,系統沒法自動爲@property
的屬性生成 set、get方法
。 【有些人可能就有疑惑了:爲何官方API和一些第三方的框架工具都有爲category添加屬性的現象呢?好比:NSIndexPath (UITableView)、MJRefresh等等】因此這是咱們就須要使用到runtime爲category關聯一些屬性對象。
/**
*經過鍵值對關聯對象
*
* @param object 關聯的對象源【一般爲 self】
* @param key 惟一鍵,經過這個鍵進行取值【const void *是用來起到聲明做用】
* @param value 值;「 key 」所對應的值
* @param policy 內存管理策略,枚舉:objc_AssociationPolicy
*
* @see objc_setAssociatedObject
* @see objc_removeAssociatedObjects
*/
OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);
/**
* 經過 key 獲取關聯值.
*
* @param object 關聯的對象源【一般爲 self】,在設置關聯時所指定的與哪一個對象關聯的那個對象
* @param key 惟一鍵,在設置關聯時所指定的鍵
*
* @return 返回惟一鍵對應的 value 值
*
* @see objc_setAssociatedObject
*/
OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);
/**
* 取消屬性關聯對象
*
* @param object 要取消關聯屬性的對象
*
* @see objc_setAssociatedObject
* @see objc_getAssociatedObject
*/
OBJC_EXPORT void objc_removeAssociatedObjects(id object)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);
複製代碼
其中的關聯策略
也是系統提供的一個枚舉:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /**表示弱引用關聯,一般是基本數據類型 */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**表示強引用關聯對象,是線程安全的; 如同:(nonatomic,strong) */
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**表示關聯對象copy,是線程安全的; 如同:(nonatomic,copy) */
OBJC_ASSOCIATION_RETAIN = 01401, /**表示強引用關聯對象,不是線程安全的; 如同:(atomic,strong)*/
OBJC_ASSOCIATION_COPY = 01403 /**表示關聯對象copy,不是線程安全的; 如同:(nonatomic,copy) */
};
複製代碼
舉個小栗子 ~
.h 中聲明
///經過runtime關聯數組屬性
@property (nonatomic,strong)NSMutableArray * array;
.m 中實現set、get 方法
/// 設置屬性關聯
-(void)setArray:(NSMutableArray *)array{
objc_setAssociatedObject(self, @selector(array),
array, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSMutableArray *)array{
return objc_getAssociatedObject(self, @selector(array));
}
複製代碼
runtime
的方法交換
method_exchangeImplementations
進行實現這個需求。首先來看一些
runtime.h
中提供的幾個方法:
/**
* 返回一個指定類對象實現的實例方法。
*
* @param cls 指定的類
* @param name 須要檢索的方法
*
*/
OBJC_EXPORT Method class_getInstanceMethod(Class cls, SEL name)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);
/**
* 返回一個指定類對象實現的 類方法。
*
* @param cls 指定的類
* @param name 須要檢索的方法
*
*/
OBJC_EXPORT Method class_getClassMethod(Class cls, SEL name)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);
/**
* 交換兩個方法的實現.
*
* @param m1 方法與第二個方法交換。
* @param m2 方法與第一個方法交換。
*
*/
OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
複製代碼
舉一個小栗子:
eat 方法
,在實現過程當中,就會默認進入到交換的
goToSchool 方法
內。這就是關於
runtime
方法交換的一個簡單示例,在實際開發中能夠根據本身的需求進行實際操做啦! 當類被引用進項目的時候就會執行load函數(在main函數開始執行以前),與這個類是否被用到無關,每一個類的load函數只會自動調用一次.也就是load函數是系統自動加載的,load方法會在runtime加載類、分類時調用。 ###四:動態類型判斷 即運行時再決定對象的類型。這類動態特性在平常應用中很是常見,簡單說就是id類型。id類型即通用的對象類,任何對象均可以被id指針所指,而在實際使用中,每每使用introspection來肯定該對象的實際所屬類:
- (BOOL)isMemberOfClass:(Class)aClass
是 NSObject 的方法,用以肯定某個 NSObject 對象是不是某個類的成員。
- (BOOL)isKindOfClass:(Class)aClass
是用以肯定某個對象是否是某個類或其子類的成員;例如:
id obj = someInstance;
if ([obj isKindOfClass:someClass])
{
someClass *classSpecifiedInstance = (someClass *)obj;
// Do Something to classSpecifiedInstance which now is an instance of someClass
//...
}
複製代碼
首先先看一下系統提供的動態綁定相關的幾個方法:
/**
當這個類被調用了一個沒有實現的方法時,會調用到這裏
@param sel 未實現的類方法名
*/
+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
/**
當這個類被調用了一個沒有實現的對象
@param sel 未實現的對象方法名
*/
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
/**
動態添加
class_addMethod([self class], @selector(resolveThisMethodDynamically), (IMP) myMethodIMP, "v@:");
一、Class cls:類的類型
二、name:方法標記 sel
三、imp:方法的實現,是一個 函數指針
四、type:返回值類型
*/
OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
/**
上述添加方法中的 IMP imp 所對應的函數格式以下:
*/
void myMethodIMP(id self, SEL _cmd)
{
// implementation ....
}
複製代碼
動態綁定所作的,便是在實例所屬類肯定後,將某些屬性和相應的方法綁定到實例上。這裏所指的屬性和方法固然包括了原來沒有在類中實現的,而是在運行時才須要的新加入的實現。在Cocoa層,咱們通常向一個NSObject對象發送-respondsToSelector:或者-instancesRespondToSelector:等來肯定對象是否能夠對某個SEL作出響應,而在OC消息轉發機制被觸發以前,對應的類的+resolveClassMethod:和+resolveInstanceMethod:將會被調用,在此時有機會動態地向類或者實例添加新的方法,也即類的實現是能夠動態綁定的。舉個小栗子:
resolveInstanceMethod :
方法,而後咱們可使用
class_addMethod
方法進行動態綁定,從而達到預期效果。 其中須要注意的是:
performSelector: withObject: 方法
,由於調用這個方法時,系統在編譯時是不會進行校驗方法是否存在,只有在運行時纔會進行查詢方法。而直接調用方法時,在編譯過程當中就會進行校驗方法是否存在。 ###六:獲取對象屬性 最典型的用法就是一個對象在
歸檔 encodeWithCoder
和
解檔initWithCoder:
方法中須要該對象全部的屬性進行
encodeObject:
和
decodeObjectForKey:
,經過runtime咱們聲明中不管寫多少個屬性,都不須要再修改實現中的代碼了。
得到某個類的全部成員變量(outCount 會返回成員變量的總數)
參數:
/**
一、哪一個類
二、放一個接收值的地址,用來存放屬性的個數
三、返回值:存放全部獲取到的屬性,經過下面兩個方法能夠調出名字和類型
*/
Ivar *class_copyIvarList(Class cls , unsigned int *outCount)
//得到成員變量的名字
const char *ivar_getName(Ivar v)
//得到成員變量的類型(除了基本數據類型)
const char *ivar_getTypeEndcoding(Ivar v)
複製代碼
舉個小栗子:
// C語言內 但凡看到 copy creat new 須要釋放
// ARC
// 告訴系統歸檔哪些東西
- (void)encodeWithCoder:(NSCoder *)coder
{
unsigned int count = 0;
Ivar * ivars = class_copyIvarList([Person class], &count);
for (int i = 0; i < count; i ++) {
//取出對應的成員Ivar
Ivar ivar = ivars[i];
const char * name = ivar_getName(ivar);
const char *type = ivar_getTypeEncoding(ivar);
NSLog(@"成員變量名:%s 成員變量類型:%s",name,type);
NSString * key = [NSString stringWithUTF8String:name];
//歸檔
[coder encodeObject:[self valueForKey:key] forKey:key];
}
free(ivars);
}
//解檔
- (instancetype)initWithCoder:(NSCoder *)coder
{
if (self =[super init]) {
unsigned int count = 0;
Ivar * ivars = class_copyIvarList([Person class], &count);
for (int i = 0; i < count; i ++) {
//取出對應的成員Ivar
Ivar ivar = ivars[i];
const char * name = ivar_getName(ivar);
NSString * key = [NSString stringWithUTF8String:name];
//解檔
id value = [coder decodeObjectForKey:key];
//設置到屬性身上
[self setValue:value forKey:key];
}
free(ivars);
}
return self;
}
複製代碼
這就是使用runtime
實現的歸檔解檔方法。 ###七:字典轉模型 字典轉模型這個在實際開發中也是最經常使用的,由於當咱們網絡請求到數據後,咱們就須要將數據轉化爲對應的對象模型。不過如今有比較成熟的字典轉模型框架,好比:YYModel
、MJExtension
等,那咱們如何利用runtime
實現本身的字典轉模型框架呢?舉個小栗子:
NSObject+Runtime_Model.h 代碼邏輯:
@interface NSObject (Runtime_Model)
/** 字典轉模型
使用該方法進行字典轉模型時,若是使用了設置屬性名字與返回的 key 不一樣時,
須要實現 - (void)setValue:(id)value forUndefinedKey:(NSString *)key 方法,進行轉化
*/
+ (instancetype)objectWithDict:(NSDictionary *)dict;
@end
複製代碼
NSObject+Runtime_Model.m 代碼邏輯:
#import "NSObject+Runtime_Model.h"
#import <objc/runtime.h>
@implementation NSObject (Runtime_Model)
// 字典轉模型
+ (instancetype)objectWithDict:(NSDictionary *)dict{
// 建立對應模型對象
id objc = [[self alloc] init];
// 判斷字典中的 key 是否爲成員變量,以便爲成員變量進行替換賦值
for (NSString * key in dict.allKeys) {
id value = dict[key];
/*判斷當前屬性是否是Model*/
objc_property_t property = class_getProperty([self class], key.UTF8String);
unsigned int outCount = 0;
objc_property_attribute_t * attributeList = property_copyAttributeList(property, &outCount);
if (!attributeList) {// 屬於模型的屬性
if ([objc respondsToSelector:@selector(setValue:forUndefinedKey:)]) {
[objc setValue:value forUndefinedKey:key];
}
}
}
unsigned int count = 0;
// 1.獲取成員屬性數組
Ivar *ivarList = class_copyIvarList(self, &count);
// 2.遍歷全部的成員屬性名,一個一個去字典中取出對應的value給模型屬性賦值
for (int i = 0; i < count; i++) {
// 2.1 獲取成員屬性
Ivar ivar = ivarList[i];
// 2.2 獲取成員屬性名 C -> OC 字符串
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 2.3 _成員屬性名 => 字典key
NSString *key = [ivarName substringFromIndex:1];
// 2.4 去字典中取出對應value給模型屬性賦值
id value = dict[key];
// 屬性對應的類名
const char *type = ivar_getTypeEncoding(ivar);
// 獲取成員屬性類型
NSString *ivarType = [NSString stringWithUTF8String:type];
// 二級轉換,字典中還有字典,也須要把對應字典轉換成模型
//
// 判斷下value,是否是字典
if ([value isKindOfClass:[NSDictionary class]] && ![ivarType containsString:@"NS"]) { // 是字典對象,而且屬性名對應類型是自定義類型
// user User
// 處理類型字符串 @\"User\" -> User ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""]; ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
// 自定義對象,而且值是字典
// value:user字典 -> User模型
// 獲取模型(user)類對象
Class modalClass = NSClassFromString(ivarType);
// 字典轉模型
if (modalClass) {
// 字典轉模型 user
value = [modalClass objectWithDict:value];
}
// 字典,user
// NSLog(@"%@",key);
}
// 三級轉換:NSArray中也是字典,把數組中的字典轉換成模型.
// 判斷值是不是數組
if ([value isKindOfClass:[NSArray class]]) {
// 轉換成id類型,就能調用任何對象的方法
id idSelf = self;
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
// 生成模型
Class classModel = NSClassFromString(ivarType);
NSMutableArray *arrM = [NSMutableArray array];
// 遍歷字典數組,生成模型數組
for (NSDictionary *dict in value) {
// 字典轉模型
id model = [classModel objectWithDict:dict];
[arrM addObject:model];
}
// 把模型數組賦值給value
value = arrM;
}
// 2.5 KVC字典轉模型
if (value) {
[objc setValue:value forKey:key];
}
}
// 返回對象
return objc;
}
@end
複製代碼
固然這也只是RunTime
的一部分功能,但倒是在實際開發中常常用到的知識點。因此若是想要繼續拓展RunTime
技能深度的話,能夠翻看蘋果開源的RunTime
源碼http://opensource.apple.com/source/objc4/objc4-493.9/runtime/