Runtime是什么
在C語言中,將代碼轉(zhuǎn)換成為可執(zhí)行文件,一般要經(jīng)歷三個步驟:
編譯--->鏈接--->運行
在鏈接的時候,對象的類型位谋、方法的實現(xiàn)已經(jīng)確認(rèn)好了.
而在OC中,卻將一下在編譯和鏈接所處理的工作放在了運行階段,也就是說, 一個編譯好的.ipa包,在程序沒有運行的時候,誰也不知道調(diào)用一個方法會發(fā)生什么,這也為熱修復(fù)提供了可能, 因此OC是一門動態(tài)語言.
這樣的設(shè)計使Objective-C變得靈活,我們可以在程序運行的時候,動態(tài)的修改一個方法的實現(xiàn),這個黑魔法就是runtime.
runtime就是iOS的一個庫,這個庫使我們在程序運行時,創(chuàng)建對象踪央、檢查對象、修改類和對象的方法.
Runtime是怎么工作的
首先要知道類和對象在OC 中是怎么定義的?
Class(類)的定義
在objc.h中,Class被定義為指向objc_class的指針定義如下:
typedef struct objc_class *Class;
而objc_class是一個結(jié)構(gòu)體,在runtime 中定義如下:
struct objc_class {
Class isa; // 實現(xiàn)方法調(diào)用的關(guān)鍵
Class super_class; // 父類
const char * name; // 類名
long version; // 類的版本信息灾前,默認(rèn)為0
long info; // 類信息,供運行期使用的一些位標(biāo)識
long instance_size; // 該類的實例變量大小
struct objc_ivar_list * ivars; // 該類的成員變量鏈表
struct objc_method_list ** methodLists; // 方法定義的鏈表
struct objc_cache * cache; // 方法緩存
struct objc_protocol_list * protocols; // 協(xié)議鏈表
};
提示:在 Xcode 中,使用快捷鍵 command + shift + o,可以打開搜索窗口,輸入 objc_class 即可看到頭文件定義
可以看到一個類中保存了自身所有的:
成員變量(ivers)
所有方法(methodLists)
所有實現(xiàn)的協(xié)議(objc_protocol_list)
isa(指向指針)
cache(緩存)
...
OC中對象(objc)的定義
id 被定義為指向objc_object 的結(jié)構(gòu)體指針,說明objc_object就是用到的對象的定義.
typedef struct objc_object *id
在看objc_object 的定義, objc_object其實是一個結(jié)構(gòu)體, 只包含一個isa 指針, 而objc_object結(jié)構(gòu)體中唯一保存的就是Class 的isa(地址)
struct objc_object {
Class isa;
}
總結(jié): 當(dāng)一個對象調(diào)用方法時, 對象會通過自身的isa 尋找對應(yīng)的objc_class, 然后在objc_class 結(jié)構(gòu)體methodLists找到調(diào)用的方法;
再說cache 存炮,因為調(diào)用方法的過程是個查找 methodLists 的過程撼玄,如果每次調(diào)用都去查找夺姑,效率會非常低。所以對于調(diào)用過的方法掌猛,會以 map 的方式保存在 cache 中盏浙,下次再調(diào)用就會快很多。
Meta Class 元類
調(diào)用一個對象的類方法的過程是怎么樣的?還有 objc_class 中也有一個 isa 指針废膘,它是干嘛用的竹海?
其實觀察 objc_class 和 objc_object 的定義,會發(fā)現(xiàn)兩者其實本質(zhì)相同(都包含 isa 指針)丐黄,只是 objc_class 多了一些額外的字段,相應(yīng)的,類也是一個對象,只是保存了一些字段.
既然說類也是對象,那么類的類型是什么呢?這里就引出了另外一個概念 —— Meta Class(元類).
在 Objective-C 中斋配,每一個類都有對應(yīng)的元類。而在元類的 methodLists 中灌闺,保存了類的方法鏈表许起,即所謂的「類方法」。并且類的 isa 指針指向?qū)?yīng)的元類菩鲜。因此上面的問題答案就呼之欲出园细,調(diào)用一個對象的類方法的過程如下:
- 通過對象的isa 指針找到對應(yīng)的Class;
2.通過類的isa 找到Class對應(yīng)的 Meta Class; - 在Meta Class 的methodLists, 找到對應(yīng)的方法, 然后執(zhí)行;
那么Meta Class(元類)也應(yīng)該是一個對象, OC 的設(shè)計者讓所有的元類的isa 指向基類的元類, 而基類的元類指向自己;
OC Method(方法調(diào)用)
Method 的定義:
// Method 被定義為一個 objc_method 指針
typedef struct objc_method *Method;
在看結(jié)構(gòu)體obje_method 怎么定義的:
##### objc_method
//在 objc_method 結(jié)構(gòu)體中,包含一個 SEL 和一個 IMP
struct objc_method {
SEL method_name;
char *method_types;
IMP method_imp;
}
SEL定義
SEL 是一個指向 objc_selector 的指針接校,而 objc_selector 在頭文件中找不到明確的定義
typedef struct objc_selector *SEL;
我們來測試以下代碼:
SEL sel = @selector(viewDidLoad);
NSLog(@"%s", sel); // 輸出:viewDidLoad
SEL sel1 = @selector(viewDidLoad1);
NSLog(@"%s", sel1); // 輸出:viewDidLoad1
可以看到,SEL 不過是保存了方法名的一串字符,因此我們可以認(rèn)為,SEL 就是一個保存方法名的字符串.
由于一個 Method 只保存了方法的方法名猛频,并最終要根據(jù)方法名來查找方法的實現(xiàn),因此在Objective-C 中不支持下面這種定義.
-(void)setWidth:(int)width;
-(void)setWidth:(double)width;
IMP定義
typedef id (*IMP)(id, SEL, ...);
再來說 IMP ≈朊悖可以看到它是一個「函數(shù)指針.簡單來說鹿寻,「函數(shù)指針」就是用來找到函數(shù)地址,然后執(zhí)行函數(shù).
這里要注意诽凌, IMP 指向的函數(shù)的前兩個參數(shù)是默認(rèn)參數(shù)毡熏, id 和 SEL 。這里的 SEL 好理解侣诵,就是函數(shù)名,而 id 痢法,對于實例方法來說, self 保存了當(dāng)前對象的地址杜顺;對于類方法來說财搁, self 保存了當(dāng)前對應(yīng)類對象的地址,后面的省略號即是參數(shù)列表。
敲黑板: Method 建立了 SEL 和 IMP 的關(guān)聯(lián)躬络,當(dāng)對一個對象發(fā)送消息時尖奔,會通過給出的 SEL 去找到 IMP ,然后執(zhí)行.
在 Objective-C 中穷当,所有的方法調(diào)用提茁,都會轉(zhuǎn)化成向?qū)ο蟀l(fā)送消息。發(fā)送消息主要是使用 objc_msgSend 函數(shù); 看一下頭文件定義:
id objc_msgSend(id self, SEL op, ...);
// 譬如:
[self loadView];
// 調(diào)用時就會變成:
objc_msgSend(self, @selector(loadView));
當(dāng)向一個對象發(fā)送消息時馁菜,會去這個類的 methodLists 中查找相應(yīng)的 SEL 茴扁,如果查不到,則通過 super_class 指針找到父類火邓,再去父類的 methodLists 中查找丹弱,層層遞進(jìn)。最后仍然找不到铲咨,才走拋異常流程;
當(dāng)一個方法找不到的時候躲胳,會走攔截調(diào)用和消息轉(zhuǎn)發(fā)流程。我們可以重寫 +resolveClassMethod: 和 +resolveInstanceMethod: 方法纤勒,在程序崩潰前做一些處理坯苹。通常的做法是動態(tài)添加一個方法,并返回 YES 告訴程序已經(jīng)成功處理消息摇天。如果這兩個方法返回 NO 粹湃,這個流程會繼續(xù)往下走.
Category(分類)
我們來看一下 Category 在頭文件中的定義:
typedef struct objc_category *Category;
struct objc_category {
char * category_name;
char * class_name;
struct objc_method_list * instance_methods;
struct objc_method_list * class_methods;
struct objc_protocol_list * protocols;
}
Category 是一個指向 objc_category 結(jié)構(gòu)體的指針乾吻,在 objc_category 中包含對象方法列表铣缠、類方法列表崇棠、協(xié)議列表撒汉。從這里我們也可以看出蟋字, Category 支持添加對象方法承粤、類方法择葡、協(xié)議肴茄,但不能保存成員變量纯丸。
注意:在 Category 中是可以添加屬性的偏形,但不會生成對應(yīng)的成員變量、 getter 和 setter 觉鼻。因此俊扭,調(diào)用 Category 中聲明的屬性時會報錯。
我們可以通過「關(guān)聯(lián)對象」的方式來添加可用的屬性坠陈。具體操作如下:
1萨惑、在 UIViewController+Tag.h 文件中聲明 property 。
@property (nonatomic, strong) NSString *tag;
2仇矾、在 UIViewController+Tag.m 中實現(xiàn) getter 和 setter 咒钟。記得添加頭文件 #import。主要是用到 objc_setAssociatedObject 和 objc_getAssociatedObject 這兩個方法若未。
static void *tag = &tag;
@implementation UIViewController (Tag)
- (void)setTag:(NSString *)t {
objc_setAssociatedObject(self, &tag, t, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)tag {
return objc_getAssociatedObject(self, &tag);
}
@end
3朱嘴、在子類中調(diào)用。
// 子類 ViewController.m
- (void)testCategroy {
self.tag = @"TAG";
NSLog(@"%@", self.tag); // 這里輸出:TAG
}
注意:當(dāng)一個對象被釋放后粗合, Runtime 回去查找這個對象是否有關(guān)聯(lián)的對象萍嬉,有的話,會將它們釋放掉隙疚。因此不需要我們手動去釋放壤追。
Runtime 常規(guī)操作
1. Method Swizzling 方法交換
首先來介紹一下被稱為「黑魔法」的 Method Swizzling,Method Swizzling 使我們有辦法在程序運行的時候,去修改一個方法的實現(xiàn)供屉。包括原生類(比如 UIKit 中的類)的方法行冰。首先來看下通常的寫法:
Method originalMethod = class_getInstanceMethod(class, (originalSelector));
Method swizzledMethod = class_getInstanceMethod(class, (swizzledSelector));
if (!class_addMethod((class),
(originalSelector),
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod))) {
method_exchangeImplementations(originalMethod, swizzledMethod);
} else {
class_replaceMethod((class),
(swizzledSelector),
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
}
簡單描述一下:先獲取 originalMethod 和 swizzledMethod 溺蕉。將 originalMethod 加到想要交換方法的類中(注意此時的 IMP 是 swizzledMethod 的 IMP ),如果加入成功悼做,就用 originalMethod 的 IMP 替換掉 swizzledMethod 的 IMP 疯特;如果加入失敗,則直接交換 originalMethod 和 swizzledMethod 的 IMP 肛走。
那么問題來了漓雅,為什么不直接用 method_exchangeImplementations 來交換就好?
因為可能會影響父類中的方法朽色。比如我們在一個子類中邻吞,去交換一個父類中的方法,而這個方法在子類中沒有實現(xiàn)葫男,這個時候父類的方法就指向了子類的實現(xiàn)抱冷,當(dāng)這個方法被調(diào)用的時候就會出問題。所以先采取添加方法的方式梢褐,如果添加失敗徘层,證明子類已經(jīng)實現(xiàn)了這個方法,直接用 method_exchangeImplementations 來交換利职;如果添加成功趣效,則說明沒有實現(xiàn)這個方法,采取先添加后替換的方式猪贪。這樣就能保證不影響父類了跷敬。
如果每次交換都寫這么多就太麻煩了,我們可以定義成一個宏热押,使用起來更方便西傀。
#define SwizzleMethod(class, originalSelector, swizzledSelector) { \
Method originalMethod = class_getInstanceMethod(class, (originalSelector)); \
Method swizzledMethod = class_getInstanceMethod(class, (swizzledSelector)); \
if (!class_addMethod((class), \
(originalSelector), \
method_getImplementation(swizzledMethod), \
method_getTypeEncoding(swizzledMethod))) { \
method_exchangeImplementations(originalMethod, swizzledMethod); \
} else { \
class_replaceMethod((class), \
(swizzledSelector), \
method_getImplementation(originalMethod), \
method_getTypeEncoding(originalMethod)); \
} \
}
在 +load 中調(diào)用:
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SwizzleMethod([self class], @selector(viewWillAppear:), @selector(AA_viewWillAppear:));
});
}
注意:我們要保證方法只會被交換一次。因為 +load 方法原則上只會被調(diào)用一次桶癣,所以一般將 Method Swizzling 放在 +load 方法中執(zhí)行拥褂。但 +load 方法也可能被其他類手動調(diào)用,這時候就有可能會被交換多次牙寞,所以這里用 dispatch_once_t 來保證只執(zhí)行一次饺鹃。
那么上面的交換操作是否萬無一失了呢?還遠(yuǎn)遠(yuǎn)不夠间雀。
通常情況下上面的交換不會出什么問題悔详,但考慮下面一種場景。(注: ViewController 繼承自 UIViewController )
修改 UIViewController 中的 viewWillAppear: :
// UIViewController (Swizzling)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SwizzleMethod([self class], @selector(viewWillAppear:), @selector(AA_viewWillAppear:));
});
}
- (void)AA_viewWillAppear:(BOOL)animated {
NSLog(@"UIViewController");
[self AA_viewWillAppear:animated];
}
修改 ViewController 中的 viewWillAppear: (注: ViewController 沒有重寫該方法):
// ViewController (Swizzling)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SwizzleMethod([self class], @selector(viewWillAppear:), @selector(BB_viewWillAppear:));
});
}
- (void)BB_viewWillAppear:(BOOL)animated {
NSLog(@"ViewController");
[self BB_viewWillAppear:animated];
}
這里父類和子類同時對 viewWillAppear: 方法進(jìn)行交換惹挟,每次交換都加入一句輸出語句茄螃。則當(dāng) ViewController 調(diào)用 viewWillAppear: 時,我們期望輸出下面結(jié)果:
ViewController
UIViewController
大部分情況的確是這樣连锯,但也有可能只輸出:
ViewController
因為我們是在 +load 中做交換操作归苍,而子類的 +load 卻有可能先于父類執(zhí)行用狱。這樣造成的結(jié)果是,子類先拷貝父類的 viewWillAppear: 拼弃,并進(jìn)行交換夏伊,然后父類再進(jìn)行交換。但這個時候父類的交換結(jié)果并不會影響子類肴敛,也無法將 NSLog(@"UIViewController") 寫入子類的 viewWillAppear: 方法中,所以不會輸出吗购。
這里解決這個問題的思路是:在子類的 swizzledMethod 中医男,動態(tài)地去查找父類替換后方法的實現(xiàn)。每次調(diào)用都會去父類重新查找捻勉,而不是拷貝寫死在子類的新方法中镀梭。這樣子類 viewWillAppear: 方法的執(zhí)行結(jié)果就和 +load 的加載順序無關(guān)了。
至于怎么實現(xiàn)動態(tài)查找踱启,這里推薦 RSSwizzle 报账,這個庫不僅解決了上面提到的問題,還保證了 Method Swizzling 的線程安全埠偿,是一種更安全優(yōu)雅的解決方案透罢。簡單使用舉例:
RSSwizzleInstanceMethod([self class],
@selector(viewWillAppear:),
RSSWReturnType(void),
RSSWArguments(BOOL animated),
RSSWReplacement({
NSLog(@"ViewController");
RSSWCallOriginal(animated);
}), RSSwizzleModeAlways, NULL);
2.獲取所有屬性和方法
Runtime 中提供了一系列 API 來獲取 Class 的成員變量( Ivar )、屬性( Property )冠蒋、方法( Method )羽圃、協(xié)議( Protocol )等.
// 測試 打印屬性列表
- (void)testPrintPropertyList {
unsigned int count;
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i=0; i
const char *propertyName = property_getName(propertyList[i]);
NSLog(@"property----="">%@", [NSString stringWithUTF8String:propertyName]);
}
free(propertyList);
}
// 測試 打印方法列表
- (void)testPrintMethodList {
unsigned int count;
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i=0; i
Method method = methodList[i];
NSLog(@"method----="">%@", NSStringFromSelector(method_getName(method)));
}
free(methodList);
}
// 測試 打印成員變量列表
- (void)testPrintIvarList {
unsigned int count;
Ivar *ivarList = class_copyIvarList([self class], &count);
for (unsigned int i=0; i
Ivar myIvar = ivarList[i];
const char *ivarName = ivar_getName(myIvar);
NSLog(@"ivar----="">%@", [NSString stringWithUTF8String:ivarName]);
}
free(ivarList);
}
// 測試 打印協(xié)議列表
- (void)testPrintProtocolList {
unsigned int count;
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (unsigned int i=0; i
Protocol *myProtocal = protocolList[i];
const char *protocolName = protocol_getName(myProtocal);
NSLog(@"protocol----="">%@", [NSString stringWithUTF8String:protocolName]);
}
free(protocolList);
}
RunTime 的使用場景
1.面向切面編程(AOP)
AOP為Aspect Oriented Programming的縮寫,意為:面向切面編程抖剿,通過預(yù)編譯方式和運行期動態(tài)代理實現(xiàn)程序功能的統(tǒng)一維護(hù)的一種技術(shù)朽寞。AOP是OOP的延續(xù),是軟件開發(fā)中的一個熱點斩郎,也是Spring框架中的一個重要內(nèi)容脑融,是函數(shù)式編程的一種衍生范型。利用AOP可以對業(yè)務(wù)邏輯的各個部分進(jìn)行隔離缩宜,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低肘迎,提高程序的可重用性,同時提高了開發(fā)的效率.
畫重點锻煌,對業(yè)務(wù)邏輯進(jìn)行分離膜宋,降低耦合度。
假設(shè)現(xiàn)在有這樣一個需求炼幔,我們要對應(yīng)用中所有按鈕的點擊事件進(jìn)行上報秋茫,統(tǒng)計每個按鈕被點擊的次數(shù)。
首先我們要明確乃秀,統(tǒng)計功能應(yīng)該與業(yè)務(wù)無關(guān)肛著,即統(tǒng)計代碼不應(yīng)該與業(yè)務(wù)代碼耦合在一起圆兵。因此用上面「AOP」的思想來實現(xiàn)是合適的,而 Runtime 給我們提供了這樣一條途徑枢贿。因為當(dāng)按鈕點擊時殉农,會調(diào)用 sendAction:to:forEvent: 方法,所以我們可以使用 Method Swizzling 來修改該方法局荚,在其中添加上報的邏輯.
// UIButton+Swizzling.m
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
RSSwizzleInstanceMethod([self class],
@selector(sendAction:to:forEvent:),
RSSWReturnType(void),
RSSWArguments(SEL action, id target, UIEvent *event),
RSSWReplacement({
NSString *name = NSStringFromClass([self class]);
NSLog(@"UIButton+Swizzling:%@ 按鈕被點擊--上報", name);
RSSWCallOriginal(action, target, event);
}), RSSwizzleModeAlways, NULL);
});
}
注意:盡管上面的需求也可以用繼承一個基類的方式來實現(xiàn)超凳,但是如果此時已經(jīng)有很多類繼承自 UIButton ,則修改起來會很麻煩耀态,其次我們也不能保證后續(xù)的所有按鈕都繼承這個基類轮傍。另外上面提到,統(tǒng)計邏輯不應(yīng)該和業(yè)務(wù)邏輯耦合首装,如果為了統(tǒng)計的需求去修改業(yè)務(wù)代碼创夜,也是不可取的(除非迫不得已)。因此上面利用 Method Swizzling 的方式更為合適仙逻,也更為簡潔驰吓。
2.dictionary 轉(zhuǎn)model
我們可以用 KVC 來實現(xiàn)字典轉(zhuǎn)模型,方法是調(diào)用 setValuesForKeysWithDictionary: 系奉。但這種方法要求 Model 的屬性和 NSDictionary 的 key 一一對應(yīng)檬贰,否則就會報錯。這里可以用 Runtime 配合 KVC 缺亮,來實現(xiàn)更靈活的字典轉(zhuǎn)模型偎蘸。
下面為 NSObject 添加一個分類,添加一個初始化方法瞬内,來看代碼:
// NSObject+JSONExtension.h
- (instancetype)initWithDictionary:(NSDictionary *)dictionary {
self = [self init];
if (self) {
unsigned int count;
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i=0; i
// 獲取屬性列表
const char *propertyName = property_getName(propertyList[i]);
NSString *name = [NSString stringWithUTF8String:propertyName];
id value = [dictionary objectForKey:name];
if (value) {
// 注意這里用到 KVC
[self setValue:value forKey:name];
}
}
free(propertyList);
}
return self;
}
嘗試調(diào)用:
NSDictionary *info = @{@"title": @"標(biāo)題", @"count": @(1), @"test": @"hello"};
ObjectA *objectA = [[ObjectA alloc] initWithDictionary:info];
NSLog(@"%@", objectA.title); // 輸出:標(biāo)題
NSLog(@"%ld", (long)objectA.count); // 輸出:1
注意:在實際的應(yīng)用中迷雪,會有更多復(fù)雜的情況需要考慮,比如字典中包含數(shù)組虫蝶、對象等章咧。這里只是做個簡單示例.
3. 進(jìn)行歸解檔
「歸檔」是將對象序列化存入沙盒文件的過程,會調(diào)用 encodeWithCoder: 來序列化能真×扪希「解檔」是將沙盒文件中的數(shù)據(jù)反序列化讀入內(nèi)存的過程,會調(diào)用 initWithCoder: 來反序列化粉铐。
通常來說疼约,歸解檔需要對實例對象的各個屬性依次進(jìn)行歸檔和解檔,十分繁瑣且易出錯蝙泼。這里我們參照「字典轉(zhuǎn)模型」的例子程剥,通過獲取類的所有屬性,實現(xiàn)自動歸解檔汤踏。
觸發(fā)對象歸檔可以調(diào)用 NSKeyedArchiver 的 + archiveRootObject:toFile: 方法织鲸;觸發(fā)對象解檔可以調(diào)用 NSKeyedUnarchiver 的 + unarchiveObjectWithFile: 方法舔腾。
注: xib 文件在載入的時候,也會觸發(fā) initWithCoder: 方法搂擦,可見讀取 xib 文件也是一個解檔的過程稳诚。
首先在 NSObject 的分類中添加兩個方法:
// NSObject+JSONExtension.m
- (void)initAllPropertiesWithCoder:(NSCoder *)coder {
unsigned int count;
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i=0; i
const char *propertyName = property_getName(propertyList[i]);
NSString *name = [NSString stringWithUTF8String:propertyName];
id value = [coder decodeObjectForKey:name];
[self setValue:value forKey:name];
}
free(propertyList);
}
- (void)encodeAllPropertiesWithCoder:(NSCoder *)coder {
unsigned int count;
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i=0; i
const char *propertyName = property_getName(propertyList[i]);
NSString *name = [NSString stringWithUTF8String:propertyName];
id value = [self valueForKey:name];
[coder encodeObject:value forKey:name];
}
free(propertyList);
}
在 NSObject 的子類中實現(xiàn)歸解檔方法:
// ObjectA.m
- (id)initWithCoder:(NSCoder *)aDecoder{
self = [super init];
if (self) {
[self initAllPropertiesWithCoder:aDecoder];
}
return self;
}
-(void)encodeWithCoder:(NSCoder *)aCoder{
[self encodeAllPropertiesWithCoder:aCoder];
}
嘗試調(diào)用:
NSDictionary *info = @{@"title": @"標(biāo)題11", @"count": @(11)};
NSString *path = [NSString stringWithFormat:@"%@/objectA.plist", NSHomeDirectory()];
// 歸檔
ObjectA *objectA = [[ObjectA alloc] initWithDictionary:info];
[NSKeyedArchiver archiveRootObject:objectA toFile:path];
// 解檔
ObjectA *objectB = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
NSLog(@"%@", objectB.title); // 輸出:標(biāo)題11
NSLog(@"%ld", (long)objectB.count); // 輸出:11
注:上面的代碼邏輯并不完善,只是做簡單示例用瀑踢。
4. 逆向開發(fā)
在「逆向開發(fā)」中扳还,會用到一個叫 class-dump 的工具。這個工具可以將已脫殼的 APP 的所有類的頭文件導(dǎo)出橱夭,為分析 APP 做準(zhǔn)備氨距。這里也是利用 Runtime 的特性,將存儲在mach-O文件中的 @interface 和 @protocol 信息提取出來徘钥,并生成對應(yīng)的 .h 文件衔蹲。
5. 熱修復(fù)
「熱修復(fù)」是一種不需要發(fā)布新版本肢娘,通過動態(tài)下發(fā)修復(fù)文件來修復(fù) Bug 的方式呈础。比如 JSPatch,就是利用 Runtime 強大的動態(tài)能力橱健,對出問題的代碼段進(jìn)行替換而钞。
源碼
請到 GitHub 上查看完整例子。
參考
OC最實用的runtime總結(jié)拘荡,面試臼节、工作你看我就足夠了!
runtime 面試題
1. [self class]和[super class]返回的分別是什么珊皿?
[self class]會被編譯器轉(zhuǎn)化成 objc_msgSend(self, @selector(class))
[super class]會被編譯器轉(zhuǎn)化成 objc_msgSendSuper(super, @selector(class)).
super在這里只是一個結(jié)構(gòu)體(objc_super)网缝,objc_super結(jié)構(gòu)體重有一個 receiver的變量,也就是方法的接收者蟋定,這里的接收者也是當(dāng)前對象self粉臊。
所以這兩個消息傳遞的接收者都是當(dāng)前對象self,兩個方法的運行結(jié)果也就相同驶兜,都會返回當(dāng)前對象的class扼仲。
2. 講一下對象、類對象抄淑、元類跟元類的結(jié)構(gòu)體是如何關(guān)聯(lián)的屠凶?
1). 所有的OC對象的基礎(chǔ)結(jié)構(gòu)體都是objc_object,而類和元類的結(jié)構(gòu)體objc_class也是繼承于objc_object的肆资,所以類和元類其實也是一種“對象”.
2). objc_object有isa指針矗愧,對象、類對象郑原、元類對象正是通過isa指針相連.
3. 為什么對象方法沒有保存到對象的結(jié)構(gòu)體里贱枣,而是保存在類對象的結(jié)構(gòu)體里监署?
1).假如保存在對象中,當(dāng)我已經(jīng)創(chuàng)建一個對象后纽哥,再為這個類動態(tài)的添加一個對象方法钠乏,這時由于已經(jīng)創(chuàng)建的對象的內(nèi)存布局已經(jīng)確定,也就沒辦法添加這個方法了春塌,這個對象就會有問題晓避。也就無法實現(xiàn)動態(tài)添加方法的功能。
2). 將對象方法保存在類對象類只壳,那么所有的對象方法只要在類對象中保存一份即可俏拱,而不需要再每個對象里都保存一份,會節(jié)省大量的內(nèi)存吼句。
3). 正是因為方法保存在類對象中锅必,而每次調(diào)用時通過isa指針找到類對象再找到方法調(diào)用,才保證了OC語言的動態(tài)特性惕艳,方法可以動態(tài)添加搞隐、修改,否則就不能實現(xiàn)動態(tài)的效果远搪。
4). 在方法的實際調(diào)用中有這樣的過程:先找緩存->在找當(dāng)前類中的對象方法列表->再逐級查找父類的對象方法列表劣纲,通過把常用的方法放入緩存中,及提高了訪問速度谁鳍,也節(jié)省了不必要的內(nèi)存開銷癞季。通過把對象方法放在父類才能很好的實現(xiàn)這個功能,放在對象中實現(xiàn)的話倘潜,就非常浪費內(nèi)存绷柒。
4.class_ro_t和class_rw_t的區(qū)別? PS: ro == readonly, rw == readwrite
1). class_ro_t是class_rw_t的一個變量涮因,用于保存宿主類的成員變量列表信息废睦、協(xié)議列表、對象方法列表蕊退、屬性列表信息(這三個列表都是一位數(shù)組)郊楣,這些是在類注冊到runtime中就固定了,無法再修改.
2). class_rw_t的成員變量除了class_ro_t之外瓤荔,還有methods净蚤、protocols、property三個二維數(shù)組输硝,之所以是二維數(shù)組是因為一個類可以有多個分類今瀑,而每個分類都可以有多個方法、協(xié)議、屬性橘荠,所以就是二維數(shù)組屿附。class_rw_t是可以動態(tài)修改的,當(dāng)運行時動態(tài)的給一個類添加一個方法時哥童,這個方法就保存在methods中挺份。
這里再說明一下,類在編譯時初始化時會創(chuàng)建class_ro_t結(jié)構(gòu)體贮懈,里邊的內(nèi)容都是只讀的匀泊,在運行時初始化會創(chuàng)建class_rw_t,將class_rw_t的ro指針指向之前創(chuàng)建的class_ro_t結(jié)構(gòu)體朵你,并將class_ro_t中的方法列表添加到class_rw_t的methodList中各聘,在之后進(jìn)行方法查找時就不需要再查class_ro_t了.
5. SEL和IMP的區(qū)別?
SEL相當(dāng)于是方法名抡医,IMP則是函數(shù)指針躲因,在類對象的方法緩存哈希表中,SEL和IMP相當(dāng)于對應(yīng)著key和value.
6. 能否向編譯后的類中增加實例變量忌傻?能否向運行時創(chuàng)建的類中添加實例變量大脉?為什么?
1). 不能芯勘,當(dāng)一個類已經(jīng)編譯好了箱靴,它就已經(jīng)注冊到runtime中腺逛,其對象的內(nèi)存布局已經(jīng)確定荷愕,無法再修改。假如能修改的話棍矛,當(dāng)我已經(jīng)創(chuàng)建了一個對象A安疗,這時這個A的內(nèi)存大小和布局已經(jīng)確定,如果這時動態(tài)向類A中增加一個Int型的變量够委,則意味著新的對象的內(nèi)存大小和布局都要改變荐类,而這個對象A已經(jīng)確定下來無法改變,也就成了廢對象茁帽,這會引起嚴(yán)重的后果玉罐。
2). 但在運行時創(chuàng)建的類還沒注冊到runtime中,可以為其添加成員變量潘拨,因為此時的內(nèi)存布局還未確定下來。
7. 在運行時創(chuàng)建類的方法objc_allocateClassPair的方法名尾部為什么是pair(成對的意思)铁追?
一個是類對象季蚂,一個是元類對象,所以是pair
8. 談一談對消息機(jī)制的理解?
1). runtime的核心就是消息機(jī)制,而runtime又是OC語言的核心扭屁。當(dāng)我們正常通過OC語言調(diào)用一個方法[objc func]時算谈,并不是直接調(diào)用其函數(shù)實現(xiàn),而是向objc對象發(fā)送了一個消息料滥。至于具體的方法實現(xiàn)是什么然眼,在編譯時是并不知道的,只有在運行時才能確定下來葵腹。這就是消息機(jī)制罪治,而通過這種消息的方式,則體現(xiàn)了OC語言的動態(tài)屬性礁蔗,使用起來更加靈活觉义。
2). 具體在調(diào)用[objc func]時,是在調(diào)用obj對象的名稱為func的對象方法浴井,對象方法是保存在類對象中的晒骇,所以obj會通過其isa指針找到其類對象,然后先查方法緩存中是否已經(jīng)有此方法(具體查找方法為哈希查找)磺浙。如果沒找到的話洪囤,就在類對象的對象方法列表中查找func方法(如果方法列表已經(jīng)排過序就按二分法查找,如果沒排序就按遍歷查找撕氧,并且以先查找分類的方法瘤缩,后查找宿主類的方法)。如果還沒有查找到伦泥,就逐級查找父類的方法緩存和方法列表剥啤。如果找到func方法,就把其IMP緩存到obj的類對象的方法緩存中不脯。如果到根類都沒找到的話府怯,就進(jìn)入到消息轉(zhuǎn)發(fā)流程。
9.轉(zhuǎn)發(fā)消息的流程?
1). 動態(tài)解析環(huán)節(jié):查看當(dāng)前類有沒有實現(xiàn)動態(tài)解析
+(BOOL)resolveInstanceMethod:(SEL)selector 或者
+(BOOL)resolveClassMethod:(SEL)selector 如果已經(jīng)實現(xiàn)就調(diào)用防楷。使用這個方法解決問題的前提是牺丙,相關(guān)方法的實現(xiàn)代碼已經(jīng)存在,只是要在運行時動態(tài)的插到類里面复局。此方法可以用來實現(xiàn)@dynamic(阻止編譯器自動生成類方法)
2). 快速轉(zhuǎn)發(fā)環(huán)節(jié):如果沒實現(xiàn)就進(jìn)入到快速轉(zhuǎn)發(fā)流程 - (id)forwardingTargetForSelector:(SEL)selector;將當(dāng)前selector消息轉(zhuǎn)發(fā)到其他對象上冲簿。
3). 完整轉(zhuǎn)發(fā)環(huán)節(jié):如果還沒實現(xiàn)則進(jìn)入到最終的轉(zhuǎn)發(fā)流程
3.1 先調(diào)用-(NSMethodSignature *)methodSignatureForSelector:(SEL)selector,根據(jù)selector返回一個方法簽名亿昏,這個簽名按規(guī)則包含了返回值類型峦剔、傳參類型(最常見的“v@:”,v表示返回值為void類型,@表示參數(shù)為id類型龙优,:表示參數(shù)為SEL)
3.2 再調(diào)用 -(void)forwardInvocation:(NSInvocation *)invocation;可以進(jìn)行消息轉(zhuǎn)發(fā)羊异。如果已經(jīng)重寫了此方法事秀,那么即使沒有實現(xiàn)轉(zhuǎn)發(fā),也不會崩潰了野舶,因此消息轉(zhuǎn)發(fā)也常用來兼容crash情況易迹。