本文較長,分爲如下幾個部分html
cache_t
什麼是runtimec++
蘋果官方說法git
The Objective-C language defers as many decisions as it can from compile time and link time to runtime. (儘可能將決定放到運行的時候,而不是在編譯和連接過程)github
runtime是有個兩個版本的: legacy 、 modern 在Objective-C 1.0使用的是legacy,在2.0使用的是modern。這裏簡單介紹下區別:數組
如今通常來講runtime都是指modern緩存
要想學習Runtime,首先要了解它底層的一些經常使用數據結構,好比isa指針bash
在arm64架構以前,isa就是一個普通的指針,存儲着Class、Meta-Class對象的內存地址數據結構
從arm64架構開始,對isa進行了優化,變成了一個共用體(union)結構,還使用位域來存儲更多的信息。架構
查看runtime源碼能夠看到關於isa結構。官方的源碼是不能編譯的。我本身編譯了一份能夠運行的源碼在github上。app
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
struct {
ISA_BITFIELD; // defined in isa.h
};
};
複製代碼
在runtime723版本之前,直接把結構體放在isa裏面了。750版本以後,抽成宏了,展開宏ISA_BITFIELD
在__arm64__
架構下 以下所示
下面的代碼對isa_t中的結構體進行了位域聲明,地址從nonpointer
起到extra_rc
結束,從低到高進行排列。位域也是對結構體內存佈局進行了一個聲明,經過下面的結構體成員變量能夠直接操做某個地址。位域總共佔8字節,全部的位域加在一塊兒正好是64位。
小提示:union
中bits
能夠操做整個內存區,而位域只能操做對應的位。
eg: 一個對象的地址是0x7faf1b580450
轉換成二進制11111111010111100011011010110000000010001010000
,而後根據不一樣位置,去匹配不一樣的含義
define ISA_BITFIELD \
uintptr_t nonpointer : 1; //指針是否優化過 \
uintptr_t has_assoc : 1; //是否有設置過關聯對象,若是沒有,釋放時會更快 \
uintptr_t has_cxx_dtor : 1; //是否有C++的析構函數(.cxx_destruct),若是沒有,釋放時會更快 \
uintptr_t shiftcls : 33; //存儲着Class、Meta-Class對象的內存地址信息 \
uintptr_t magic : 6; //用於在調試時分辨對象是否未完成初始化 \
uintptr_t weakly_referenced : 1; //是否有被弱引用指向過,若是沒有,釋放時會更快 \
uintptr_t deallocating : 1; //對象是否正在釋放 \
uintptr_t has_sidetable_rc : 1; //引用計數器是否過大沒法存儲在isa中 \
uintptr_t extra_rc : 19 //裏面存儲的值是引用計數器減1
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
複製代碼
nonpointer
has_assoc
has_cxx_dtor
shiftcls
magic
weakly_referenced
deallocating
extra_rc
has_sidetable_rc
eg: 查看objc_runtime-new.mm
文件中有以下代碼。
void *objc_destructInstance(id obj)
{
if (obj) {
//是否有C++的析構函數
bool cxx = obj->hasCxxDtor();
//是否有設置過關聯對象
bool assoc = obj->hasAssociatedObjects();
//有C++的析構函數,就去銷燬
if (cxx) object_cxxDestruct(obj);
//有設置過關聯對象,就去移除管理對象
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
複製代碼
能夠看出,釋放時候,會先判斷是否有設置過關聯對象,若是沒有,釋放時會更快。 是否有C++的析構函數(.cxx_destruct),若是沒有,釋放時會更快。其餘的弱引用,nonpointer等,讀者可自行看源碼。
關Tagged Pointer技術,深刻研究的話,能夠參考唐巧博客深刻理解Tagged Pointer
用一幅圖來表示
查看源碼(只保留了主要代碼)
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; //方法緩存
class_data_bits_t bits; // 用於獲取具體的類的信息
}
複製代碼
也就是說結構體objc_class
裏面
struct objc_class : objc_object {
Class isa;
Class superclass;
cache_t cache; //方法緩存
class_data_bits_t bits; // 用於獲取具體的類的信息}
複製代碼
class_rw_t
根據bits能夠獲得class_rw_t
,class_rw_t
裏面的methods、properties、protocols是二維數組,是可讀可寫的,包含了類的初始內容、分類的內容
eg:方法列表methods中存放着不少一維數組method_list_t
,而每個method_list_t
中存放這method_t
,method_t
中是對應方法的imp指針,名字。類型等方法信息,在詳解iOS中分類Cateogry一文中,咱們知道,每一個分類編譯完成以後都會生成一個_category_t
,對應着method_list_t
。
#define FAST_DATA_MASK 0x00007ffffffffff8UL
class_rw_t *data() {
return bits.data();
}
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
複製代碼
由代碼可知 bits & FAST_DATA_MASK
可得到class_rw_t
.
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
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;
}
複製代碼
結構體method_array_t
class method_array_t :
public list_array_tt<method_t, method_list_t>
{
typedef list_array_tt<method_t, method_list_t> Super;
public:
method_list_t **beginCategoryMethodLists() {
return beginLists();
}
method_list_t **endCategoryMethodLists(Class cls);
method_array_t duplicate() {
return Super::duplicate<method_array_t>();
}
};
複製代碼
method_t
method_t
是對方法、函數的封裝
// IMP表明函數的具體實現
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
複製代碼
SEL
char *
相似@selector()
和sel_registerName()
得到sel_getName()
和NSStringFromSelector()
轉成字符串typedef struct objc_selector *SEL;
複製代碼
types包含了函數返回值、參數編碼的字符串
v16@0:8
表明,返回值void類型,第一個參數是id類型,第二個參數是SEL類型。後面會詳細說明。struct method_t {
SEL name; //函數名
const char *types; //編碼(返回值類型,參數類型)
MethodListIMP imp; //指向函數的指針(函數地址)
struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};
複製代碼
@interface TestObject : NSObject
- (void)testMethod;
@end
@interface TestObject2 : NSObject
- (void)testMethod;
@end
// TestObject實現文件
@implementation TestObject
- (void)testMethod {
NSLog(@"TestObject testMethod %p", @selector(testMethod));
}
@end
// TestObject2實現文件也同樣
@implementation TestObject
- (void)testMethod {
NSLog(@"TestObject testMethod %p", @selector(testMethod));
}
// 結果:
TestObject testMethod 0x100000f81
TestObject2 testMethod 0x100000f81
複製代碼
class_ro_t
class_ro_t
裏面的baseMethodList、baseProtocols、ivars、baseProperties是一維數組,是隻讀的,包含了類的初始內容
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;//instance對象佔用的內存空間
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name; //類名
method_list_t * baseMethodList; //方法列表
protocol_list_t * baseProtocols; //協議列表
const ivar_list_t * ivars; //成員變量列表
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
複製代碼
前面說了,v16@0:8
表明,返回值void類型,第一個參數是id類型,第二個參數是SEL類型。這裏詳細說明
iOS中提供了一個叫作@encode的指令,能夠將具體的類型表示成字符串編碼,連接爲 Type Encodings
eg: 咱們有以下函數
void objc_msgSend(id receiver, SEL selector)
{
if (receiver == nil) return;
// 查找緩存
...
}
複製代碼
就能夠用v16@0:8
表示
v@:
,這在後面講到消息轉發的時候會用到。再如:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
Method method = class_getClassMethod([self class], @selector(test));
const char *str = method_getTypeEncoding(method);
Method method2 = class_getClassMethod([self class], @selector(testWithNum:));
const char *str2 = method_getTypeEncoding(method2);
NSLog(@"test的類型 = %s ",str);
NSLog(@"testWithNum: = %s ",str2);
}
// v16@0:8
+(void)test{
}
// i20@0:8i16
+(int)testWithNum:(int)num{
return num;
}
複製代碼
輸出結果爲:
RuntimeDemo[28247:303205] test的類型 = v16@0:8 RuntimeDemo[28247:303205] testWithNum: = i20@0:8i16
對於方法testWithNum
來講
@encode
cache_t
前面講了Class內部結構,其中有個方法緩存cache_t
,用散列表(哈希表)來緩存曾經調用過的方法,能夠提升方法的查找速度
struct cache_t {
struct bucket_t *_buckets; //散列表
mask_t _mask; //散列表的長度 -1
mask_t _occupied; //已經緩存的方法數量
}
複製代碼
散列表數組_buckets
中存放着bucket_t
,bucket_t
的結構以下
struct bucket_t {
MethodCacheIMP _imp; //函數的內存地址
cache_key_t _key; //SEL做爲Key
}
複製代碼
cache_t
查找原理在cache_t
中如何查找方法,其實對於其餘散列表也是通用的。
在文件objc-cache.mm
中找到bucket_t * cache_t::find(cache_key_t k, id receiver)
// 散列表中查找方法緩存
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
assert(k != 0);
bucket_t *b = buckets();
mask_t m = mask();
mask_t begin = cache_hash(k, m);
mask_t i = begin;
do {
if (b[i].key() == 0 || b[i].key() == k) {
return &b[i];
}
} while ((i = cache_next(i, m)) != begin);
// hack
Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
cache_t::bad_cache(receiver, (SEL)k, cls);
}
複製代碼
其中,根據key
和散列表長度減1 mask
計算出下標 key & mask
,取出的值若是key和當初傳進來的Key相同,就說明找到了。不然,就不是本身要找的方法,就有了hash衝突,把i的值加1,繼續計算。以下代碼
// 計算下標
static inline mask_t cache_hash(cache_key_t key, mask_t mask)
{
return (mask_t)(key & mask);
}
//hash衝突的時候
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}
複製代碼
cache_t
的擴容當方法緩存太多的時候,超過了容量的3/4s時候,就須要擴容了。擴容是,把原來的容量增長爲2倍
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
...
if (cache->isConstantEmptyCache()) {
// Cache is read-only. Replace it.
cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
}
else if (newOccupied <= capacity / 4 * 3) {
// Cache is less than 3/4 full. Use it as-is.
}
else {
// 來到這裏說明,超過了3/4,須要擴容
cache->expand();
}
...
}
複製代碼
具體擴容代碼爲
enum {
INIT_CACHE_SIZE_LOG2 = 2,
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2)
};
// cache_t的擴容
void cache_t::expand()
{
cacheUpdateLock.assertLocked();
uint32_t oldCapacity = capacity();
// 擴容爲原來的2倍
uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
if ((uint32_t)(mask_t)newCapacity != newCapacity) {
// mask overflow - can't grow further // fixme this wastes one bit of mask newCapacity = oldCapacity; } reallocate(oldCapacity, newCapacity); } 複製代碼
咱們常常寫的OC方法調用,到底是怎麼個調用流程,怎麼找到方法的呢?
咱們以下代碼
Person *per = [[Person alloc]init];
[per test];
複製代碼
執行指令
clang -rewrite-objc main.m -o main.cpp
生成cpp文件,對應上面的代碼爲
((void (*)(id, SEL))(void *)objc_msgSend)((id)per, sel_registerName("test"));
複製代碼
簡化爲
objc_msgSend)(per, sel_registerName("test"));
複製代碼
其中,per稱爲消息接收者(receiver), test稱爲消息名稱,也就是說,OC中方法的調用其實都是轉換爲objc_msgSend函數的調用
OC中的方法調用,其實都是轉換爲objc_msgSend
函數的調用
objc_msgSend的執行流程能夠分爲3大階段
消息發送
動態方法解析
消息轉發
運行時期,調用方法流程爲
實例對象中存放 isa 指針以及實例變量,有 isa 指針能夠找到實例對象所屬的類對象 (類也是對象,面向對象中一切都是對象),類中存放着實例方法列表,在這個方法列表中 SEL 做爲 key,IMP 做爲 value。 在編譯時期,根據方法名字會生成一個惟一標識,這個標識就是 SEL。IMP 其實就是函數指針 指向了最終的函數實現。整個 Runtime 的核心就是 objc_msgSend 函數,經過給類發送 SEL 以傳遞消息,找到匹配的 IMP 再獲取最終的實現
類中的 super_class
指針能夠追溯整個繼承鏈。向一個對象發送消息時,Runtime 會根據實例對象的 isa 指針找到其所屬的類,並自底向上直至根類(NSObject)中 去尋找 SEL 所對應的方法,找到後就運行整個方法。
用一張經典的圖來表示就是
類中的 super_class 指針能夠追溯整個繼承鏈。向一個對象發送消息時,Runtime 會根據實例對象的 isa 指針找到其所屬的類,並自底向上直至根類(NSObject)中 去尋找 SEL 所對應的方法,找到後就運行整個方法。
metaClass是元類,也有 isa 指針、super_class 指針。其中保存了類方法列表。
objc-msg-arm64.s
裏面都是彙編
objc-msg-arm64.s
ENTRY _objc_msgSend
b.le LNilOrTagged
CacheLookup NORMAL
.macro CacheLookup
.macro CheckMiss
STATIC_ENTRY __objc_msgSend_uncached
.macro MethodTableLookup
__class_lookupMethodAndLoadCache3
複製代碼
objc-runtime-new.mm
objc-runtime-new.mm
_class_lookupMethodAndLoadCache3
lookUpImpOrForward
getMethodNoSuper_nolock、search_method_list、log_and_fill_cache
cache_getImp、log_and_fill_cache、getMethodNoSuper_nolock、log_and_fill_cache
_class_resolveInstanceMethod
_objc_msgForward_impcache
複製代碼
一直跟到 __forwarding__
的時候,已經不開源的了。
objc-msg-arm64.s
STATIC_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
Core Foundation
__forwarding__(不開源
複製代碼
_objc_msgSend
先來看 objc-msg-arm64.s
主要代碼爲
//1.進入objcmsgSend
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
// x0 recevier
// 消息接收者 消息名稱
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
//2.isa 優化
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone: // 3.isa優化完成
CacheLookup NORMAL //4.執行 CacheLookup NORMAL // calls imp or objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
...省略不少代碼
.macro MethodTableLookup
// push frame
SignLR
stp fp, lr, [sp, #-16]!
mov fp, sp
...省略不少代碼
// receiver and selector already in x0 and x1
mov x2, x16
bl __class_lookupMethodAndLoadCache3 //方法爲_class_lookupMethodAndLoadCache3調用的彙編語言
...省略不少代碼
.endmacro
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
MethodTableLookup //查找IMP
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
STATIC_ENTRY __objc_msgLookup_uncached
UNWIND __objc_msgLookup_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
MethodTableLookup
ret
END_ENTRY __objc_msgLookup_uncached
STATIC_ENTRY _cache_getImp
GetClassFromIsa_p16 p0
CacheLookup GETIMP
...省略不少代碼
複製代碼
從上面的代碼能夠看出方法查找 IMP 的工做交給了 OC 中的 _class_lookupMethodAndLoadCache3 函數,並將 IMP 返回(從 r11 挪到 rax)。最後在 objc_msgSend 中調用 IMP。
彙編代碼比較晦澀難懂,所以這裏將函數的實現反彙編成C語言的僞代碼:
/*
objc_msgSend的C語言版本僞代碼實現.
receiver: 是調用方法的對象
op: 是要調用的方法名稱字符串
*/
id objc_msgSend(id receiver, SEL op, ...)
{
//1............................ 對象空值判斷。
//若是傳入的對象是nil則直接返回nil
if (receiver == nil)
return nil;
//2............................ 獲取或者構造對象的isa數據。
void *isa = NULL;
//若是對象的地址最高位爲0則代表是普通的OC對象,不然就是Tagged Pointer類型的對象
if ((receiver & 0x8000000000000000) == 0) {
struct objc_object *ocobj = (struct objc_object*) receiver;
isa = ocobj->isa;
}
else { //Tagged Pointer類型的對象中沒有直接保存isa數據,因此須要特殊處理來查找對應的isa數據。
//若是對象地址的最高4位爲0xF, 那麼表示是一個用戶自定義擴展的Tagged Pointer類型對象
if (((NSUInteger) receiver) >= 0xf000000000000000) {
//自定義擴展的Tagged Pointer類型對象中的52-59位保存的是一個全局擴展Tagged Pointer類數組的索引值。
int classidx = (receiver & 0xFF0000000000000) >> 52
isa = objc_debug_taggedpointer_ext_classes[classidx];
}
else {
//系統自帶的Tagged Pointer類型對象中的60-63位保存的是一個全局Tagged Pointer類數組的索引值。
int classidx = ((NSUInteger) receiver) >> 60;
isa = objc_debug_taggedpointer_classes[classidx];
}
}
//由於內存地址對齊的緣由和虛擬內存空間的約束緣由,
//以及isa定義的緣由須要將isa與上0xffffffff8才能獲得對象所屬的Class對象。
struct objc_class *cls = (struct objc_class *)(isa & 0xffffffff8);
//3............................ 遍歷緩存哈希桶並查找緩存中的方法實現。
IMP imp = NULL;
//cmd與cache中的mask進行與計算獲得哈希桶中的索引,來查找方法是否已經放入緩存cache哈希桶中。
int index = cls->cache.mask & op;
while (true) {
//若是緩存哈希桶中命中了對應的方法實現,則保存到imp中並退出循環。
if (cls->cache.buckets[index].key == op) {
imp = cls->cache.buckets[index].imp;
break;
}
//方法實現並無被緩存,而且對應的桶的數據是空的就退出循環
if (cls->cache.buckets[index].key == NULL) {
break;
}
//若是哈希桶中對應的項已經被佔用可是又不是要執行的方法,則經過開地址法來繼續尋找緩存該方法的桶。
if (index == 0) {
index = cls->cache.mask; //從尾部尋找
}
else {
index--; //索引減1繼續尋找。
}
} /*end while*/
//4............................ 執行方法實現或方法未命中緩存處理函數
if (imp != NULL)
return imp(receiver, op, ...); //這裏的... 是指傳遞給objc_msgSend的OC方法中的參數。
else
return objc_msgSend_uncached(receiver, op, cls, ...);
}
/*
方法未命中緩存處理函數:objc_msgSend_uncached的C語言版本僞代碼實現,這個函數也是用匯編語言編寫。
*/
id objc_msgSend_uncached(id receiver, SEL op, struct objc_class *cls)
{
//這個函數很簡單就是直接調用了_class_lookupMethodAndLoadCache3 來查找方法並緩存到struct objc_class中的cache中,最後再返回IMP類型。
IMP imp = _class_lookupMethodAndLoadCache3(receiver, op, cls);
return imp(receiver, op, ....);
}
複製代碼
前面跟到_class_lookupMethodAndLoadCache3
以後,後面就不是彙編了,是C語言的實現
class_rw_t
中查找方法(分爲二分查找和線性查找,兩種),若是找到了,就結束查找,緩存一份到本身緩存中,調用方法class_rw_t
中查找方法,若是找到了,就結束查找,緩存一份到本身緩存中,調用方法IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
複製代碼
關鍵代碼在lookUpImpOrForward
裏面,下面的代碼,增長了註釋
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO; //是否動態解析過的標記
runtimeLock.assertUnlocked();
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
runtimeLock.lock();
checkIsKnownClass(cls);
if (!cls->isRealized()) {
realizeClass(cls);
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
}
retry:
runtimeLock.assertLocked();
// 這裏先查緩存,雖然前面彙編裏面已經查過了。可是有可能動態添加,致使緩存有更新
imp = cache_getImp(cls, sel);
//若是查到了,就直接跳轉到最後
if (imp) goto done;
//來到這裏,說明緩存沒有
// Try this class's method lists. { // 查找方法列表 Method meth = getMethodNoSuper_nolock(cls, sel); if (meth) { log_and_fill_cache(cls, meth->imp, sel, inst, cls); imp = meth->imp; //若是查到了,就直接跳轉到最後 goto done; } } // Try superclass caches and method lists. { //查找父類的緩存和方法列表 unsigned attempts = unreasonableClassCount(); // for循環層層向上找 for (Class curClass = cls->superclass; curClass != nil; curClass = curClass->superclass) { // Halt if there is a cycle in the superclass chain. if (--attempts == 0) { _objc_fatal("Memory corruption in class list."); } // Superclass cache. // 父類緩存 imp = cache_getImp(curClass, sel); if (imp) { if (imp != (IMP)_objc_msgForward_impcache) { // 若是父類緩存有,也要緩存一份到本身的緩存中 log_and_fill_cache(cls, imp, sel, inst, curClass); //跳轉到最後 goto done; } else { break; } } // Superclass method list. 查找父類方法列表 Method meth = getMethodNoSuper_nolock(curClass, sel); if (meth) { /// 若是父類方法列表有,也要緩存一份到本身的緩存中 log_and_fill_cache(cls, meth->imp, sel, inst, curClass); imp = meth->imp; //若是查到了,就跳轉到最後 goto done; } } } // 來到這裏,進入第二階段,動態方法解析階段,並且要求沒有動態解析過 if (resolver && !triedResolver) { runtimeLock.unlock(); _class_resolveMethod(cls, sel, inst); runtimeLock.lock(); // Don't cache the result; we don't hold the lock so it may have // changed already. Re-do the search from scratch instead. triedResolver = YES; //動態解析過,標記設爲YES // 回到查找緩存的地方開始查找,緩存中沒有加過,此次去查找,能夠再方法列表中查到 goto retry; } // No implementation found, and method resolver didn't help.
// Use forwarding.
// 來到這裏,說明進入第三階段,消息轉發階段
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlock();
// 返回方法地址
return imp;
}
複製代碼
前面的消息發送階段,沒有找到,就來到動態方法解析階段
頭文件中定義兩個方法
- (void)test;
- (void)run;
複製代碼
只實現test
-(void)test{
NSLog(@"%s",__func__);
}
複製代碼
調用的是時候
Person *per = [[Person alloc]init];
[per run];
複製代碼
由前面的消息發送階段知道,去查緩存,查方法列表,查父類等等,這些操做以後,都沒有找到這個方法的實現,若是後面不作處理,必然拋出異常
報錯方法找不到
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person run]: unrecognized selector sent to instance 0x100f436c0'
若是要處理的話,消息發送階段處理不了。那麼就來到第二階段,動態解析階段。這個階段的處理,從前面的源碼可知
// 動態方法解析
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) { //若是不是元類對象
// try [cls resolveInstanceMethod:sel]
_class_resolveInstanceMethod(cls, sel, inst);
}
else { // 是元類對象
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
複製代碼
系統默認的resolveClassMethod
和resolveInstanceMethod
默認返回NO
+ (BOOL)resolveClassMethod:(SEL)sel {
return NO;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;
}
複製代碼
咱們能夠在動態解析階段,重寫resolveInstanceMethod
並添加方法的實現
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(run)) {
// 獲取其餘方法 實例方法,或者類方法均可以
Method method = class_getInstanceMethod(self, @selector(test));
// 動態添加test方法的實現
class_addMethod(self, sel,
method_getImplementation(method),
method_getTypeEncoding(method));
//等價於下面的
// class_addMethod(self, sel,
method_getImplementation(method),
"v@:");
// 返回YES表明有動態添加方法 其實這裏返回NO,也是能夠的,返回YES只是增長了一些打印
return NO;
}
return [super resolveInstanceMethod:sel];
}
複製代碼
上面的代碼中,由於-(void)test
無參無返回值,函數類型爲v@:
,因此,上面的method_getTypeEncoding(method)
能夠換成"v@:"
也是沒問題的。
這樣的話,就至關於,調用run的時候,實際上調用的是test。由源碼可知,動態解析完以後,回到查找緩存的地方開始查找,緩存中沒有加過,此次去查找,能夠再方法列表中查到。這樣就能夠正確執行了。輸出結果爲
objc-test[6681:75992] -[Person test]
直接運行源碼,以下圖
若是前面消息發送和動態解析階段,對方法都沒有處理,咱們還有最後一個階段,消息轉發階段來處理。從源碼的imp = (IMP)_objc_msgForward_impcache;
能夠看出,_objc_msgForward_impcache
的代碼是在彙編裏面
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
// 這裏進去以後,不開源了
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
... 還有不少代碼
複製代碼
跟到 ___forwarding___
以後就不開源了
objc-test[15568:163497] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person run]: unrecognized selector sent to instance 0x100f039f0'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff307de063 __exceptionPreprocess + 250
1 libobjc.A.dylib 0x000000010038ec9f objc_exception_throw + 47
2 CoreFoundation 0x00007fff308671bd -[NSObject(NSObject) __retain_OA] + 0
3 CoreFoundation 0x00007fff307844b4 ___forwarding___ + 1427
4 CoreFoundation 0x00007fff30783e98 _CF_forwarding_prep_0 + 120
5 objc-test 0x0000000100000e11 main + 97
6 libdyld.dylib 0x00007fff672e93f9 start + 1
7 ??? 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
複製代碼
在上述調用棧中,發現了在 Core Foundation 中會調用___forwarding___
。根據資料也能夠了解到,在 objc_setForwardHandler
時會傳入 __CF_forwarding_prep_0
和 ___forwarding_prep_1___
兩個參數,而這兩個指針都會調用____forwarding___
。這個函數中,也交代了消息轉發的邏輯
接下來怎麼辦呢?能夠經過彙編調試,或逆向來進一步分析後續的實現。
站在前人的代碼上,能看的更遠 ---魯迅.尼古拉斯
___forwarding___
的實現國外有大神復原了___forwarding___
的實現,具體可參考Hmmm, What's that Selector?
須要注意的是,復原了___forwarding___
的實現是僞代碼。具體代碼我已經放在了github上。
僞代碼
// 兩個參數:前者爲被轉發消息的棧指針 IMP ,後者爲是否返回結構體
int __forwarding__(void *frameStackPointer, int isStret) {
id receiver = *(id *)frameStackPointer;
SEL sel = *(SEL *)(frameStackPointer + 8);
const char *selName = sel_getName(sel);
Class receiverClass = object_getClass(receiver);
// 調用 forwardingTargetForSelector:
// 進入 備援接收 主要步驟
if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
// 得到方法簽名
id forwardingTarget = [receiver forwardingTargetForSelector:sel];
// 判斷返回類型是否正確
if (forwardingTarget && forwardingTarget != receiver) {
if (isStret == 1) {
int ret;
objc_msgSend_stret(&ret,forwardingTarget, sel, ...);
return ret;
}
return objc_msgSend(forwardingTarget, sel, ...);
}
}
// 殭屍對象
const char *className = class_getName(receiverClass);
const char *zombiePrefix = "_NSZombie_";
size_t prefixLen = strlen(zombiePrefix); // 0xa
if (strncmp(className, zombiePrefix, prefixLen) == 0) {
CFLog(kCFLogLevelError,
@"*** -[%s %s]: message sent to deallocated instance %p",
className + prefixLen,
selName,
receiver);
<breakpoint-interrupt>
}
// 調用 methodSignatureForSelector 獲取方法簽名後再調用 forwardInvocation
// 進入消息轉發系統
if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
// 判斷返回類型是否正確
if (methodSignature) {
BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct;
if (signatureIsStret != isStret) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'. Signature thinks it does%s return a struct, and compiler thinks it does%s.",
selName,
signatureIsStret ? "" : not,
isStret ? "" : not);
}
if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
// 傳入消息的所有細節信息
NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];
[receiver forwardInvocation:invocation];
void *returnValue = NULL;
[invocation getReturnValue:&value];
return returnValue;
} else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message",
receiver,
className);
return 0;
}
}
}
SEL *registeredSel = sel_getUid(selName);
// selector 是否已經在 Runtime 註冊過
if (sel != registeredSel) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort",
sel,
selName,
registeredSel);
} // doesNotRecognizeSelector,主動拋出異常
// 代表未能獲得處理
else if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
[receiver doesNotRecognizeSelector:sel];
}
else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort",
receiver,
className);
}
// The point of no return.
kill(getpid(), 9);
}
複製代碼
forwardingTargetForSelector
的返回值,若是有值,就向這個返回值發送消息。也就是objc_msgSend(返回值, SEL)
。methodSignatureForSelector
方法,若是有值,就調用forwardInvocation
,其中的參數是一個 NSInvocation 對象,並將消息所有屬性記錄下來。 NSInvocation 對象包括了選擇子、target 以及其餘參數。其中的實現僅僅是改變了 target 指向,使消息保證可以調用。假若發現本類沒法處理,則繼續想父類進行查找。直至 NSObject 。methodSignatureForSelector
方法返回nil,就調用doesNotRecognizeSelector:
方法上面都是源碼分析,那下面代碼驗證
在源碼中forwardingTargetForSelector
系統默認返回nil 。
+ (id)forwardingTargetForSelector:(SEL)sel {
return nil;
}
- (id)forwardingTargetForSelector:(SEL)sel {
return nil;
}
複製代碼
咱們有類Person
只定義了方法- (void)run;
可是沒有實現,另外有類Car
,實現了方法- (void)run;
@interface Car : NSObject
- (void)run;
@end
#import "Car.h"
@implementation Car
- (void)run{
NSLog(@"%s",__func__);
}
@end
複製代碼
在person中,重寫forwardingTargetForSelector
讓返回Car
對象
// 消息轉發
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(run)) {
return [[Car alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
複製代碼
調用的時候
Person *per = [[Person alloc]init];
[per run];
複製代碼
輸出objc-test[16694:174917] -[Car run]
驗證了前面說的,forwardingTargetForSelector
返回值不爲空的話,就向這個返回值發送消息,也就是 objc_msgSend(返回值, SEL)
若是前面的forwardingTargetForSelector
返回爲空, 就會調用 methodSignatureForSelector
獲取方法簽名後再調用 forwardInvocation
// 方法簽名:返回值類型、參數類型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(run)) {
return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
}
return [super methodSignatureForSelector:aSelector];
}
// NSInvocation封裝了一個方法調用,包括:方法調用者、方法名、方法參數
// anInvocation.target 方法調用者
// anInvocation.selector 方法名
// [anInvocation getArgument:NULL atIndex:0]
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
[anInvocation invokeWithTarget:[[Car alloc] init]];
}
複製代碼
依然能夠調用到-[Car run]
注意點1
消息轉發的forwardingTargetForSelector
和methodSignatureForSelector
以及forwardInvocation
不只支持實例方法,還支持類方法。不過系統沒有提示,須要寫成實例方法,而後把前面的-
改爲+
便可。
注意點2 只能向運行時動態建立的類添加ivars,不能向已經存在的類添加ivars
這是由於在編譯時只讀結構體class_ro_t就會被肯定,在運行時是不可更改的。ro結構體中有一個字段是instanceSize,表示當前類在建立對象時須要多少空間,後面的建立都根據這個size分配類的內存。 若是對一個已經存在的類增長一個參數,改變了ivars的結構,這樣在訪問改變以前建立的對象時,就會出現問題。
更多資料,歡迎關注我的公衆號,不定時分享各類技術文章。