1. 程式人生 > >Objective-C runtime原始碼學習之IMP定址(不包括訊息轉發部分)

Objective-C runtime原始碼學習之IMP定址(不包括訊息轉發部分)

寫在前面

前段時間寫了一篇部落格runtime如何通過selector找到對應的IMP地址?(分別考慮類方法和例項方法),這是在看《招聘一個靠譜的iOS》時回答第22題時總結的一篇部落格,不過這篇部落格中並沒有牽涉到底層的程式碼,而且也留下了幾個沒有解決的問題,這篇部落格將深入runtime原始碼繼續探索這個問題,並嘗試解決上篇部落格中未解決的問題,本人第一次閱讀原始碼,如果有分析錯誤的地方,歡迎大家糾正。

引入

首先大家都知道,在oc中呼叫方法(或者說傳送一個訊息是)runtime底層都會翻譯成objc_msgSend(id self, SEL op, ...),蘋果為了優化效能,這個方法是用匯編寫成的

/********************************************************************
 *
 * id objc_msgSend(id self, SEL _cmd,...);
 *
 ********************************************************************/

    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

實話說我沒有學過彙編,所以看到這段程式碼我的內心是崩潰的,更可怕的是針對不同的平臺,還有不同彙編程式碼的實現

雖然不懂彙編,但是蘋果的註釋很詳細,看註釋也可以大致明白在幹什麼,首先檢查傳入的self是否為空,然後根據selector尋找方法實現IMP,找到則呼叫並返回,否則丟擲異常。由此可以有以下虛擬碼

id objc_msgSend(id self, SEL _cmd, ...) {
  Class class = object_getClass(self);
  IMP imp = class_getMethodImplementation(class, _cmd);
  return imp ? imp(self, _cmd, ...) : 0;
}

虛擬碼中我們看到class_getMethodImplementation(Class cls, SEL sel) 方法用來尋找IMP地址,有趣的是蘋果真的提供了這個方法,可以讓我們呼叫,通過selector去尋找方法實現IMP,而這個函式的實現,以及其延伸就是這篇部落格所要探討的重點。

正文

在我前面的文章中也說到IMP定址總共有兩種方法:

IMP class_getMethodImplementation(Class cls, SEL name);
IMP method_getImplementation(Method m);

而在NSObject中提供了幾個對class_getMethodImplementation封裝的方法

+ (IMP)instanceMethodForSelector:(SEL)sel {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return class_getMethodImplementation(self, sel);
}

+ (IMP)methodForSelector:(SEL)sel {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return object_getMethodImplementation((id)self, sel);
}

- (IMP)methodForSelector:(SEL)sel {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return object_getMethodImplementation(self, sel);
}

但這些方法卻並沒有在標頭檔案中暴露,所以我並不明白蘋果這樣做的用意,如果有人知道,希望能夠告知,感激不盡!
這裡出現的object_getMethodImplementation其實就是對class_getMethodImplementation的封裝,蘋果的解釋是:

Equivalent to: class_getMethodImplementation(object_getClass(obj), name);

下面我們就暫時把目光轉向class_getMethodImplementation這個函式,看看它底層到底是如何實現的

IMP class_getMethodImplementation(Class cls, SEL sel)
{
    IMP imp;

    if (!cls  ||  !sel) return nil;

    imp = lookUpImpOrNil(cls, sel, nil, 
                         YES/*initialize*/, YES/*cache*/, YES/*resolver*/);

    // Translate forwarding function to C-callable external version
    if (!imp) {
        return _objc_msgForward;
    }

    return imp;
}

首先判斷傳入的引數是否為空,然後進入lookUpImpOrNil這個方法,實際上這個這個方法是對lookUpImpOrForward的簡單封裝:

/***********************************************************************
* lookUpImpOrNil.
* Like lookUpImpOrForward, but returns nil instead of _objc_msgForward_impcache
**********************************************************************/
IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
                   bool initialize, bool cache, bool resolver)
{
    IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
    if (imp == _objc_msgForward_impcache) return nil;
    else return imp;
}

