RunTime的消息機制 & NSTimer的循環引用

引言

總所周知,高級語言想要成爲可執行文件須要 先編譯爲彙編語言 -> 再彙編爲機器語言,機器語言也就是計算機可以識別的惟一語言,可是OC並不能直接編譯爲彙編語言,而是須要先轉寫爲純C語言再進行編譯和彙編的操做。html

從OC到C語言的過渡就是由RunTime來實現的,然而OC是進行面向對象的開發,而C語言更多的是面向過程開發,這就須要將面向對象的類轉變爲面向過程的結構體。ios

什麼是RunTime

RunTime簡稱運行時,就是系統在運行的時候的一些機制,其中最主要的是消息機制。git

Objective-C語言做爲一門動態語言,就意味着它不只須要一個編譯器,也須要一個運行時系統來動態得建立類和對象、進行消息傳遞和轉發等。這種動態語言的優點在於:咱們的代碼更具備靈活性。而這個運行時系統就是Objc RunTimegithub

  • Objc RunTime 實際上是一個RunTime庫,它基本上是使用 C彙編 語言寫的,具備面向對象的能力,是Objective-C面向對象和動態機制的基石。api

  • 對於C語言,函數的調用在編譯的時候會決定調用哪一個函數數組

    • 在編碼階段,若是C語言調用未實現的函數就會報錯
  • 對於OC語言,是屬於動態調用的,在編譯時並不能決定真正調用哪一個函數,只有在真正運行的時候纔會根據函數的名稱找到對應函數來調用。緩存

    • 在編譯階段,OC能夠調用任何函數,即便用這個函數並未實現,只要聲明過就不會報錯。
    • 當調用A對象上的某個方法B時,若是A對象並無實現這個方法,能夠經過「 消息轉發 」來解決,只要對B方法進行聲明,則在編譯時不會報錯。

消息相關經常使用內容

想了解清RunTime的消息傳遞機制,首先咱們須要先對下面的一些內容有個概念性的認識。bash

SEL

SEL又叫選擇器,是表示一個方法的selector的指針,其定義以下架構

typedef struct objc_selector *SEL;
複製代碼

objc_selector結構體的詳細定義沒有在<objc/runtime.h>頭文件中找到。方法的selector用於表示運行時方法的名字。Objective-C在編譯時,會依據每個方法的名字、參數序列,生成一個惟一的整型標識(Int類型的地址),這個標識就是SEL。以下代碼所示:app

SEL sel1 = @selector(testMethod1);
NSLog(@"sel1: %p", sel1);
複製代碼

上面代碼的輸出爲:

2019-10-31 00:33:23.271841+0800 RunTimeTestDemo[4736:725890] sel1: 0x103f2b856
複製代碼

本質上,SEL只是一個指向方法的指針(準確的說,只是一個根據方法名hash化了的KEY值,能惟一表明一個方法)。SEL其主要做用是快速的經過方法名字查找到對應方法的函數指針,而後調用其函數。

工程中的全部的SEL組成一個Set集合,而Set的特色就是惟一,所以SEL也是惟一的。因此,若是咱們想到這個方法集合中查找某個方法時,只須要去找到這個方法對應的SEL就行。

在運行時咱們能夠添加新的selector或者獲取已知的selector,有下面三種方法能夠實現獲取SEL

  • sel_registerName函數
  • Objective-C編譯器提供的@selector()
  • NSSelectorFromString()方法

IMP

IMP其實是一個函數指針,指向方法實現的首地址。其定義以下:

id (*IMP)(id, SEL, ...)
複製代碼

這個函數使用當前CPU架構實現的標準的C調用約定。

  • 第一個參數是指向 self 的指針(若是是實例方法,則是類實例的內存地址;若是是類方法,則是指向元類的指針)

  • 第二個參數是方法選擇器 (selector)

  • 接下來是方法的實際參數列表。

前面介紹過的SEL就是爲了查找方法的最終實現IMP的。因爲每一個方法對應惟一的SEL,所以咱們能夠經過SEL方便快速準確地得到它所對應的IMP,查找過程將在下面討論。

取得IMP後,咱們就得到了執行這個方法代碼的入口點,此時,咱們就能夠像調用普通的C語言函數同樣來使用這個函數指針了。

經過取得IMP,咱們能夠跳過 Runtime 的消息傳遞機制,直接執行IMP指向的函數實現,這樣省去了 Runtime 消息傳遞過程當中所作的一系列查找操做,會比直接向對象發送消息高效一些。

