iOS底層探索之Runtime(一):運行時&方法的本質

主題

1. 回顧

在之前的幾篇博客里面,已經對OC類的底層結構進行了分析,并對內部主要的成員變量(isa/bits)做了詳細的分析。在上兩個博客
iOS底層探索之類的結構—cache分析(上)
iOS底層探索之類的結構—cache分析(下)

對類中的cache做了比較詳細的分析。后面通過斷點查看匯編可以發現在insert方法調用流程之前,還有一個cache讀取流程,objc_msgSendcache_getImp。這就涉及到Runtime的知識點了,之前的內容都是承上啟下的,是互相關聯的。

2. Runtime

2.1 什么是Runtime

runtime翻譯過來稱為運行時,與之對應的是編譯時。大部分的iOS開發人員,都聽過runtime這個詞,也知道運行時。但只是停留在表面,只是知道而已,并沒有去深入的去探索和分析過。

OC語言是一門動態語言,擁有動態語言的三大特性:動態類型動態綁定、動態加載。而底層實現就是熟悉又陌生的Runtime。

  • 運行時是一種面向對象的編程語言(面向對象編程)的運行環境。運行時表明了在某個時間段內,哪個程序正在運行。運行時是計算機程序運行生命周期內的一個階段,其它階段還包括:編譯時、鏈接時和加載時。簡單理解就是, 代碼跑起來,被裝載到內存中的過程。(你的代碼保存在磁盤上沒裝入內存之前是個死家伙,只有跑到內存中才變成活的)。
  • 編譯時顧名思義就是正在編譯的時候。那什么叫編譯呢?就是編譯器幫你把源代碼翻譯成機器能識別的二進制代碼 。
  • (當然只是一般意義上這么說,實際上可能只是翻譯成某個中間狀態的語言。比如 Java 只有JVM識別的字節碼,C#中只有CLR能識別的MSIL。另外還有鏈接器、匯編器、為了了便于理解我們可以統稱為編譯器)
  • 那編譯時就是簡單的作一些翻譯工作,比如檢查老兄你有沒有粗心寫錯啥關鍵字了。
  • 詞法分析,語法分析之類的過程。就像個老師檢查學生的作文中有沒有錯別字和病句一樣 。
  • 如果發現啥錯誤編譯器就告訴你,平時使用Xcode時,點下build那就開始編譯。
  • 如果下面有errors或者warning信息,那都是編譯器檢查出來的。這時的錯誤就叫編譯時錯誤,這個過程中做的類型檢查也就叫編譯時類型檢查,或靜態類型檢查(所謂靜態嘛就是沒把真把代碼放內存中運行起來,而只是把代碼當作文本來掃描下)。
image

2.2 runtime的使用的三種方式

runtime的使用的三種方式,其三種實現方法與編譯層和底層的關系如圖所示

  • 通過OC上層的代碼實現,例如 [JPerson hello]

  • 通過NSObject方法實現,例如isKindOfClass

  • 通過Runtime API底層方法實現,例如class_getInstanceSize

    Runtime三種方式及底層的關系圖

圖中的compiler就是編譯器,就是我們熟悉的LLVM

image

3. OC方法的本質

在之前的一篇博客iOS開發之結構體底層探索我們知道平時寫的OC代碼,底層實現其實都是C/C++的代碼實現的,再經過編譯器LLVM編譯,最終轉化為機器語言。
通過clang編譯的源碼,理解了OC對象的本質(結構體),同樣的,我們也可以使用clang命令編譯成main.cpp文件,看看方法的本質是什么?

3.1 objc_msgSend

  • 編譯前
@interface JPPerson : NSObject
@property (nonatomic, readwrite , copy) NSString *personName;
- (void)superTest;
@end

@implementation JPPerson
- (void)superTest {
    NSLog(@"這是父類");
}
@end

@interface JPStudent : JPPerson

@property (nonatomic, readwrite , copy) NSString *studentName;
- (void)test;

@end

@implementation JPStudent
- (void)test {
    NSLog(@"%s",__func__);
}
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        JPStudent *stu = [[JPStudent alloc]init];
        [stu test];
        [stu superTest];
    }
    return 0;
}
  • 編譯后