註釋寫的也很清楚,這個方法不會進行訊息的轉發,而直接返回nil,這個倒是比較有趣,明明呼叫lookUpImpOrForward可以直接進行訊息轉發,可是這裡偏不這樣做,呼叫訊息轉發返回nil的函式,然後判斷imp為nil時,自己手動返回_objc_msgForward,進行訊息轉發,還真是有意思,不過蘋果在這裡做了註釋:Translate forwarding function to C-callable external version,將這個轉發函式轉換為C語言能夠呼叫的版本。
接下來我們繼續深入,看一下lookUpImpOrForward是如何實現的:

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    Class curClass;
    IMP methodPC = nil;
    Method meth;
    bool triedResolver = NO;

    methodListLock.assertUnlocked();

    if (cache) {
        methodPC = _cache_getImp(cls, sel);
        if (methodPC) return methodPC;    
    }

    if (cls == _class_getFreedObjectClass())
        return (IMP) _freedHandler;
    }

 retry:
    methodListLock.lock();

    // Ignore GC selectors
    if (ignoreSelector(sel)) {
        methodPC = _cache_addIgnoredEntry(cls, sel);
        goto done;
    }

    // Try this class's cache.
    methodPC = _cache_getImp(cls, sel);
    if (methodPC) goto done;

    // Try this class's method lists.
    meth = _class_getMethodNoSuper_nolock(cls, sel);
    if (meth) {
        log_and_fill_cache(cls, cls, meth, sel);
        methodPC = method_getImplementation(meth);
        goto done;
    }

    // Try superclass caches and method lists.
    curClass = cls;
    while ((curClass = curClass->superclass)) {
        // Superclass cache.
        meth = _cache_getMethod(curClass, sel, _objc_msgForward_impcache);
        if (meth) {
            if (meth != (Method)1) {
                // Found the method in a superclass. Cache it in this class.
                log_and_fill_cache(cls, curClass, meth, sel);
                methodPC = method_getImplementation(meth);
                goto done;
            }
            else {
                // Found a forward:: entry in a superclass.
                // Stop searching, but don't cache yet; call method 
                // resolver for this class first.
                break;
            }
        }

        // Superclass method list.
        meth = _class_getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            log_and_fill_cache(cls, curClass, meth, sel);
            methodPC = method_getImplementation(meth);
            goto done;
        }
    }

    // No implementation found. Try method resolver once.

    if (resolver  &&  !triedResolver) {
        methodListLock.unlock();
        _class_resolveMethod(cls, sel, inst);
        triedResolver = YES;
        goto retry;
    }

    // No implementation found, and method resolver didn't help. 
    // Use forwarding.

    _cache_addForwardEntry(cls, sel);
    methodPC = _objc_msgForward_impcache;

 done:
    methodListLock.unlock();

    // paranoia: look for ignored selectors with non-ignored implementations
    assert(!(ignoreSelector(sel)  &&  methodPC != (IMP)&_objc_ignored_method));

    return methodPC;
}

我天,好長的一段程式碼,我刪了好多註釋,還是很多

首先這裡有一個我並不懂的東西methodListLock.assertUnlocked(); 我看到Objective-C 訊息傳送與轉發機制原理 中對此的解釋是

對 debug 模式下的 assert 進行 unlock,runtimeLock 本質上是對 Darwin 提供的執行緒讀寫鎖 pthread_rwlock_t 的一層封裝,提供了一些便捷的方法。

需要注意的是在objc-runtime-new.mm中有一段幾乎相同的lookUpImpOrForward的實現,在該實現中,加鎖操作是runtimeLock.read(); 所以這篇上述部落格使用的程式碼應該是objc-runtime-new.mm的程式碼,而我的原始碼是來自於objc-class-old.mm 雖然名稱不同,但我想底層應該是一樣的。

