博客連接html
如今對於一個 iOS 開發人員來講若是不看一些 Runtime 的源碼都很差意思出去面試. 那麼 Runtime 中常常被問到的除了 Method Swizzling 大概就是 Weak 的實現了吧. 網上搜一下, 所講的內容基本上都是大同小異, 那麼拋開面試的角度, 再去扒一扒 Runtime 中 Weak 的相關源碼, 咱們又能學到什麼新鮮的內容呢? 這裏我將嘗試從其餘角度對源碼進行解讀, 歡迎你們一塊兒探討.c++
本文采用的 Runtime 源碼版本是 750, 官方版本在官網上能夠下載到, 本文使用的是 GitHub 熱心網友提供的可編譯版本, 使用 Xcode 10.1 便可編譯運行. 固然 Runtime 的源碼都是 C/C++ 寫的, 因此熟悉一些 C++ 編程知識仍是頗有必要的.git
爲何從註釋提及? 註釋也是代碼的重要組成部分, 特別是對於一些邏輯較爲複雜的代碼, 重要的核心代碼, '神奇'代碼, 對外暴露的接口等, 註釋是很好的補充, 對於研究和接手代碼的人來講, 是你留下的寶貴財產. 咱們知道 Apple 的 Runtime 源碼是開源的, 那麼開源代碼是怎麼寫註釋的呢? 對於咱們又有什麼借鑑意義呢?
打開 objc-weak.h
頭文件, 咱們來看幾個例子:github
/* The weak table is a hash table governed by a single spin lock. An allocated blob of memory, most often an object, but under GC any such allocation, may have its address stored in a __weak marked storage location through use of compiler generated write-barriers or hand coded uses of the register weak primitive. Associated with the registration can be a callback block for the case when one of the allocated chunks of memory is reclaimed. The table is hashed on the address of the allocated memory. When __weak marked memory changes its reference, we count on the fact that we can still see its previous reference. So, in the hash table, indexed by the weakly referenced item, is a list of all locations where this address is currently being stored. For ARC, we also keep track of whether an arbitrary object is being deallocated by briefly placing it in the table just prior to invoking dealloc, and removing it via objc_clear_deallocating just prior to memory reclamation. */
複製代碼
嘗試翻譯下: 弱引用表是由單個自旋鎖控制的哈希表(線程安全). 一個已分配的內存塊(一般是一個對象, GC 下能夠是任何開闢的內存),經過使用編譯器生成的 write-barriers
或手動編碼註冊弱原語
能夠將其地址存儲在 __weak
標記的存儲位置...golang
// The address of a __weak variable.
// These pointers are stored disguised so memory analysis tools
// don't see lots of interior pointers from the weak table into objects.
typedef DisguisedPtr<objc_object *> weak_referrer_t;
複製代碼
這裏很好的解釋了爲何要對 objc_object *
包一層的緣由, 大意就是內存分析工具只能要看到值而看不到內部結構, 由於經過指針是能直接訪問到該內存區域的.面試
/** * The global weak references table. Stores object ids as keys, * and weak_entry_t structs as their values. */
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
複製代碼
這裏解釋了 weak_table_t
這個結構體的做用.算法
// out_of_line_ness field overlaps with the low two bits of inline_referrers[1].
// inline_referrers[1] is a DisguisedPtr of a pointer-aligned address.
// The low two bits of a pointer-aligned DisguisedPtr will always be 0b00
// (disguised nil or 0x80..00) or 0b11 (any other address).
// Therefore out_of_line_ness == 0b10 is used to mark the out-of-line state.
#define REFERRERS_OUT_OF_LINE 2
複製代碼
這裏把常量定義爲 2 的前因後果都解釋清楚了.shell
/// Adds an (object, weak pointer) pair to the weak table.
id weak_register_no_lock(weak_table_t *weak_table, id referent, id *referrer, bool crashIfDeallocating);
複製代碼
這裏描述的也比較清晰, 將對象和弱指針對添加到弱引用表中. 這裏有兩個細節, _no_lock
後綴說明這個函數沒有加鎖操做是線程不安全的, 調用的時候須要注意是否要進行加鎖處理. 另外 crashIfDeallocating
這個參數說明了對象釋放的過程當中調用 weak_register_no_lock
函數是否會觸發 crash.編程
咱們寫業務代碼的時候通常不太會重視註釋, 包括一些提供給其餘團隊的 SDK 裏面的註釋有的也不是很全, 這裏經過對開源代碼的學習能夠很好的借鑑下良好的註釋應該怎麼去編寫和維護. 這樣後續維護業務代碼也會清晰不少, 咱們也會少一些"技術客服"工做.
另外, 以前不多去研讀註釋, 而是直接看代碼. 仔細讀了下注釋以後, 會對代碼的前世此生, 心路歷程會有更好的瞭解, 說不定還有意外的收穫. 回過頭去 GitHub 上掃了眼經常使用的三方庫, 發現那些受歡迎的開源庫都對註釋不惜筆墨.數組
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
// out_of_line_ness field is low bits of inline_referrers[1]
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
// ...
};
複製代碼
weak_entry_t
結構體中使用了 union
, 此處爲什麼使用 union
? 另外 struct
中爲什麼又使用了位域? 下面會逐個分析下.
什麼 union? 翻譯過來叫共用體或者聯合體, c/c++ 裏面用的比較多, iOS 開發的時候用的比較少. 因此對於我我的來講仍是比較陌生的. 那麼它有什麼好處? 咱們先用一個例子來講明下:
struct s1 {
char mark;
int num;
double score;
};
union u1 {
char mark;
int num;
double score;
};
複製代碼
這裏定義了一個結構體 s1, 一個聯合體 u1. 在 Mac (x86_64) 上面咱們分別 sizeof(struct s1)
sizeof(union u1)
一下獲得的結果是 16 和 8.
對於 s1
在 x86_64 結構計算機上面, char 佔 1 個字節, int 佔 4 個字節, double 佔 8 個字節, 由於 struct
會進行內存對齊, 因此 char 會向 int 對齊, 整個就是 4 + 4 + 8 = 16. 對於 u1
在 x86_64 結構計算機上面, 會以最寬的 double
做爲所佔大小就是 8. 也就是說 union
直觀的一個好處就是省內存, 對於 weak
這種較爲頻繁的操做來講, 也是個不小的優化.
可是使用 union
須要注意的是:
因此使用 union
須要格外的當心, 否則比較容易出錯.
咱們再來看看, weak_entry_t
中 union
的用法. 咱們能夠運行 Runtime 的工程源碼, 斷點查看.
(lldb) po sizeof(weak_entry_t)
40
複製代碼
咱們來分析下:
DisguisedPtr<objc_object>
佔 8 個字節.union
中第一個 struct
中 referrers
佔 8 個字節, out_of_line_ness
佔 2 位, num_refs
佔 62 位, 那麼 out_of_line_ness
和 num_refs
加起來佔 8 個字節, mask
佔 8 個字節, max_hash_displacement
佔 8 個字節. 因此總的佔 8 * 4 = 32 個字節.union
中第二個 struct
中 WEAK_INLINE_COUNT
爲 4, 那麼 inline_referrers
佔 32 個字節.struct
都佔 32 個字節, 因此 8 + 32 = 40 個字節.咱們看到源碼中還有一個技巧就是位域的使用:
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
複製代碼
這樣作的好處, 固然也是節約內存了, 由於 out_of_line_ness
自己只是一個標誌位, 不須要存大數, 因此兩位就夠了, 咱們從註釋中就能夠看出來:
// out_of_line_ness field overlaps with the low two bits of inline_referrers[1].
// inline_referrers[1] is a DisguisedPtr of a pointer-aligned address.
// The low two bits of a pointer-aligned DisguisedPtr will always be 0b00
// (disguised nil or 0x80..00) or 0b11 (any other address).
// Therefore out_of_line_ness == 0b10 is used to mark the out-of-line state.
#define REFERRERS_OUT_OF_LINE 2
複製代碼
也就是代碼中只用到了 out_of_line_ness == REFERRERS_OUT_OF_LINE
進行判斷, 兩位就夠了. 而對於 num_refs
引用計數來講用剩餘的 62 位也夠了. 對於內存的節約, 也是作到了極致了.
咱們知道 union
中同一時刻只有一個 struct
生效, 這裏先看一下代碼:
id obj = [[NSObject alloc] init];
// 前4個存在 `inline_referrers` 中, 下面的 `struct` 生效
__weak id weakObj1 = obj;
__weak id weakObj2 = obj;
__weak id weakObj3 = obj;
__weak id weakObj4 = obj;
// 第5個超限, 進行擴容並從新將這5個存在 `referrers` 中, 上面的 `struct` 生效
__weak id weakObj5 = obj;
複製代碼
因此, 若是一個對象被弱引用的次數較少(<=4), 那麼直接存在數組裏面, 數據在棧中操做相對快些. 若是被弱引用的次數較多, 那麼會在堆上從新開闢內存進行擴容存儲, 並且須要手動管理內存, 操做相對慢些, 處理邏輯上也要複雜不少. 因此使用 weak
的代價仍是有的, 大部分對象被弱引用的次數仍是不超過閾值的, 可以平衡內存和性能.
先來熱個身, 咱們來看一道 LeetCode 的原題 Design HashMap, 這題難度爲 Easy. 若是不考慮內存上的優化的話, 直接使用 1000000 大小數組進行實現便可, 而這個 HashMap 的 key 就是值自己, value 也是值自己. 可是若是考慮上內存優化, 那麼一個快速的 hash 函數和 hash 衝突的處理都是必不可少的. 關於這題的解我用 golang 實現了一遍. golang 版本解, 目前該解已經經過 LeetCode 的單元測試.
weak_table_t
本質上也是一個 Hash Map, 上題中的解也是參考了 weak_table_t
的部分實現, weak_table_t
實現上仍是有不少值得思考和學習的地方.
hash 函數的重要性沒必要多說了, 由於咱們最終須要把 key 映射成下標而後存到數組裏面去, 那麼一個快速的 hash 函數可以保證頻繁操做時的效率, 同時這個 hash 函數計算出來的值又要足夠的均勻和隨機, 這樣可以減小散列衝突的機率, 保證存儲的高效.
那麼 Runtime 源碼中是怎麼實現的呢, 咱們看一下代碼:
// ...
size_t begin = w_hash_pointer(new_referrer) & (entry->mask);
// ...
/** * Unique hash function for weak object pointers only. * * @param key The weak object pointer. * * @return Size unrestricted hash of pointer. */
static inline uintptr_t w_hash_pointer(objc_object **key) {
return ptr_hash((uintptr_t)key);
}
// Pointer hash function.
// This is not a terrific hash, but it is fast
// and not outrageously flawed for our purposes.
// Based on principles from http://locklessinc.com/articles/fast_hash/
// and evaluation ideas from http://floodyberry.com/noncryptohashzoo/
#if __LP64__
static inline uint32_t ptr_hash(uint64_t key) {
key ^= key >> 4;
key *= 0x8a970be7488fda55;
key ^= __builtin_bswap64(key);
return (uint32_t)key;
}
#else
static inline uint32_t ptr_hash(uint32_t key) {
key ^= key >> 4;
key *= 0x5052acdb;
key ^= __builtin_bswap32(key);
return key;
}
#endif
複製代碼
其中 ptr_hash
就是 fast hash 函數了, 固然這段函數的實現邏輯確實沒太看懂, 咱們能夠去 locklessinc.com/articles/fa… 這個網站上參考下, 具體就不深究了, 博客裏面的內容仍是蠻複雜的, 甚至用到了彙編. 正如註釋中描述的那樣, ptr_hash 函數不夠完美(但符合目標)且足夠快, 至於 Apple 作了多少嘗試和測試咱們就無從考證了. 不過下面註釋的代碼能夠看出, 這個簡簡單單的函數仍是通過深思熟慮的.
/* Higher-quality hash function. This is measurably slower in some workloads. #if __LP64__ uint32_t ptr_hash(uint64_t key) { key -= __builtin_bswap64(key); key *= 0x8a970be7488fda55; key ^= __builtin_bswap64(key); key *= 0x8a970be7488fda55; key ^= __builtin_bswap64(key); return (uint32_t)key; } #else static uint32_t ptr_hash(uint32_t key) { key -= __builtin_bswap32(key); key *= 0x5052acdb; key ^= __builtin_bswap32(key); key *= 0x5052acdb; key ^= __builtin_bswap32(key); return key; } #endif */
複製代碼
源碼中還有一處細節值得學習
size_t begin = w_hash_pointer(new_referrer) & (entry->mask);
複製代碼
注意此處用到了位與(&), 而不是一般使用的取餘(%)操做, 爲何呢? 由於 weak table 老是以 2 的 N 次冪進行擴容的, 因此咱們能夠經過位運算進行取餘操做, 可是須要注意的是這種方式只是適合於求一個數除以 2 的 N 次冥. 固然位運算確定比常規取餘要快, 這也算一個小技巧吧.
上文提到, ptr_hash
函數並不是完美的 hash 函數(目前爲止並無完美的 hash 函數), 那麼散列衝突再所不免, 通常的處理散列衝突有兩種主要的方式 -- 開放尋址法和鏈表法. weak table 中使用了開放尋址法, 下面咱們就經過源碼一探究竟:
if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {
return grow_refs_and_insert(entry, new_referrer);
}
size_t begin = w_hash_pointer(new_referrer) & (entry->mask);
size_t index = begin;
size_t hash_displacement = 0;
while (entry->referrers[index] != nil) {
hash_displacement++;
index = (index+1) & entry->mask;
if (index == begin) bad_weak_table(entry);
}
if (hash_displacement > entry->max_hash_displacement) {
entry->max_hash_displacement = hash_displacement;
}
weak_referrer_t &ref = entry->referrers[index];
ref = new_referrer;
entry->num_refs++;
複製代碼
這裏經過 hash 函數和取餘計算出散列值(數組下標), 若是該下標中已經有值, 那麼遞增1從新取餘計算下標, 直到找到一個沒有存值的位置. 固然這裏還有一些其餘邊界條件的判斷. 這裏使用的是線性探測法, 除了這個方法之外還有另外兩種比較經典的方法 -- 二次探測和雙重散列, 這裏暫時就不介紹了, 有興趣的同窗能夠左轉 Google 下. 固然線性探測法有一個比較大的缺陷. 當散列表中插入的數據愈來愈多, 散列表衝突的可能性越大, 空閒位置愈來愈少, 那麼線性探測的時間也會愈來愈長. 這裏咱們看到爲了減小此類狀況的發生, 在開始位置會檢查下當前 weak table 的容量, 若是已經達到總容量的 3/4 就會進行擴容.
代碼雖短, 設計仍是很周到的, 須要細細閱讀, 細細體會.
對於線性探測發來講, 刪除操做也須要特殊注意下, 不能簡單的就把該位置的元素置空. 由於查找操做時, 若是找到一個置空的位置, 咱們就認爲其是有效位置, 若是這個位置是咱們後來刪除並置空的, 那麼原先查找算法就會失效, 原本存在的數據就會認爲不存在, 那麼這個問題該如何解呢? 咱們仍是直接來看源碼:
/** * Remove entry from the zone's table of weak references. */
static void weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry) {
// remove entry
if (entry->out_of_line()) free(entry->referrers);
bzero(entry, sizeof(*entry));
weak_table->num_entries--;
weak_compact_maybe(weak_table);
}
複製代碼
這裏奇怪的地方是, free(entry->referrers);
以後並無將 entry
置空, 並且使用 bzero
對內存存儲的數據進行了擦除. 那麼問題來了, 若是不置空的話, 那麼隨着不斷插入和刪除, 原來的被刪除的元素仍是佔着這位置, 勢必會形成較多的浪費. 因此 Apple 在最後調用 weak_compact_maybe
檢查下, 冗餘空間達到必定閾值就進行壓縮.
由於存在散列衝突, 因此讀取操做多了一步校驗工做:
/** * Return the weak reference table entry for the given referent. * If there is no entry for referent, return NULL. * Performs a lookup. * * @param weak_table * @param referent The object. Must not be nil. * * @return The table of weak referrers to this object. */
static weak_entry_t * weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent) {
assert(referent);
weak_entry_t *weak_entries = weak_table->weak_entries;
if (!weak_entries) return nil;
size_t begin = hash_pointer(referent) & weak_table->mask;
size_t index = begin;
size_t hash_displacement = 0;
// 當存在散列衝突時, hash 函數計算的下標所取出的值可能不是正確值.
// 這裏須要進行遍歷, 直到找到正確的值或者超限返回空值.
// 因此當散列衝突較多時, 數據存取性能都會大幅降低.
while (weak_table->weak_entries[index].referent != referent) {
index = (index+1) & weak_table->mask;
if (index == begin) bad_weak_table(weak_table->weak_entries);
hash_displacement++;
if (hash_displacement > weak_table->max_hash_displacement) {
return nil;
}
}
return &weak_table->weak_entries[index];
}
複製代碼
擴容和壓縮的處理沒有特別的地方, 主要是一些閾值的設定和判斷.
// Grow the given zone's table of weak references if it is full.
static void weak_grow_maybe(weak_table_t *weak_table) {
size_t old_size = TABLE_SIZE(weak_table);
// Grow if at least 3/4 full.
if (weak_table->num_entries >= old_size * 3 / 4) {
weak_resize(weak_table, old_size ? old_size*2 : 64);
}
}
// Shrink the table if it is mostly empty.
static void weak_compact_maybe(weak_table_t *weak_table) {
size_t old_size = TABLE_SIZE(weak_table);
// Shrink if larger than 1024 buckets and at most 1/16 full.
if (old_size >= 1024 && old_size / 16 >= weak_table->num_entries) {
weak_resize(weak_table, old_size / 8);
// leaves new table no more than 1/2 full
}
}
複製代碼
當存儲容量已經大於等於當前總容量的 3/4 時就要進行擴容. 當總容量大於等於 1024 且存儲容量不足 1/16 時, 就須要壓縮. 經過這兩個動態的內存空間處理, 保證 weak table 處於一個可控的內存佔用狀態.
以上是對 weak table 大體分析, 其實它的"周邊"也有很多有意思的地方, 下面咱們繼續扒一扒.
上面提到了, objc-weak.h
裏面暴露出來的函數基本都是 _no_lock
結尾的, 也就是說 __weak
線程安全的問題交給了調用者去處理. 那麼到底是誰再保證線程安全呢? 答案是 SideTable
, 咱們簡單看一下代碼:
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
~SideTable() {
_objc_fatal("Do not delete SideTable.");
}
void lock() { slock.lock(); }
void unlock() { slock.unlock(); }
void forceReset() { slock.forceReset(); }
// Address-ordered lock discipline for a pair of side tables.
template<HaveOld, HaveNew>
static void lockTwo(SideTable *lock1, SideTable *lock2);
template<HaveOld, HaveNew>
static void unlockTwo(SideTable *lock1, SideTable *lock2);
};
複製代碼
SideTable
裏面有 spinlock_t
類型的變量 slock
, 還有 weak_table_t
類型的變量 weak_table
, 同時還有一些鎖方法, 這裏暫時不深究. 加鎖的處理通常有兩種方式:
// 調用靜態函數鎖住兩個 SideTable
SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
//...
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
// 單個 SideTable 調用
table.lock();
//...
table.unlock();
複製代碼
經過上文得知 weak table 是 "The global weak references table". 那麼這個 "global" 是怎麼實現的呢? 經過上面的代碼來看, weak_table
是被 SideTable
持有的, 而 SideTable
是被全局的 SideTables
持有的. 那麼爲何須要這麼設計呢? 在我看來, 應該是爲了存儲和查找的效率考慮吧. 畢竟若是把全部的 weak 變量都存在一個 weak table 中, 那麼這個 weak table 的負擔會有些重. 咱們先看看 SideTables
是怎麼實現的:
// We cannot use a C++ static initializer to initialize SideTables because
// libc calls us before our C++ initializers run. We also don't want a global
// pointer to this struct because of the extra indirection.
// Do it the hard way.
alignas(StripedMap<SideTable>) static uint8_t
SideTableBuf[sizeof(StripedMap<SideTable>)];
static void SideTableInit() {
new (SideTableBuf) StripedMap<SideTable>();
}
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
複製代碼
首先用到 alignas
這裏有 alignas 的說明. 另外初始化函數也比較特殊, 這裏註釋上也給出了詳細解釋, 原來庫的加載順序還會影響代碼的實現. 這裏又接觸到了一個新的類型 StripedMap
. 這又是個什麼數據類型呢? 你暫時能夠理解爲是個簡版的 Hash Map. StripedMap
的詳細實現, 這裏就不展開了, 你們能夠移步到 objc-private.h
看一下. 這裏說一個細節吧:
enum { CacheLineSize = 64 };
// StripedMap<T> is a map of void* -> T, sized appropriately
// for cache-friendly lock striping.
// For example, this may be used as StripedMap<spinlock_t>
// or as StripedMap<SomeStruct> where SomeStruct stores a spin lock.
template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif
struct PaddedT {
T value alignas(CacheLineSize);
};
// 此處省略N行代碼 ...
}
複製代碼
目前來看 iPhone 真機上的大小爲 8 個 CacheLineSize, SideTable 就分散存儲在不一樣的區域上. 那麼怎麼判斷一個對象最終落到那個區域呢? 仍是經過位運算和取餘的方式:
static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}
複製代碼
咱們看一下怎麼調用的:
// 從 SideTables 中獲取一個 SideTable
table = &SideTables()[obj];
template<typename T>
class StripedMap {
// 此處省略N行代碼 ...
// 實際調用代碼
public:
T& operator[] (const void *p) {
return array[indexForPointer(p)].value;
}
const T& operator[] (const void *p) const {
return const_cast<StripedMap<T>>(this)[p];
}
// 此處省略N行代碼 ...
};
複製代碼
這裏咱們對一個全局存儲的數據結構有了一些認識, 後續也能夠借鑑相似的實現, 好比線程安全的設計, 數據存儲結構的設計等等.
固然, 這裏只是分析總結了 weak 實現相關的一些源碼, 整個 NSObject.mm
文件裏面的實現仍是很複雜的, 包含不少的 C/C++ 的實現技巧(好比模板的使用場景仍是比較多的, 後續能夠仔細學習下), 也有不少精妙的設計. 後續應該花更多的時間去專研下, 而不只僅是爲了應付面試. 另外在學習源碼的時候, 每讀一次都會有不一樣的收穫, 也能夠拋開語言層面去想一想通用的設計, 都是頗有意思的.