對Objective-C中runtime的理解

Objective-C是面向runtime(運行時)的語言,在應用程序運行的時候來決定函數內部實現什麼以及作出其它決定的語言。程序員能夠在程序運行時建立,檢 查,修改類,對象和它們的方法,Objective-C runtime庫也負責找出方法的最終執行代碼。舉例說明,當程序執行[object doSomething]時,不會直接調用doSomething這個方法,而是一條消息(doSomething)會發送給對象(object)。runtime庫裏有個c函數來傳遞這條消息,[object doSomething]這條代碼將會被轉化成c函數形式的函數調用:objc_msgSend(object,@selector(doMethodWith:)),函數內部按照如下順序進行:ios

1.檢查接受對象是否爲nil,若是是,調用nil處理程序。直接表現就是什麼都不作。程序員

2.若是是在支持垃圾收集環境中,進行一些處理,因爲ios不支持垃圾收集,因此咱們沒必要關心這步。緩存

3.檢查類緩存中是否是已經有方法實現了,有的話,直接調用。(每一個類都有一些常常執行的方法,有些方法則不多執行,當代碼裏執行到某一個方法時若是都要去類的方法列表裏找一遍就太影響效率了,因此運行時系統會把調用過的方法緩存起來,以提升查找的效率,這一步就是在緩存裏查找)函數

4.比較請求的選擇器(就是方法名,好比doSomething)和類中定義的選擇器,若是找到了,調用方法執行。測試

5.比較請求的選擇器和父類中定義的選擇器,找到就執行,找不到繼續向父類的父類尋找,若是找到就執行,若是一直找到根類都找不到,那麼將執行第6步。編碼

6.調用resolveInstanceMethod:(或resolveClassMethod:)。這個方法返回一個BOOL值,若是返回no,將執行第7步,若是返回yes,則從新從上面的第1步開始。咱們能夠在這個方法裏用runtime給咱們提供的class_addMethod函數爲類添加方法,這即是給類動態加方法的一個機會。atom

7.調用forwardingTargetForSelector:,這個方法返回一個對象,消息將轉發給咱們返回的對象,這是咱們能夠把經過以上6步都不能處理的方法轉發到其餘對象上的機會。spa

8.調用methodSignatureForSelector:,生成一個方法簽名,建立一個NSInvocation(這個類把target,selector,方法簽名和參數打包在一塊兒)並傳給forwardInvocation:方法。在forwardInvocation:裏這是咱們對這個消息進行處理的最後機會,裏若是咱們不作處理,默認的程序就會crash,拋出找不到該方法的錯誤信息。指針

在這個過程當中咱們能夠給類添加方法(第6步),能夠將消息轉發到別的對象上(第7步),對NSInvocation(包含了這條消息的全部信息)進行咱們想要的處理(第8步)。日誌

下面咱們將經過代碼來看看如何在運行時給類添加方法。

假設咱們有個Person類,

@interface Person : NSObject

@end

@implementation Person

@end

這個類沒有任何方法,接下來咱們在執行的地方建立一個Person對象,出於測試目的,咱們將person定爲id類型,這樣咱們就能夠隨便調用一個NSObject子類的方法,這裏咱們調用tableView的reloadData方法。

id person = [[Person alloc] init];
[person reloadData];

而後執行以上語句,咱們來詳細看看執行[person reloadData]時的過程,[person reloadData]執行時實際上是c函數objc_msgSend(person,@selector(reloadData)),進入函數內部,因爲在Person類中咱們並無reloadData這個方法,按照咱們前面列出的順序,因而就走到了第6步,消息分發函數內部調用這個類中的resolveInstanceMethod:方法,咱們能夠在這一步中給person類加上reloadData方法,

@implementation Person

