引言
Objective-C是通過消息機(jī)制調(diào)用方法的痴晦,編譯器會(huì)把所有消息發(fā)送轉(zhuǎn)為objc_msgSend方法調(diào)用肛根。說到objc_msgSend的匯編實(shí)現(xiàn),大多數(shù)人會(huì)覺的是因?yàn)?strong>性能高才用匯編實(shí)現(xiàn),幾乎沒有文章說其它原因狼荞。Objective-C所有方法都會(huì)轉(zhuǎn)為objc_msgSend方法調(diào)用,然而每個(gè)方法參數(shù)和返回值都可能不一樣帮碰,參數(shù)和返回值要怎么處理相味?
- 本文首先會(huì)結(jié)合Objective-C Runtime機(jī)制深入分析objc_msgSend匯編實(shí)現(xiàn)。
- 本文最后會(huì)從Calling Conventions角度分析objc_msgSend實(shí)現(xiàn)殉挽,利用Calling Conventions和匯編還可以實(shí)現(xiàn)很多黑科技丰涉。
Objective-C對象結(jié)構(gòu)
Objective-C中消息發(fā)送核心數(shù)據(jù)結(jié)構(gòu)如下:
//以下代碼均為arm64平臺(tái)
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 isa OBJC_ISA_AVAILABILITY;
}
union isa_t
{
Class cls;
uintptr_t bits;
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# 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;//實(shí)際上是selector
IMP _imp; //實(shí)際上是函數(shù)指針
}
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子類的實(shí)例都有個(gè)isa指針,isa指向Class斯碌,Class有superclass一死、cache、實(shí)例方法傻唾、屬性投慈、protocol等Runtime信息,調(diào)用實(shí)例方法的時(shí)候就是通過isa指針找到Class冠骄,然后找到IMP調(diào)用實(shí)際的方法伪煤。
Class本身也是一個(gè)對象,也有isa指針凛辣,指向meta-class抱既,meta-class也是一個(gè)對象,有類方法等屬性扁誓,調(diào)用類方法的時(shí)候防泵,就是通過Class對象的isa指針找到meta-class,然后找到IMP調(diào)用實(shí)際的方法跋理。
實(shí)例择克、Class、meta-class關(guān)系如下圖前普,圖片來源:
消息機(jī)制
當(dāng)編譯器遇到一個(gè)方法調(diào)用時(shí)肚邢,它會(huì)將方法的調(diào)用翻譯成以下函數(shù)中的一個(gè),
objc_msgSend拭卿、 objc_msgSend_stret骡湖、 objc_msgSendSuper 和 objc_msgSendSuper_stret。
- 發(fā)送給對象的父類的消息會(huì)使用 objc_msgSendSuper ;
- 有數(shù)據(jù)結(jié)構(gòu)作為返回值的方法會(huì)使用 objc_msgSendSuper_stret 或 objc_msgSend_stret ;
- 其它的消息都是使用 objc_msgSend 發(fā)送的峻厚。
objc_msgSend查找selector的IMP响蕴,然后調(diào)用實(shí)際的方法,主要包括以下流程:
- 查看cache是否有selector的IMP惠桃,如果有的話直接調(diào)用
- 如果沒cache浦夷,最終會(huì)調(diào)用lookUpImpOrForward辖试,從類方法列表查找IMP并緩存到cache
- 如果方法列表沒有則會(huì)查找基類的方法,直到最上層基類(查找基類的時(shí)候也是先查找緩存劈狐,再查找方法列表)
- 如果基類也沒查找到罐孝,則返回_objc_msgForward的IMP,走消息轉(zhuǎn)發(fā)流程肥缔。
我們也可以自己通過class_getMethodImplementation拿到方法IMP(IMP是實(shí)際方法的函數(shù)指針)莲兢,然后調(diào)用:
//[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匯編代碼不長续膳,結(jié)合objc源碼比較容易看懂改艇。需要注意的是isa和TaggedPointer格式,isa指針不是純粹的指針坟岔,還保存很多其它信息谒兄,具體可以參考isa_t union定義,其中只有3到35位才是class指針社付,所以查找之前會(huì)通過mask轉(zhuǎn)換成class指針舵变。
iOS系統(tǒng)為了提高性能和減小內(nèi)存,使用了TaggedPointer來表示NSNumber瘦穆、NSIndexPath等對象,對象并沒有分配內(nèi)存空間赊豌,而是把對象值保存在指針里面扛或,只有指針無法容納對象才會(huì)分配實(shí)際內(nèi)存。TaggedPointer具體格式如下圖碘饼,tag index表示具體Class熙兔,系統(tǒng)有維護(hù)一個(gè)全局映射表來保存tag index和Class的關(guān)系,具體可以查看objc_tag_index_t定義艾恼,查找到具體Class之后就跟正常oc對象一樣查找IMP了住涉。
Calling Conventions
arm64架構(gòu)是通過q0-q7和x0-x7來傳函數(shù)參數(shù),可以看到objc_msgSend沒對這幾個(gè)寄存器做任何操作钠绍,找到IMP后直接通過br x17調(diào)用IMP舆声,br告訴cpu不是子程序調(diào)用。
Objective-C所以方法發(fā)送都是通過objc_msgSend柳爽,每個(gè)方法返回值和參數(shù)都不一樣媳握,如果objc_msgSend像普通函數(shù)一樣處理參數(shù),為了處理不同參數(shù)類型和參數(shù)個(gè)數(shù)磷脯,可以使用varargs 蛾找,Objective-C調(diào)用的地方必須包裹成varargs,這樣處理非常不靈活赵誓,objc_msgSend用了個(gè)很巧妙的技巧打毛,就是對參數(shù)不做任何處理柿赊,查找到IMP后直接調(diào)用,因?yàn)樵?strong>objc_msgSend開始執(zhí)行時(shí)幻枉,棧幀(stack frame)的狀態(tài)碰声、數(shù)據(jù),和各個(gè)寄存器的組合形式展辞、數(shù)據(jù)奥邮,跟調(diào)用具體的函數(shù)指針(IMP)時(shí)所需的狀態(tài)、數(shù)據(jù)罗珍,是完全一致的洽腺,所以我們用xcode調(diào)試的時(shí)候函數(shù)棧是看不到objc_msgSend,看上去就是消息發(fā)送過程完全沒發(fā)生過覆旱,跟調(diào)用普通的c方法一摸一樣蘸朋。
黑科技
objc_msgSend用很巧妙的技巧處理參數(shù)問題,利用這種技巧可以做很多方法扣唱,比如可以實(shí)現(xiàn)Aspects的效果藕坯,在調(diào)用實(shí)際方法前做些hook操作,hook完后再調(diào)實(shí)際方法噪沙。也可以使用libffi處理參數(shù)問題炼彪,可以搞很多事情。
引用
Why objc_msgSend Must be Written in Assembly
面向切面 Aspects 源碼閱讀
What is a meta-class in Objective-C?
Objective-C 中的消息與消息轉(zhuǎn)發(fā)