Method

上面介紹完了SELIMP,咱們就能夠來說講Method了。Method用於表示類定義中的方法,其定義以下:

typedef struct objc_method *Method;
struct objc_method {
    SEL method_name                 OBJC2_UNAVAILABLE;	// 方法名
    char *method_types              OBJC2_UNAVAILABLE;
    IMP method_imp             	    OBJC2_UNAVAILABLE;	// 方法實現
}
複製代碼

咱們能夠看到該結構體中包含一個 SELIMP ,實際上至關於在 SELIMP 之間做了一個映射。有了SEL,咱們即可以找到對應的IMP,從而調用方法的實現代碼。具體操做流程咱們將在下面討論。

Method List

每個類都有一個方法列表Method List,它保存着類裏面全部的方法,根據SEL傳入的方法編號找到對應的方法,而後找到方法的實現,最後在方法的實現裏面實現對應的具體操做。

//方法列表
struct objc_method_list {
    struct objc_method_list *obsolete           OBJC2_UNAVAILABLE;
    int method_count                            OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                   OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]           OBJC2_UNAVAILABLE;
}   
複製代碼

Class

Objective-C 類是由Class類型來表示的,它其實是一個指向objc_class結構體的指針。它的定義以下:

typedef struct objc_class *Class;
複製代碼

查看objc/runtime.hobjc_class結構體的定義以下:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY; //isa指針指向Meta Class

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE; // 父類
    const char * _Nonnull name                               OBJC2_UNAVAILABLE; // 類名
    long version                                             OBJC2_UNAVAILABLE; // 類的版本信息,默認爲0
    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;
/* Use `Class` instead of `struct objc_class *` */ 
複製代碼

在這個定義中,着重注意下面幾個字段:

  1. isa:須要注意的是在 Objective-C 中,全部的類自身也是一個對象,這個對象的Class裏面也有一個isa指針,它指向metaClass(元類)。

    • 當咱們向一個對象發送消息時,RunTime 會在這個對象所屬的這個類的方法列表中查找方法;而向一個類發送消息時,會在這個類的meta-class的方法列表中查找。
  2. super_class:指向該類的父類,若是該類已是最頂層的根類(如NSObject或NSProxy),則super_class爲NULL。

  3. cache:用於緩存最近使用的方法。一個接收者對象接收到一個消息時,它會根據isa指針去查找可以響應這個消息的對象。在實際使用中,這個對象只有一部分方法是經常使用的,不少方法其實不多用或者根本用不上。這種狀況下,若是每次消息來時,咱們都是methodLists中遍歷一遍,性能勢必不好。這時,cache就派上用場了。在咱們每次調用過一個方法後,這個方法就會被緩存到cache列表中,下次調用的時候runtime就會優先去cache中查找,若是cache沒有,纔去methodLists中查找方法。這樣,對於那些常常用到的方法的調用,但提升了調用的效率。

  4. version:咱們可使用這個字段來提供類的版本信息。這對於對象的序列化很是有用,它但是讓咱們識別出不一樣類定義版本中實例變量佈局的改變。

消息的關鍵在於objc_class結構體,這個結構體有兩個字段是咱們在分發消息的關注的:

  1. 指向父類的指針isa
  2. 一個類的方法分發表,即methodLists

當咱們建立一個新對象時,先爲其分配內存,並初始化其成員變量。其中isa指針也會被初始化,讓對象能夠訪問類及類的繼承體系。

消息傳遞 - 動態查找

消息機制是運行時裏面最重要的機制,OC是動態語言,本質都是發送消息,每一個方法在運行時會被動態轉化爲消息發送,即:objc_msgSend(receiver, selector)

要想了解消息的轉發咱們須要先明確消息是如何被動態的找到和發送的。

栗子:

  • OC代碼 - 實例方法 調用底層的實現:
BackView *backView = [[BackView alloc] init];
[backView changeBgColor];

//編譯時底層轉化
//objc對象的isa指針指向他的類對象,從而能夠找到對象上的方法
//SEL:方法編號,根據方法編號就能夠找到對應方法的實現。
[backView performSelector:@selector(changeBgColor)];

//performSelector本質即爲運行時,發送消息,誰作事情就調用誰 
objc_msgSend(backView, @selector(changeBgColor));
// 帶參數
objc_msgSend(backView, @selector(changeBgColor:),[UIColor RedColor]);
複製代碼
  • OC代碼 - 類方法 調用底層的實現
