官方描述: The Objective-C language defers as many decisions as it can from compile time and link time to runtime.
「儘可能將決定放到運行的時候,而不是在編譯和連接過程」數組
runtime是一個C語言庫,包含了不少底層的純C語言API。 平時編寫的OC代碼中,程序運行,其實最終都是轉成了runtime的C語言代碼,runtime算是OC的幕後工做者 。緩存
OC與其餘語言不一樣的一點就是,函數調用採用了消息轉發
的機制,但直到程序運行以前,消息都沒有與任何方法綁定起來。只有在真正運行的時候,纔會根據函數的名字來,肯定該調用的函數。markdown
runtime 是有個兩個版本的:數據結構
在Objective-C 1.0
使用的是legacy,在2.0
使用的是modern。 如今通常來講runtime都是指modern。less
首先要了解它底層的一些經常使用數據結構,好比isa指針。ide
當建立一個新對象時,會爲它分配一段內存,該對象的實例變量也會被初始化。第一個變量就是一個指向它的類的指針(isa)。 經過isa指針,一個對象能夠訪問它的類,並經過它的類來訪問全部父類。函數
// 描述類中的一個方法
typedef struct objc_method *Method;
// 實例變量
typedef struct objc_ivar *Ivar;
// 類別Category
typedef struct objc_category *Category;
// 類中聲明的屬性
typedef struct objc_property *objc_property_t;
複製代碼
查看runtime源碼能夠看到關於isa結構。佈局
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
struct {
ISA_BITFIELD; // defined in isa.h
};
};
複製代碼
下面的代碼對isa_t
中的結構體進行了位域聲明,地址從nonpointer
起到extra_rc
結束,從低到高進行排列。位域也是對結構體內存佈局進行了一個聲明,經過下面的結構體成員變量能夠直接操做某個地址。位域總共佔8字節,全部的位域加在一塊兒正好是64位。優化
小提示:union
中bits
能夠操做整個內存區,而位域只能操做對應的位。ui
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
0:表明普通的指針,存儲着Class、Meta-Class
對象的內存地址。 1:表明優化過,使用位域存儲更多的信息。
- has_assoc
是否有設置過關聯對象。若是沒有,釋放時會更快。
- has_cxx_dtor
是否有C++的析構函數.cxx_destruct
若是沒有,釋放時會更快。
- shiftcls
存儲着Class、Meta-Class
對象的內存地址信息
- magic
用於在調試時,分辨對象是否未完成初始化
- weakly_referenced
是否有被弱引用指向過。若是沒有,釋放時會更快
- deallocating
對象是否正在釋放
- extra_rc
裏面存儲的值是引用計數器減1
- has_sidetable_rc
引用計數器是否過大沒法存儲在isa中 若是爲1,那麼引用計數會存儲在一個叫SideTable的類的屬性中
結構體
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; // 用於獲取具體的類的信息
}
複製代碼
查看源碼(只保留了主要代碼)
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;
}
複製代碼
其中的methods、properties、protocols
是二維數組,是可讀可寫的,包含了類的初始內容、分類的內容。
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_list_t,而每個method_list_t中存放着method_t。method_t中是對應方法的imp指針、名字、類型等方法信息。
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; }
};
};
複製代碼
IMP
:表明函數的具體實現 SEL
:表明方法、函數名,通常叫作選擇器。 types
:包含了函數返回值、參數編碼的字符串
關於SEL:
能夠經過@selector()
和sel_registerName()
得到 能夠經過sel_getName()
和NSStringFromSelector()
轉成字符串 不一樣類中相同名字的方法,所對應的方法選擇器是相同的。即,不一樣類的相同SEL是同一個對象。
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;
}
};
複製代碼
class_ro_t裏面的baseMethodList、baseProtocols、ivars、baseProperties
是一維數組,是隻讀的,包含了類的初始內容
iOS中提供了一個叫作@encode的指令,能夠將具體的類型表示成字符串編碼。好比:
+(int)testWithNum:(int)num{
return num;
}
複製代碼
上面的方法能夠用 i20@0:8i16
來表示:
i表示返回值是int類型,20是參數總共20字節
@表示第一個參數是id類型,0表示第一個參數從第0個字節開始 :表示第二個參數是SEL類型。8表示第二個參數從第8個字節開始。 i表示第三個參數是int類型,16表示第三個參數從第16個字節開始 第三個參數從第16個字節開始,是Int類型,佔用4字節。總共20字節
用散列表來緩存曾經調用過的方法,能夠提升方法的查找速度。 結構體 cache_t
struct cache_t {
struct bucket_t *_buckets; // 散列表
mask_t _mask; //散列表的長度 -1
mask_t _occupied; //已經緩存的方法數量
}
// 其中的 散列表
struct bucket_t {
MethodCacheIMP _imp; //函數的內存地址
cache_key_t _key; //SEL做爲Key
}
複製代碼
// 散列表中查找方法緩存
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;
}
複製代碼
當方法緩存太多的時候,超過了容量的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方法調用的本質是,消息轉發機制。好比: 對象instance 調用dotest方法[instance1 dotest];
底層會轉化爲:objc_msgSend(instance1, sel_registerName("dotest"));
OC中方法的調用,其實都是轉換爲objc_msgSend
函數的調用。
實例對象中存放着 isa 指針以及實例變量。由 isa 指針找到實例對象所屬的類對象 (類也是對象)。類中存放着實例方法列表。在這個列表中,方法的保存形式是 SEL
做 key,IMP
做value。
這是在編譯時根據方法名,生成惟一標識
SEL
,IMP
其實就是函數指針 ,指向最終的函數實現。
整個 Runtime 的核心就是 objc_msgSend(receiver, @selector (message))
函數,經過給類發送 SEL
以傳遞消息,找到匹配的 IMP
再獲取最終的實現。
執行流程能夠分爲3大階段:消息發送->動態方法解析->消息轉發
首先判斷receiver是否爲空 若是不爲空,從receiverClass的緩存中,查找方法。(找到了就調用) 若是沒找到,就從receiverClass的class_rw_t
中查找方法。(找到就調用,並緩存) 若是沒找到,就去receiverClassd的父類的緩存中查找。 若是沒找到,就從父類的class_rw_t
中查找方法。 若是沒找到,就看是否還有父類,有就繼續查父類的緩存,方法列表。
由上述知道,去查緩存、方法列表、查父類等這些操做以後,都沒有找到這個方法的實現,這時若是後面不作處理,必然拋出異常:
...due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘-[xxx xxxx]: 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
並添加方法的實現。假如,沒有找到run
這個方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector( run )) {
// 獲取其餘方法 實例方法 或類方法,做爲run的實現
Method method = class_getInstanceMethod(self, @selector(test));
// 動態添加test方法的實現
class_addMethod(self, sel,
method_getImplementation(method),
method_getTypeEncoding(method));
// 返回YES表明有動態添加方法 其實這裏返回NO,也是能夠的,返回YES只是增長了一些打印
return NO;
}
return [super resolveInstanceMethod:sel];
}
複製代碼
上面的代碼,就至關於,調用run的時候,實際上調用的是test。
若是前面消息發送 和動態解析階段,都沒有對方法進行處理,咱們還有最後一個階段。以下
____forwarding___
這個函數中,交代了消息轉發的邏輯。可是不開源。
先判斷forwardingTargetForSelector
的返回值。有,就向這個返回值發送消息,讓它調用方法。 若是返回nil
,就調用methodSignatureForSelector
方法,有就調用forwardInvocation
。
其中的參數是一個
NSInvocation
對象,並將消息所有屬性記錄下來。NSInvocation
對象包括了Selector、target
以及其餘參數。其中的實現僅僅是改變了target
指向,使消息保證可以調用。
假若發現本類沒法處理,則繼續查找父類,直至 NSObject
。若是methodSignatureForSelector
方法返回nil
,就調用doesNotRecognizeSelector:
方法。
應用舉例:
類Person只定義了方法run但沒有實現,另外有類Car實現了方法run。
如今Person中,重寫forwardingTargetForSelector
返回Car對象
// 消息轉發
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(run)) {
return [[Car alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
複製代碼
這時,當person實例調用run方法時,會變成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];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
[anInvocation invokeWithTarget:[[Car alloc] init]];
}
複製代碼
這樣,依然能夠調用到car的run方法。
NSInvocation封裝了一個方法調用,包括:方法調用者、方法名、方法參數
anInvocation.target 方法調用者 anInvocation.selector 方法名 [anInvocation getArgument:NULL atIndex:0]
補充: 一、消息轉發的forwardingTargetForSelector、methodSignatureForSelector、forwardInvocation
不只支持實例方法,還支持類方法。不過系統沒有提示,須要寫成實例方法,而後把前面的-改爲+便可。
+(IMP)instanceMethodForSelector:(SEL)aSelector{
}
-(IMP)methodForSelector:(SEL)aSelector{
}
複製代碼
二、只能向運行時動態建立的類添加ivars
,不能向已經存在的類添加ivars
。 這是由於在編譯時,只讀結構體class_ro_t
就被肯定,在運行時不可更改。