深入理解 Objective-C:方法快取
摘要
只要用到Objective-C,我們每天都會跟方法呼叫打交道。我們都知道Objective-C的方法決議是動態的,但是在底層一個方法究竟是怎麼找到的,方法快取又是怎麼運作的卻鮮為人知。本文主要從原始碼角度探究了Objective-C在runtime層的方法決議(Method resolving)過程和方法快取(Method cache)的實現。
簡介
本文作者來自美團酒店旅遊事業群iOS研發組。我們致力於創造價值、提升效率、追求卓越。歡迎大家加入我們(簡歷請傳送到郵箱 majia03@meituan.com )。
本文系學習Objective-C的runtime原始碼時整理所成,主要剖析了Objective-C在runtime層的方法決議過程和方法快取,內容包括:
- 從訊息決議說起
- 快取為誰而生
- 追本溯源,何為方法快取
- 快取和雜湊
- 十萬個為什麼
- 快取 – 效能優化的萬金油?
- 優化,永無止境
從訊息決議說起
我們都知道,在Objective-C裡呼叫一個方法是這樣的:
[object methodA];
這表示我們想去呼叫object的methodA。
但是在Objective-C裡面呼叫一個方法到底意味著什麼呢,是否和C++一樣,任何一個非虛方法都會被編譯成一個唯一的符號,在呼叫的時候去查詢符號表,找到這個方法然後呼叫呢?
答案是否定的。在Objective-C裡面呼叫一個方法的時候,runtime層會將這個呼叫翻譯成
objc_msgSend(id self, SEL op, ...)
而objc_msgSend具體又是如何分發的呢? 我們來看下runtime層objc_msgSend的原始碼。
在objc-msg-arm.s中,objc_msgSend的程式碼如下:
(ps:Apple為了高度優化objc_msgSend的效能,這個檔案是彙編寫成的,不過即使我們不懂彙編,詳盡的註釋也可以讓我們一窺其真面目)
ENTRY objc_msgSend # check whether receiver is nil teq a1, #0 beq LMsgSendNilReceiver # save registers and load receiver's class for CacheLookup stmfd sp!, {a4,v1} ldr v1, [a1, #ISA] # receiver is non-nil: search the cache CacheLookup a2, v1, LMsgSendCacheMiss # cache hit (imp in ip) and CacheLookup returns with nonstret (eq) set, restore registers and call ldmfd sp!, {a4,v1} bx ip # cache miss: go search the method lists LMsgSendCacheMiss: ldmfd sp!, {a4,v1} b _objc_msgSend_uncached LMsgSendNilReceiver: mov a2, #0 bx lr LMsgSendExit: END_ENTRY objc_msgSend STATIC_ENTRY objc_msgSend_uncached # Push stack frame stmfd sp!, {a1-a4,r7,lr} add r7, sp, #16 # Load class and selector ldr a3, [a1, #ISA] /* class = receiver->isa */ /* selector already in a2 */ /* receiver already in a1 */ # Do the lookup MI_CALL_EXTERNAL(__class_lookupMethodAndLoadCache3) MOVE ip, a1 # Prep for forwarding, Pop stack frame and call imp teq v1, v1 /* set nonstret (eq) */ ldmfd sp!, {a1-a4,r7,lr} bx ip
從上述程式碼中可以看到,objc_msgSend(就arm平臺而言)的訊息分發分為以下幾個步驟:
- 判斷receiver是否為nil,也就是objc_msgSend的第一個引數self,也就是要呼叫的那個方法所屬物件
- 從快取裡尋找,找到了則分發,否則
- 利用objc-class.mm中_class_lookupMethodAndLoadCache3(為什麼有個這麼奇怪的方法。本文末尾會解釋)方法去尋找selector
- 如果支援GC,忽略掉非GC環境的方法(retain等)
- 從本class的method list尋找selector,如果找到,填充到快取中,並返回selector,否則
- 尋找父類的method list,並依次往上尋找,直到找到selector,填充到快取中,並返回selector,否則
- 呼叫_class_resolveMethod,如果可以動態resolve為一個selector,不快取,方法返回,否則
- 轉發這個selector,否則
- 報錯,丟擲異常
快取為誰而生
從上面的分析中我們可以看到,當一個方法在比較“上層”的類中,用比較“下層”(繼承關係上的上下層)物件去呼叫的時候,如果沒有快取,那麼整個查詢鏈是相當長的。就算方法是在這個類裡面,當方法比較多的時候,每次都查詢也是費事費力的一件事情。
考慮下面的一個呼叫過程:
for ( int i = 0; i < 100000; ++i) { MyClass *myObject = myObjects[i]; [myObject methodA]; }
當我們需要去呼叫一個方法數十萬次甚至更多地時候,查詢方法的消耗會變的非常顯著。
就算我們平常的非大規模呼叫,除非一個方法只會呼叫一次,否則快取都是有用的。在執行時,那麼多物件,那麼多方法呼叫,節省下來的時間也是非常可觀的。
追本溯源,何為方法快取
本著原始碼面前,了無祕密的原則,我們看下原始碼中的方法快取到底是什麼,在objc-cache.mm中,objc_cache的定義如下:
struct objc_cache { uintptr_t mask; /* total = mask + 1 */ uintptr_t occupied; cache_entry *buckets[1]; };
嗯,objc_cache的定義看起來很簡單,它包含了下面三個變數:
1)、mask:可以認為是當前能達到的最大index(從0開始的),所以快取的size(total)是mask+1
2)、occupied:被佔用的槽位,因為快取是以雜湊表的形式存在的,所以會有空槽,而occupied表示當前被佔用的數目
3)、buckets:用陣列表示的hash表,cache_entry型別,每一個cache_entry代表一個方法快取
(buckets定義在objc_cache的最後,說明這是一個可變長度的陣列)
而cache_entry的定義如下:
typedef struct { SEL name; // same layout as struct old_method void *unused; IMP imp; // same layout as struct old_method } cache_entry;
cache_entry定義也包含了三個欄位,分別是:
1)、name,被快取的方法名字
2)、unused,保留欄位,還沒被使用。
3)、imp,方法實現
快取和雜湊
快取的儲存使用了雜湊表。
為什麼要用雜湊表呢?因為雜湊表檢索起來更快,我們來看下是方法快取如何雜湊和檢索的:
// Scan for the first unused slot and insert there. // There is guaranteed to be an empty slot because the // minimum size is 4 and we resized at 3/4 full. buckets = (cache_entry **)cache->buckets; for (index = CACHE_HASH(sel, cache->mask); buckets[index] != NULL; index = (index+1) & cache->mask) { // empty } buckets[index] = entry;
這是往方法快取裡存放一個方法的程式碼片段,我們可以看到sel被雜湊後找到一個空槽放在buckets中,而CACHE_HASH的定義如下:
#define CACHE_HASH(sel, mask) (((uintptr_t)(sel)>>2) & (mask))
這段程式碼就是利用了sel的指標地址和mask做了一下簡單計算得出的。
而從雜湊表取快取則是利用匯編語言寫成的(是為了高度優化objc_msgSend而使用匯編的)。我們看objc-msg-arm.mm 裡面的CacheLookup方法:
.macro CacheLookup /* selReg, classReg, missLabel */ MOVE r9, $0, LSR #2 /* index = (sel >> 2) */ ldr a4, [$1, #CACHE] /* cache = class->cache */ add a4, a4, #BUCKETS /* buckets = &cache->buckets */ /* search the cache */ /* a1=receiver, a2 or a3=sel, r9=index, a4=buckets, $1=method */ 1: ldr ip, [a4, #NEGMASK] /* mask = cache->mask */ and r9, r9, ip /* index &= mask */ ldr $1, [a4, r9, LSL #2] /* method = buckets[index] */ teq $1, #0 /* if (method == NULL) */ add r9, r9, #1 /* index++ */ beq $2 /* goto cacheMissLabel */ ldr ip, [$1, #METHOD_NAME] /* load method->method_name */ teq $0, ip /* if (method->method_name != sel) */ bne 1b /* retry */ /* cache hit, $1 == method triplet address */ /* Return triplet in $1 and imp in ip */ ldr ip, [$1, #METHOD_IMP] /* imp = method->method_imp */ .endmacro
雖然是彙編,但是註釋太詳盡了,理解起來並不難,還是求hash,去buckets裡找,找不到按照hash衝突的規則繼續向下,直到最後。
十萬個為什麼
瞭解了方法快取的定義之後,我們提出幾個問題並一一解答
- 方法快取存在什麼地方?
讓我們去翻看類的定義,在Objective-C 2.0中,Class的定義大致是這樣的(見objc-runtime.mm)struct _class_t { struct _class_t *isa; struct _class_t *superclass; void *cache; void *vtable; struct _class_ro_t *ro; };
我們看到在類的定義裡就有cache欄位,沒錯,類的所有快取都存在metaclass上,所以每個類都只有一份方法快取,而不是每一個類的object都儲存一份。
- 父類方法的快取只存在父類麼,還是子類也會快取父類的方法?
在第一節對objc_msgSend的追溯中我們可以看到,即便是從父類取到的方法,也會存在類本身的方法快取裡。而當用一個父類物件去呼叫那個方法的時候,也會在父類的metaclass裡快取一份。 - 類的方法快取大小有沒有限制?
要回答這個問題,我們需要再看一下原始碼,在objc-cache.mm有一個變數定義如下:/* When _class_slow_grow is non-zero, any given cache is actually grown * only on the odd-numbered times it becomes full; on the even-numbered * times, it is simply emptied and re-used. When this flag is zero, * caches are grown every time. */ static const int _class_slow_grow = 1;
其實不用再看進一步的程式碼片段,僅從註釋我們就可以看到問題的答案。註釋中說明,當_class_slow_grow是非0值的時候,只有當方法快取第奇數次滿(使用的槽位超過3/4)的時候,方法快取的大小才會增長(會清空快取,否則hash值就不對了);當第偶數次滿的時候,方法快取會被清空並重新利用。 如果_class_slow_grow值為0,那麼每一次方法快取滿的時候,其大小都會增長。
所以單就問題而言,答案是沒有限制,雖然這個值被設定為1,方法快取的大小增速會慢一點,但是確實是沒有上限的。 - 為什麼類的方法列表不直接做成雜湊表呢,做成list,還要單獨快取,多費事?
這個問題麼,我覺得有以下三個原因:- 雜湊表是沒有順序的,Objective-C的方法列表是一個list,是有順序的;Objective-C在查詢方法的時候會順著list依次尋找,並且category的方法在原始方法list的前面,需要先被找到,如果直接用hash存方法,方法的順序就沒法保證。
- list的方法還儲存了除了selector和imp之外其他很多屬性
- 雜湊表是有空槽的,會浪費空間
快取 – 效能優化的萬金油?
非也,就算有了有了Objective-C本身的方法快取,我們還是有很多呼叫方法的優化空間,對於這件事情,這篇文章講的非常詳細,大家可以自行移步觀摩http://www.mulle-kybernetik.com/artikel/Optimization/opti-3-imp-deluxe.html (強烈推薦,雖然我們一般不會遇到需要這麼強度優化的地方,但是這種精神和思想是值得我們學習的)
優化,永無止境
在文章末尾,我們再來回答一下第一節提出的問題:“為什麼會有_class_lookupMethodAndLoadCache3這個方法?”
這個方法的實現如下所示:
/*********************************************************************** * _class_lookupMethodAndLoadCache. * Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp(). * This lookup avoids optimistic cache scan because the dispatcher * already tried that. **********************************************************************/ IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls) { return lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/); }
如果單純看方法名,這個方法應該會從快取和方法列表中查詢一個方法,但是如第一節所講,在呼叫這個方法之前,我們已經是從快取無法找到這個方法了,所以這個方法避免了再去掃描快取查詢方法的過程,而是直接從方法列表找起。從Apple程式碼的註釋,我們也完全可以瞭解這一點。不顧一切地追求完美和效能,是一種品質。
後記
本文是Objective-C runtime原始碼研究的第二篇,主要對Objective-C的方法決議和方法快取做了剖析。runtime的原始碼可以在http://www.opensource.apple.com/tarballs/ 下載。如有錯誤,敬請指正。
相關文章
- 深入理解Android中的快取機制(三)磁碟快取Android快取
- 深入理解Objective-C:CategoryObjectGo
- 深入理解HTTP快取機制及原理HTTP快取
- 深入理解瀏覽器快取機制瀏覽器快取
- 深入理解Android中的快取機制(一)快取簡介Android快取
- 深入理解Objective-C RuntimeObject
- 深入理解瀏覽器的快取機制瀏覽器快取
- 深入理解HTML5離線快取機制HTML快取
- 理解 DNS 快取DNS快取
- 深入理解 Objective-C Runtime 機制Object
- HTTP深入之快取HTTP快取
- 深入理解webpack的chunkId對線上快取的思考Web快取
- 深入理解 MyBatis的二級快取的設計原理MyBatis快取
- 深入理解Android中的快取機制(二)RecyclerView跟ListView快取機制對比Android快取View
- Objective-C基礎之四(深入理解Block)ObjectBloC
- 深入理解分散式系統中的快取架構(下)分散式快取架構
- 深入解析 HTTP 快取控制HTTP快取
- 對於前端快取的理解(快取機制和快取型別)前端快取型別
- 從前端角度理解快取前端快取
- 深入Nginx + PHP 快取詳解NginxPHP快取
- 深入理解 Java 方法Java
- 理解 Objective-C 中的指定構造方法Object構造方法
- 輕鬆理解HTTP快取策略HTTP快取
- 理解Java Integer的快取策略Java快取
- 深入剖析瀏覽器快取策略瀏覽器快取
- 快取穿透、快取擊穿、快取雪崩的場景以及解決方法快取穿透
- 區分http請求狀態碼來理解快取(協商快取和強制快取)HTTP快取
- 深入分散式快取 — 學習總結分散式快取
- 方法快取與查詢快取
- 清除xhmlhttp快取的方法HTTP快取
- 微信Access Token 快取方法快取
- 清除DNS快取資訊方法DNS快取
- Java hashCode() 方法深入理解Java
- [深入理解Redis]讀取RDB檔案Redis
- 我理解的瀏覽器快取策略瀏覽器快取
- 瀏覽器快取機制個人理解瀏覽器快取
- 一篇文章理解Web快取Web快取
- 深入淺出瀏覽器快取機制瀏覽器快取