iOS中responseToSelector()方法是不是需要優化

Edgars發表於2019-01-21

在日常開發中我經常會呼叫responseToSelector這個方法,尤其是是在我們寫的協議的類中我們經常會有這樣的判斷。最近重新看《編寫高質量iOS於OS X程式碼的52個有效方法》這本書的時候作者提供了不一樣的思路,作者認為可以在物件初始化的時候去判斷,然後用列舉的方式進行判斷,來防止每次查詢的過程,提高開發效率,我當時思考的問題是,應該是第一次呼叫方法之後就儲存在了類的方法快取列表中,之後每次呼叫responseToSelector()方法都會優先在快取列表中查詢麼。當時認為每次查詢的演算法複雜度O(1)的,對於程式的優化沒有必要,而且如果代理方法很多的情況下,物件初始化也會增加程式的負荷,因為這個時候會把所有的方法加入到快取列表,這樣的方案本身意義不大。然後我就去看runtime的原始碼驗證我的想法,現在我們一起來看一下我們是不是有必要做這一塊的優化。

1.responseToSelector() vs 作者的方案

日常使用方案

iOS中responseToSelector()方法是不是需要優化
日常protocol的使用方式

作者的方案用OC的方案寫出來是下邊的樣子

iOS中responseToSelector()方法是不是需要優化
iOS中responseToSelector()方法是不是需要優化

對比兩種方法我們很容易明白作者的意圖是在程式每次建立的時候就先去我們的代理類中去讀取當前代理類有沒有實現當前的代理方法,然後建立一個狀態列舉動態的儲存類的返回結果,每次判斷方法可否執行前只要進行一次簡單的位運算即可。這時我發現即使responseToObject()方法即便是O(1)操作,在時間級的優化上作者的方案顯然也是更好的。但是作者的方案同樣有一些需要注意的問題。

1)假如說你的程式在收到一些伺服器的返回結果之後用runtime新增代理方法,而這個時候我們已經建立了代理,那麼這個時候是無法執行代理方法的。所以這時還要再有動態的改變列舉狀態的新的介面,此時程式的複雜性就會上升。這個時候我覺得還是使用responseToSelect()方法更好一些。而且很少有程式會遇到這樣的情況,所以這一點只有遇到這種情況的時候注意即可。

2)在我們的程式中,可能會有多個物件成為一個協議的代理,我們不可能每次在協議建立的時候,再去專門的寫一個類來記錄狀態那樣程式的耦合性無疑有上升了,而且也沒有意義,所以這個時候,我們每一個協議都要跟隨一個類來做繫結,用於記錄代理物件是不是可以執行某個協議方法,即便是協議的繼承,一樣要建立多個相應的繫結類。這樣雖然稍有麻煩,但是我覺得這一部分還是合理的。

3)如果代理方法很多的情況下,在初始化階段無疑就增加了對應的繫結的類的負荷,因為需要在初始化的時候去方法列表中讀取所有的方法,在儲存到方法快取列表中去還是需要一些耗時的,所以這時其實需要做時間上的衡量之後進行選擇的。如果協議多次呼叫,方法少,那麼作者的方案更好。如果協議的每個方法呼叫次數不多,且協議的方法還很多的情況下,我認為responseToSelect()方法應該更好一些。

4)想到上邊的3個問題之後,我就思考是不是有更好的處理方案呢,將繫結的類的負荷拆分到每一次方法的呼叫階段,即便出現方法一的情況,也不需要在協議的繫結類中新增狀態改變的操作。也不用再去考慮呼叫次數和方法數量之間的抉擇。我認為我們可以將繫結關係的類進行一下修改即可。繫結類中的程式碼修改如下。

iOS中responseToSelector()方法是不是需要優化
優化後的方案

經過上邊的優化後我們在第一次呼叫的時候會相應responseToSelector()方法之後,如果響應到了之後都是根據列舉狀態去相應。也沒有了繫結類一開始載入很多方法時會產生的高負荷,這樣的程式碼看起來很好,但是對於那些佔記憶體很大的程式,我覺得為了保護寶貴的記憶體,不去一次次建立這個雖然很小的繫結類。還是使用的系統的方案。因為我認為系統的方案本身也是很好的(下文會有介紹),當然這樣的方法對於程式的效能還是有優化的。與本文相關的demo下載地址

當然看了我的思路之後,有更好的解決方案,煩請您給我留言,我會在本文中進行補充,大家一起成長~

說完了上邊的優化,我們回頭再來看一下responseToSelector()到底在底層做了什麼,它的執行效率到底是怎麼樣的呢?

2)responseToSelector()的執行效率怎麼樣?

這個時候我做的第一件事情就是去檢視相關的原始碼,起初先到objc-class.mm檔案中去呼叫下邊的兩個方法。 如果sel傳入空的或者類被釋放的話,其實直接就返回了NO. 

iOS中responseToSelector()方法是不是需要優化

通過一系列的查詢之後,會發現程式被定位到了下邊的程式碼處,這個方法在objc-runtime-new.mm檔案中可以找到

iOS中responseToSelector()方法是不是需要優化

檢視了上邊的程式碼的原始碼,我知道程式之後的呼叫方式其實就是先從快取池中進行查詢,沒有的情況下會從方法列表中取等一系列的操作。這篇文章可以看到lookupImporForward的實現的一些大體上的程式碼,有比較詳細的註釋。這時我發現其實從快取中讀取主要就是呼叫了  _cache_getImp(cls, sel) 這個方法,但是我從原始碼中沒有找到這個方法,後來在網上查到了美團技術團隊對於這一塊的解釋,才知道這一部分的實現使用匯編語言。但是讀取美團的原文加上我從國外網站上看到的文章,我才知道原來在方法列表的快取列表中讀取到已執行過的方法在演算法的複雜度上並不是O(1)的。但是那一塊的執行都是彙編實現,所以執行效率還是比較快的,所以系統對於這一塊的執行速度還是很有保證的,但是速度上肯定比不上一次位運算。

所以在具體的場景下,我們追求極致的使用者體驗的時候,這樣的方式真的值得我們思考和引用。

如果文章中有任何問題,希望您不吝指出,我一定會及時進行修復。

相關文章