Runtime 系列文章
深入淺出 Runtime(一):初識
深入淺出 Runtime(二):資料結構
深入淺出 Runtime(三):訊息機制
深入淺出 Runtime(四):super 的本質
深入淺出 Runtime(五):具體應用
深入淺出 Runtime(六):相關面試題
1. objc_object
Objective-C
的物件導向都是基於C/C++
的資料結構——結構體實現的。
我們平時使用的所有物件都是id
型別,id
型別物件對應到runtime
中,就是objc_object
結構體。
// A pointer to an instance of a class.
typedef struct objc_object *id;
複製程式碼
struct objc_object {
private:
isa_t isa;
/*...
isa操作相關
弱引用相關
關聯物件相關
記憶體管理相關
...
*/
};
複製程式碼
2. objc_class
Class
指標用來指向一個 Objective-C 的類,它是objc_class
結構體型別,所以class、meta-class
底層結構都是objc_class
結構體,objc_class
繼承自objc_object
,所以它也有isa
指標,它也是物件。
// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
複製程式碼
struct objc_class : objc_object {
// Class ISA;
Class superclass; // 指向父類
cache_t cache; // 方法快取 formerly cache pointer and vtable
class_data_bits_t bits; // 用於獲取具體的類資訊 class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
};
複製程式碼
2.1 class_data_bits_t
class_data_bits_t
主要是對class_rw_t
的封裝,可以通過bits & FAST_DATA_MASK
獲得class_rw_t
。
struct class_data_bits_t {
// Values are the FAST_ flags above.
uintptr_t bits;
public:
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
};
複製程式碼
class_rw_t
代表了類相關的讀寫資訊,它是對class_ro_t
的封裝;class_rw_t
中主要儲存著類的方法列表、屬性列表、協議列表等;class_rw_t
裡面的methods
、properties
、protocols
都繼承於list_array_tt
二維陣列,是可讀可寫的,包含了類的初始內容、分類的內容。
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;
};
複製程式碼
class_ro_t
代表了類相關的只讀資訊;class_ro_t
中主要儲存著類的成員變數列表、類名等;class_ro_t
裡面的baseMethodList
、baseProtocols
、ivars
、baseProperties
是一維陣列,是隻讀的,包含了類的初始內容;- 一開始類的資訊都存放在
class_ro_t
裡,當程式執行時,經過一系列的函式呼叫棧,在realizeClass()
函式中,將class_ro_t
裡的東西和分類的東西合併起來放到class_rw_t
裡,並讓bits
指向class_rw_t
。
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;
}
};
複製程式碼
method_array_t
與method_list_t
。
2.2 cache_t
- 用於快速查詢方法執行函式;
- 是可增量擴充套件的雜湊表結構,用雜湊表來快取曾經使用過的方法,可以提高方法的查詢速度(空間換時間:犧牲記憶體空間來換取執行效率);
- 是區域性性原理的最佳應用(比如一些方法呼叫的頻率高,存放到
cache
中,下一次呼叫這些方法的命中率就會更高些); - hash 函式式為
f(@selector()) = index, @selector() & _mask
; - 當我們呼叫過一個方法後,
runtime
會將這個方法快取到cache
中,下次再呼叫此方法的時候,runtime
會優先去cache
中查詢。
struct cache_t {
struct bucket_t *_buckets; // 雜湊表
mask_t _mask; // 雜湊表的長度 - 1
mask_t _occupied; // 已經快取的方法數量
};
struct bucket_t {
private:
cache_key_t _key; // SEL
IMP _imp; // IMP 函式的記憶體地址
};
複製程式碼
2.2.1 快取查詢流程
//objc-cache.mm(objc4)
bucket_t * cache_t::find(cache_key_t k, id receiver) // 根據 k 即 @selector 進行查詢
{
assert(k != 0);
bucket_t *b = buckets(); // 獲取_buckets
mask_t m = mask(); // 獲取_mask
mask_t begin = cache_hash(k, m); // 計算起始索引
mask_t i = begin;
do {
// 根據索引 i 從 _buckets 雜湊表中取值
// 如果取出來的 bucket_t 的 _key = 0,說明在索引的位置上還沒有快取過方法,返回該 bucket_t,中止快取查詢,用於 cache_fill_nolock() 函式
// 如果取出來的 bucket_t 的 _key = k,說明查詢成功,返回該 bucket_t
if (b[i].key() == 0 || b[i].key() == k) {
return &b[i];
}
// 在 __arm64__ 下將索引 i -1,繼續查詢,反向遍歷 _buckets 雜湊表
// 直到 i 指向首個元素即索引 = 0 時,將 mask 賦值給 i,使其指向雜湊表最後一個元素,繼續反向遍歷
// 如果此時還沒有找到 k 對應的 bucket_t ,或者是空的 bucket_t ,則迴圈結束,查詢失敗,呼叫 bad_cache() 函式
// 接下來去類物件中 class_rw_t 中的 methods 查詢
} 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);
}
static inline mask_t cache_hash(cache_key_t key, mask_t mask)
{
return (mask_t)(key & mask);
}
static inline mask_t cache_next(mask_t i, mask_t mask) {
// return (i+1) & mask; // __arm__ || __x86_64__ || __i386__
return i ? i-1 : mask; // __arm64__
}
複製程式碼
2.2.2 快取新增流程
//objc-cache.mm(objc4)
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
cacheUpdateLock.assertLocked();
// Never cache before +initialize is done
if (!cls->isInitialized()) return; // 如果類還未初始化,直接返回
// Make sure the entry wasn't added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
if (cache_getImp(cls, sel)) return; // 可能有其它執行緒搶先將該方法快取了,所以要檢查一次快取,如果存在,直接返回
cache_t *cache = getCache(cls); // ️取出該 class 的 cache_t
cache_key_t key = getKey(sel); // ️根據 sel 獲得 _key
// Use the cache as-is if it is less than 3/4 full
mask_t newOccupied = cache->occupied() + 1; // 將 cache_t 的 _occupied 即已經快取的方法數量 + 1,這裡只是為了判斷 +1 後快取容量是否滿
mask_t capacity = cache->capacity(); // 獲得快取容量 = _mask + 1
if (cache->isConstantEmptyCache()) { // 如果快取是隻讀的,重新申請快取空間
// Cache is read-only. Replace it.
cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE); // 申請新的快取空間,並釋放舊的
}
else if (newOccupied <= capacity / 4 * 3) { // ️如果當前已經快取的方法數量 +1 <= 快取容量的 3/4,就繼續往下操作
// Cache is less than 3/4 full. Use it as-is.
}
else { // ️如果以上條件不滿足,說明快取已滿,進行快取擴容
// Cache is too full. Expand it.
cache->expand();
}
// Scan for the first unused slot and insert there. // 掃描第一個未使用的插槽(bucket_t)並將其插入
// There is guaranteed to be an empty slot because the // 必然會有一個空的插槽(bucket_t)
// minimum size is 4 and we resized at 3/4 full. // 因為最小大小是4,我們調整為3/4滿
bucket_t *bucket = cache->find(key, receiver); // ️呼叫 find() 函式進行一次快取查詢,必然會得到一個空的 bucket_t
if (bucket->key() == 0) cache->incrementOccupied(); // ️如果該 bucket_t 為空,將 _occupied 即已經快取的方法數量 + 1
bucket->set(key, imp); // ️新增快取
}
void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
mutex_locker_t lock(cacheUpdateLock);
cache_fill_nolock(cls, sel, imp, receiver);
#else
_collecting_in_critical();
return;
#endif
}
複製程式碼
2.2.3 快取擴容流程
- ① 設定新的快取
bucket_t
,容量 = 舊的兩倍; - ② 設定新的
_mask
=bucket_t
長度 - 1; - ③ 釋放舊的快取(在
runtime
動態交換方法實現時也會釋放快取)。
//objc-cache.mm(objc4)
void cache_t::expand()
{
cacheUpdateLock.assertLocked();
uint32_t oldCapacity = capacity();
// ️將快取擴容為原來的兩倍,如果是首次呼叫,設定快取容量的初始值為 4
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); // ️申請新的快取空間,並釋放舊的
}
enum {
INIT_CACHE_SIZE_LOG2 = 2,
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2)
};
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
bool freeOld = canBeFreed(); // ️判斷一下快取是不是空的,如果為空,就沒必要釋放空間
bucket_t *oldBuckets = buckets();
bucket_t *newBuckets = allocateBuckets(newCapacity);
// Cache's old contents are not propagated.
// This is thought to save cache memory at the cost of extra cache fills.
// fixme re-measure this
assert(newCapacity > 0);
assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
setBucketsAndMask(newBuckets, newCapacity - 1);
if (freeOld) {
cache_collect_free(oldBuckets, oldCapacity);
cache_collect(false);
}
}
bool cache_t::canBeFreed()
{
return !isConstantEmptyCache();
}
bool cache_t::isConstantEmptyCache()
{
return
occupied() == 0 &&
buckets() == emptyBucketsForCapacity(capacity(), false);
}
複製程式碼
- 更多關於
cache_t
的內容,請檢視:
深入淺出 Runtime(三):訊息機制
3. isa 指標
isa
指標用來維護物件和類之間的關係,並確保物件和類能夠通過isa
指標找到對應的方法、例項變數、屬性、協議等;- 在 arm64 架構之前,
isa
就是一個普通的指標,直接指向objc_class
,儲存著Class
、Meta-Class
物件的記憶體地址。instance
物件的isa
指向class
物件,class
物件的isa
指向meta-class
物件; - 從 arm64 架構開始,對
isa
進行了優化,變成了一個共用體(union
)結構,還使用位域來儲存更多的資訊。將 64 位的記憶體資料分開來儲存著很多的東西,其中的 33 位才是拿來儲存class
、meta-class
物件的記憶體地址資訊。要通過位運算將isa
的值& ISA_MASK
掩碼,才能得到class
、meta-class
物件的記憶體地址。
struct objc_object {
Class isa; // 在 arm64 架構之前
};
struct objc_object {
private:
isa_t isa; // 在 arm64 架構開始
};
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if SUPPORT_PACKED_ISA
// extra_rc must be the MSB-most field (so it matches carry/overflow flags)
// nonpointer must be the LSB (fixme or get rid of it)
// shiftcls must occupy the same bits that a real class pointer would
// bits + RC_ONE is equivalent to extra_rc + 1
// RC_HALF is the high bit of extra_rc (i.e. half of its range)
// future expansion:
// uintptr_t fast_rr : 1; // no r/r overrides
// uintptr_t lock : 2; // lock for atomic property, @synch
// uintptr_t extraBytes : 1; // allocated with extra bytes
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL // 用來取出 Class、Meta-Class 物件的記憶體地址
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t nonpointer : 1; // 0:代表普通的指標,儲存著 Class、Meta-Class 物件的記憶體地址
// 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; // 如果為1,代表引用計數過大無法儲存在 isa 中,那麼超出的引用計數會儲存在一個叫 SideTable 結構體的 RefCountMap(引用計數表)雜湊表中
uintptr_t extra_rc : 19; // 裡面儲存的值是引用計數 retainCount - 1
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
};
複製程式碼
3.1 isa 與 superclass 指標指向
3.2 類物件(class)與元類物件(meta-class)
class
、meta-class
底層結構都是objc_class
結構體,objc_class
繼承自objc_object
,所以它也有isa
指標,所以它也是物件;class
中儲存著例項方法、成員變數、屬性、協議等資訊,meta-class
中儲存著類方法等資訊;isa
指標和superclass
指標的指向(如上圖);- 基類的
meta-class
的superclass
指向基類的class
, 決定了一個性質:當我們呼叫一個類方法,會通過class
的isa
指標找到meta-class
,在meta-class
中查詢有無該類方法,如果沒有,再通過meta-class
的superclass
指標逐級查詢父meta-class
,一直找到基類的meta-class
如果還沒找到該類方法的話,就會去找基類的class
中同名的例項方法的實現。
3.3 獲得 class 或者 meta-class 的方式
- 獲得 class 有 3 種方式
- (Class)class;
+ (Class)class;
Class object_getClass(id obj); // 傳參:instance 物件
複製程式碼
- 獲得 meta-class 只有 1 種方式
Class object_getClass(id obj); // 傳參:Class 物件
複製程式碼
示例程式碼如下
NSObject *object1 = [NSObject alloc] init];
NSObject *object2 = [NSObject alloc] init];
// objectClass1 ~ objectClass5 都是 NSObject 的類物件
Class objectClass1 = [object1 class];
Class objectClass2 = [object2 class];
Class objectClass3 = [NSObject class];
Class objectClass4 = object_getClass(object1);
Class objectClass5 = object_getClass(object2);
// objectMetaClass1 ~ objectMetaClass4 都是 NSObject 的元類物件
Class objectMetaClass1 = object_getClass([object1 class];
Class objectMetaClass2 = object_getClass([NSObject class]);
Class objectMetaClass3 = object_getClass(object_getClass(object1));
Class objectMetaClass4 = object_getClass(objectClass5);
複製程式碼
方法實現
- (Class)class {
return object_getClass(self);
}
+ (Class)class {
return self;
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
objc_object::getIsa()
{
if (!isTaggedPointer()) return ISA();
......
}
objc_object::ISA()
{
assert(!isTaggedPointer());
#if SUPPORT_INDEXED_ISA
if (isa.nonpointer) {
uintptr_t slot = isa.indexcls;
return classForIndex((unsigned)slot);
}
return (Class)isa.bits;
#else
return (Class)(isa.bits & ISA_MASK);
#endif
}
#if __ARM_ARCH_7K__ >= 2
# define SUPPORT_INDEXED_ISA 1
#else
# define SUPPORT_INDEXED_ISA 0
#endif
複製程式碼
3.4 為什麼要設計 meta-class ?
目的是將例項和類的相關方法列表以及構建資訊區分開來,方便各司其職,符合單一職責設計原則。
4. method_t
Method
就是method_t
型別的指標;method_t
是對方法/函式的封裝(函式四要素:函式名、返回值、引數、函式體)。
typedef struct method_t *Method;
複製程式碼
struct method_t {
SEL name; // 方法名
const char *types; // 編碼(返回值型別、引數型別)
IMP imp; // 方法的地址/實現
};
複製程式碼
4.1 SEL
- SEL 又稱“選擇器”,它是一個指向方法的
selector
的指標,代表方法/函式名; - SEL 維護在一個全域性的 Map 中,所以它是全域性唯一的,不同類中相同名字的方法的 SEL 是相同的。
typedef struct objc_selector *SEL;
複製程式碼
- SEL 可以通過以下方式獲得
SEL sel1 = @selector(selector);
SEL sel2 = sel_registerName("selector");
SEL sel3 = NSSelectorFromString(@"selector");
複製程式碼
- SEL 可以通過以下方式轉換成字串
char *string1 = sel_getName(sel1);
NSString *string2 = NSStringFromSelector(sel1);
複製程式碼
4.2 IMP
- IMP 是指向方法實現的函式指標;
- 我們呼叫方法,實際上就是根據方法 SEL 查詢 IMP;
method_t
實際上相當於在 SEL 和 IMP 之間做了一個對映。
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
複製程式碼
4.3 Type Encodings
- Type Encodings 編碼技術就是配合
runtime
的技術,把一個方法的返回值型別、引數型別通過字串的形式描述; @encode()
指令可以將型別轉換為 Type Encodings 字串編碼, 如@encode(int)
=i
;OC
方法都有兩個隱式引數,方法呼叫者(id)self
和方法名(SEL) _cmd
,所以我們才能在方法中使用self
和_cmd
;- 如
-(void)test
,它的編碼為“v16@0:8
”,可以簡寫為“v@:
”v
:代表返回值型別為 void@
:代表引數 1 型別為 id:
:代表引數 2 型別為 SEL16
:代表所有引數所佔的總位元組數0
:代表引數 1 從第幾個位元組開始儲存8
:代表引數 2 從第幾個位元組開始儲存 - 下圖為型別對應的 Type Encodings 編碼:
- Type Encodings 在
runtime
的訊息轉發中會使用到; - 更多關於 Type Encodings 的內容,可以檢視官方文件 Type Encodings。