【iOS開發進階】-RunTime

人類一思考發表於2020-11-14

1.基本概念

編譯時與執行時

原始碼轉換為可執行的程式,通常需要經過三個步驟:編譯、連結、執行,不同的編譯語言,這三個步驟中所進行的操作又有些不同。

編譯時就是正在編譯的時候,即編譯器將原始碼翻譯成機器能識別的程式碼的過程。編譯時知識對語言進行最基本的檢查報錯,包括詞法分析、語法分析等,編譯通過並不意味著程式就可以成功執行。

執行時就是程式通過編譯後,編譯好的程式碼被裝載到記憶體中跑起來的階段,這個時候會具體對型別進行檢查,而不僅僅是對程式碼簡單掃描分析,此時如果出錯,程式會崩潰。

靜態語言與動態語言

靜態語言就是在編譯階段就已經確定了所有變數的資料型別,同時也確定好了要呼叫的函式,以及函式的實現,如C語言。

動態語言在編譯階段並不知道變數的具體資料型別,也不知道真正呼叫的哪個函式。只有在執行期間才檢查變數的資料型別,同時在執行時才會根據函式名查詢要呼叫的具體函式。程式沒執行的時候,並不知道呼叫一個方法具體會發生什麼。如Objective-C語言。

 

Objective-C將一些決定性的工作從編譯階段、連結階段推遲到執行時階段的機制,使得Objective-C變得更加靈活。甚至可以在程式執行的時候,動態的去修改一個方法的實現,為“熱更新”機制提供可能性。

實現Objective-C語言執行時機制的一切基礎就是Runtime,Runtime實際上就是一個庫,這個庫可以在程式執行時動態的建立物件、檢查物件、修改類和物件的方法。

 

2.方法呼叫機制

Objective-C作為擴充套件於C語言的一種物件導向的程式語言,然而其方法的呼叫方式又和大多數面嚮物件語言大有不同,其採用訊息傳遞、轉發的方式進行方法的呼叫。這種機制使得Objective-C中物件的真正行為往往在執行時確定而非在編譯時確定,所以被稱為執行時動態語言。

在Objective-C語言中採用中括號包裹的方式,例如[obj function]進行方法的呼叫。實際上,Objective-C中的每一句方法呼叫最後都會被轉換成一條訊息進行傳送。一條訊息包含三部分內容:方法選擇器、接收訊息的物件以及引數。obj_msgSend函式就用來傳送這種訊息

MyObject.m

#import "MyObject.h"

@implementation MyObject
- (void)showSelf:(NSString *)name age:(int)age{
    NSLog(@"MyObject:%@,%d",name,age);
}
@end

main.m

#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "MyObject.h"
#import "objc/message.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        MyObject *obj = [[MyObject alloc] init];
        [obj class];
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Wundeclared-selector"
//        ((void(*)(id,SEL))objc_msgSend)(obj,@selector(showSelf));
        ((void(*)(id,SEL,NSString*,int))objc_msgSend)(obj,@selector(showSelf:age:),@"abc",22);
#pragma clang diagnostic pop
        
    }
    return 0;
}

如上可知,通過@selector(方法名)可以獲取到一個SEL型別的物件,SEL實際上是object_selector結構體指標,也可以將其理解為函式簽名,在程式的編譯階段,定義類中所有方法會生成一個方法簽名列表,這個列表是類直接關聯的,在執行時通過方法簽名表來找到具體要執行的函式。

再看上例使用到的objc_msgSend()函式,其第一個引數為接收訊息的物件,第二個引數為方法簽名,之後為傳遞的引數,那麼Objective-C執行時是如何根據一個物件例項來找到方法簽名表再找到要執行的方法呢?此處則需要理解相關的內部構造

3.Runtime概念解析

1.Class(類)

在Runtime中,Class被定義為指向objc_class結構體的指標,objc_class結構體的資料結構如下:

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

struct objc_class {
    Class _Nonnull isa;                                          // objc_class 結構體的例項指標

#if !__OBJC2__
    Class _Nullable super_class;                                 // 指向父類的指標
    const char * _Nonnull name;                                  // 類的名字
    long version;                                                // 類的版本資訊,預設為 0
    long info;                                                   // 類的資訊,供執行期使用的一些位標識
    long instance_size;                                          // 該類的例項變數大小;
    struct objc_ivar_list * _Nullable ivars;                     // 該類的例項變數列表
    struct objc_method_list * _Nullable * _Nullable methodLists; // 方法定義的列表
    struct objc_cache * _Nonnull cache;                          // 方法快取
    struct objc_protocol_list * _Nullable protocols;             // 遵守的協議列表
#endif

};