很佩服這位博主對底層認識的如此深刻,我們暫時就按照這裡寫的理解,繼續往下看

無鎖的快取查詢(Optimistic cache lookup)

在沒有鎖的狀態下進行快取搜尋,效能會比較好

if (cache) {
        methodPC = _cache_getImp(cls, sel);
        if (methodPC) return methodPC;    
    }

首先如果cache傳入的是YES,則呼叫cache_getImp在快取中搜索,當然這裡傳入的是YES(而在objc_msgSend方法裡在這裡進行了優化,objc_msgSend最開始就在快取中進行了搜尋,所以有了一個很有趣的方法_class_lookupMethodAndLoadCache3,這個方法在呼叫lookUpImpOrForward時傳入cache是NO,避免兩次搜尋快取),而cache_getImp 是用匯編寫的(又是彙編。。。(T_T))

    STATIC_ENTRY cache_getImp

    mov r9, r0
    CacheLookup GETIMP      // returns IMP on success

LCacheMiss:
    mov     r0, #0              // return nil if cache miss
    bx  lr

LGetImpExit: 
    END_ENTRY cache_getImp

具體的快取搜尋是在巨集CacheLookup 中實現的,具體這裡就不展開了(也展開不了,我還沒看懂(^-^) )。

釋放檢測

if (cls == _class_getFreedObjectClass())
        return (IMP) _freedHandler;

檢測傳送訊息的物件是否已經被釋放,如果已經釋放,則返回_freedHandler 的IMP

static void _freedHandler(id obj, SEL sel)
{
    __objc_error (obj, "message %s sent to freed object=%p", 
                  sel_getName(sel), (void*)obj);
}

在該方法中丟擲message sent to freed object的錯誤資訊(不過我還從來沒有遇到過這樣的錯誤資訊)

初始化檢查

if (initialize  &&  !cls->isInitialized()) {
        _class_initialize (_class_getNonMetaClass(cls, inst));
    }

這裡我不是很理解+initialize方法是做什麼的

// +initialize bits are stored on the metaclass only
    bool isInitialized() {
        return getMeta()->info & CLS_INITIALIZED;
    }

但是從isInitialized() 的實現來看初始化的資訊儲存在元類中,由此推測是元類或者是類物件的初始化工作,而我在上文中提到的部落格中是這樣寫的:

如果是第一次用到這個類且 initialize 引數為 YES(initialize && !cls->isInitialized()),需要進行初始化工作,也就是開闢一個用於讀寫資料的空間。先對 runtimeLock 寫操作加鎖,然後呼叫 cls 的 initialize 方法。如果 sel == initialize 也沒關係,雖然 initialize 還會被呼叫一次,但不會起作用啦,因為 cls->isInitialized() 已經是 YES 啦。

這裡的表述也大致印證了我的猜測,是對類物件或者是元類物件進行初始化的工作,不過我還是有一點不明白:類物件都還沒有初始化,那是如何產生這個類的例項物件呢?然而在別人部落格中看到:+load是在runtime之前就被呼叫的,+initialize是在runtime才呼叫

retry語句標號(在該類的父類中查詢)

這裡對方法列表進行了加鎖的操作methodListLock.lock();

The lock is held to make method-lookup + cache-fill atomic with respect to method addition. Otherwise, a category could be added but ignored indefinitely because the cache was re-filled with the old value after the cache flush on behalf of the category.

考慮執行時方法的動態新增,加鎖是為了使方法搜尋和快取填充成為原子操作。否則category新增時重新整理的快取可能會因為舊資料的重新填充而被完全忽略掉。

typedef struct {
    SEL name;     // same layout as struct old_method
    void *unused;
    IMP imp;  // same layout as struct old_method
} cache_entry;
//objective-c 2.0以前
struct old_method {
    SEL method_name;
    char *method_types;
    IMP method_imp;
};

typedef struct old_method *Method;