//本質是將類名轉化成類對象,初始化方法實際上是建立類對象。
[BackView changeBgColor];
//BackView 只是表示一個類名,調用方法實際上是用的類對象去調用的。(類對象既然稱爲對象,那它也是一個實例。類對象中也有一個isa指針指向它的元類(meta class),即類對象是元類的實例。元類內部存放的是類方法列表,根元類的isa指針指向本身,superclass指針指向NSObject類。)

//編譯時底層轉化
//RunTime 調用類方法一樣,類方法也是類對象去調用,因此須要獲取類對象,而後使用類對象去調用方法
Class backViewClass = [BackView class];
[backViewClass performSelector:@selector(changeBgColor)];
//performSelector本質即爲運行時,發送消息,誰作事情就調用誰 

//類對象發送消息
objc_msgSend(backViewClass, @selector(changeBgColor));
// 帶參數
objc_msgSend(backViewClass, @selector(changeBgColor:),[UIColor RedColor]);
複製代碼

一個對象的方法像這樣[obj changeBgColor],編譯器轉成消息發送objc_msgSend(obj, changeBgColor)Runtime 時執行的流程是這樣的:

  • 實例對象調用方法後,底層調用[objc performSelector:@selector(SEL)];方法,編譯器將代碼轉化爲objc_msgSend(receiver, selector)
  • objc_msgSend函數中:
    • 首先經過objcisa指針找到objc對應的class類的結構體
    • class中,先去cache中經過SEL查找對應函數的 method,若是找到則經過 method中的函數指針跳轉到對應的函數中去執行。
    • 若是在cacha中未找到,再去methodList中查找,若是能找到,則將method加入到cache中,以方便下次查找,並經過method中的函數指針跳轉到對應的函數中去執行。
    • 若是在methodlist中未找到,則經過objc_msgSend結構體中的指向父類的指針找到其父類,並在superClass的分發表中去查找方法的selector,若是能找到,則將method加入到cache中,以方便下次查找,並經過method中的函數指針跳轉到對應的函數中去執行。
    • 依此,會一直沿着類的繼承體系到達NSObject類。
    • 若是最後依舊沒有定位到selector,則會走消息轉發流程。

消息轉發

咱們對消息的傳遞有了必定了解,當一個對象能接收一個消息時,就會走正常的方法調用流程。但若是一個對象沒法接收指定消息時,又會發生什麼事呢?

默認狀況下,若是是以[object message]的方式調用方法,若是object沒法響應message消息時,編譯器會報錯。但若是是以perform...的形式來調用,則須要等到運行時才能肯定object是否能接收message消息。若是不能,則程序崩潰並拋出異常,經過控制檯,咱們能夠看到如下異常信:

- xxxx : unrecognized selector sent to instance xxxx
複製代碼

這段異常信息其實是由 NSObject 的」doesNotRecognizeSelector「方法拋出的。

爲了不程序泵能夠,咱們能夠採起一些措施,讓咱們的程序執行特定的邏輯,從而避免崩潰。這就啓動了所謂的」消息轉發(message forwarding)「機制,經過這一機制,咱們能夠告訴對象如何處理未知的消息。

消息轉發機制的三個步驟

  1. 動態方法解析
  2. 備援接收者
  3. 消息重定向

下面咱們詳細討論一下這三個步驟。

第一步:動態方法解析

對象在接收到未知的消息時,首先會調用所屬類的實例方法 +resolveInstanceMethod: 或者類方法 +resolveClassMethod:

在這個方法中,咱們有機會爲該未知消息新增一個」處理方法」。不過使用該方法的前提是咱們已經實現了該」處理方法」,只須要在運行時經過class_addMethod函數動態添加到類裏面就能夠了。以下代碼所示:

void functionForMethod1(id self, SEL _cmd) {
   NSLog(@"%@, %p", self, _cmd);
}
	
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selectorString = NSStringFromSelector(sel);
    if ([selectorString isEqualToString:@"method1"]) {
        class_addMethod(self.class, @selector(method1), (IMP)functionForMethod1, "@:");
    }
    return [super resolveInstanceMethod:sel];
}
複製代碼

