objc_msgSend彙編原始碼分析
引言
Objective-C是通過訊息機制呼叫方法的,編譯器會把所有訊息傳送轉為objc_msgSend方法呼叫。說到objc_msgSend的彙編實現,大多數人會覺的是因為 效能高才用匯編實現 ,幾乎沒有文章說其它原因。 Objective-C所有方法都會轉為objc_msgSend方法呼叫,然而每個方法引數和返回值都可能不一樣,引數和返回值要怎麼處理?
- 本文首先會結合Objective-C Runtime機制深入分析objc_msgSend彙編實現。
- 本文最後會從Calling Conventions角度分析objc_msgSend實現,利用Calling Conventions和彙編還可以實現很多黑科技。
Objective-C物件結構
Objective-C中訊息傳送核心資料結構如下:
//以下程式碼均為arm64平臺 typedef struct objc_class *Class; typedef struct objc_object *id; struct objc_object { isa_t isa; } struct objc_class : objc_object { Class superclass; cache_t cache;// formerly cache pointer and vtable class_data_bits_t bits;//class_rw_t* } @interface NSObject <NSObject> { Class isaOBJC_ISA_AVAILABILITY; } union isa_t { Class cls; uintptr_t bits; #define ISA_MASK0x0000000ffffffff8ULL #define ISA_MAGIC_MASK0x000003f000000001ULL #define ISA_MAGIC_VALUE 0x000001a000000001ULL struct { uintptr_t nonpointer: 1; uintptr_t has_assoc: 1; uintptr_t has_cxx_dtor: 1; uintptr_t shiftcls: 33; // MACH_VM_MAX_ADDRESS 0x1000000000 uintptr_t magic: 6; uintptr_t weakly_referenced : 1; uintptr_t deallocating: 1; uintptr_t has_sidetable_rc: 1; uintptr_t extra_rc: 19; }; } struct cache_t { struct bucket_t *_buckets; mask_t _mask; mask_t _occupied; } struct bucket_t { cache_key_t _key;//實際上是selector IMP _imp; //實際上是函式指標 } struct class_rw_t { uint32_t flags; uint32_t version; const class_ro_t *ro; method_array_t methods; property_array_t properties; protocol_array_t protocols; Class firstSubclass; Class nextSiblingClass; char *demangledName; #if SUPPORT_INDEXED_ISA uint32_t index; #endif }
NSObject子類的例項都有個isa指標,isa指向Class,Class有superclass、cache、例項方法、屬性、protocol等Runtime資訊,呼叫例項方法的時候就是通過isa指標找到Class,然後找到IMP呼叫實際的方法。
Class本身也是一個物件,也有isa指標,指向meta-class,meta-class也是一個物件,有類方法等屬性,呼叫類方法的時候,就是通過Class物件的isa指標找到meta-class,然後找到IMP呼叫實際的方法。
例項、Class、meta-class關係如下圖, ofollow,noindex">圖片來源 :

物件關係圖
訊息機制
當編譯器遇到一個方法呼叫時,它會將方法的呼叫翻譯成以下函式中的一個,
objc_msgSend、 objc_msgSend_stret、 objc_msgSendSuper 和 objc_msgSendSuper_stret。
- 傳送給物件的父類的訊息會使用 objc_msgSendSuper ;
- 有資料結構作為返回值的方法會使用 objc_msgSendSuper_stret 或 objc_msgSend_stret ;
- 其它的訊息都是使用 objc_msgSend 傳送的。
objc_msgSend查詢selector的IMP,然後呼叫實際的方法,主要包括以下流程:
- 檢視cache是否有selector的IMP,如果有的話直接呼叫
- 如果沒cache,最終會呼叫lookUpImpOrForward,從類方法列表查詢IMP並快取到cache
- 如果方法列表沒有則會查詢基類的方法,直到最上層基類(查詢基類的時候也是先查詢快取,再查詢方法列表)
- 如果基類也沒查詢到,則返回_objc_msgForward的IMP,走訊息轉發流程。
我們也可以自己通過class_getMethodImplementation拿到方法IMP(IMP是實際方法的函式指標),然後呼叫:
//[view addSubview:view2] void (*funtion_pointer)(id, SEL, UIView*) = (void(*)(id, SEL, UIView*)) class_getMethodImplementation((id)view, @selector(addSubview:)); funtion_pointer(view, @selector(addSubview:), view2)
彙編原始碼
objc_msgSend彙編原始碼在 Messengers.subproj 目錄,具體彙編如下:

objc_msgSend彙編原始碼

_objc_msgSend_uncached彙編原始碼
objc_msgSend彙編程式碼不長,結合objc原始碼比較容易看懂。需要注意的是isa和TaggedPointer格式,isa指標不是純粹的指標,還儲存很多其它資訊,具體可以參考isa_t union定義,其中只有3到35位才是class指標,所以查詢之前會通過mask轉換成class指標。

isa格式
iOS系統為了提高效能和減小記憶體,使用了TaggedPointer來表示NSNumber、NSIndexPath等物件,物件並沒有分配記憶體空間,而是把物件值儲存在指標裡面,只有指標無法容納物件才會分配實際記憶體。TaggedPointer具體格式如下圖,tag index表示具體Class,系統有維護一個全域性對映表來儲存tag index和Class的關係,具體可以檢視 objc_tag_index_t 定義,查詢到具體Class之後就跟正常oc物件一樣查詢IMP了。

TaggedPointer指標格式
Calling Conventions
arm64架構是通過q0-q7和x0-x7來傳函式引數,可以看到objc_msgSend沒對這幾個暫存器做任何操作,找到IMP後直接通過br x17呼叫IMP,br告訴cpu不是子程式呼叫。
Objective-C所以方法傳送都是通過objc_msgSend,每個方法返回值和引數都不一樣,如果objc_msgSend像普通函式一樣處理引數,為了處理不同引數型別和引數個數,可以使用varargs ,Objective-C呼叫的地方必須包裹成varargs,這樣處理非常不靈活,objc_msgSend用了個很巧妙的技巧,就是對引數不做任何處理,查詢到IMP後直接呼叫,因為在 objc_msgSend開始執行時,棧幀(stack frame)的狀態、資料,和各個暫存器的組合形式、資料,跟呼叫具體的函式指標(IMP)時所需的狀態、資料,是完全一致的 ,所以我們用xcode除錯的時候函式棧是看不到objc_msgSend,看上去就是訊息傳送過程完全沒發生過,跟呼叫普通的c方法一摸一樣。
黑科技
objc_msgSend用很巧妙的技巧處理引數問題,利用這種技巧可以做很多方法,比如可以實現Aspects的效果,在呼叫實際方法前做些hook操作,hook完後再調實際方法。也可以使用libffi處理引數問題,可以搞很多事情。