iOS 底層探究:方法的本質

這是我參與8月更文挑戰的第9天,活動詳情查看:8月更文挑戰

1 Runtime

1.1 什麼是Runtime

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

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

  • 運行時是一種面向對象的編程語言(面向對象編程)的運行環境。運行時代表了在某個時間段內,哪一個程序正在運行。運行時是計算機程序運行生命週期內的一個階段,其餘階段還包括:編譯時、連接時和加載時。簡單理解就是,代碼跑起來,被裝載到內存中的過程。
  • 編譯時顧名思義就是正在編譯的時候。那什麼叫編譯呢?就是編譯器幫你把源代碼翻譯成機器能識別的二進制代碼。(只是通常意義上說,實際上可能只是翻譯成某個中間狀態的語言。好比Java只有JVM 識別的字節碼,C#中只有CLR能識別的MSIL。另外還有連接器、彙編器,爲了便於理解咱們能夠統稱爲編譯器)
  • 編譯時就是簡單的作一些翻譯工做,好比檢查有沒有關鍵字的錯誤
  • 詞法分析,語法分析之類的過程。
  • 若是發現錯誤,編譯器就會告訴你,平時使用Xcode時,點擊build就開始編譯
  • 若是由errors或者warning信息,那就是編譯器檢查出來的。這是的錯誤叫作編譯時錯誤,這個過程當中作的類型檢查也就是編譯時類型檢查,或靜態類型檢查(所謂靜態就是沒有把代碼放內存中運行起來,而只是把代碼看成文原本掃描下)

1.2 runtime餓使用的三種方式

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

image.png

  • 經過OC上層的代碼實現,例如 [LGPerson Hello]
  • 經過NSObject方法實現,例如isKindOfClass
  • 經過Runtime API底層方法實現,例如class_getImstanceSize

圖中的compiler就是編譯器,就是咱們熟悉的LLVMmarkdown

2 OC方法的本質

咱們知道平時寫的OC代碼,底層實現其實都是C/C++的代碼實現的,再通過編譯器LLVM編譯,最終轉化爲機器語言。 經過clang編譯的源碼,理解了OC對象的本質是結構體,一樣的,咱們也可使用clang命令編譯成main.cpp文件,看安方法的本質是什麼?架構

2.1方法底層的實現

在main函數中,寫入如下代碼app

LGPerson *person = [LGPerson alloc]; 
[person sayNB]; 
[person say:@"NB"];
複製代碼

生成cpp文件,底層代碼以下編程語言

LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc")); 
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB")); 
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)person, sel_registerName("say:"), (NSString *)&__NSConstantStringImpl__var_folders_jl_d06jlfkj2ws74_5g45kms07m0000gn_T_main_d8842a_mi_3);
複製代碼

能夠看出,方法的本質:objc_msgSend消息發送 objc_msgSend的參數函數

  • 消息接收者
  • 消息主體(SEL+參數)

2.2objc_msgSend

在Build Setting中,將Enable Strict Checking of obc_msgSend Calls設置爲NO,將嚴厲的檢查機制關掉,不然objc_msgSend的參數會報錯 導入頭文件post

#import <objc/message.h>
複製代碼

在main函數中,寫入如下代碼ui

@interface LGPerson : NSObject 
- (void)sayNB; 
@end 
@implementation LGPerson 
- (void)sayNB{ NSLog(@"666"); } 
@end 
int main(int argc, const char * argv[]) { 
    @autoreleasepool { 
        LGPerson *person = [LGPerson alloc]; 
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB")); 
    } 
    return 0; 
} 
------------------------- 
//輸出結果: 
666
複製代碼
  • 和利用OC方法的執行結果相同
  • sel_registerName爲@selector()的底層實現

2.2 objc_msgSendSuper

子類調用父類方法時,可以使用objc_msgSendSuper,向父類發送消息 在objc源碼中,找到objc_msgSendSuper的定義

OBJC_EXPORT void 
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ ) 
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
複製代碼
  • 傳入objc_super類型的結構體指針

找到objc_super結構體的定義

struct objc_super { 
    /// Specifies an instance of a class. 
    __unsafe_unretained _Nonnull id receiver; 
    /// Specifies the particular superclass of the instance to message. 
    __unsafe_unretained _Nonnull Class super_class; 
    /* super_class is the first class to search */ 
};
複製代碼
  • receiver:消息接收者
  • super_class傳入第一查找的類,若是找不到,繼續向所屬父類一層一層的查找

在項目中調用objc_msgSendSuper 定義LGeacher,繼承於LGPerson

@interface LGTeacher : LGPerson 
    - (void)sayNB; 
@end 
@implementation LGTeacher 
@end
複製代碼
  • 定義sayNB當法,但不實現

在main函數中,寫入如下代碼

