
關(guān)于 Runtime ,網(wǎng)上已經(jīng)有很多很好的文章,寫(xiě)得很詳盡。本篇主要是從新手的角度出發(fā),逐步介紹 Runtime 的原理、常用方法、應(yīng)用場(chǎng)景等。
一、Runtime 是什么
在 C 語(yǔ)言中,將代碼轉(zhuǎn)換為可執(zhí)行程序,一般要經(jīng)歷三個(gè)步驟,即編譯、鏈接、運(yùn)行。在鏈接的時(shí)候,對(duì)象的類(lèi)型、方法的實(shí)現(xiàn)就已經(jīng)確定好了。
而在 Objective-C 中,卻將一些在編譯和鏈接過(guò)程中的工作,放到了運(yùn)行階段。也就是說(shuō),就算是一個(gè)編譯好的 .ipa 包,在程序沒(méi)運(yùn)行的時(shí)候,也不知道調(diào)用一個(gè)方法會(huì)發(fā)生什么。這也為后來(lái)大行其道的「熱修復(fù)」提供了可能。因此我們稱(chēng) Objective-C 為一門(mén)動(dòng)態(tài)語(yǔ)言。
這樣的設(shè)計(jì)使 Objective-C 變得靈活,甚至可以讓我們?cè)诔绦蜻\(yùn)行的時(shí)候,去動(dòng)態(tài)修改一個(gè)方法的實(shí)現(xiàn)。而實(shí)現(xiàn)這一切的基礎(chǔ)就是 Runtime 。
簡(jiǎn)單來(lái)說(shuō), Runtime 是一個(gè)庫(kù),這個(gè)庫(kù)使我們可以在程序運(yùn)行時(shí)創(chuàng)建對(duì)象、檢查對(duì)象,修改類(lèi)和對(duì)象的方法。
至于這個(gè)庫(kù)是怎么實(shí)現(xiàn)的,請(qǐng)緊張刺激地往下看。
二、Runtime 是怎么工作的
要了解 Runtime 是怎么工作的,首先要知道類(lèi)和對(duì)象在 Objective-C 中是怎么定義的。
注意:以下會(huì)用到
C語(yǔ)言中結(jié)構(gòu)體的內(nèi)容,包括結(jié)構(gòu)體的定義、為結(jié)構(gòu)體定義別名等。如果你對(duì)這塊不熟悉,建議先復(fù)習(xí)一下這塊的語(yǔ)法。傳送門(mén)
1. Class 和 Object
在 objc.h 中, Class 被定義為指向 objc_class 的指針,定義如下:
typedef struct objc_class *Class;
而 objc_class 是一個(gè)結(jié)構(gòu)體,在 runtime.h 中的定義如下:
struct objc_class {
Class isa; // 實(shí)現(xiàn)方法調(diào)用的關(guān)鍵
Class super_class; // 父類(lèi)
const char * name; // 類(lèi)名
long version; // 類(lèi)的版本信息,默認(rèn)為0
long info; // 類(lèi)信息,供運(yùn)行期使用的一些位標(biāo)識(shí)
long instance_size; // 該類(lèi)的實(shí)例變量大小
struct objc_ivar_list * ivars; // 該類(lèi)的成員變量鏈表
struct objc_method_list ** methodLists; // 方法定義的鏈表
struct objc_cache * cache; // 方法緩存
struct objc_protocol_list * protocols; // 協(xié)議鏈表
};
為了方便理解,我這里去掉了一些聲明,主要是和
Objective-C語(yǔ)言版本相關(guān),這里可以暫時(shí)忽略。完整的定義可以自己去runtime.h中查看。
提示:在 Xcode 中,使用快捷鍵
command + shift + o,可以打開(kāi)搜索窗口,輸入objc_class即可看到頭文件定義。
可以看到,一個(gè)類(lèi)保存了自身所有的成員變量( ivars )、所有的方法( methodLists )、所有實(shí)現(xiàn)的協(xié)議( objc_protocol_list )。
比較重要的字段還有 isa 和 cache ,它們是什么東西,先不著急,我們來(lái)看下 Objective-C 中對(duì)象的定義。
struct objc_object {
Class isa;
};
typedef struct objc_object *id;
這里看到了我們熟悉的 id ,一般我們用它來(lái)實(shí)現(xiàn)類(lèi)似于 C++ 中泛型的一些操作,該類(lèi)型的對(duì)象可以轉(zhuǎn)換為任意一種對(duì)象。在這里 id 被定義為一個(gè)指向 objc_object 的指針。說(shuō)明 objc_object 就是我們平時(shí)常用的對(duì)象的定義,它只包含一個(gè) isa 指針。
也就是說(shuō),一個(gè)對(duì)象唯一保存的信息就是它的 Class 的地址。當(dāng)我們調(diào)用一個(gè)對(duì)象的方法時(shí),它會(huì)通過(guò) isa 去找到對(duì)應(yīng)的 objc_class,然后再在 objc_class 的 methodLists 中找到我們調(diào)用的方法,然后執(zhí)行。
再說(shuō)說(shuō) cache ,因?yàn)檎{(diào)用方法的過(guò)程是個(gè)查找 methodLists 的過(guò)程,如果每次調(diào)用都去查找,效率會(huì)非常低。所以對(duì)于調(diào)用過(guò)的方法,會(huì)以 map 的方式保存在 cache 中,下次再調(diào)用就會(huì)快很多。
2. Meta Class 元類(lèi)
上一小節(jié)講了 Objective-C 中類(lèi)和對(duì)象的定義,也講了調(diào)用對(duì)象方法的實(shí)現(xiàn)過(guò)程。但還留下了許多問(wèn)題,比如調(diào)用一個(gè)對(duì)象的類(lèi)方法的過(guò)程是怎么樣的?還有 objc_class 中也有一個(gè) isa 指針,它是干嘛用的?
現(xiàn)在劃重點(diǎn),在 Objective-C 中,類(lèi)也被設(shè)計(jì)為一個(gè)對(duì)象。
其實(shí)觀察 objc_class 和 objc_object 的定義,會(huì)發(fā)現(xiàn)兩者其實(shí)本質(zhì)相同(都包含 isa 指針),只是 objc_class 多了一些額外的字段。相應(yīng)的,類(lèi)也是一個(gè)對(duì)象,只是保存了一些字段。
既然說(shuō)類(lèi)也是對(duì)象,那么類(lèi)的類(lèi)型是什么呢?這里就引出了另外一個(gè)概念 —— Meta Class(元類(lèi))。
在 Objective-C 中,每一個(gè)類(lèi)都有對(duì)應(yīng)的元類(lèi)。而在元類(lèi)的 methodLists 中,保存了類(lèi)的方法鏈表,即所謂的「類(lèi)方法」。并且類(lèi)的 isa 指針指向?qū)?yīng)的元類(lèi)。因此上面的問(wèn)題答案就呼之欲出,調(diào)用一個(gè)對(duì)象的類(lèi)方法的過(guò)程如下:
- 通過(guò)對(duì)象的
isa指針找到對(duì)應(yīng)的類(lèi)。 - 通過(guò)類(lèi)的
isa指針找到對(duì)應(yīng)元類(lèi)。 - 在元類(lèi)的
methodLists中,找到對(duì)應(yīng)的方法,然后執(zhí)行。
注意:上面類(lèi)方法的調(diào)用過(guò)程不考慮繼承的情況,這里只是說(shuō)明一下類(lèi)方法的調(diào)用原理,完整的調(diào)用流程在后面會(huì)提到。
這么說(shuō)來(lái)元類(lèi)也有一個(gè) isa 指針,元類(lèi)也應(yīng)該是一個(gè)對(duì)象。的確是這樣。那么元類(lèi)的 isa 指向哪里呢?為了不讓這種結(jié)構(gòu)無(wú)限延伸下去, Objective-C 的設(shè)計(jì)者讓所有的元類(lèi)的 isa 指向基類(lèi)(比如 NSObject )的元類(lèi)。而基類(lèi)的元類(lèi)的 isa 指向自己。這樣就形成了一個(gè)完美的閉環(huán)。
下面這張圖可以清晰地表示出這種關(guān)系。