在objc運行時會調用 +resolveInstanceMethod: 或者 +resolveClassMethod: ,讓你有機會提供一個函數的實現。若是你添加了函數,那運行時系統就會從新啓動一次消息發送的過程,不然 ,運行時就會移到下一步,消息轉發(Message Forwarding)。

第二步:備援接收者

若是在上一步沒法處理消息,則 Runtime 會繼續調如下方法:

- (id)forwardingTargetForSelector:(SEL)aSelector
複製代碼

若是一個對象實現了這個方法,並返回一個非nil的結果,則這個對象會做爲消息的新接收者,且消息會被分發到這個對象。固然這個對象不能是self自身,不然就是出現無限循環。整個消息發送的過程會被重啓,而且發送的對象會變成你返回的那個對象。固然,若是咱們沒有指定相應的對象來處理 aSelector,那麼應該調用父類的實現來返回結果。

使用這個方法一般是在對象內部,可能還有一系列其它對象能處理該消息,咱們即可借這些對象來處理消息並返回,這樣在對象外部看來,仍是由該對象親自處理了這一消息。

這一步合適於咱們只想將消息轉發到另外一個能處理該消息的對象上。但這一步沒法對消息進行處理,如操做消息的參數和返回值等。

第三步:消息重定向

若是在上一步還不能處理未知消息,則惟一能作的就是啓用完整的消息轉發機制進行消息重定向了。這個時候 RunTime 會將未知消息的全部細節都封裝爲 NSInvocation 對象,而後調用下述方法:

- (void)forwardInvocation:(NSInvocation *)anInvocation
複製代碼

運行時系統會在這一步給消息接收者最後一次機會將消息轉發給其它對象。對象會建立一個表示消息的NSInvocation對象,把 與還沒有處理的消息有關的所有細節都封裝在anInvocation中,包括selector,目標(target)和參數。咱們能夠在forwardInvocation方法中選擇將消息轉發給其它對象。

forwardInvocation:方法的實現有兩個任務:

  • 定位能夠響應封裝在anInvocation中的消息的對象。這個對象不須要能處理全部未知消息。

  • 使用anInvocation做爲參數,將消息發送到選中的對象。anInvocation將會保留調用結果,運行時系統會提取這一結果並將其發送到消息的原始發送者。

還有一個很重要的問題,咱們必須重寫如下方法:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
複製代碼

消息轉發機制 須要使用從這個方法中獲取的信息來建立NSInvocation對象。所以咱們必須重寫這個方法,爲給定的selector提供一個合適的方法簽名。

從某種意義上來說,forwardInvocation:就像一個未知消息的分發中心,將這些未知的消息轉發給其它對象。或者也能夠像一個運輸站同樣將全部未知消息都發送給同一個接收對象。這取決於具體的實現。

調用這個方法若是不能處理就會調用父類的相關方法,一直到NSObject的這個方法,若是NSObject都沒法處理就會調用doesNotRecognizeSelector:方法拋出異常。

NSTimer的循環引用

NSTimer常見的使用方式

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
複製代碼
  • timerWith的方式建立,須要本身手動添加到runloop中執行,而且須要啓動子線程的runloop。
  • scheduledTimerWith的方式建立,系統默認幫你添加到runloop的defaultmood中了。

NSTimer形成循環引用的緣由

主要是NSTimer的target被強引用了,而一般target就是所在的控制器,他又強引用的timer,形成了循環引用。下面是target參數的說明:

target: The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to this object until it (the timer) is invalidated.
複製代碼

在這裏首先聲明一下:不是全部的NSTimer都會形成循環引用。就像不是全部的block都會形成循環引用同樣。如下兩種timer不會有循環引用:

  • 非repeat類型的。非repeat類型的timer不會強引用target,所以不會出現循環引用。

  • block類型的,新api。iOS 10以後才支持,所以對於還要支持老版本的app來講,這個API暫時沒法使用。固然,block內部的循環引用也要避免。

NSTimer循環引用示例

@interface TimerViewController ()

@property (nonatomic, strong) NSTimer * timer;
@property (nonatomic, assign) NSInteger number;
@property (weak, nonatomic) IBOutlet UILabel *timeLab;

@end

@implementation TimerViewController

- (void)viewDidLoad {
    [super viewDidLoad];

// self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerRun) userInfo:nil repeats:YES];

    self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerRun) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];

    _number = 0;
}

- (void)dealloc {
    NSLog(@"TimerViewController 界面銷燬");
    if (_timer) {
        [_timer invalidate];
        _timer = nil;
    }
}