int main(int argc, const char * argv[]) {
 /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
  JPStudent *stu = ((JPStudent *(*)(id, SEL))(void *)objc_msgSend)((id)((JPStudent *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("JPStudent"), sel_registerName("alloc")), sel_registerName("init"));
     
  ((void (*)(id, SEL))(void *)objc_msgSend)((id)stu, sel_registerName("test"));
  ((void (*)(id, SEL))(void *)objc_msgSend)((id)stu, sel_registerName("superTest"));
 }
 return 0;
}

通過上述代碼可以看出,OC的方法調用,底層變成了objc_msgSend,也就是我們熟悉的消息發送

我們可以通過模仿objc_msgSend方法來實現,[stu test]的調用

objc_msgSend測試

從控制臺的輸出可以看到,是一模摸一樣樣。
由此可以斷定 [stu test]等價于objc_msgSend(stu,sel_registerName("test"))

厲害.png

注意

不能直接調用objc_msgSend,需要導入頭文件#import <objc/message.h>

需要將target --> Build Setting -->搜索msg -- 將enable strict checking of obc_msgSend callsYES改為NO,將嚴厲的檢查機制關掉,否則objc_msgSend的參數會報錯。

  • 未導入#import <objc/message.h>
未導入頭文件報錯
  • 啟用 objc_msgSend 調用的嚴格檢查,設置為NO
嚴格檢查設置

objc_msgSend(消息的接受者,消息的主體(sel + 參數))

3.2 objc_msgSendSuper

在上面??在main函數中調用了父類的方法[stu superTest]clang編譯的源碼里面發現了objc_msgSendSuper。

objc_msgSendSuper

這是子類完全調用了父類的方法,那么我們子類要是也有一個superTest方法,但是子類并沒有實現這個方法,那么我們看看結果如何?

@interface JPPerson : NSObject
@property (nonatomic, readwrite , copy) NSString *personName;
- (void)superTest;
@end

@implementation JPPerson
- (void)superTest {
    NSLog(@"%s",__func__);
}
@end

@interface JPStudent : JPPerson

@property (nonatomic, readwrite , copy) NSString *studentName;
- (void)test;
- (void)superTest;
@end

@implementation JPStudent
- (void)test {
    NSLog(@"%s",__func__);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        JPStudent *stu = [[JPStudent alloc]init];
        [stu test];
        NSLog(@"-------華麗的分割線-----------");
        objc_msgSend(stu,sel_registerName("test"));
        [stu superTest];
    }
    return 0;
}

打印結果:

 -[JPStudent test]
 -------華麗的分割線-----------
 -[JPStudent test]
 -[JPPerson superTest]
 
Program ended with exit code: 0

對象的方法調用,實際是父類的實現方法,為了驗證這個說法,我們可以嘗試通過objc_msgSendSuper實現驗證。

objc_msgSendSuper方法中有兩個參數(結構體,sel),其結構體類型是objc_super定義的結構體對象,且需要指定receiversuper_class兩個屬性,源碼實現定義如下

objc_msgSendSuper

通過查看蘋果的源碼,找到了如下方法

OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
    
OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
objc_super

代碼改造:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        JPStudent *stu = [[JPStudent alloc]init];
        JPPerson *person = [JPPerson alloc];
        [person superTest];

        struct objc_super jpsuper;
        jpsuper.receiver = stu; //消息的接收者
        jpsuper.super_class = [JPStudent class]; //告訴父類是誰,改成 [JPPerson class]也是一樣的
        //消息的接受者還是自己 -> 父類 -> 方法么有找到請你直接找我的父親
        objc_msgSendSuper(&jpsuper, sel_registerName("superTest"));
    }
    return 0;
}

打印結果

[26066:278406] -[JPPerson superTest]
[26066:278406] -[JPPerson superTest]

4. 總結

  • OC調用方法,其實本質是發送消息(objc_msgSend)

  • OC方法的調用,首先是在類中查找,如果類中沒有找到,會到類的父類中查找。

  • 子類調用父類的方法,底層會調用objc_msgSendSuper

更多內容持續更新

?? 請動動你的小手,點個贊????

?? 喜歡的可以來一波,收藏+關注,評論 + 轉發,以免你下次找不到我,哈哈????

??歡迎大家留言交流,批評指正,互相學習??,提升自我??

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容