Objective-C:方法緩衝

來源:互聯網
上載者:User

標籤:

 

 

摘要

只要用到Objective-C,我們每天都會跟方法調用打交道。我們都知道Objective-C的方法決議是動態,但是在底層一個方法究竟是怎麼找到的,方法緩衝又是怎麼運作的卻鮮為人知。本文主要從源碼角度探究了Objective-C在runtime層的方法決議(Method resolving)過程和方法緩衝(Method cache)的實現。

簡介

本文作者來自美團酒店旅遊事業群iOS研發組。我們致力於創造價值、提升效率、追求卓越。歡迎大家加入我們(簡曆請發送到郵箱 [email protected] )。

本文系學習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 nilteq     a1, #0    beq     LMsgSendNilReceiver# save registers and load receiver‘s class for CacheLookupstmfd   sp!, {a4,v1}ldr     v1, [a1, #ISA]# receiver is non-nil: search the cacheCacheLookup a2, v1, LMsgSendCacheMiss# cache hit (imp in ip) and CacheLookup returns with nonstret (eq) set, restore registers and callldmfd   sp!, {a4,v1}bx      ip# cache miss: go search the method listsLMsgSendCacheMiss:ldmfd sp!, {a4,v1}b _objc_msgSend_uncachedLMsgSendNilReceiver:    mov     a2, #0    bx      lrLMsgSendExit:END_ENTRY objc_msgSendSTATIC_ENTRY objc_msgSend_uncached# Push stack framestmfd sp!, {a1-a4,r7,lr}add     r7, sp, #16# Load class and selectorldr a3, [a1, #ISA] /* class = receiver->isa  *//* selector already in a2 *//* receiver already in a1 */# Do the lookupMI_CALL_EXTERNAL(__class_lookupMethodAndLoadCache3)MOVE    ip, a1# Prep for forwarding, Pop stack frame and call impteq 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/ 下載。如有錯誤,敬請指正。

Objective-C:方法緩衝

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.