- (void)timerRun {
    _number++;
    NSLog(@"_number: %ld", _number);
    _timeLab.text = [NSString stringWithFormat:@"定時時間:%ld", (long)_number];
}

@end
複製代碼

咱們的初衷是想在界面銷燬的時候釋放timer,可是因爲控制器與timer之間相互引用着,致使內存泄漏,沒法釋放。

NSTimer引用圖1

循環引用之解決方案

方案一:將timer的引用變爲弱指針(❌)

//代碼改動
// 將timer的類型變爲了weak,其餘不變
@property (nonatomic, weak) NSTimer *timer;
複製代碼

經嘗試,然並卵。

由於雖然這裏沒有循環引用了,可是RunLoop依舊引用着timer,而timer又引用着VC,雖然在pop的時候指向VC的強指針銷燬了,可是仍然有timer的強指針指向VC,所以仍舊有內存泄漏。

方案二:藉助中間代理間接持有timer(✅)

//.h文件
@interface GYTimerProxy : NSObject

+ (instancetype) timerProxyWithTarget:(id)target;
@property (weak, nonatomic) id target;

@end

//.m文件
#import "GYTimerProxy.h"

@implementation GYTimerProxy

+ (instancetype) timerProxyWithTarget:(id)target {
    GYTimerProxy *proxy = [[GYTimerProxy alloc] init];
    proxy.target = target;
    return proxy;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return self.target;
}

@end
複製代碼

VC控制器裏只須要修改下面一句代碼便可

//這裏的target發生了變化
self.timer = [NSTimer timerWithTimeInterval:1.0 target:[GYTimerProxy timerProxyWithTarget:self] selector:@selector(timerRun) userInfo:nil repeats:YES];
複製代碼

咱們藉助一箇中間代理對象GYTimerProxy,讓VC控制器不直接持有timer,而是持有GYTimerProxy實例,讓GYTimerProxy實例來弱引用VC控制器,timer強引用GYTimerProxy實例。

實踐嘗試,頗有效果。

  • 當pop的時候,1號指針被銷燬,VC控制器無強引用,能夠被正常銷燬
  • VC控制銷燬,會走dealloc方法,在dealloc裏調用了[self.timer invalidate],那麼timer將從RunLoop中移除,3號指針會被銷燬。
  • 當VC銷燬了,2號指針天然也被銷燬了
  • 此時timer已經沒有被別的對象強引用了,因此timer會被銷燬,代理實例GYTimerProxy也就自動銷燬了。

方案三:繼承NSProxy類對消息處理(✅)

NSProxy是一個專門用於作消息轉發的類,咱們須要經過子類的方式來使用它。

//.h文件
@interface GYProxy : NSProxy

+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;

@end

//.m文件
#import "GYProxy.h"

@implementation GYProxy

+ (instancetype)proxyWithTarget:(id)target {
    GYProxy *proxy = [GYProxy alloc];
    proxy.target = target;
    return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
}

@end
複製代碼

VC控制器裏也只須要修改下面一句代碼便可

//這裏的target發生了變化
self.timer = [NSTimer timerWithTimeInterval:1.0 target:[GYProxy proxyWithTarget:self] selector:@selector(timerRun) userInfo:nil repeats:YES];
複製代碼

看上去方法二和方法三彷佛沒有什麼區別,但實際原理仍是略有不一樣的:

  • GYTimerProxy的父類是NSObjectGYProxy的父類是NSProxy
  • GYTimerProxy只實現了forwardingTargetForSelector:方法,可是GYProxy是實現了methodSignatureForSelector:forwardInvocation:

NSProxy具體是什麼?

  • NSProxy是一個專門用來作消息轉發的類
  • NSProxy是個抽象類,使用需本身寫一個子類繼承自NSProxy
  • NSProxy的子類須要實現兩個方法,就是上面那兩個

OC中消息的轉發

經過上面的RunTime咱們瞭解OC中消息轉發的機制,當某個對象的方法找不到的時候,最後拋出doesNotRecognizeSelector:的時候,會經歷如下幾個步驟:

  • 1.消息發送,從方法緩存中找方法,找不到去方法列表中找,找到了將該方法加入方法緩存,仍是找不到,去父類裏重複前面的步驟,若是找到底都找不到那麼進入

  • 2.動態方法解析,看該類是否實現了resolveInstanceMethod:resolveClassMethod:,若是實現了就解析動態添加的方法,並調用該方法,若是沒有實現進入

  • 3.消息轉發,這裏分二步

    • 調用forwardingTargetForSelector:,看返回的對象是否爲nil,若是不爲nil,調用objc_msgSend傳入對象和SEL。
    • 若是上面爲nil,那麼就調用methodSignatureForSelector:返回方法簽名,若是方法簽名不爲nil,調用forwardInvocation:來執行該方法

