Runtime 從NullSafe原始碼看訊息轉發 機制

3033發表於2019-02-23

#####開篇
馬上就要年底了再碼一波,自己總結一下Runtime,打算總結一下Runtime的各種用法,結合一些常見的原始碼來分析一下,有錯誤在所難免,希望儘量少一點。。。
#####NullSafe與訊息轉發
在處理後臺返回的資料時會碰到返回的空的情況,大家有自己的處理方式來增加程式碼的穩健性,這裡就借常見的NullSafe的原始碼來舉例。
NullSafe原始碼內容並不算很多,主要的實現程式碼如下

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
    //look up method signature
    NSMethodSignature *signature = [super methodSignatureForSelector:selector];
    if (!signature)
    {
        //check implementation cache first
        NSString *selectorString = NSStringFromSelector(selector);
        signature = signatureCache[selectorString];
        if (!signature)
        {
            @synchronized([NSNull class])
            {
                //check again, in case it was resolved while we were waitimg
                signature = signatureCache[selectorString];
                if (!signature)
                {
                    //建立一個快取 獲取到所有的類名
                    //not supported by NSNull, search other classes
                    if (signatureCache == nil)
                    {
                        if ([NSThread isMainThread])
                        {
                            cacheSignatures();
                        }
                        else
                        {
                            dispatch_sync(dispatch_get_main_queue(), ^{
                                cacheSignatures();
                            });
                        }
                    }
                    //遍歷快取,尋找是否已經有可以執行此方法的類
                    //find implementation
                    for (Class someClass in classList)
                    {
                        if ([someClass instancesRespondToSelector:selector])
                        {
                            //其次如果有方法簽名返回,runtime則根據方法簽名建立描述該訊息的NSInvocation,向當前物件傳送forwardInvocation:訊息,以建立的NSInvocation物件作為引數;
                            signature = [someClass instanceMethodSignatureForSelector:selector];
                            break;
                        }
                    }

                    //cache for next time
                    signatureCache[selectorString] = signature ?: [NSNull null];
                }
                else if ([signature isKindOfClass:[NSNull class]])
                {
                    signature = nil;
                }
            }
        }
    }
    return signature;
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
    invocation.target = nil;
    [invocation invoke];
}

複製程式碼

主要方法
int objc_getClassList(Class *buffer, int bufferLen) //獲取class列表
獲取到,專案中類的所有類名。

static void cacheSignatures()
{
    classList = [[NSMutableSet alloc] init];
    signatureCache = [[NSMutableDictionary alloc] init];

    //get class list
    int numClasses = objc_getClassList(NULL, 0);
    Class *classes = (Class *)malloc(sizeof(Class) * (unsigned long)numClasses);
    numClasses = objc_getClassList(classes, numClasses);

    //add to list for checking
    for (int i = 0; i < numClasses; i++)
    {
        //determine if class has a superclass
        Class someClass = classes[i];
        Class superclass = class_getSuperclass(someClass);
        while (superclass)
        {
            if (superclass == [NSObject class])
            {
                [classList addObject:someClass];
                [classList removeObject:[someClass superclass]];
                break;
            }
            superclass = class_getSuperclass(superclass);
        }
    }

    //free class list
    free(classes);
}
複製程式碼

######NullSafe的實現原理
把傳送給NSNull的而NSNull又無法處理的訊息經過如下幾步處理:

  • 建立一個方法快取,這個快取會快取專案中類的所有類名( cacheSignatures()方法),並且對快取查詢,看是否有可以執行的類方法。
  • 其次如果有方法簽名返回,runtime則根據方法簽名建立描述該訊息的NSInvocation,向當前物件傳送forwardInvocation:訊息,以建立的NSInvocation物件作為引數。(想要走 forwardInvocation方法, 必須 先實現 methodSignatureForSelector 而且 返回 NSMethodSignature 必須不能為空。)
  • 如果沒有的話,返回nil,接下來會走forwardInvocation:方法。
  • [invocation invokeWithTarget:nil];將訊息轉發給nil。

下面我們看一下Runtime的訊息轉發原理以及實現。
####Runtime中的訊息轉發
原理我就不一個個敲了。如下:
訊息的轉發分為兩大階段。第一階段先徵詢接收者,所屬的類,看其是否能動態新增方法,以處理當前這個“未知的選擇子”(unknown selector),這叫做“動態方法解析”(dynamic method resolution)。第二階段涉及“完整的訊息轉發機制”。如果執行期系統已經把第一階段執行完了,那麼接收者自己就無法再以動態新增方法的手段來響應包含該選擇子的訊息了。此時,執行期系統會請求接受者以其他手段來處理與訊息相關的方法呼叫。這又細分為兩小步。首先,請接受者看看有沒有其他物件處理這條訊息。若有,則執行期系統會把訊息轉給那個物件,於是訊息轉發過程結束,一起如常。若沒有“備援的接收者”,則啟動完整的訊息轉發機制,執行期系統會把於訊息有關的全部細節都封裝到NSInvocation物件中,再給接收者最後一次機會,令其設法解決當前還未處理的這條訊息。(深入理解Objective-C訊息轉發機制)

舉個例子

我們新建一個專案,定義一個goHome方法,但是不去實現,直接呼叫,可以見到如下錯誤