struct lg_objc_super { 
    __unsafe_unretained _Nonnull id receiver; 
    __unsafe_unretained _Nonnull Class super_class;
}; 
int main(int argc, const char * argv[]) { 
    @autoreleasepool { 
        LGTeacher *teacher = [LGTeacher alloc]; 
        struct lg_objc_super lgSuper; 
        lgSuper.receiver=teacher; 
        lgSuper.super_class=[LGPerson class];
        objc_msgSendSuper(&lgSuper, @selector(sayNB)); 
    } 
    return 0; 
} 
------------------------- 
//輸出結果: 
666
複製代碼
  • 按照objc_super相同結構,定義lg_objc_super結構體
  • 咱們已經知道LGTeacher中沒有sayNB方法的實現,因此super_class直接傳入LGPerson的類對象
  • 若是super_class傳入LGTeacher的類方法,打印結果相同。區別在於須要向父類多查找一層。

3 objc_msgSend彙編代碼

在objc4-818.2源碼中,不一樣系統架構的彙編指令都有差別,咱們只針對最經常使用的arn64架構下的彙編代碼進行探索

image.png

3.1 objc_msgSend

ENTRY _objc_msgSend 
    UNWIND _objc_msgSend, NoFrame 

    cmp p0, #0     // nil check and tagged pointer check 
#if SUPPORT_TAGGED_POINTERS 
    b.le LNilOrTagged     // (MSB tagged pointer looks negative) 
#else 
    b.eq LReturnZero 
#endif 
    ldr p13, [x0]     // p13 = isa 
    GetClassFromIsa_p16 p13, 1, x0    // p16 = class
複製代碼
  • ENTRY _objc_msgSend:入口
  • p0寄存器,存儲消息接收者
  • cmp p0, #0:消息接收者和#0比較
    • 彙編代碼中,沒有nil,只有0和1
    • 這裏的#0比較,能夠理解消息接收者爲nil
  • SUPPORT_TAGGED_POINTERS:真機arm64架構,SUPPORT_TAGGED_POINTERS 定義爲1
  • b.le LNilOrTagger:小於等於0,進入LNilOrTagged流程
  • b.eq LReturnZero:等於0,進入LReturnZero流程
  • 不然,消息接收者存在,繼續執行代碼
    • ldr p13, [x0]:將X0寄存器取地址,賦值p13寄存器
    • p13:存儲消息接收者的isa
  • 進入GetClassFromIsa_p16流程,傳入isa、一、消息接收者

3.2 LNilOrTagged

LNilOrTagged: 
    b.eq LReturnZero // nil check 
    GetTaggedClass 
    b LGetIsaDone
複製代碼
  • b.eq:等於0,進入LReturnZero流程
  • 不然,小於0,繼續執行代碼
  • 進入GetTaggedClass流程
  • 進入LGetIsaDone流程

3.3 LReturnZero

LReturnZero: 
    // x0 is already zero 
    mov x1, #0 
    movi d0, #0 
    movi d1, #0 
    movi d2, #0 
    movi d3, #0 
    ret
複製代碼
  • 返回nil

3.4 GetClassFromIsa_p16

.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */ 
#if SUPPORT_INDEXED_ISA 
    // Indexed isa 
    mov p16, \src // optimistically set dst = src 
    tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa 
    // isa in p16 is indexed 
    adrp x10, _objc_indexed_classes@PAGE 
    add x10, x10, _objc_indexed_classes@PAGEOFF 
    ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS // extract index 
    ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array 
1: 

#elif __LP64__ 
    .if \needs_auth == 0 // _cache_getImp takes an authed class already 
        mov p16, \src 
    .else 
        // 64-bit packed isa 
        ExtractISA p16, \src, \auth_address 
    .endif 
#else 
    // 32-bit raw isa 
    mov p16, \src 
#endif 

.endmacro
複製代碼
  • 入參:
    • srcisa
    • needs_auth:1
    • auth_address:消息接收者
  • 當前不符合SUPPORT_INDEXED_ISA條件,跳過
  • 符合__LP64__條件,不知足needs_auth等於0 的條件,進入else分支
  • ExtractISA p16, \src, \auth_address:進入ExtractISA流程
    • 傳入p16寄存器,isa,消息接收者
    • p16寄存器的值,存儲的是什麼不重要,由於傳入到ExtractISA中會被賦值

3.5 ExtractISA

.macro ExtractISA 
    and   $0, $1, #ISA_MASK 
.endmacro
複製代碼
  • $0:p16寄存器
  • $1:isa
  • and 0 , 0, 1, #ISA_MASK:將isa&ISA_MASK的結果,存儲到p16寄存器
    • p16:存儲類對象地址

3.6 LGetIsaDone

LGetIsaDone: 
    // calls imp or objc_msgSend_uncached 
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
複製代碼
  • objc_msgSend的後續流程
    • 消息接收者存在,而且獲得類對象,繼續LGetIsaDone流程
  • 進入CacheLookup緩存查找流程,也就是所謂的sel-imp快速查找流程
  • 內部會調用imp或objc_msgSend_uncached

4 流程圖

image.png --- 爲何要獲取類對象?

  • 在快速查找流程中,查找的cache存儲在類對象中,因此必須拿到類對象才能進行後面的流程

相關文章
相關標籤/搜索