從上可知,objc_class結構體定義了很多變數,如自身的所有例項變數(ivars)、所有方法定義(methodLists)、遵守的協議列表(protocols)等。objc_class結構體存放的資料稱為後設資料(metadata)。

2.Object(物件)

Object(物件)被定義為objc_object結構體,資料結構如下:

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa;       // objc_object 結構體的例項指標
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

此處的id被定義為一個指向objc_object的結構體指標,而objc_object結構體只包含一個Class型別的isa指標。

也就是說,Object(物件)唯一儲存的就是它所屬Class(類)的地址。當呼叫一個物件的方法時,比如[receiver selector];它就會通過objc_object結構體的isa指標去找對應的object_class結構體,然後在object_class結構體的methodLists(方法列表)中找到相應的方法,然後執行。

3.Meta Class(元類)

object_class結構體的isa指標指向的是類物件自身的Meta Class(元類)。

而元類就是一個類物件所屬的類。一個物件所屬的類叫作類物件,而一個類物件所屬的類叫作元類

Runtime中將類物件所屬型別叫作Meta Class(元類),用於描述類物件本身所具有的特徵,而元類的methodLists中,儲存了類的方法連結串列,即“類方法”。並且類物件中的isa指標指向的就是元類,每個類物件有且僅有一個與之相關的元類。

因此,方法呼叫的基本機制為:

物件方法呼叫過程

1.通過物件isa指標找到對應的Class(類)

2.在Class(類)的methodList(方法列表)中尋找對應的selector

3.執行對應的selector

例:

[@"a" stringByAppendingString:@"b"];

類方法呼叫過程

1.通過類物件isa指標找到所屬的Meta Class(元類)

2.在Meta Class(元類)的methodList(方法列表)中尋找對應的selector

3.執行對應的selector

例:

[NSString stringWithFormat:@"%@",@"hello"];

因此,例項物件、類、元類之間的關係為:

4.Method(方法)

objc_class結構體的methodLists(方法列表)中存放的元素就是Method(方法)。

表示Method的objc_method結構體的資料結構為

/// An opaque type that represents a method in a class definition.
/// 代表類定義中一個方法的不透明型別
typedef struct objc_method *Method;

struct objc_method {
    SEL _Nonnull method_name;                    // 方法名
    char * _Nullable method_types;               // 方法型別
    IMP _Nonnull method_imp;                     // 方法實現
};

由上可知,objc_method結構體中包含了method_name、method_types和method_imp三個變數

1.SEL method_name

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

SEL是一個指向objc_selector的指標。

2.IMP method_imp

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

IMP實質上是一個函式指標,所指向的就是方法的實現。IMP用來找到函式地址,然後執行函式。

3.char *method_types

方法型別method_types是個字串,用來儲存方法的引數型別和返回值型別。

 

因此,Method將SEL(方法名)和IMP(函式指標)關聯起來,當一個物件傳送訊息時,通過給出的SEL(方法名)去找到IMP(函式指標),然後執行。

4.訊息轉發機制

如果接收物件無法處理,其父類、父類的父類都無法處理,那麼應該怎麼辦?在這種情況下,Objective-C為了增強語言的動態性,程式並不會馬上Crash,在Crash前,有三次機會可以挽救本條訊息的命運。

1.動態新增(resolveInstanceMethod)

如果物件的整個繼承鏈都無法處理當前訊息,那麼首先會呼叫接收物件所屬類的resolveInstanceMethod方法(如果是類方法,則會呼叫resolveClassMethod方法),在這個方法中,開發者有機會為類動態新增方法,如果動態新增了方法,則可以在這個方法中返回YES,那麼這條訊息依然會被成功處理。

例:

main.m

#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "MyObject.h"
#import "objc/message.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        MyObject *obj = [[MyObject alloc] init];
        [obj class];
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Wundeclared-selector"
        ((void(*)(id,SEL))objc_msgSend)(obj,@selector(showSelf));
#pragma clang diagnostic pop
        
    }
    return 0;
}

MyObject.m

#import "MyObject.h"
#import "objc/message.h"