從上面能夠看出,當繼承自 NSObject 的對象,方法沒有找到實現的時候,是須要通過第1步,第2步,第3步的操做才能拋出錯誤,若是在這個過程當中咱們作了補救措施,好比GYTimerProxy就是在第3步的第1小步作了補救,那麼就不會拋出doesNotRecognizeSelector:,程序就能夠正常執行。

可是若是是繼承自 NSProxyGYProxy,就會跳過前面的全部步驟,直接到第3步的第2小步,直接找到對象,執行方法,提升了性能。

Objc RunTime函數的定義

  • 對對象進行操做的方法通常以object_開頭
  • 對類進行操做的方法通常以class_開頭
  • 對類或對象的方法進行操做的方法通常以method_開頭
  • 對成員變量進行操做的方法通常以ivar_開頭
  • 對屬性進行操做的方法通常以property_開頭開頭
  • 對協議進行操做的方法通常以protocol_開頭

根據以上的函數的前綴 能夠大體瞭解到層級關係。

對於以objc_開頭的方法,則是RunTime最終的管家,能夠獲取內存中類的加載信息,類的列表,關聯對象和關聯屬性等操做。

擴展 - RunTime應用

交換方法(攔截/替換方法)

交換方法實現的需求場景:本身建立了一個功能性的方法,在項目中屢次被引用,當項目的需求發生改變時,要使用另外一種功能代替這個功能,要求是不改變舊的項目(也就是不改變原來方法的實現)。

能夠在類的分類中,再寫一個新的方法(是符合新的需求的),而後交換兩個方法的實現。這樣,在不改變項目的代碼,而只是增長了新的代碼 的狀況下,就完成了項目的改進。

交換兩個方法的實現通常寫在類的load方法裏面,由於load方法會在程序運行前加載一次,而initialize方法會在類或者子類在 第一次使用的時候調用,當有分類的時候會調用屢次。

用到的方法名以下:

//獲取方法地址
class_getClassMethod(Class _Nullable cls, SEL _Nonnull name)

//交換方法地址,至關於交換實現方式
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 
複製代碼

類/對象的關聯對象

關聯對象不是爲類\對象添加屬性或者成員變量(由於在設置關聯後也沒法經過ivarList或者propertyList取得) ,而是爲類添加一個相關的對象,一般用於存儲類信息,例如存儲類的屬性列表數組,爲未來字典轉模型的方便。

例如:給分類(通常系統類)添加屬性

// 根據關聯的key,獲取關聯的值。
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)

//將key跟關聯的對象進行綁定
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy)
複製代碼

動態添加方法

開發使用場景:若是一個類方法很是多,加載類到內存的時候也比較耗費資源,須要給每一個方法生成映射表,可使用動態給某個類,添加方法解決。

  • (消息轉發機制應用)

字典轉模型 KVC實現

KVC:把字典中全部值給模型的屬性賦值。這個是要求字典中的Key,必需要在模型裏能找到相應的值,若是找不到就會報錯。

可是,在實際開發中,從字典中取值,不必定要所有取出來。所以,咱們能夠經過重寫KVC 中的 forUndefinedKey這個方法,就不會進行報錯處理。

另外,咱們能夠經過runtime的方式去實現。咱們把KVC的原理倒過來,經過遍歷模型的值,從字典中取值。

  • (RunTime的類和對象以及屬性和成員變量的應用)

總結

RunTime 的功能遠比咱們想象的強大,這也是OC的動態特性的奇妙之處。瞭解運行時機制有助於咱們更好的去了解程序底層的實現,在實際的開發中也能更靈活的應用這些機制,去實現一些特殊的功能等。 在此僅拋磚引玉,但願你們能有更多的探索,期待一塊兒分享和探討。

參考文章:

iOS Runtime原理及使用

ios RunTime機制詳解

Objective-C Runtime運行時

iOS runtime探究

iOS Runtime詳解

相關文章
相關標籤/搜索