//objective-c 2.0
struct method_t {
    SEL name;
    const char *types;
    IMP imp;

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};
typedef struct method_t *Method;
  1. 檢查selector是否是垃圾回收方法,如果是則填充快取_cache_fill(cls, (Method)entryp, sel);(這裡entryp的型別是結構體cache_entry,將其強轉為Method,我們可以看到上面的程式碼,OC2.0前,這個cache_entry和method的定義幾乎是相同的,2.0後加入了一個我完全看不懂的東西(T_T))並讓methodPC指向該方法的實現即entryp->imp(實際上這是一個彙編程式的入口_objc_ignored_method),然後跳轉到done語句標號。否則進行下一步
  2. 在本類的快取中查詢,也是使用匯程式設計序入口_cache_getImp,如果找到,跳轉到done語句標號,否則進行下一步。
  3. 在上一步快取中沒有發現,然後進入本類的方法列表中查詢,如果找到了則進行快取填充,並讓methodPC指向該方法的實現,跳轉到done語句標號,否則進行下一步。
  4. 在父類的方法列表和快取中遞迴查詢,首先是查詢快取,又是呼叫一個彙編的程式入口_cache_getMethod 比較奇怪的是我只在objc-msg-i386.s中發現了這個程式入口,與前面不同的是,這裡傳入了一個_objc_msgForward_impcache 的彙編程式入口作為快取中訊息轉發的標記,如果發現快取的方法,則使method_PC指向其實現,跳轉到done語句標號,如果找到了Method,但發現其IMP是一個轉發的彙編程式入口即_objc_msgForward_impcache ,立即跳出迴圈,但是不立刻快取,而是call method resolver,即進行第5步。如果快取中沒發現Method,就在列表中尋找,同樣是找到即跳轉到done,否則進行下一步。
  5. 當傳入的resolver為YES且triedResolver為NO時(即此步驟只會進入一次,進入後triedResolver會設為YES),進入method resolver(動態方法解析),首先對methodListLock解鎖,然後呼叫_class_resolveMethod 傳送_class_resolveInstanceMethod_class_resolveClassMethod 訊息,程式設計師此時可以動態的給selector新增一個對應的IMP。完成後再回到第1步重新來一遍。這一步訊息轉發前最後一次機會。
  6. 沒有找到方法的實現,method resolver(動態方法解析)也沒有作用,此時進行訊息的轉發,使methodPC指向_objc_msgForward_impcache 彙編程式入口,並進入done。

done語句標號

首先將methodListLock解鎖,然後斷言不會存在一個被忽略的selector其implementation是沒有被忽略的(官方的意思是非要找到這樣一個selector,真是有趣)

paranoia: look for ignored selectors with non-ignored implementations

最後返回這個methodPC。

然後就是訊息轉發部分了,其objc_setForwardHandler實現機制不在Objective-C Runtime (libobjc.dylib)中,而是在CoreFoundation(CoreFoundation.framework)中,所以這裡就先不討論了,等我以後研究了那部分以後,再專門寫一篇關於訊息轉發的部落格。

關於正文開始處所說的第二種方法method_getImplementation(),首先需要呼叫class_getInstanceMethod() 而在這個方法里加了一個warning

#warning fixme build and search caches

    // Search method lists, try method resolver, etc.
    lookUpImpOrNil(cls, sel, nil, 
                   NO/*initialize*/, NO/*cache*/, YES/*resolver*/);

#warning fixme build and search caches

我這裡呼叫了lookUpImpOrNil方法,卻沒有使用其返回值,而且標註需要fix and search caches,我猜測可能因為某種原因,在這裡無法進行快取查詢,而後面return _class_getMethod(cls, sel);本質上就是在方法列表中進行查詢,而且也沒有進行訊息轉發。
這裡也印證了蘋果對於這個方法的註釋:class_getMethodImplementation may be faster than method_getImplementation(class_getInstanceMethod(cls, name)),因為第一個方法進行了快取的查詢,如果快取中能找到,效率會提高很多。

以前的問題