@implementation MyObject
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"resolveInstanceMethod");
    if ([NSStringFromSelector(sel) isEqualToString:@"showSelf"]) {
        class_addMethod(self, sel, (IMP)newFunc, "v@");
    }
    return [super resolveInstanceMethod:sel];
}

void newFunc(id obj, SEL _cmd) {
    NSLog(@"newFunc");
}
@end

2.訊息轉發(forwardingTargetForSelector)

當通過執行時新增方法被否定後,系統會接著呼叫forwardingTargetForSelector方法,這個方法用來對訊息進行轉發。forwardingTargetForSelector方法需要返回一個id型別的物件,系統會將當前物件服務處理的訊息轉發給這個方法返回的物件,如果這個返回的物件可以處理,那麼程式依然可以執行下去。如果返回nil,則表示不進行訊息轉發。

例:

SubObject.m

#import "SubObject.h"

@implementation SubObject
- (void)showSelf {
    NSLog(@"subObject");
}
@end

MyObject.m

#import "MyObject.h"
#import "SubObject.h"
#import "objc/message.h"

@implementation MyObject
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"forwardingTargetForSelector");
    if ([NSStringFromSelector(aSelector) isEqualToString:@"showSelf"]) {
        return [SubObject new];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

3.方法簽名(methodSignatureForSelector)

如果訊息轉發策略也被否定,系統會呼叫methodSignatureForSelector方法,這個方法的主要用途是詢問這個選擇器是否是有效的,需要返回一個NSMthoedSignature物件,這個物件就是函式簽名的抽象。

如果返回了有效的函式簽名,那麼接著系統會呼叫forwardInvocation方法,這個函式會直接將訊息包裝成NSInvocation物件傳入,再直接將其傳送給可以處理此訊息的物件即可。

#import "MyObject.h"
#import "SubObject.h"
#import "objc/message.h"

@implementation MyObject
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"methodSignatureForSelector");
    if ([NSStringFromSelector(aSelector) isEqualToString:@"showSelf"]) {
        return [[SubObject new] methodSignatureForSelector:aSelector];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"forwardInvocation");
    if ([NSStringFromSelector(anInvocation.selector) isEqualToString:@"showSelf"]) {
        [anInvocation invokeWithTarget:[SubObject new]];
    } else {
        [super forwardInvocation:anInvocation];
    }
}
@end

因此,完整的流程如下:

 

訊息傳送與轉發機制總結

呼叫[receiver selector]之後,流程如下:

編譯時階段:[receiver selector]方法被編譯器轉換為:

  • objc_msgSend(receiver,selector) 不帶引數

  • objc_msgSend(receiver,selector,org1,org2,....) 帶引數

執行時階段:訊息接受者receiver尋找對應的selector

  1. 通過receiver的isa指標找到receiver的class(類);

  2. 在class的cache(方法快取)的雜湊表中尋找對應的IMP(方法實現);

  3. 如果在cache中沒有找到對應的IMP的話,繼續在Class的methodList(方法列表)中找對應的selector,如果找到,填充到cache中,並返回selector;

  4. 如果在Class中沒有找到這個selector,就繼續在它的superClass(父類)中尋找;

  5. 一旦找到對應的selector,直接執行receiver對應selector方法實現的IMP;

  6. 如果找不到對應的selector,Runtime系統進入訊息轉發機制

執行時訊息轉發階段

  1. 動態解析:通過重寫resolveInstanceMethod或者resolveClassMethod方法,利用class_addMethod方法新增其他函式實現。

  2. 訊息轉發:在當前物件中利用forwardingTargetForSelector方法將訊息的接受者轉發給其他物件

  3. 訊息重定向:如果上一步返回值為nil,則利用methodSignatureForSelector方法獲取函式的引數和返回值型別:

    1. 如果methodSignatureForSelector返回了一個NSMethodSignature物件,Runtime系統就會建立一個NSInvocation物件,並通過forwardInvocation訊息通知當前物件,給予此次訊息傳送最後一次尋找IMP的機會;

    2. 如果methodSignatureForSelector返回nil,則Runtime系統會發出doesNotRecognizeSelector訊息,程式Crash。

5.具體應用

動態建立一個類

main.m

#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "MyObject.h"
#import "objc/message.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        MyObject *obj = [[MyObject alloc] init];
        [obj class];
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Wundeclared-selector"
        ((void(*)(id,SEL))objc_msgSend)(obj,@selector(showSelf));
