Object-C采用"消息結(jié)構(gòu)"而非”函數(shù)調(diào)用“。對于函數(shù)調(diào)用的語言,是由編譯器決定的。而消息結(jié)構(gòu)的語言,其運行所執(zhí)行的代碼由運行環(huán)境來決定。 而這個運行環(huán)境,就是Runtime。

一. 消息機制
1.1. 消息傳遞
消息機制是Runtime的核心,方法調(diào)用的過程可以看做是消息傳遞的過程。
先來熟悉下類的基本結(jié)構(gòu),在iOS中,基本上所有類都直接或者間接繼承于NSObject(也有NSProxy這種例外),那么來看下NSObject:
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
NSObject中持有一個Class類型的isa指針,那么這個Class是什么呢?來看一下:
typedef struct objc_class *Class;
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY; // 指向metaclass
Class _Nullable super_class OBJC2_UNAVAILABLE; // 指向其父類
const char * _Nonnull name OBJC2_UNAVAILABLE; // 類名
long version OBJC2_UNAVAILABLE; // 類的版本信息,初始化默認(rèn)為0,可以通過runtime函數(shù)class_setVersion和class_getVersion進行修改、讀取
long info OBJC2_UNAVAILABLE; // 一些標(biāo)識信息,如CLS_CLASS (0x1L) 表示該類為普通 class ,其中包含對象方法和成員變量;CLS_META (0x2L) 表示該類為 metaclass,其中包含類方法;
long instance_size OBJC2_UNAVAILABLE; // 該類的實例變量大小(包括從父類繼承下來的實例變量);
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE; // 用于存儲每個成員變量的地址
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
// 與 info 的一些標(biāo)志位有關(guān),如CLS_CLASS (0x1L),則存儲對象方法,如CLS_META (0x2L),則存儲類方法;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; //指向最近使用的方法的指針,用于提升效率;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE; // 存儲該類遵守的協(xié)議
}
可以看到,objc_class中也有多個元素,除了類型父類等能夠理解顧名思義的元素,特別需要注意的是isa和cache兩個元素。cache是將用過的方法存儲到其內(nèi),優(yōu)先查找,是典型的時空裝換。而對于類的isa, 它是指向元類的,也就是說:
mateClass(元類)生成Class(類/類對象), Class(類)生成obj(對象)。用一張經(jīng)典圖來說明:

有了以上的基礎(chǔ),那么消息傳遞就會容易理解很多。例如我們調(diào)用一個實例方法:
[obj test];
轉(zhuǎn)化為匯編代碼:
objc_msgSend(obj,sel_registerName("test"));
接下來會調(diào)用_class_lookupMethodAndLoadCache3方法,看下其具體實現(xiàn):
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
runtimeLock.lock();
checkIsKnownClass(cls);
if (!cls->isRealized()) {
realizeClass(cls);
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
}
retry:
runtimeLock.assertLocked();
imp = cache_getImp(cls, sel);
if (imp) goto done;
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
break;
}
}
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
triedResolver = YES;
goto retry;
}
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlock();
return imp;
}
即開始先從cache查找:
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
如果緩存命中,直接返回imp。如果沒有命中,繼續(xù)往下走,先判斷類有沒有加載到內(nèi)存,如果沒有,先加載類:
checkIsKnownClass(cls);
if (!cls->isRealized()) {
realizeClass(cls);
}
判斷是否實現(xiàn)了initialize,如果有實現(xiàn),先調(diào)用initialize:
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
}
在類對象的方法列表查找imp:
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
如果沒有找到,繼續(xù)在父類的緩存的方法列表中查找imp。
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
break;
}
}
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
imp還沒有找到,則嘗試做一次動態(tài)方法解析:
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);//這里做一次動態(tài)方法解析。
runtimeLock.lock();
triedResolver = YES;
goto retry;
}
最終沒有找到imp,并且方法解析也沒有處理,那么則進入消息轉(zhuǎn)發(fā)流程:
imp = (IMP)_objc_msgForward_impcache;
1.2. 消息轉(zhuǎn)發(fā)
在調(diào)用對象拿到對應(yīng)的selector之后,如果自己無法執(zhí)行這個方法,那么該條消息要被轉(zhuǎn)發(fā)?;蛘吲R時動態(tài)的添加方法實現(xiàn)。如果轉(zhuǎn)發(fā)到最后依舊沒法處理,程序就會崩潰。
如以下例子:
新建一個Person類繼承于NSObject,并聲明一個msgTest方法(不實現(xiàn));
@interface Person : NSObject
- (void)msgTest;
@end
調(diào)用該方法:
- (void)viewDidLoad {
[super viewDidLoad];
Person * p = [Person new];
[p msgTest];
}
此時我們將項目跑起來就會發(fā)現(xiàn),項目是能通過編譯的,但是會崩潰掉:
-[Person msgTest]: unrecognized selector sent to instance 0x6000020543e0
在方法在調(diào)用時,系統(tǒng)會查看這個對象能否接收這個消息(沒有實現(xiàn)這個方法),如果不能接收,就會調(diào)用下面這幾個方法,會采用拯救模式,給你“補救”的機會。
第一次補救: 動態(tài)方法解析
/*
cls:要添加方法的類
name:選擇器
imp:方法實現(xiàn),IMP在objc.h中的定義是:typedef id (*IMP)(id, SEL, ...);該方法至少有兩個參數(shù),self(id)和_cmd(SEL)
types:方法,參數(shù)和返回值的描述,"v@:"表示返回值為void,沒有參數(shù)
*/
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(msgTest)){
return class_addMethod([self class],sel, (IMP)reTest, "v@:");
}
return [super resolveInstanceMethod:sel];
}
void reTest(id self, SEL _cmd) {
NSLog(@"test");
}
可看到打印數(shù)據(jù):
learn[47237:860941] test
注: resolveInstanceMethod處理對象方法,resolveClassMethod處理類方法。
第二次補救: 消息重定向
我們繼續(xù)以實例方法舉例:
創(chuàng)建一個新的類RePerson,該類包含有msgTest的實現(xiàn)方法。
#import "RePerson.h"
@implementation RePerson
- (void)msgTest{
NSLog(@"rePerson");
}
@end
在Person類中進行下兩步操作:
- resolveInstanceMethod返回值設(shè)為NO。
- forwardingTargetForSelector返回值為RePerson對象。
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(msgTest)){
return NO;
}
return [super resolveInstanceMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(msgTest)){
return [RePerson new];
}
return [super forwardingTargetForSelector:aSelector];
}
這樣就可以得到結(jié)果:
learn[47519:906599] rePerson
第三次補救: 消息轉(zhuǎn)發(fā)
關(guān)于消息轉(zhuǎn)發(fā),希望您對Type Encodings 、NSMethodSignature 、NSInvocation已經(jīng)有基本的認(rèn)知,可查看本人呢另一篇文章Type Encodings 、NSMethodSignature 、NSInvocation三部曲。
也是改變調(diào)用對象,使該消息在新對象上調(diào)用;不同是forwardInvocation方法帶有一個NSInvocation對象,這個對象保存了這個方法調(diào)用的所有信息,包括SEL,參數(shù)和返回值描述等。
同樣的,我們利用上文中描述的RePerson類,實現(xiàn)以下方法:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
if (anInvocation.selector == @selector(msgTest)){
[anInvocation invokeWithTarget:[RePerson new]];
return;
}
[super forwardInvocation:anInvocation];
}
同樣的,我們也可以拿到如下答案:
learn[47574:911006] rePerson
經(jīng)典圖:

消息轉(zhuǎn)發(fā)也是我們處理unrecognized selector crash 的主要方案,減少對應(yīng)的崩潰。
1.3. 關(guān)于NSProxy
說到消息轉(zhuǎn)發(fā)這一問題,NSProxy才是消息轉(zhuǎn)發(fā)、消息分發(fā)的終極答案。
對比上面的一套消息查找過程,NSProxy就簡單多了,接收到 unkonwn selector后,直接調(diào)用- (NSMethodSignature *)methodSignatureForSelector: 和 - (void)forwardInvocation:進行消息轉(zhuǎn)發(fā)。看下YYWeakProxy的源碼:
@implementation YYWeakProxy
- (instancetype)initWithTarget:(id)target {
_target = target;
return self;
}
+ (instancetype)proxyWithTarget:(id)target {
return [[YYWeakProxy alloc] initWithTarget:target];
}
- (id)forwardingTargetForSelector:(SEL)selector {
return _target;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
void *null = NULL;
[invocation setReturnValue:&null];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}
- (BOOL)respondsToSelector:(SEL)aSelector {
return [_target respondsToSelector:aSelector];
}
- (BOOL)isEqual:(id)object {
return [_target isEqual:object];
}
- (NSUInteger)hash {
return [_target hash];
}
- (Class)superclass {
return [_target superclass];
}
- (Class)class {
return [_target class];
}
- (BOOL)isKindOfClass:(Class)aClass {
return [_target isKindOfClass:aClass];
}
- (BOOL)isMemberOfClass:(Class)aClass {
return [_target isMemberOfClass:aClass];
}
- (BOOL)conformsToProtocol:(Protocol *)aProtocol {
return [_target conformsToProtocol:aProtocol];
}
- (BOOL)isProxy {
return YES;
}
- (NSString *)description {
return [_target description];
}
- (NSString *)debugDescription {
return [_target debugDescription];
}
@end
其實就是簡單的實現(xiàn)這兩種方法而已。
它的主要功能之一就是避免循環(huán)引用:
@implementation MyView {
NSTimer *_timer;
}
- (void)initTimer {
YYWeakProxy *proxy = [YYWeakProxy proxyWithTarget:self];
_timer = [NSTimer timerWithTimeInterval:0.1 target:proxy selector:@selector(tick:) userInfo:nil repeats:YES];
}
- (void)tick:(NSTimer *)timer {...}
@end
如上例子, MyView持有Timer, Timer強引用Proxy, Proxy雖然能發(fā)送消息到MyView卻不會形成強引用。
二. Runtime 應(yīng)用
Runtime的是iOS中的高頻詞,具體的使用大致分為以下幾個類別:
- 關(guān)聯(lián)對象(
Objective-C Associated Objects)添加對象。 - 方法交換
Method Swizzling。 - 字典和模型的自動轉(zhuǎn)換。
2.1. 關(guān)聯(lián)對象
首先拋出一個問題:分類Category為什么不能直接添加屬性。
從邏輯角度來說,Category本來就不是一個真實的類,是在Runtime期間,動態(tài)的為相關(guān)類添加方法。在編譯期間連相關(guān)對象都沒拿到,如何添加屬性?
另一方面,從Category的結(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; // 協(xié)議
struct property_list_t *instanceProperties; // 屬性
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
雖然其中包括了屬性的list,但是并不包含成員變量的list, 屬性是要自動合成相關(guān)的成員變量的,而其明顯不具備這一特點。so,該如何做呢 ? 當(dāng)然還是回到Runtime。
Runtime提供了三個函數(shù)進行屬性關(guān)聯(lián):
// 關(guān)聯(lián)對象 setter
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
// objec: 被關(guān)聯(lián)對象。key:關(guān)聯(lián)key, 唯一標(biāo)識。 value:關(guān)聯(lián)的對象。policy: 內(nèi)存管理的策略。
// 獲取關(guān)聯(lián)的對象 getter
id objc_getAssociatedObject(id object, const void *key);
// 移除關(guān)聯(lián)對象 delloc
void objc_removeAssociatedObjects(id object);
內(nèi)存策略:
OBJC_ASSOCIATION_ASSIGN, //等價于 @property(assign)。
OBJC_ASSOCIATION_RETAIN_NONATOMIC, //等價于 @property(strong, nonatomic)。
OBJC_ASSOCIATION_COPY_NONATOMIC, //等價于 @property(copy, nonatomic)。
OBJC_ASSOCIATION_RETAIN //等價于@property(strong,atomic)。
OBJC_ASSOCIATION_COPY //等價于@property(copy, atomic)。
如我們給一個UIViewController分類添加一個params字典用戶接受傳遞過來的參數(shù):
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIViewController (Base)
@property (nonatomic, strong) NSDictionary * params;
@end
NS_ASSUME_NONNULL_END
#import "UIViewController+Base.h"
#import <objc/runtime.h>
static const void * jParamsKey = &jParamsKey;
@implementation UIViewController (Base)
- (void)setParams:(NSDictionary *)params{
objc_setAssociatedObject(self, jParamsKey, params, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSDictionary *)params{
return objc_getAssociatedObject(self, jParamsKey);
}
@end
2.2. 方法交換 (Method Swizzling)
Method Swizzling 被稱為黑魔法, 在iOS編程具有不可動搖的核心地位,修改原有方法指向的特性使其能夠十分出色完成以下任務(wù):
-
hook系統(tǒng)方法,例如hook系統(tǒng)字體設(shè)置動態(tài)修改不同屏幕下字體大小,hook系統(tǒng)生命周期方法達到埋點統(tǒng)計的目的。 - 在
debug過程中hook原方法來進行bug修復(fù)。hook例如NSArray的indexof去防崩潰。 - 實現(xiàn)
KVO類的觀察者方案。
先看代碼吧,如果我們實現(xiàn)UIFont的動態(tài)方案:
#import "UIFont+Adapt.h"
@implementation UIFont (Adapt)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self exchangeMethod];
});
}
+ (void)exchangeMethod{
Class class = [self class];
SEL originalSelector = @selector(systemFontOfSize:);
SEL swizzledSelector = @selector(runTimeFitFont:);
Method systemMethod = class_getClassMethod(class, originalSelector );
Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
method_exchangeImplementations(systemMethod, swizzledMethod);
}
+ (UIFont *)runTimeFitFont:(CGFloat)fontSize{
UIFont *fitFont = nil;
//這里并不會造成循環(huán)調(diào)用,方法已經(jīng)被交換
fitFont = [UIFont runTimeFitFont:fontSize * (Main_Screen_Width / 375 )];
return fitFont;
}
@end
這里解釋下這些代碼:
一般情況下,都會寫一個分類來實現(xiàn)Method Swizzling。 一般情況下會在load方法里調(diào)用,保證在該方法調(diào)用之前,已經(jīng)完成了方法交換。
load方法在不同系統(tǒng)下有不同表現(xiàn),在iOS10或者其它情況下,會出現(xiàn)多次調(diào)用的情況,所以使用dispatch_once方案保證方法交換只實現(xiàn)一次。
在hook完成后,我們調(diào)用原方法,最終就會調(diào)用到交換后的方法,而在交換方法如需調(diào)用原方法,類似上面的本來該調(diào)用systemFontOfSize:的,但是systemFontOfSize:已經(jīng)被交換了,所以調(diào)用runTimeFitFont:(CGFloat)fontSize就是調(diào)用systemFontOfSize:,并不會引起循環(huán)調(diào)用。
另一個需要注意點是在hook父類的方法時候存在的問題,比如我們有一個HookViewController繼承于 BaseViewController繼承于UIViewController,如果我們想hook它的viewDidAppear,如果我們直接hook:
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// 原方法名和替換方法名
SEL originalSelector = @selector(viewDidAppear:);
SEL swizzledSelector = @selector(swizzle_viewDidAppear:);
// 原方法結(jié)構(gòu)體和替換方法結(jié)構(gòu)體
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// 調(diào)用交互兩個方法的實現(xiàn)
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
就會報錯:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[JTabbarController swizzle_viewDidAppear:]: unrecognized selector sent to instance 0x7fb191811400'
修改成:
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// 原方法名和替換方法名
SEL originalSelector = @selector(viewDidAppear:);
SEL swizzledSelector = @selector(swizzle_viewDidAppear:);
// 原方法結(jié)構(gòu)體和替換方法結(jié)構(gòu)體
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// 如果當(dāng)前類沒有原方法的實現(xiàn)IMP,先調(diào)用class_addMethod來給原方法添加默認(rèn)的方法實現(xiàn)IMP
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
// 添加方法實現(xiàn)IMP成功后,修改替換方法結(jié)構(gòu)體內(nèi)的方法實現(xiàn)IMP和方法類型編碼TypeEncoding
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
// 添加失敗,調(diào)用交互兩個方法的實現(xiàn)
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
當(dāng)然如果我們重寫這個方法,也是可以的。
為什么會這樣呢?
根據(jù)方法的的查找路徑,沒有重寫的話實質(zhì)會去調(diào)用父類的方法,但是父類沒有實現(xiàn)Imp,就會失敗。
2.3. 字典和模型的自動轉(zhuǎn)換
根據(jù)上文,我們已經(jīng)明白, 類的結(jié)構(gòu)體中包含了成員變量的list, 那么在這個前提下,我們就很輕松的做到字典到模型或者說json到模型的轉(zhuǎn)換。
具體方案如下:
+ (instancetype)modelWithDict:(NSDictionary *)dict{
id objc = [[self alloc] init];
//1.獲取成員變量
unsigned int count = 0;
//獲取成員變量數(shù)組
Ivar *ivarList = class_copyIvarList(self, &count);
for (int i = 0; i < count; i++) {
//獲取成員變量
Ivar ivar = ivarList[i];
//獲取成員變量名稱
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
//獲取成員變量類型
NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
//獲取key
NSString *key = [ivarName substringFromIndex:1];
id value = dict[key];
// 二級轉(zhuǎn)換:判斷下value是否是字典,如果是,字典轉(zhuǎn)換層對應(yīng)的模型
// 并且是自定義對象才需要轉(zhuǎn)換
if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]){
//獲取class
Class modelClass = NSClassFromString(ivarType);
value = [modelClass modelWithDict:value];
}
if (value) {
[objc setValue:value forKey:key];
}
}
return objc;
}
搭配Type Encodings 、NSMethodSignature 、NSInvocation三部曲,相信就能輕松理解這段代碼,就不多敘。
三. 總結(jié)
這篇文章算是寫的比較快的,大致就是想到哪就寫一寫,Runtime這個話題其實也有無數(shù)人寫過了,我只是想用自己的思路把這個話題順一下,有什么問題,歡迎留言。