在我上一篇部落格runtime如何通過selector找到對應的IMP地址?(分別考慮類方法和例項方法)裡我有一個沒有解決的問題:為什麼對於無法找到的IMP,class_getMethodImplementation(),method_getImplementation()返回值會不一樣?

IMP method_getImplementation(Method m)
{
    return m ? m->imp : nil;
}

看完原始碼,就很清楚了,如果這個method不存在,直接返回nil,而
class_getMethodImplementation()會經歷訊息轉發機制,最後返回的是forwardInvocation的結果,而這部分是不開源的,也不知道具體是怎麼返回的,但每次執行確實是會返回的一個固定的地址,我猜測最後這個地址可能和NSInvocation這個物件的記憶體地址有關,具體那是什麼地址,以後有機會在去尋找答案。

結語

如果我上面的分析或推測有錯誤,歡迎指正,大家一同成長,我在寫這篇部落格時參考的部落格有:Objective-C 訊息傳送與轉發機制原理Objective-C 原始碼(二)+load 以及 +initialize這裡將其貼出,感謝這些部落格的作者,跟這些部落格相比,我的部落格寫的真的很菜,畢竟剛開始,相信有一天我也能寫出如此優秀的部落格。

這篇部落格中使用的runtime原始碼版本是objc4-680。

相關推薦

Objective-C runtime原始碼學習IMP包括訊息轉發部分

寫在前面 前段時間寫了一篇部落格runtime如何通過selector找到對應的IMP地址?(分別考慮類方法和例項方法),這是在看《招聘一個靠譜的iOS》時回答第22題時總結的一篇部落格,不過這篇部落格中並沒有牽涉到底層的程式碼,而且也留下了幾個沒有解決的

iOS學習筆記56Runtime-Objective-C Runtime 執行時三:方法與訊息

前面我們討論了Runtime中對類和物件的處理,及對成員變數與屬性的處理。這一章,我們就要開始討論Runtime中最有意思的一部分:訊息處理機制。我們將詳細討論訊息的傳送及訊息的轉發。不過在討論訊息之前,我們先來了解一下與方法相關的一些內容。 基礎資料型別 SEL

Objective-C Runtime 執行時五:協議與分類

Objective-C中的分類允許我們通過給一個類新增方法來擴充它(但是通過category不能新增新的例項變數),並且我們不需要訪問類中的程式碼就可以做到。 Objective-C中的協議是普遍存在的介面定義方式,即在一個類中通過@protocol定義介面,在另外

Objective-C Runtime 執行時六:拾遺

前面幾篇基本介紹了runtime中的大部分功能,包括對類與物件、成員變數與屬性、方法與訊息、分類與協議的處理。runtime大部分的功能都是圍繞這幾點來實現的。 本章的內容並不算重點,主要針對前文中對Objective-C Runtime Reference內容遺漏

Objective-C Runtime 執行時二:成員變數與屬性

在前面一篇文章中,我們介紹了Runtime中與類和物件相關的內容,從這章開始,我們將討論類實現細節相關的內容,主要包括類中成員變數,屬性,方法,協議與分類的實現。 本章的主要內容將聚集在Runtime對成員變數與屬性的處理。在討論之前,我們先介紹一個重要的概念:型別

Objective-C Runtime 執行時三:方法與訊息

前面我們討論了Runtime中對類和物件的處理,及對成員變數與屬性的處理。這一章,我們就要開始討論Runtime中最有意思的一部分:訊息處理機制。我們將詳細討論訊息的傳送及訊息的轉發。不過在討論訊息之前,我們先來了解一下與方法相關的一些內容。 基礎資料型別 SEL

Objective-C Runtime 執行時四:Method Swizzling

理解Method Swizzling是學習runtime機制的一個很好的機會。在此不多做整理,僅翻譯由Mattt Thompson發表於nshipster的Method Swizzling一文。 Method Swizzling是改變一個selector的實際實現的

objective-c runtime安全措施二:反注入