#pragma clang diagnostic pop
        
    }
    return 0;
}

MyObject.m

#import "MyObject.h"
#import "SubObject.h"
#import "objc/message.h"

@implementation MyObject
- (void)showSelf {
   NSString *className = @"NSStringSubClass";
    //定義類名
    Class newClass = objc_allocateClassPair(NSString.class, className.UTF8String, 0);
    //動態建立類,對類進行記憶體分配,三個引數:父類、類名稱、額外位元組
    class_addMethod(newClass, @selector(eat), (IMP)EatFunction, "v@:");
    //動態為類新增方法,四個引數:為哪個類新增引數、方法名、方法實現、返回值與方法引數
    objc_registerClassPair(newClass);
    //註冊該類
    
    id instanceOfClass = [[newClass alloc] init];
    //建立該類
//    [instanceOfClass eat];  編譯通不過
    [instanceOfClass performSelector:@selector(eat)];
    //呼叫該類方法
}

void EatFunction(id self, SEL _cmd) {
    //方法實現
    NSLog(@"EatFunction....");
}
@end

動態替換一個類

#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "MyObject.h"
#import "SubObject.h"
#import "objc/message.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        MyObject *obj = [[MyObject alloc] init];
        [obj class];
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Wundeclared-selector"
        ((void(*)(id,SEL))objc_msgSend)(obj,@selector(showSelf));
        
        object_setClass(obj, [SubObject class]);
        //將例項替換成另外一個類
        ((void(*)(id,SEL))objc_msgSend)(obj,@selector(showSelf));
#pragma clang diagnostic pop
        
    }
    return 0;
}

2020-09-24 12:01:22.941989+0800 Test[32351:1216193] MyObject showSelf....

2020-09-24 12:01:22.942219+0800 Test[32351:1216193] SubObject showSelf....

動態獲取類資訊

NSLog(@"%s",class_getName([obj class]));
//獲取類名
NSLog(@"%d",class_getVersion([obj class]));
//獲取類的版本
NSLog(@"%@",class_getSuperclass([obj class]));
//獲取父類
NSLog(@"%zu",class_getInstanceSize([obj class]));

動態獲取所有屬性(非私有與私有皆可)

unsigned int *count = malloc(sizeof(unsigned int));
Ivar *mem = class_copyIvarList([MyObject class], count);
for (int i = 0; i < *count; i++) {
    Ivar var = *(mem + i);
    const char *name = ivar_getName(var);
    const char *type = ivar_getTypeEncoding(var);
    NSLog(@"%s:%s\n",name,type);
}
free(count);
count = nil;

動態設定相關屬性

unsigned int *count = malloc(sizeof(unsigned int));
Ivar *mem = class_copyIvarList([MyObject class], count);
MyObject *obj = [[MyObject alloc] init];
NSLog(@"%@",obj);
object_setIvar(obj, mem[0], (__bridge id)(void *)10);
object_setIvar(obj, mem[1], @"isTwo");
object_setIvar(obj, mem[2], @"isThree");
NSLog(@"%@",obj);

動態獲取所有方法

unsigned int count = 0;
Method *mem = class_copyMethodList([MyObject class], &count);
for (int i = 0; i < count; i++) {
    SEL name = method_getName(mem[i]);
    NSString *method = [NSString stringWithCString:sel_getName(name) encoding:NSUTF8StringEncoding];
    NSLog(@"\n%@",method);
}

動態替換方法

//建立物件
MyObject *obj = [[MyObject alloc] init];
NSLog(@"%@",[obj method1]);
//替換方法
class_replaceMethod([MyObject class], @selector(method1), (IMP)logHHH, "v");
[obj method1];

動態新增方法

unsigned int count = 0;
class_addMethod([MyObject class], @selector(method3), (IMP)logHAHA, "v");
Method *mem = class_copyMethodList([MyObject class], &count);
for (int i = 0; i < count; i++) {
    SEL name = method_getName(mem[i]);
    NSString *method = [NSString stringWithCString:sel_getName(name) encoding:NSUTF8StringEncoding];
    NSLog(@"\n%@",method);
}

參考:

OC語言的動態特性

iOS 開發:『Runtime』詳解

iOS Runtime 教程

Runtime-iOS執行時應用篇

iOS - OC 使用執行時來獲取並修改類

runtime底層實現原理

相關文章