同時(shí)注意 super_class 的指向,基類(lèi)的 super_class 指向 nil 。
3. Method
上面講到,「找到對(duì)應(yīng)的方法,然后執(zhí)行」,那么這個(gè)「執(zhí)行」是怎樣進(jìn)行的呢?下面就來(lái)介紹一下 Objective-C 中的方法調(diào)用。
先來(lái)看一下 Method 在頭文件中的定義:
typedef struct objc_method *Method;
struct objc_method {
SEL method_name;
char * method_types;
IMP method_imp;
};
Method 被定義為一個(gè) objc_method 指針,在 objc_method 結(jié)構(gòu)體中,包含一個(gè) SEL 和一個(gè) IMP ,同樣來(lái)看一下它們的定義:
// SEL
typedef struct objc_selector *SEL;
// IMP
typedef id (*IMP)(id, SEL, ...);
1、先說(shuō)一下 SEL 。 SEL 是一個(gè)指向 objc_selector 的指針,而 objc_selector 在頭文件中找不到明確的定義。
我們來(lái)測(cè)試以下代碼:
SEL sel = @selector(viewDidLoad);
NSLog(@"%s", sel); // 輸出:viewDidLoad
SEL sel1 = @selector(viewDidLoad1);
NSLog(@"%s", sel1); // 輸出:viewDidLoad1
可以看到, SEL 不過(guò)是保存了方法名的一串字符。因此我們可以認(rèn)為, SEL 就是一個(gè)保存方法名的字符串。
由于一個(gè) Method 只保存了方法的方法名,并最終要根據(jù)方法名來(lái)查找方法的實(shí)現(xiàn),因此在 Objective-C 中不支持下面這種定義。
- (void)setWidth:(int)width;
- (void)setWidth:(double)width;
2、再來(lái)說(shuō) IMP ??梢钥吹剿且粋€(gè)「函數(shù)指針」。簡(jiǎn)單來(lái)說(shuō),「函數(shù)指針」就是用來(lái)找到函數(shù)地址,然后執(zhí)行函數(shù)。(「函數(shù)指針」了解一下)
這里要注意, IMP 指向的函數(shù)的前兩個(gè)參數(shù)是默認(rèn)參數(shù), id 和 SEL 。這里的 SEL 好理解,就是函數(shù)名。而 id ,對(duì)于實(shí)例方法來(lái)說(shuō), self 保存了當(dāng)前對(duì)象的地址;對(duì)于類(lèi)方法來(lái)說(shuō), self 保存了當(dāng)前對(duì)應(yīng)類(lèi)對(duì)象的地址。后面的省略號(hào)即是參數(shù)列表。
3、到這里, Method 的結(jié)構(gòu)就很明了了。 Method 建立了 SEL 和 IMP 的關(guān)聯(lián),當(dāng)對(duì)一個(gè)對(duì)象發(fā)送消息時(shí),會(huì)通過(guò)給出的 SEL 去找到 IMP ,然后執(zhí)行。
在 Objective-C 中,所有的方法調(diào)用,都會(huì)轉(zhuǎn)化成向?qū)ο蟀l(fā)送消息。發(fā)送消息主要是使用 objc_msgSend 函數(shù)??匆幌骂^文件定義:
id objc_msgSend(id self, SEL op, ...);
可以看到參數(shù)列表和 IMP 指向的函數(shù)參數(shù)列表是相對(duì)應(yīng)的。 Runtime 會(huì)將方法調(diào)用做下面的轉(zhuǎn)換,所以一般也稱(chēng) Objective-C 中的調(diào)用方法為「發(fā)送消息」。
[self doSomething];
objc_msgSend(self, @selector(doSomething));
4、上面看到 objc_msgSend 會(huì)默認(rèn)傳入 id 和 SEL 。這對(duì)應(yīng)了兩個(gè)隱含參數(shù), self 和 _cmd 。這意味著我們可以在方法的實(shí)現(xiàn)過(guò)程中拿到它們,并使用它們。下面來(lái)看個(gè)例子:
- (void)testCmd:(NSNumber *)num {
NSLog(@"%ld", (long)num.integerValue);
num = [NSNumber numberWithInteger:num.integerValue-1];
if (num.integerValue > 0) {
[self performSelector:_cmd withObject:num];
}
}
嘗試調(diào)用:
[self testCmd:@(5)];
上面會(huì)按順序輸出 5, 4, 3, 2, 1 ,然后結(jié)束。即我們可以在方法內(nèi)部用 _cmd 來(lái)調(diào)用方法自身。
5、上面已經(jīng)介紹了方法調(diào)用的大致過(guò)程,下面來(lái)討論類(lèi)之間繼承的情況。重新回去看 objc_class 結(jié)構(gòu)體的定義,當(dāng)中包含一個(gè)指向父類(lèi)的指針 super_class 。
即當(dāng)向一個(gè)對(duì)象發(fā)送消息時(shí),會(huì)去這個(gè)類(lèi)的 methodLists 中查找相應(yīng)的 SEL ,如果查不到,則通過(guò) super_class 指針找到父類(lèi),再去父類(lèi)的 methodLists 中查找,層層遞進(jìn)。最后仍然找不到,才走拋異常流程。
下面的圖演示了一個(gè)基本的消息發(fā)送框架:

6、當(dāng)一個(gè)方法找不到的時(shí)候,會(huì)走攔截調(diào)用和消息轉(zhuǎn)發(fā)流程。我們可以重寫(xiě) +resolveClassMethod: 和 +resolveInstanceMethod: 方法,在程序崩潰前做一些處理。通常的做法是動(dòng)態(tài)添加一個(gè)方法,并返回 YES 告訴程序已經(jīng)成功處理消息。如果這兩個(gè)方法返回 NO ,這個(gè)流程會(huì)繼續(xù)往下走,完整的流程如下圖所示:

4. Category
我們來(lái)看一下 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 是一個(gè)指向 objc_category 結(jié)構(gòu)體的指針,在 objc_category 中包含對(duì)象方法列表、類(lèi)方法列表、協(xié)議列表。從這里我們也可以看出, Category 支持添加對(duì)象方法、類(lèi)方法、協(xié)議,但不能保存成員變量。
注意:在
Category中是可以添加屬性的,但不會(huì)生成對(duì)應(yīng)的成員變量、getter和setter。因此,調(diào)用Category中聲明的屬性時(shí)會(huì)報(bào)錯(cuò)。
我們可以通過(guò)「關(guān)聯(lián)對(duì)象」的方式來(lái)添加可用的屬性。具體操作如下:
1、在 UIViewController+Tag.h 文件中聲明 property 。
@property (nonatomic, strong) NSString *tag;
2、在 UIViewController+Tag.m 中實(shí)現(xiàn) getter 和 setter 。記得添加頭文件 #import <objc/runtime.h> 。主要是用到 objc_setAssociatedObject 和 objc_getAssociatedObject 這兩個(gè)方法。
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、在子類(lèi)中調(diào)用。
// 子類(lèi) ViewController.m
- (void)testCategroy {
self.tag = @"TAG";
NSLog(@"%@", self.tag); // 這里輸出:TAG
}
注意:當(dāng)一個(gè)對(duì)象被釋放后,
Runtime回去查找這個(gè)對(duì)象是否有關(guān)聯(lián)的對(duì)象,有的話,會(huì)將它們釋放掉。因此不需要我們手動(dòng)去釋放。
三、Runtime 的常規(guī)操作
上面簡(jiǎn)單介紹了 Runtime 的原理,接下來(lái)介紹下 Runtime 常用的操作。
1. Method Swizzling 方法交換
首先來(lái)介紹一下被稱(chēng)為「黑魔法」的 Method Swizzling 。 Method Swizzling 使我們有辦法在程序運(yùn)行的時(shí)候,去修改一個(gè)方法的實(shí)現(xiàn)。包括原生類(lèi)(比如 UIKit 中的類(lèi))的方法。首先來(lái)看下通常的寫(xiě)法:
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));
}
簡(jiǎn)單描述一下:先獲取 originalMethod 和 swizzledMethod 。將 originalMethod 加到想要交換方法的類(lèi)中(注意此時(shí)的 IMP 是 swizzledMethod 的 IMP ),如果加入成功,就用 originalMethod 的 IMP 替換掉 swizzledMethod 的 IMP ;如果加入失敗,則直接交換 originalMethod 和 swizzledMethod 的 IMP 。
那么問(wèn)題來(lái)了,為什么不直接用 method_exchangeImplementations 來(lái)交換就好?
因?yàn)榭赡軙?huì)影響父類(lèi)中的方法。比如我們?cè)谝粋€(gè)子類(lèi)中,去交換一個(gè)父類(lèi)中的方法,而這個(gè)方法在子類(lèi)中沒(méi)有實(shí)現(xiàn),這個(gè)時(shí)候父類(lèi)的方法就指向了子類(lèi)的實(shí)現(xiàn),當(dāng)這個(gè)方法被調(diào)用的時(shí)候就會(huì)出問(wèn)題。所以先采取添加方法的方式,如果添加失敗,證明子類(lèi)已經(jīng)實(shí)現(xiàn)了這個(gè)方法,直接用 method_exchangeImplementations 來(lái)交換;如果添加成功,則說(shuō)明沒(méi)有實(shí)現(xiàn)這個(gè)方法,采取先添加后替換的方式。這樣就能保證不影響父類(lèi)了。
如果每次交換都寫(xiě)這么多就太麻煩了,我們可以定義成一個(gè)宏,使用起來(lái)更方便。
#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:));
});
}
注意:我們要保證方法只會(huì)被交換一次。因?yàn)?
+load方法原則上只會(huì)被調(diào)用一次,所以一般將 Method Swizzling 放在+load方法中執(zhí)行。但+load方法也可能被其他類(lèi)手動(dòng)調(diào)用,這時(shí)候就有可能會(huì)被交換多次,所以這里用dispatch_once_t來(lái)保證只執(zhí)行一次。
那么上面的交換操作是否萬(wàn)無(wú)一失了呢?還遠(yuǎn)遠(yuǎn)不夠。
通常情況下上面的交換不會(huì)出什么問(wèn)題,但考慮下面一種場(chǎng)景。(注: 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 沒(méi)有重寫(xiě)該方法):
// 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];
}
這里父類(lèi)和子類(lèi)同時(shí)對(duì) viewWillAppear: 方法進(jìn)行交換,每次交換都加入一句輸出語(yǔ)句。則當(dāng) ViewController 調(diào)用 viewWillAppear: 時(shí),我們期望輸出下面結(jié)果:
ViewController
UIViewController
大部分情況的確是這樣,但也有可能只輸出:
ViewController
因?yàn)槲覀兪窃?+load 中做交換操作,而子類(lèi)的 +load 卻有可能先于父類(lèi)執(zhí)行。這樣造成的結(jié)果是,子類(lèi)先拷貝父類(lèi)的 viewWillAppear: ,并進(jìn)行交換,然后父類(lèi)再進(jìn)行交換。但這個(gè)時(shí)候父類(lèi)的交換結(jié)果并不會(huì)影響子類(lèi),也無(wú)法將 NSLog(@"UIViewController") 寫(xiě)入子類(lèi)的 viewWillAppear: 方法中,所以不會(huì)輸出。
這里解決這個(gè)問(wèn)題的思路是:在子類(lèi)的 swizzledMethod 中,動(dòng)態(tài)地去查找父類(lèi)替換后方法的實(shí)現(xiàn)。每次調(diào)用都會(huì)去父類(lèi)重新查找,而不是拷貝寫(xiě)死在子類(lèi)的新方法中。這樣子類(lèi) viewWillAppear: 方法的執(zhí)行結(jié)果就和 +load 的加載順序無(wú)關(guān)了。
至于怎么實(shí)現(xiàn)動(dòng)態(tài)查找,這里推薦 RSSwizzle ,這個(gè)庫(kù)不僅解決了上面提到的問(wèn)題,還保證了 Method Swizzling 的線程安全,是一種更安全優(yōu)雅的解決方案。簡(jiǎn)單使用舉例:
RSSwizzleInstanceMethod([self class],
@selector(viewWillAppear:),
RSSWReturnType(void),
RSSWArguments(BOOL animated),
RSSWReplacement({
NSLog(@"ViewController");
RSSWCallOriginal(animated);
}), RSSwizzleModeAlways, NULL);
2. 獲取所有屬性和方法
Runtime 中提供了一系列 API 來(lái)獲取 Class 的成員變量( Ivar )、屬性( Property )、方法( Method )、協(xié)議( Protocol )等。直接看代碼:
// 測(cè)試 打印屬性列表
- (void)testPrintPropertyList {
unsigned int count;
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i=0; i<count; i++) {
const char *propertyName = property_getName(propertyList[i]);
NSLog(@"property----="">%@", [NSString stringWithUTF8String:propertyName]);
}
free(propertyList);
}
// 測(cè)試 打印方法列表
- (void)testPrintMethodList {
unsigned int count;
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i=0; i<count; i++) {
Method method = methodList[i];
NSLog(@"method----="">%@", NSStringFromSelector(method_getName(method)));
}
free(methodList);
}
// 測(cè)試 打印成員變量列表
- (void)testPrintIvarList {
unsigned int count;
Ivar *ivarList = class_copyIvarList([self class], &count);
for (unsigned int i=0; i<count; i++) {
Ivar myIvar = ivarList[i];
const char *ivarName = ivar_getName(myIvar);
NSLog(@"ivar----="">%@", [NSString stringWithUTF8String:ivarName]);
}
free(ivarList);
}
// 測(cè)試 打印協(xié)議列表
- (void)testPrintProtocolList {
unsigned int count;
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (unsigned int i=0; i<count; i++) {
Protocol *myProtocal = protocolList[i];
const char *protocolName = protocol_getName(myProtocal);
NSLog(@"protocol----="">%@", [NSString stringWithUTF8String:protocolName]);
}
free(protocolList);
}
因?yàn)檫@里用到的是 C 語(yǔ)言風(fēng)格的變量,所以要注意用 free 來(lái)釋放。至于獲取這些屬性方法有什么用,在下面的「應(yīng)用場(chǎng)景」中會(huì)提到。
四、Runtime 的應(yīng)用場(chǎng)景
說(shuō)了這么多, Runtime 到底有什么用,下面就來(lái)介紹一下常見(jiàn)的幾種應(yīng)用場(chǎng)景。
1. AOP 面向切面編程
來(lái)看一下 百度百科 上對(duì)「AOP」的解釋?zhuān)?/p>
在軟件業(yè),AOP為Aspect Oriented Programming的縮寫(xiě),意為:面向切面編程,通過(guò)預(yù)編譯方式和運(yùn)行期動(dòng)態(tài)代理實(shí)現(xiàn)程序功能的統(tǒng)一維護(hù)的一種技術(shù)。AOP是OOP的延續(xù),是軟件開(kāi)發(fā)中的一個(gè)熱點(diǎn),也是Spring框架中的一個(gè)重要內(nèi)容,是函數(shù)式編程的一種衍生范型。利用AOP可以對(duì)業(yè)務(wù)邏輯的各個(gè)部分進(jìn)行隔離,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性,同時(shí)提高了開(kāi)發(fā)的效率。
畫(huà)重點(diǎn),對(duì)業(yè)務(wù)邏輯進(jìn)行分離,降低耦合度。
假設(shè)現(xiàn)在有這樣一個(gè)需求,我們要對(duì)應(yīng)用中所有按鈕的點(diǎn)擊事件進(jìn)行上報(bào),統(tǒng)計(jì)每個(gè)按鈕被點(diǎn)擊的次數(shù)。
首先我們要明確,統(tǒng)計(jì)功能應(yīng)該與業(yè)務(wù)無(wú)關(guān),即統(tǒng)計(jì)代碼不應(yīng)該與業(yè)務(wù)代碼耦合在一起。因此用上面「AOP」的思想來(lái)實(shí)現(xiàn)是合適的,而 Runtime 給我們提供了這樣一條途徑。因?yàn)楫?dāng)按鈕點(diǎn)擊時(shí),會(huì)調(diào)用 sendAction:to:forEvent: 方法,所以我們可以使用 Method Swizzling 來(lái)修改該方法,在其中添加上報(bào)的邏輯。來(lái)看代碼:
// 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:%@ 按鈕被點(diǎn)擊--上報(bào)", name);
RSSWCallOriginal(action, target, event);
}), RSSwizzleModeAlways, NULL);
});
}
注意:盡管上面的需求也可以用繼承一個(gè)基類(lèi)的方式來(lái)實(shí)現(xiàn),但是如果此時(shí)已經(jīng)有很多類(lèi)繼承自
UIButton,則修改起來(lái)會(huì)很麻煩,其次我們也不能保證后續(xù)的所有按鈕都繼承這個(gè)基類(lèi)。另外上面提到,統(tǒng)計(jì)邏輯不應(yīng)該和業(yè)務(wù)邏輯耦合,如果為了統(tǒng)計(jì)的需求去修改業(yè)務(wù)代碼,也是不可取的(除非迫不得已)。因此上面利用 Method Swizzling 的方式更為合適,也更為簡(jiǎn)潔。
2. 字典轉(zhuǎn)模型
我們可以用 KVC 來(lái)實(shí)現(xiàn)字典轉(zhuǎn)模型,方法是調(diào)用 setValuesForKeysWithDictionary: 。但這種方法要求 Model 的屬性和 NSDictionary 的 key 一一對(duì)應(yīng),否則就會(huì)報(bào)錯(cuò)。這里可以用 Runtime 配合 KVC ,來(lái)實(shí)現(xiàn)更靈活的字典轉(zhuǎn)模型。
下面為 NSObject 添加一個(gè)分類(lèi),添加一個(gè)初始化方法,來(lái)看代碼:
// 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<count; 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
注意:在實(shí)際的應(yīng)用中,會(huì)有更多復(fù)雜的情況需要考慮,比如字典中包含數(shù)組、對(duì)象等。這里只是做個(gè)簡(jiǎn)單示例。
3. 進(jìn)行歸解檔
「歸檔」是將對(duì)象序列化存入沙盒文件的過(guò)程,會(huì)調(diào)用 encodeWithCoder: 來(lái)序列化?!附鈾n」是將沙盒文件中的數(shù)據(jù)反序列化讀入內(nèi)存的過(guò)程,會(huì)調(diào)用 initWithCoder: 來(lái)反序列化。
通常來(lái)說(shuō),歸解檔需要對(duì)實(shí)例對(duì)象的各個(gè)屬性依次進(jìn)行歸檔和解檔,十分繁瑣且易出錯(cuò)。這里我們參照「字典轉(zhuǎn)模型」的例子,通過(guò)獲取類(lèi)的所有屬性,實(shí)現(xiàn)自動(dòng)歸解檔。
觸發(fā)對(duì)象歸檔可以調(diào)用 NSKeyedArchiver 的 + archiveRootObject:toFile: 方法;觸發(fā)對(duì)象解檔可以調(diào)用 NSKeyedUnarchiver 的 + unarchiveObjectWithFile: 方法。
注:
xib文件在載入的時(shí)候,也會(huì)觸發(fā)initWithCoder:方法,可見(jiàn)讀取xib文件也是一個(gè)解檔的過(guò)程。
首先在 NSObject 的分類(lèi)中添加兩個(gè)方法:
// 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<count; 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<count; 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 的子類(lèi)中實(shí)現(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
注:上面的代碼邏輯并不完善,只是做簡(jiǎn)單示例用。
4. 逆向開(kāi)發(fā)
在「逆向開(kāi)發(fā)」中,會(huì)用到一個(gè)叫 class-dump 的工具。這個(gè)工具可以將已脫殼的 APP 的所有類(lèi)的頭文件導(dǎo)出,為分析 APP 做準(zhǔn)備。這里也是利用 Runtime 的特性,將存儲(chǔ)在mach-O文件中的 @interface 和 @protocol 信息提取出來(lái),并生成對(duì)應(yīng)的 .h 文件。
5. 熱修復(fù)
「熱修復(fù)」是一種不需要發(fā)布新版本,通過(guò)動(dòng)態(tài)下發(fā)修復(fù)文件來(lái)修復(fù) Bug 的方式。比如 JSPatch,就是利用 Runtime 強(qiáng)大的動(dòng)態(tài)能力,對(duì)出問(wèn)題的代碼段進(jìn)行替換。
源碼
請(qǐng)到 GitHub 上查看完整例子。
參考
iOS運(yùn)行時(shí)(Runtime)詳解+Demo
OC最實(shí)用的runtime總結(jié),面試、工作你看我就足夠了!
iOS Runtime 詳解
Method swizzling的正確姿勢(shì)
Method Swizzling 安全性分析 和 RSSwizzle解決方案分析
【南峰子的技術(shù)博客】Objective-C Runtime 運(yùn)行時(shí)系列
獲取更佳的閱讀體驗(yàn),請(qǐng)?jiān)L問(wèn)原文地址 【Lyman's Blog】新手也看得懂的 iOS Runtime 教程