《O'Reilly.Hacking.and.Securing.iOS.Applications>>讀書筆記 反注入:在類函式被呼叫前做完整性檢測(預防應用自定義函式或apple標準庫函式被修改或替換) 原理:呼叫dladdr()函式檢查類方法的基本資訊是否合法

spring原始碼學習路---IOC初探

首先把spring原始碼匯入,怎麼匯入百度下。 首先我們來說一下IOC,IOC是spring最核心的理念,包括AOP也要屈居第二,那麼IOC到底是什麼呢,四個字,控制反轉。 網上有不少是這麼解釋IOC的,說IOC是將物件的建立和依賴關係交給容器,這句話我相信不少人都知道,在我個人的理解

python學習網站的編寫HTML,CSS,JS十七----------示例,構造一個網頁的框架,上部標題,登入,logo,左側選單,右側內容,原始碼

結果: 顏色為了明顯,所以較為難看,可以根據自己的需要進行更改 原始碼: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title

Java併發包原始碼學習執行緒池ThreadPoolExecutor原始碼分析

Java中使用執行緒池技術一般都是使用Executors這個工廠類,它提供了非常簡單方法來建立各種型別的執行緒池: public static ExecutorService newFixedThreadPool(int nThreads) public static ExecutorService

Spring原始碼學習IOC實現原理-ApplicationContext

一.Spring核心元件結構      總的來說Spring共有三個核心元件,分別為Core,Context,Bean.三大核心元件的協同工作主要表現在 :Bean是包裝我們應用程式自定義物件Object的,Object中存有資料,而Context就是為了這些資料存放提供一個生存環境,儲存各個 bean之間的

Spring原始碼學習路---深入AOP

原文地址:https://blog.csdn.net/zuoxiaolong8810/article/details/8962353    上一章和各位一起看了一下springAOP的工作流程,當我們給出AOP相關的配置以後,直接從IOC容器中拿出來的就是已經加強過的bean

mybatis原始碼學習執行過程分析2——config.xml配置檔案和mapper.xml對映檔案解析過程

在上一篇中跟蹤了SqlSessionFactory及SqlSession的建立過程。這一篇,主要跟蹤Mapper介面和XML檔案對映及獲取。 1.xml檔案的解析 1.1Mybatis-config.xml的解析 在SqlSessionFactor

java學習單例模式餓漢式與懶漢式

分用 單例設計 單例 null 並發 auth 設計 pack 過多 ---恢復內容開始--- 設計模式:解決某一類問題最行之有效的方法 java中有23種設計模式 今天學習其中一種:單例設計模式:解決一個類在內存只存在一個對象 想要保證對象唯一。 1.為了避免其他程序

python學習網站的編寫HTML,CSS,JS十六----------示例,構造一個左側管理選單的功能,點選主選單才顯示下面的內容

結果: 程式碼: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>逆水行舟不進則退</title>

python學習網站的編寫HTML,CSS,JS十五----------示例,彈出一個背景為半黑色,前面是白框的彈窗功能已經編好的框架

效果圖,程式碼直接可應用,按自己的需要在其中加入想要的內容:  程式碼及講解: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <

python學習網站的編寫HTML,CSS,JS十四----------CSS的display行內標籤和塊級標籤的轉換,控制標籤是否顯示

行內標籤:有多大就佔多大,無法設定高度,寬度和邊距。 塊級標籤:佔一行,可以設定高度,寬度和邊距。 塊級標籤轉為行內標籤:display:inline 行內標籤轉為塊級標籤:display:block 還有一個特殊的轉換,既包含塊級標籤的屬性,又具有行內標籤的屬性,自己有多少佔多少,

python學習網站的編寫HTML,CSS,JS十三----------CSS字型和對齊方式的設定

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>逆水行舟不進則退</title> </head> <b

python學習網站的編寫HTML,CSS,JS十二----------CSS邊框的編寫

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>逆水行舟不進則退</title> </head> <b