goHome

常見的錯誤

unrecognized selector sent to instance
複製程式碼

因為我們的方法並沒有實現,去呼叫,在訊息的傳送過程中並沒有找到接受的物件去處理這個訊息,導致了專案拋錯。在Runtime中我們可以通過其他的方式去進行專案Crash的補救。

#####訊息轉發的三種處理方式

  • 動態方法解析 接受者1
+ (BOOL)resolveInstanceMethod:(SEL)sel
+ (BOOL)resolveClassMethod:(SEL)name 
複製程式碼

在呼叫方法時,物件在收到無法解讀的訊息後,首先將呼叫其所屬類的上述類方法,然後在方法中判斷方法名,利用class_addMethod動態新增新的方法名,IMP的功能是實現的新方法 必須有兩個引數。這時我們再執行專案,呼叫 [self goHome];雖然沒有實現,但是會走我們動態新增的方法
newGoHOme並列印資料。

- (void)viewDidLoad {
    [super viewDidLoad];
    [self goHome];
}

//1 防止crash
void newGoHOme(id self, SEL _cmd){
    // implementation
    NSLog(@"這裡執行goHOme的列印");
}
+ (BOOL)resolveInstanceMethod:(SEL)name {
    NSLog(@"resolveInstanceMethod: %@", NSStringFromSelector(name));
    if (name == @selector(goHome)) {
        class_addMethod([self class], name, (IMP)newGoHOme, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:name];
}
+ (BOOL)resolveClassMethod:(SEL)name {
    NSLog(@"resolveClassMethod %@", NSStringFromSelector(name));
    return [super resolveClassMethod:name];
}
/****
 class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
 const char * _Nullable types)
 IMP的功能是實現的新方法  必須有兩個引數
 ****/
複製程式碼
  • 備援接收者2
- (id)forwardingTargetForSelector:(SEL)aSelector
複製程式碼

如果接受者1並未執行,則訊息傳遞到被接受者2處理。我們新建一個CompanyVC,設定並實現goHome方法,

CompanyVC.png
- (void)viewDidLoad {
    [super viewDidLoad];
    [self goHome];
}
//執行其他已實現的方法
-(id)forwardingTargetForSelector:(SEL)aSelector{
    return [[CompanyVC alloc]init];
}
複製程式碼

我們在ViewController中引入CompanyVC標頭檔案,然後在forwardingTargetForSelector返回CompanyVC,然後執行專案,我們可以看到雖然我們仍然沒有實現ViewController中的goHome方法,但是沒有crash並且列印出了CompanyVC中方法的資料。
其實他實現的原理就是接收到轉發資訊時看當前是否能找到援助物件,如果有則將其返回,若找不到就返回nil。在一個物件內部,可能還有一系列其他物件,該物件可經由此方法將能夠處理某選擇子的相關內部物件返回。這裡找到了CompanyVC,所以不會崩潰。

  • 備援接收者3
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 
- (void)forwardInvocation:(NSInvocation *)anInvocation
複製程式碼

如果訊息轉發到這一步基本就是一個完整的訊息轉發了。
完整的訊息轉發過程的圖片,如下(手繪,可能有點醜?)

訊息轉發機制.png

如上圖由1開始事件的調取,然後由2轉發至3再轉發至4進入如下程式碼

- (void)viewDidLoad {
    [super viewDidLoad];
    [self goHome];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    // 為方法生成方法簽名,這個簽名就是給下面的anInvocation呼叫的   invocation中有一個方法就是通過sig生成的
    NSString *sel = NSStringFromSelector(aSelector);
    if ([sel isEqualToString:@"goHome"]) {
        NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"v@:@"]; // 方法簽名
        return sig;
    }
    return [super methodSignatureForSelector:aSelector];

}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = [anInvocation selector];
    CompanyVC *newHome = [CompanyVC new];
    if ([newHome respondsToSelector:selector]) {
        [anInvocation invokeWithTarget:newHome];
    }
}
複製程式碼

查詢goHome方法,然後生成簽名,給anInvocation去呼叫,一樣執行了CompanyVC中的方法,同時避免了程式的Crash。

#####訊息轉發機制總結
如轉發機制圖,在由1到5的過程中,不管是2,還是3對接受到的訊息進行了處理,就結束了一次訊息的轉發不會再往下進行訊息的轉發處理,只有2和3都沒有找到相關的物件可以處理這個方法,轉發訊息給4 5處理,進行一次完整的訊息轉發過程。
如果所有的都無法找到處理這個方法的物件執行下一個方法

- (void)doesNotRecognizeSelector:(SEL)aSelector
複製程式碼

之後呼叫

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

到這裡一樣結束一次訊息的轉發過程。
到這裡我們再去看之前的NullSafe就會清楚這個分類的實現,在接收到Null無法處理時利用轉發機制對訊息進行了處理,防止了專案Crash。

#####參考資料及擴充閱讀
http://www.cocoachina.com/ios/20160830/17424.html
http://blog.csdn.net/mangosnow/article/details/36183535
http://blog.csdn.net/hello_hwc/article/details/49687543
http://blog.csdn.net/app_ios/article/details/52411076
http://www.jianshu.com/p/151edae1d6ee

相關文章