static void testIMP(id self, SEL _cmd) 
{ NSLog(
@"reload data"); } + (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if ([NSStringFromSelector(aSEL) isEqualToString:@"reloadData"]) {//這裏只是測試,只針對person調用了reloadData這個方法 class_addMethod([self class], aSEL, (IMP)testIMP, "v@:");//這樣咱們就在運行時爲person類動態添加了reloadData方法。 return YES;//retrun YES後消息分發函數又會從頭開始執行,由與咱們上面已經給類添加了relodData方法,消息分發函數將會終止在第5步再也不往下執行。 } return NO; } @end

 對class_addMethod(Class cls, SEL name, IMP imp,const char *types)函數參數的說明:
    
     Class cls:就是咱們要給哪一個類添加方法
     SEL name:添加的方法叫什麼名字(方法名)
     IMP:一個函數指針,這是它的定義:typedef id (*IMP)(id self,SEL _cmd,...),由編譯器生成,指向最終執行的函數的地址。該函數類型接受一個目標對象,一個選擇器,以及其餘參數。
     const char *types:方法的返回類型、參數類型編碼,咱們能夠用@encode(type)得到類型的字符串編碼,好比上面的-(void)reloadData方法的返回類型和參數類型(有兩個參數會隱式傳遞)編碼爲「v@:」,詳細是v=@encode(void),@=encode(id),:=encode(SEL),(每一個object方法會把id self和SEL _cmd隱式傳遞)

到這,咱們就成功的爲person類添加了他原本沒有的方法。

下面咱們繼續來看經過運行時如何將消息轉發。仍是這個Person類,假如咱們沒有動態的給類添加方法,那麼消息分發函數將會運行到第7步,咱們能夠在這一步經過方法forwardingTargetForSelector:把消息轉發到另外一個對象上。

假設有另外一個類叫Computer類,它有實現reloadData方法。

@interface Computer : NSObject
-(void)reloadData;
@end @implementation Computer -(void)reloadData {   NSLog(@"reload data"); } @end

下面咱們就在Person類裏將消息轉發到Computer類去

@implementation Person

- (id)forwardingTargetForSelector:(SEL)selector 
{
if ([NSStringFromSelector(selector) isEqualToString:@"reloadData"]) { return [[Computer alloc] init]; } return nil; } @end

運行後,[person reloadData]真正執行的是Computer裏的reloadData方法。

假如咱們在第6步和第7步都不作處理,那麼消息分發函數將會進入到第8步,這是咱們處理這條消息的最後機會,若是咱們不處理,程序將會拋出unrecognized selector sent to instance錯誤。接下來咱們來看看如何處理。

由於person類裏沒有reloadData這個方法,因此返回了一個空的方法簽名,最終程序報錯崩潰,因此咱們要在報錯以前返回一個方法簽名

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

得到方法簽名後,會生成一個NSInvocation傳給person類的如下方法,在這個方法中,咱們就能夠對該條消息處理了,咱們能夠改變消息的target,也能夠改變selector,就看咱們的須要了,如下咱們將target改成Computer,將消息轉發到Computer類去,最終執行的是Computer裏的reloadData方法。

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    Computer *test = [[Computer alloc] init];
    [anInvocation setTarget:test];
    [anInvocation invoke];
}

 利用運行時的這種動態特性,咱們能夠改變那些沒有源代碼的對象(包括系統對象)的行爲。好比咱們要對UINavigationController的pushViewController:animated:進行改寫,每當咱們調用pushViewController: animated:時就打印一下日誌。咱們用分類的方式給UINavigationController添加一個方法,而後再咱們須要這個功能的地方運行一下咱們的分類方法,這以後的全部pushViewController: animated:將會執行咱們指定的行爲。

@interface UINavigationController (Mypush)

+ (void)tellmePushVC;

@end


@implementation UINavigationController (Mypush)

typedef void (*voidIMP)(id, SEL, ...);

//用來保存系統原有的實現
static voidIMP sOrigPushVCIMP = NULL;

//咱們自定義的實現
static void tellmePush(id self, SEL _cmd, id viewcontroller,bool flag) {
    
    //打印日誌
    NSLog(@"push push vc");
    
    // 調用系統原有的實現
    if (sOrigPushVCIMP) {
        sOrigPushVCIMP(self, _cmd, viewcontroller,flag);
    }
}

+ (void)tellmePushVC
{
    //確保如下代碼只運行一次,不然將會引起遞歸循環
    static dispatch_once_t onceToken;
    
    dispatch_once(&onceToken,^{
        
        Class class = [self class];
        
        SEL sel = @selector(pushViewController:animated:);
        
        Method origMethod = class_getInstanceMethod(class,sel);
        
        sOrigPushVCIMP = (void *)method_getImplementation(origMethod);//把系統原有的實現存起來
        
        method_setImplementation(origMethod, (IMP)tellmePush);//把新實現替換舊實現
    });
}
@end


在咱們須要的地方寫上[UINavigationController tellmePushVC];這以後凡是咱們調用了系統的pushViewController: animated:方法,就會先打印日誌,再執行系統原有的pushViewController: animated:方法。經過以上手段,就達到了給系統方法添加行爲的目的。

以上都是對方法的操做,咱們還能夠利用運行時的公開的c函數接口修改類的實例變量的值。仍是以Person類舉例,假如Person類裏有個_age實例變量,下面咱們就在運行時修改_age的值。

@interface Person : NSObject
- (void)printAge;
@end @implementation Person { NSString *_age; } - (id)init
{
if ((self = [super init])) { _age = @"18"; } return self; } - (void)printAge { Ivar ivar = class_getInstanceVariable([self class], "_age"); object_setIvar(self, ivar, @"100"); NSLog(@"age %@",_age); } @end

這樣咱們便在運行時把實例變量的值修改了。

咱們都知道分類只能給類添加方法,不能添加實例變量,可是利用運行時,咱們能夠給分類動態添加實例變量,這樣即便咱們不建立子類,也可以對類動態擴展。這個功能稱之爲關聯引用。須要注意的是經過分類添加的實例變量並非真正的實例變量,因此在對象複製和歸檔的時候要特別注意。咱們給Person類建立一個分類,添加屬性address。

#import "Person.h"

@interface Person (AddProperty)

@property (strong, nonatomic) NSString *address;

@end

 
#import "Person+AddProperty.h"
#import <objc/runtime.h>

@implementation Person (AddProperty)

static char key;//因爲一個對象能夠有不少個關聯引用,因此須要一個key來區分,通常咱們用一個靜態變量的地址做爲key。

- (void)setAddress:(NSString *)address
{
    objc_setAssociatedObject(self, &key, address, OBJC_ASSOCIATION_COPY);
}

- (NSString *)address
{
    return objc_getAssociatedObject(self, &key);
}
@end

這樣就完成了在分類中給類添加實例變量。

相關文章
相關標籤/搜索