runtime簡(jiǎn)介
runtime簡(jiǎn)稱運(yùn)行時(shí),OC是運(yùn)行時(shí)機(jī)制,也就是在運(yùn)行時(shí)才做一些處理,是一套比較底層的純C語(yǔ)言API, 屬于1個(gè)C語(yǔ)言庫(kù), 包含了很多底層的C語(yǔ)言API。
在我們平時(shí)編寫的OC代碼中, 程序運(yùn)行過程時(shí), 最終都是轉(zhuǎn)成了runtime的C語(yǔ)言代碼。例如:C語(yǔ)言在編譯的時(shí)候就知道要調(diào)用哪個(gè)方法函數(shù),而OC在編譯的時(shí)候并不知道要調(diào)用哪個(gè)方法函數(shù),而是推遲到運(yùn)行的時(shí)候才知道調(diào)用的方法函數(shù)名稱,來(lái)找到對(duì)應(yīng)的方法函數(shù)進(jìn)行調(diào)用。
runtime消息傳遞流程
在OC語(yǔ)言中,任何類的定義都是對(duì)象。類和類的實(shí)例(對(duì)象)沒有任何本質(zhì)上的區(qū)別,任何對(duì)象都有isa指針。
一、isa指針
打開Runtime源碼,全局搜索isa_t,鎖定objc-private.h類,里面有這么一段代碼,isa指針是isa_t的實(shí)例。代碼如下:
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
};
#endif
};
arm64之前,isa僅僅是一個(gè)指針,保存著對(duì)象或類對(duì)象內(nèi)存地址,在arm64架構(gòu)之后,apple對(duì)isa進(jìn)行了優(yōu)化,變成了一個(gè)聯(lián)合體union結(jié)構(gòu),同時(shí)使用位域來(lái)存儲(chǔ)更多的信息。
對(duì)象是通過isa的bits進(jìn)行位運(yùn)算,取出響應(yīng)位置的值。runtime中的isa是被聯(lián)合體位域優(yōu)化過的,
它不單單是指向類對(duì)象了,而是把64位中的每一位都運(yùn)用了起來(lái),其中的shiftcls為33位,代表了類對(duì)象的地址,其他的位都有各自的用處。
那么我來(lái)看看這個(gè)isa內(nèi)部元素代表的是什么含義?
-
nonpointer:表示是否對(duì)isa指針開啟指針優(yōu)化
- 0:不開啟,表示純isa指針。
- 1開啟,不單單是類對(duì)象的地址,isa中包含了類信息和對(duì)象的引用計(jì)數(shù)等。
-
has_assoc:表示是否有關(guān)聯(lián)對(duì)象標(biāo)識(shí)位
- 0表示:沒有,沒有關(guān)聯(lián)對(duì)象會(huì)釋放的更快
- 1表示:有
-
has_cxx_dtor:表示該對(duì)象是否有C++或者Objc的析構(gòu)器
- 如果有析構(gòu)函數(shù),則需要做析構(gòu)邏輯, 如果沒有,則可以更快的釋放對(duì)象
-
shiftcls:表示存儲(chǔ)類指針class的值。
- 開啟指針優(yōu)化的情況下,在arm64 架構(gòu)中有33位用來(lái)存儲(chǔ)類指針。
-
magic:固定值為0xd2
- 用于在調(diào)試時(shí)分辨對(duì)象是否完成初始化
-
weakly_referenced:表示對(duì)象是否被指向或者曾經(jīng)指向一個(gè) ARC 的弱引用變量。
- 沒有弱引?用的對(duì)象可以更更快釋放。
-
deallocating:標(biāo)志對(duì)象是否正在釋放內(nèi)存
- 0 表示沒有 1表示正在釋放內(nèi)存
has_sidetable_rc:表示當(dāng)對(duì)象的引用計(jì)數(shù)大于10,以至于無(wú)法存儲(chǔ)在isa指針中時(shí),用散列表去計(jì)數(shù)。
-
extra_rc:表示該對(duì)象的引用計(jì)數(shù),實(shí)際上是引用計(jì)數(shù)值減1
- 例如如果對(duì)象的引用計(jì)數(shù)為10,那么extra_rc為9。如果引用計(jì)數(shù)?于10, 則需要使?到has_sidetable_rc
二、類對(duì)象(objc_class)
在OC中的類是用Class類型來(lái)表示的,它實(shí)際上是一個(gè)指向objc_class結(jié)構(gòu)體的指針。
typedef struct objc_class *Class;
查看objc/runtime.h中objc_class結(jié)構(gòu)體的定義如下:
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;
struct objc_class結(jié)構(gòu)體定義了很多變量如下:
Class isa : 實(shí)現(xiàn)方法調(diào)用的關(guān)鍵
Class super_class : 父類
const char * name : 類名
long version : 類的版本信息,默認(rèn)為0
long info : 類信息,供運(yùn)行期使用的一些位標(biāo)識(shí)
long instance_size : 該類的實(shí)例變量大小
struct objc_ivar_list * ivars : 該類的成員變量鏈表
struct objc_method_list ** methodLists : 方法定義的鏈表
struct objc_cache * cache : 方法緩存
struct objc_protocol_list * protocols : 協(xié)議鏈表
objc_class結(jié)構(gòu)體的第一個(gè)成員變量也是isa指針,這就說明了Class本身其實(shí)也是一個(gè)對(duì)象,因此我們稱之為類對(duì)象,類對(duì)象在編譯期產(chǎn)生用于創(chuàng)建實(shí)例對(duì)象,是單例。
三、實(shí)例(objc_object)
實(shí)例對(duì)象的isa指針指向類對(duì)象,類對(duì)象的isa指針指向元類。類對(duì)象和元類的結(jié)構(gòu)都是objc_class類型。
typedef struct objc_object *id;
struct objc_object {
/*
*實(shí)例對(duì)象的isa指針指向類對(duì)象
*實(shí)質(zhì)就是指向objc_class結(jié)構(gòu)體的指針
*/
Class isa;
};
- id表示泛型對(duì)象,指向objc_object結(jié)構(gòu)體的指針。objc_object 就是我們平時(shí)常用的實(shí)例對(duì)象的定義,它只包含一個(gè)isa指針。
- 一個(gè)對(duì)象唯一保存的信息就是它的 Class 的地址
- 當(dāng)我們調(diào)用一個(gè)對(duì)象的方法時(shí),它會(huì)通過 isa 去找到對(duì)應(yīng)的objc_class,然后再在objc_class的 methodLists 中找到我們調(diào)用的方法,然后執(zhí)行。
四、元類(Meta Class)
在 Objective-C 中,類也被設(shè)計(jì)為一個(gè)對(duì)象。
結(jié)構(gòu)體中也包含isa指針,這個(gè)指針指向元類。
調(diào)用一個(gè)對(duì)象的類方法的過程如下:
- 通過對(duì)象的 isa 指針找到對(duì)應(yīng)的類。
- 通過類的 isa 指針找到對(duì)應(yīng)元類。
- 在元類的 methodLists 中,找到對(duì)應(yīng)的方法,然后執(zhí)行。
元類也是一個(gè)對(duì)象,也有一個(gè)isa指針,isa指針指向哪里呢?
為了不讓這種結(jié)構(gòu)無(wú)限延伸下去,Objective-C 的設(shè)計(jì)者讓所有的元類的isa指向基類(比如 NSObject)的元類。而基類的元類的 isa 指向自己。這樣就形成了一個(gè)完美的閉環(huán)。
因此消息傳遞整個(gè)結(jié)構(gòu)圖可以如下表示:

五、objc_msgSend
當(dāng)對(duì)象調(diào)用方法時(shí)候,[objc test] 會(huì)轉(zhuǎn)換成
id objc_msgSend(id objc, SEL op, ...);
// 發(fā)送消息給無(wú)參數(shù),無(wú)返回值的方法
((void (*)(id, SEL)) objc_msgSend)(self, NSSelectorFromString(@"functionName"));
// 發(fā)送消息給有參數(shù),有返回值的方法
((NSString *(*)(id, SEL, NSString *)) objc_msgSend)(self, NSSelectorFromString(@"functionName"), @"parameter");
objc_msgSend 會(huì)默認(rèn)傳入 id 和 SEL,分別對(duì)應(yīng)兩個(gè)隱含參數(shù), self 和 _cmd,其中 _cmd 指向方法本身。
六、objc_method_list(方法列表)、objc_cache(方法緩存)
* struct objc_method_list ** methodLists : 方法定義的鏈表
* struct objc_cache * cache : 方法緩存
methodLists:存著該對(duì)象的實(shí)例方法(類對(duì)象的話存放這類方法)
cache的作用就是:
- 因?yàn)閙ethodLists方法有很多,如果每次調(diào)用方法都去遍歷一遍methodLists的話,效率很低。所以objc_cache方法緩存存在的意義。
- 當(dāng)methodLists遍歷找到方法,找到該方法之后,cache會(huì)以map的方式把method_name作為key,method_imp作為value給存起來(lái)。
- 下次當(dāng)你在再調(diào)用該方法的時(shí)候,會(huì)先從cache中去找。
七、objc_method 方法
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE; //方法名
char *method_types OBJC2_UNAVAILABLE; //方法類型
IMP method_imp //方法實(shí)現(xiàn)
}
在objc_method結(jié)構(gòu)體中中,SEL和IMP都是Method的屬性。
一、SEL(objc_selector)
/// 代表一個(gè)方法的不透明類型
typedef struct objc_selector *SEL;
//發(fā)送消息
id objc_msgSend(id self, SEL op, ...);
objc_msgSend函數(shù)第二個(gè)參數(shù)類型為SEL,selector是方法選擇器,可以理解為區(qū)分方法的 ID(唯一標(biāo)識(shí)符),而這個(gè)ID的數(shù)據(jù)結(jié)構(gòu)是SEL:
@property SEL selector;
可以看到selector是SEL的一個(gè)實(shí)例。其實(shí)selector就是個(gè)映射到方法的C字符串,selector既然是一個(gè)string,我覺得應(yīng)該是類似className+method的組合,命名規(guī)則有兩條:
- 同一個(gè)類,selector不能重復(fù)
- 不同的類,selector可以重復(fù)
這就是我們OC沒有重載的功能,一個(gè)方法不能有兩個(gè)方法名一樣,哪怕傳的參數(shù)個(gè)數(shù)和類型都不一樣。就是因?yàn)閟elector是通過方法名來(lái)找到對(duì)應(yīng)方法的。
二、IMP
IMP表示: 指向一個(gè)方法實(shí)現(xiàn)的指針。
typedef id (*IMP)(id, SEL, ...);
就是指向最終實(shí)現(xiàn)程序的內(nèi)存地址的指針。
在iOS的Runtime中,Method通過selector和IMP兩個(gè)屬性,實(shí)現(xiàn)了快速查詢方法及實(shí)現(xiàn),相對(duì)提高了性能,又保持了靈活性。
八、Category(objc_category)
Category是表示一個(gè)指向分類的結(jié)構(gòu)體的指針,結(jié)構(gòu)體如下:
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
};
- name:是指 class_name 而不是 category_name。
- cls:要擴(kuò)展的類對(duì)象,編譯期間是不會(huì)定義的,而是在Runtime階段通過name對(duì) 應(yīng)到對(duì)應(yīng)的類對(duì)象。
- instanceMethods:category中所有給類添加的實(shí)例方法的列表。
- classMethods:category中所有添加的類方法的列表。
- protocols:category實(shí)現(xiàn)的所有協(xié)議的列表。
- instanceProperties:表示Category里所有的properties
這就是我們可以通過objc_setAssociatedObject和objc_getAssociatedObject增加實(shí)例變量的原因,不過這個(gè)和一般的實(shí)例變量是不一樣的。
從上面的category_t的結(jié)構(gòu)體中可以看出,分類中可以添加實(shí)例方法,類方法,甚至可以實(shí)現(xiàn)協(xié)議,添加屬性,不可以添加成員變量。
Category的加載處理過程是怎樣的呢?
- 通過Runtime加載某個(gè)類的所有Category數(shù)據(jù)
- 把所有Category的方法、屬性、協(xié)議數(shù)據(jù),合并到一個(gè)大數(shù)組中
- 后面參與編譯的Category數(shù)據(jù),會(huì)在數(shù)組的前面
- 將合并后的分類數(shù)據(jù)(方法、屬性、協(xié)議),插入到類原來(lái)數(shù)據(jù)的前面
最后對(duì)象發(fā)送消息,整個(gè)執(zhí)行流程是什么樣的?
八、Runtime消息傳遞
一個(gè)對(duì)象的方法像這樣[obj work],編譯器轉(zhuǎn)成消息發(fā)送objc_msgSend(obj, work),Runtime時(shí)執(zhí)行的流程是這樣的:
- 首先通過obj的isa指針找到它的類對(duì)象
- class的methodlist找到work函數(shù)
- 如果class中沒找到work函數(shù)方法,會(huì)繼續(xù)類對(duì)象的superclass(父類)中去找,一直找到父類是根類
- 一旦知道work這個(gè)函數(shù)方法就去執(zhí)行的work的實(shí)現(xiàn)IMP
如果只是這么簡(jiǎn)單的消息傳遞,效率就非常低了。往往一個(gè)class只有20%的函數(shù)會(huì)被經(jīng)常調(diào)用,可能占總調(diào)用次數(shù)的80%。每個(gè)消息都需要遍歷一次objc_method_list并不合理。如果把經(jīng)常被調(diào)用的函數(shù)緩存下來(lái),那可以大大提高函數(shù)查詢的效率。objc_cache做的事情如下:
- 當(dāng)methodLists遍歷找到方法,找到該方法之后,cache會(huì)以map的方式把method_name作為key,method_imp作為value給存起來(lái)。
- 下次當(dāng)你在再調(diào)用該方法的時(shí)候,會(huì)先從cache中去找。
還有一個(gè)問題就是發(fā)現(xiàn)類別的方法優(yōu)先級(jí)更好,當(dāng)一個(gè)對(duì)象和類別都有一個(gè)work方法的時(shí)候,發(fā)現(xiàn)類別方法的優(yōu)先級(jí)更高,消息傳遞是怎么執(zhí)行的呢?
- 就是當(dāng)你遍歷類對(duì)象的methodlist以前,會(huì)把category里面的instanceMethods(實(shí)例方法列表)、classMethods(對(duì)象方法列表)會(huì)插入到methodlist的最前面。
- 所以當(dāng)遍歷work函數(shù),會(huì)優(yōu)先找到類別的work函數(shù),然后執(zhí)行它的IMP。
runtime消息轉(zhuǎn)發(fā)流程
當(dāng)向一個(gè)對(duì)象發(fā)送一條消息,但它并沒有實(shí)現(xiàn)的時(shí)候,通過_objc_msgForward嘗試做消息轉(zhuǎn)發(fā)。_objc_msgForward是 IMP類型。
為了展示消息轉(zhuǎn)發(fā)的具體動(dòng)作,可以嘗試向一個(gè)對(duì)象發(fā)送一條錯(cuò)誤的消息,并查看一下_objc_msgForward是如何進(jìn)行轉(zhuǎn)發(fā)的。
當(dāng)上面消息傳遞過程中沒有找到方法,消息就會(huì)進(jìn)行轉(zhuǎn)發(fā),如果還是找不到并且消息轉(zhuǎn)發(fā)都失敗了就回執(zhí)行doesNotRecognizeSelector:方法報(bào)unrecognized selector錯(cuò)。消息轉(zhuǎn)發(fā)的過程有三次機(jī)會(huì)。
消息轉(zhuǎn)發(fā)的原理如下圖:

-
第一步,動(dòng)態(tài)添加響應(yīng)方法
- Objective-C運(yùn)行時(shí)會(huì)調(diào)用 實(shí)例方法調(diào)用+resolveInstanceMethod:
- 類方法調(diào)用+resolveClassMethod:
- (void)viewDidLoad {
[super viewDidLoad];
//執(zhí)行work函數(shù)
[self performSelector:@selector(work:)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(work:)) {//如果是執(zhí)行work函數(shù),就動(dòng)態(tài)解析指定新的IMP
class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
//work函數(shù)
void workMethod(id obj, SEL _cmd) {
NSLog(@"Working");
}
打印結(jié)果:
Working
表明雖然沒有實(shí)現(xiàn)work:這個(gè)函數(shù),但是我們通過class_addMethod動(dòng)態(tài)添加workMethod函數(shù),并執(zhí)行workMethod這個(gè)函數(shù)的IMP。
如果上面方法返回是NO的話,那就做走下一步。
- 第二步轉(zhuǎn)發(fā)給響應(yīng)該方法的對(duì)象
如果目標(biāo)對(duì)象實(shí)現(xiàn)了-forwardingTargetForSelector:,Runtime 這時(shí)就會(huì)調(diào)用這個(gè)方法,給你把這個(gè)消息轉(zhuǎn)發(fā)給能響應(yīng)該方法的對(duì)象。
下面通過代碼,首先寫聲明一個(gè)能響應(yīng)該方法的對(duì)象Person:
@interface Person: NSObject
- (void)work;
@end
@implementation Person
- (void)work {
NSLog(@"person working");//Person的foo函數(shù)
}
@end
其次通過forwardingTargetForSelector方法轉(zhuǎn)發(fā)給響應(yīng)該方法的對(duì)象
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//執(zhí)行work函數(shù)
[self performSelector:@selector(work)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;//返回NO,進(jìn)入下一步轉(zhuǎn)發(fā)
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(work)) {
return [Person new];//轉(zhuǎn)發(fā)給Person對(duì)象,讓Person對(duì)象接收這個(gè)消息
}
return [super forwardingTargetForSelector:aSelector];
}
打印結(jié)果:
person working
如果第二步中沒有相應(yīng)該消息的對(duì)象,就會(huì)走第三步消息轉(zhuǎn)發(fā),就把整個(gè)完整的消息轉(zhuǎn)發(fā)流程走一遍了。
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//執(zhí)行foo函數(shù)
[self performSelector:@selector(foo)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;//返回NO,進(jìn)入下一步轉(zhuǎn)發(fā)
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil;//返回nil,進(jìn)入下一步轉(zhuǎn)發(fā)
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"work"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];//簽名,進(jìn)入forwardInvocation
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;
Person *p = [Person new];
if([p respondsToSelector:sel]) {
[anInvocation invokeWithTarget:p];
}
else {
[self doesNotRecognizeSelector:sel];
}
}
@end
打印結(jié)果
working
從打印結(jié)果來(lái)看,我們實(shí)現(xiàn)了完整的轉(zhuǎn)發(fā)。通過簽名,Runtime生成了一個(gè)對(duì)象anInvocation,發(fā)送給了forwardInvocation,我們?cè)趂orwardInvocation方法里面讓Person對(duì)象去執(zhí)行了work函數(shù)。簽名參數(shù)v@:看蘋果文檔Type Encodings
如果這三步都沒有成功的話,就會(huì)doesNotRecognizeSelector報(bào)異常了。
Runtime實(shí)際應(yīng)用
Runtime簡(jiǎn)直就是做大型框架的利器。它的應(yīng)用場(chǎng)景非常多,下面就介紹一些常見的應(yīng)用場(chǎng)景。
- 給分類添加屬性(關(guān)聯(lián)對(duì)象)
- Method Swizzling交換方法
- KVO原理實(shí)現(xiàn)
- 消息轉(zhuǎn)發(fā)(熱更新)解決Bug(JSPatch)
- 實(shí)現(xiàn)NSCoding的自動(dòng)歸檔和自動(dòng)解檔
- 實(shí)現(xiàn)字典和模型的自動(dòng)轉(zhuǎn)換(MJExtension)
一、給分類添加屬性(關(guān)聯(lián)對(duì)象)
關(guān)聯(lián)對(duì)象Runtime提供了下面幾個(gè)接口:
//關(guān)聯(lián)對(duì)象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//獲取關(guān)聯(lián)的對(duì)象
id objc_getAssociatedObject(id object, const void *key)
//移除關(guān)聯(lián)的對(duì)象
void objc_removeAssociatedObjects(id object)
上面的參數(shù)解釋:
- id object:被關(guān)聯(lián)的對(duì)象
- const void *key:關(guān)聯(lián)的key,要求唯一
- id value:關(guān)聯(lián)的對(duì)象
- objc_AssociationPolicy policy:內(nèi)存管理的策略
objc_AssociationPolicy是一個(gè)枚舉如下:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, //weak nonatomic 屬性
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, //retain\strong nonatomic 屬性
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, //copy nonatomic屬性
OBJC_ASSOCIATION_RETAIN = 01401, // retain\strong atomic屬性
OBJC_ASSOCIATION_COPY = 01403 // copy atomic屬性
};
針對(duì)枚舉如下面解釋:

下面就給Person關(guān)聯(lián)一個(gè)name屬性
@interface Person (Name)
@property (nonatomic, copy) NSString *name;
@end
#import "Person+Name.h"
#import <objc/runtime.h>
static NSString *nameKey = @"namekey";
@implementation Person (Name)
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
return objc_getAssociatedObject(self, &nameKey);
}
二、方法交換(Method Swizzling)
Method Swizzling也是iOS中AOP(面相切面編程)的一種實(shí)現(xiàn)方式,我們可以利用蘋果這一特性來(lái)實(shí)現(xiàn)AOP編程。
Method Swizzling是發(fā)生在運(yùn)行時(shí),主要用于運(yùn)行時(shí)將兩個(gè)Method進(jìn)行交換,我們可以將Method Swizzle代碼寫到任何地方,但是只有在Method_Swizzling這段Method Swizzle代碼執(zhí)行完畢之后互換才起作用。
Method Swizzling交換時(shí)機(jī):盡可能在+load方法中實(shí)現(xiàn)。原因:
+load方法會(huì)在加載類的時(shí)候就被調(diào)用,也就是ios應(yīng)用啟動(dòng)的時(shí)候,就會(huì)加載所有的類,main函數(shù)之前,就會(huì)調(diào)用每個(gè)類的+load方法。
主要用途:當(dāng)需求需要在調(diào)用系統(tǒng)方法的基礎(chǔ)上需要添加額外的功能,然后要把項(xiàng)目中所有的方法都要實(shí)現(xiàn)都要改一遍的問題上,不需要?jiǎng)釉械南到y(tǒng)方法。并且能使用原有的系統(tǒng)方法功能外,添加一下原有的功能。
例如:當(dāng)系統(tǒng)需要適配ipad圖片的時(shí)候,項(xiàng)目中調(diào)用imageNamed方法的地方非常多。所以每一處都需要進(jìn)行更改肯定非常麻煩?,F(xiàn)在通過Method_Swizzling就可以sw_imageNamed替換imageNamed并且做一些處理。當(dāng)以調(diào)用imageNamed方法時(shí)候?qū)嶋H調(diào)用的是sw_imageNamed。
@implementation UIImage (Category)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(imageNamed);
SEL swizzledSelector = @selector(sw_imageNamed);
Method originalMethod = class_getInstanceMethod(class,originalSelector);
Method swizzledMethod = class_getInstanceMethod(class,swizzledSelector);
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
/*
* NO:嘗試添加需要交換的方法,若添加失敗,則說明已經(jīng)有該方法,直接交換
* YES:如果添加成功,沒有該方法,則把剛添加的方法替換為我們需要的方法
*/
if (didAddMethod) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
/*
*這里特別注意,必須調(diào)用交換的方法,因?yàn)樯厦嬉呀?jīng)把方法交換了,所以不會(huì)死循環(huán),
*如果調(diào)用originalMethod,反而會(huì)死循環(huán)
*/
- (void)sw_imageNamed {
UIImage * image;
//這個(gè)時(shí)候的sw_imageNamed已經(jīng)交換后變成了系統(tǒng)的imageNamed
if( IS_IPHONE ){
// iPhone處理
UIImage * image = [self sw_imageNamed:name];
if (image != nil) {
return image;
} else {
return nil;
}
} else {
// ipad處理,_ipad是自己定義。
UIImage *image = [self sw_imageNamed:[NSString stringWithFormat:@"%@_ipad",name]];
if (image != nil) {
return image;
}else {
image = [self swizze_imageNamed:name];
return image;
}
}
}
@end
可能有些不明白為啥sw_imageNamed方法里面又調(diào)用[self swizze_imageNamed:name]竟然沒有死循環(huán)。主要是因?yàn)檫@兩個(gè)sw_imageNamed是不一樣意義:
- load方法表示在加載類的時(shí)候就執(zhí)行了該方法,就已經(jīng)交換完畢,交換的是IMP(就是函數(shù)實(shí)現(xiàn)的內(nèi)存地址指針)。
- 當(dāng)對(duì)象調(diào)用[self imageNamed:name]調(diào)用的sw_imageNamed的IMP
- (void)sw_imageNamed {}這個(gè)就是sw_imageNamed方法真正實(shí)現(xiàn)的IMP。
- 然后在sw_imageNamed的IMP里調(diào)用[self swizze_imageNamed:name],[self swizze_imageNamed:name]這個(gè)就是selector方法名執(zhí)行的是imageNamed的IMP。
- (void)sw_imageNamed {}與[self swizze_imageNamed:name]這個(gè)兩個(gè)是有區(qū)別的,一個(gè)IMP,一個(gè)Selector。
原理圖如下:

Method Swizzing作用就是讓sel2、sel3與IMP2、IMP3交換。
三、熱更新及解決線上bug(JSPatch)
JSPatch 是一個(gè)iOS動(dòng)態(tài)更新框架,只需在項(xiàng)目中引入極小的引擎,就可以使用JavaScript調(diào)用任何Objective-C原生接口,獲得腳本語(yǔ)言的優(yōu)勢(shì):為項(xiàng)目動(dòng)態(tài)添加模塊,或替換項(xiàng)目原生代碼動(dòng)態(tài)修復(fù)bug。
JSPatch基礎(chǔ)原理:
JSPatch 能做到通過JS調(diào)用和改寫OC方法最根本的原因是 Objective-C是動(dòng)態(tài)語(yǔ)言,OC上所有方法的調(diào)用/類的生成都通過 Objective-C Runtime在運(yùn)行時(shí)進(jìn)行,我們可以通過類名/方法名反射得到相應(yīng)的類和方法:
Class class = NSClassFromString("UIViewController");
id viewController = [[class alloc] init];
SEL selector = NSSelectorFromString("viewDidLoad");
[viewController performSelector:selector];
也可以替換某個(gè)類的方法為新的實(shí)現(xiàn):
static void newViewDidLoad(id slf, SEL sel) {}
class_replaceMethod(class, selector, newViewDidLoad, @"");
還可以新注冊(cè)一個(gè)類,為類添加方法:
Class cls = objc_allocateClassPair(superCls, "JPObject", 0);
class_addMethod(cls, selector, implement, typedesc);
objc_registerClassPair(cls);
理論上你可以在運(yùn)行時(shí)通過類名/方法名調(diào)用到任何 OC 方法,替換任何類的實(shí)現(xiàn)以及新增任意類。
JSPatch的基本原理就是:JS傳遞字符串給OC,OC通過Runtime 接口調(diào)用和替換OC方法。這是最基礎(chǔ)的原理,實(shí)際實(shí)現(xiàn)過程還有很多怪要打。
最后JSPatch被蘋果禁止使用,還能用 JSPatch 嗎?
- 可以,但請(qǐng)不要自行接入,統(tǒng)一接入 JSPatch 平臺(tái)。
為什么要統(tǒng)一接入 JSPatch 平臺(tái)?
若從蘋果的審核規(guī)則來(lái)看,JSPatch 和 React Native 是一樣的,但蘋果接受 React Native,拒絕 JSPatch,主要是因?yàn)镴SPatch 可以調(diào)用任意原生 API,出于安全和濫用方面的顧慮而拒絕,之前大量 APP 自行接入 JSPatch,導(dǎo)致有幾個(gè)風(fēng)險(xiǎn)點(diǎn):
無(wú)法保證每個(gè) APP 接入方式都是安全的,容易傳輸過程被替換,被黑客中間人攻擊,成為 iOS 上的一個(gè)漏洞。
一些像地圖/推送類 SDK 接入 JSPatch 后覆蓋大量 APP,若這些 SDK 后臺(tái)被攻破,可以對(duì)這些 APP 下發(fā)惡意腳本,造成大面積危害。
開發(fā)者可以方便地不經(jīng)過蘋果審查使用 JSPatch 調(diào)用私有 API。
若大家還是通過一些混淆手段騙過蘋果繼續(xù)各自接入 JSPatch,出于上述三點(diǎn)考慮,蘋果不可能再允許,但若接入 JSPatch 平臺(tái),上述三個(gè)問題可以幫蘋果解決,不再有這類隱患,詳見這里,蘋果才會(huì)考慮允許繼續(xù)使用。
用JSPatch平臺(tái)最新的SDK接入可以通過審核嗎?
可以通過,已有接入并通過審核的 APP。
為什么平臺(tái)SDK能通過審核?
- 因?yàn)樽隽撕?jiǎn)單的類名修改混淆。
用Github上的代碼接入能通過審核嗎?
- 不能。前面也說了,不要自行接入JSPatch源碼,不要自行混淆代碼接入
四、NSCoding的自動(dòng)歸檔、解檔
原理描述:用runtime提供的函數(shù)遍歷Model自身所有屬性,并對(duì)屬性進(jìn)行encode和decode操作。
核心方法:在Model的基類中重寫方法:
//解檔
- (id)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[I];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
}
}
return self;
}
//歸檔
- (void)encodeWithCoder:(NSCoder *)aCoder {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[I];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[aCoder encodeObject:[self valueForKey:key] forKey:key];
}
}
字典轉(zhuǎn)模型
首先建立一個(gè)NSObject分類,做個(gè)簡(jiǎn)單的字典轉(zhuǎn)模型
#import <Foundation/Foundation.h>
@interface NSObject (Json)
+ (instancetype)pf_objectWithJson:(NSDictionary *)json;
@end
#import "NSObject+Json.h"
#import <objc/runtime.h>
@implementation NSObject (Json)
+ (instancetype)cs_objectWithJson:(NSDictionary *)json {
id obj = [[self alloc] init];
unsigned int count;
Ivar *ivars = class_copyIvarList(self, &count);
for (int i = 0; i < count; i++) {
// 取出位置的成員變量
Ivar ivar = ivars[I];
NSMutableString *name = [NSMutableString stringWithUTF8String:ivar_getName(ivar)];
[name deleteCharactersInRange:NSMakeRange(0, 1)];
// 設(shè)值
id value = json[name];
if ([name isEqualToString:@"ID"]) {
value = json[@"id"];
}
[obj setValue:value forKey:name];
}
free(ivars);
return obj;
}
@end
字典轉(zhuǎn)模型的模型及調(diào)用:
#import <Foundation/Foundation.h>
@interface Persion : NSObject
@property (assign, nonatomic) int ID;
@property (assign, nonatomic) int weight;
@property (assign, nonatomic) int age;
@property (copy, nonatomic) NSString *name;
@end
// 字典轉(zhuǎn)模型調(diào)用
- (void)jsonToModel {
// 字典轉(zhuǎn)模型
NSDictionary *json = @{
@"id" : @20,
@"age" : @20,
@"weight" : @60,
@"name" : @"Jack",
@"no" : @30
};
Persion *persion = [Persion pf_objectWithJson:json];
NSLog(@"id = %d, age = %d, weight = %d, name = %@",persion.ID,persion.age,persion.weight,persion.name);
}
Runtime使用場(chǎng)景很多,在這就不一一舉例子了。大家可以看下面的應(yīng)用圖:

結(jié)尾
Objective-C語(yǔ)言最重要的特點(diǎn)之一就是運(yùn)行時(shí)語(yǔ)言,可見掌握runtime至關(guān)重要。Objective-C是一個(gè)全動(dòng)態(tài)語(yǔ)言,它的一切都是基于Runtime實(shí)現(xiàn)的平時(shí)編寫的OC代碼, 在程序運(yùn)行過程中, 其實(shí)最終都是轉(zhuǎn)成了runtime的C語(yǔ)言代碼, runtime算是OC的幕后工作者。