iOS 調(diào)用IMP/objc_msgSend詳細說明


objc_msgSend

在iOS中我們調(diào)用一個函數(shù),一般是[self handle]這種方式,在Runtime里面,這種也是通過發(fā)送消息的方式執(zhí)行函數(shù),那如果在一個大量循環(huán)的地方需要執(zhí)行方法,有沒有更高效的方法?
首先寫一個示例方法(整篇文章都用這個方法做測試)

- (NSString*) addSubviewTemp:(UIView *)view with:(id)obj
{
    return @"Temp";
}

如果在代碼里面直接寫

 [self addSubviewTemp:[UIView new] with:@"temp"];

那么會被編譯成objc_msgSend的方式發(fā)送消息。那么我們可以嘗試直接寫成objc_msgSend。
首先看下objc_msgSend的定義:點擊查看message.h源碼

/runtime/message.h
/* Basic Messaging Primitives 
 * These functions must be cast to an appropriate function pointer type 
 * before being called. 
 */
#if !OBJC_OLD_DISPATCH_PROTOTYPES

OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);
OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);

#else
/** 
 * Sends a message with a simple return value to an instance of a class.
 * 
 * @param self A pointer to the instance of the class that is to receive the message.
 * @param op The selector of the method that handles the message.
 * @param ...  a variable argument list containing the arguments to the method.
 * 
 * @return The return value of the method.
 */
OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);
#end

源碼定義中有一句話需要注意
These functions must be cast to an appropriate function pointer type before being called.
這句話意思是:這些函數(shù)要調(diào)用的話必須要轉(zhuǎn)換為適當?shù)暮瘮?shù)指針類型
簡單說我們需要把objc_msgSend轉(zhuǎn)換成我們對應的函數(shù)指針類型,那么需要這么寫:

((id (*)(id, SEL))objc_msgSend)(self, op);

其中id和SEL參數(shù)是固定在最前面的,在源碼里面的注釋也說得很明白,其中self是需要執(zhí)行方法的對象,op就是objc_msgSend需要調(diào)用的方法,...不定式就是說另外如果我們的方法如果需要其他參數(shù),就可以按照函數(shù)參數(shù)的順序?qū)懺诤竺妗?/p>

接下來我們就可以用objc_msgSend來調(diào)用我們的方法 addSubviewTemp: with:

#import <objc/runtime.h>
#import <objc/message.h>
- (void) temp
{
    SEL sel = @selector(addSubviewTemp:with:); // 先獲取方法編號SEL
    // 這樣就可以成功執(zhí)行方法,相當于[self addSubviewTemp:[UIView new] with:@"Temp"];
    NSString *str = ((id (*)(id, SEL, UIView*, NSString*))objc_msgSend)(self, sel, [UIView new], @"Temp"); 
}

這里面需要強轉(zhuǎn)函數(shù)指針,有點麻煩,那有沒有更好的寫法呢?
我們再來看看message.h文件,里面有兩個objc_msgSend定義

#if !OBJC_OLD_DISPATCH_PROTOTYPES
OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )
#else
OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)

第二個方式objc_msgSend(id self, SEL op, ...) 好像更符合我們書寫代碼的習慣,這里屬于#if #else #end編譯選擇,那么如果我們把OBJC_OLD_DISPATCH_PROTOTYPES設置成1,就可以使用第二種方法編寫我們的代碼。
先來看看OBJC_OLD_DISPATCH_PROTOTYPES的定義,點擊查看objc-api.h源碼

// objc-api.h
/* OBJC_OLD_DISPATCH_PROTOTYPES == 0 enforces the rule that the dispatch 
 * functions must be cast to an appropriate function pointer type. */
#if !defined(OBJC_OLD_DISPATCH_PROTOTYPES)
#   define OBJC_OLD_DISPATCH_PROTOTYPES 1
#endif

看到#if !defined這句話就知道,這屬于編譯選項!那么我們就可以在Target -> BuildSetting -> Apple LLVM 找找。
最后在這里找到了設置選項
Apple LLVM 8.1 - Preprocessing -> Enable Strict Checking of objc_msgSend Calls

Xcode界面截圖

把Enable Strict Checking of objc_msgSend Calls 設置為NO之后,我們就可以這樣寫了:

#import <objc/runtime.h>
#import <objc/message.h>
- (void) temp
{
    SEL sel = @selector(addSubviewTemp:with:); // 先獲取方法編號SEL
    // 這樣就可以成功執(zhí)行方法,相當于[self addSubviewTemp:[UIView new] with:@"Temp"];
    NSString *str = objc_msgSend(self, sel, [UIView new], @"Temp");
}

這樣代碼是不是好看多了!

注意:如果沒設置Enable Strict Checking of objc_msgSend Calls 為NO, 這么寫objc_msgSend(self, sel, [UIView new], @"Temp")的話, 會報錯誤:Too many arguments to function call。


IMP調(diào)用

直接調(diào)用objc_msgSend會稍微減少一些步驟,但系統(tǒng)還是需要發(fā)送消息并找到對應的方法去執(zhí)行,那么有沒有更快的方法呢?
首先看下objc_msgSend的大概流程,objc_msgSend發(fā)送消息之后,系統(tǒng)需要根據(jù)sel名去查找類方法列表,找到對應的方法結(jié)構(gòu)method_t。點擊查看方法結(jié)構(gòu)objc-runtime-new.h ,以及點擊查看具體IMP獲取的過程

struct method_t {
    SEL name;  // 方法名
    const char *types;  // // 參數(shù)和返回類型的描述字串 
    IMP imp; // 方法的函數(shù)指針
};

找到method_t后呢?當然是獲取函數(shù)指針I(yè)MP啦!那如果我們直接獲取到方法的IMP指針并調(diào)用就不完啦,還需要發(fā)送什么消息?。。?br> 先來看看IMP定義,點擊查看objc.h源碼

// objc.h
// a pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id (*IMP)(id, SEL, ...); 
#endif

是不是跟objc_msgSend很像,那么就不需要太多說明啦,直接上代碼,
這里有幾種方法獲取IMP:

  • method_getImplementation(Method)
  • methodForSelector(SEL)
  • class_getMethodImplementation(Class, SEL)
#import <objc/runtime.h>
- (void) temp
{
    // 第一種方法
    SEL sel = @selector(addSubviewTemp:with:);
    Method method = class_getInstanceMethod([self class], sel);
    IMP imp = method_getImplementation(method);
    NSString *str =((id(*)(id, SEL, UIView*, id))imp)(self, sel, [UIView new], @"DFD"); // [self addSubviewTemp:[UIView new] with:@"DFD"];

    // 第二種方法
    SEL sel = @selector(addSubviewTemp:with:);
    IMP imp = [self methodForSelector:sel];
    NSString *str =  ((id(*)(id, SEL, UIView*, id))imp)(self, sel, [UIView new], @"DFD"); // [self addSubviewTemp:[UIView new] with:@"DFD"];

    // 第三種方法
    SEL sel = @selector(test);
    IMP imp = class_getMethodImplementation(self, sel);
    NSString *str =  ((id(*)(id, SEL, UIView*, id))imp)(self, sel, [UIView new], @"DFD"); // [self addSubviewTemp:[UIView new] with:@"DFD"];
}

至于這幾個方法的區(qū)別,在文章結(jié)尾再說明。

測試效率

現(xiàn)在我們來測試一下 objc_msgSend直接執(zhí)行IMP 兩種方法的時間效率

- (void) methodForTest{  / /亂寫的,不用在意,就是讓函數(shù)里面有事情干
    int i =0;
    i += 1;
    i ++;
    i -= 3;
    i = 6;
}
// 測試代碼
- (void) test{
    const int count = 10000000; // 一千萬的循環(huán)
    double timeStart = [[NSDate date] timeIntervalSince1970];
    for(int i=0; i<count; i+=1){
        [self methodForTest];
    }
    double timeEnd = [[NSDate date] timeIntervalSince1970];;
    NSLog(@"Time1 ===== %f",  timeEnd-timeStart);
    
    IMP imp = [self methodForSelector:@selector(methodForTest)];
    timeStart = [[NSDate date] timeIntervalSince1970];
    for(int i=0; i<count; i+=1){
        ((void(*)(void))imp)();
    }
    timeEnd = [[NSDate date] timeIntervalSince1970];;
    NSLog(@"Time2 ===== %f",  timeEnd-timeStart);
}

首先輸出在模擬器上的測試結(jié)果(iphone simulator 6):

2017-09-11 18:46:42.156 TempPro[11771:977749] Time1 ===== 0.019734
2017-09-11 18:46:42.173 TempPro[11771:977749] Time2 ===== 0.016906

看著效率沒什么區(qū)別呀,額,不過模擬器CPU用的是x86_64架構(gòu),還是用真機試試吧(arm64架構(gòu)),真機測試結(jié)果輸出(iphone 6):

2017-09-11 18:48:07.177493+0800 TempPro[690:85764] Time1 ===== 0.044118
2017-09-11 18:48:07.199978+0800 TempPro[690:85764] Time2 ===== 0.022298

咦,這時候區(qū)別就出來了,相差了大概一半的時間。


附加

那么現(xiàn)在看看這幾種IMP獲取的方法區(qū)別。

  • method_getImplementation(Method)
  • methodForSelector(SEL)
  • class_getMethodImplementation(Class, SEL)

因為 methodForSelector 內(nèi)部是用 class_getMethodImplementation 實現(xiàn)的,所以接下來就直接用 class_getMethodImplementation 進行分析。

用來分析的iOS源碼都在這里:https://github.com/WalkingToTheDistant/iOS_OpenSource/tree/master/runtime

// NSObject.mm
+ (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);
}

// objc-class.mm
IMP object_getMethodImplementation(id obj, SEL name)
{
    Class cls = (obj ? obj->getIsa() : nil);
    return class_getMethodImplementation(cls, name);
}

首先看下 class_getMethodImplementation 官方文檔說明:

Discussion
class_getMethodImplementation may be faster than method_getImplementation(class_getInstanceMethod(cls, name)).

The function pointer returned may be a function internal to the runtime instead of an actual method implementation. For example, if instances of the class do not respond to the selector, the function pointer returned will be part of the runtime's message forwarding machinery.

這里是說 class_getMethodImplementation 可能會比 method_getImplementation效率高,而且當找不到實現(xiàn)函數(shù)Imp時(執(zhí)行函數(shù)不存在), class_getMethodImplementation 會返回消息轉(zhuǎn)發(fā)機制的IMP。而method_getImplementation 找不到方法時會返回 nil。

是時候展示真正的源碼了!

// objc-class.mm
IMP class_getMethodImplementation(Class cls, SEL sel)
{
    IMP imp;
    if (!cls  ||  !sel) return nil;
    // lookUpImpOrNil 功能是檢查cls是否初始化cls,然后搜索 該cls 與其superClass的方法緩存、方法列表,如果找到sel就返回結(jié)果,否則返回nil,對具體實現(xiàn)有興趣的可以去 objc-runtime-new.mm 看看
    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;
}

當imp == nil時, class_getMethodImplementation 會返回 _objc_msgForward。
再看看 _objc_msgForward 是什么鬼

// message.h 
/* Use these functions to forward a message as if the receiver did not 
 * respond to it. 
 *
 * The receiver must not be nil.
 * 
 * class_getMethodImplementation() may return (IMP)_objc_msgForward.
 */
#if !OBJC_OLD_DISPATCH_PROTOTYPES
OBJC_EXPORT void _objc_msgForward(void /* id receiver, SEL sel, ... */ ) 
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);
#else
OBJC_EXPORT id _objc_msgForward(id receiver, SEL sel, ...) 
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);
#endif

簡單來說,_objc_msgForward 就是用來執(zhí)行消息轉(zhuǎn)發(fā)的,receiver是轉(zhuǎn)發(fā)消息的對象,這時候就需要 receiver 對象里面已經(jīng)實現(xiàn)好消息轉(zhuǎn)發(fā)的機制,不然會報錯 unrecognized selector sent to instance

來一波示例代碼:

SEL sel = @selector(test);
IMP imp = class_getMethodImplementation([NSObject class], sel); 

/* lldb po imp -> (IMP) imp = 0x000000010330f5c0 (libobjc.A.dylib`_objc_msgForward) 
 * 因為 NSObject里面沒有實現(xiàn) test ,所以imp 返回了 _objc_msgForward */

((void(*)(id, SEL))imp)(self, sel); // 執(zhí)行 _objc_msgForward

上面的示例代碼在執(zhí)行imp之后,即執(zhí)行_objc_msgForward,會首先觸發(fā) self 對象的 resolveInstanceMethod的方法,接下來就是執(zhí)行消息轉(zhuǎn)發(fā)機制,整個消息轉(zhuǎn)發(fā)機制有這幾個方法:

+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;

- (id)forwardingTargetForSelector:(SEL)aSelector;

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;

不明白消息轉(zhuǎn)發(fā)機制的話,那就網(wǎng)上查一下唄,現(xiàn)在有很多資料說的很清楚呢,這里就不說明啦。

既然知道有_objc_msgForward這個,那就可以實現(xiàn)一個功能啦,修改某個指定的函數(shù)方法,在執(zhí)行這個函數(shù)的時候立即觸發(fā)消息轉(zhuǎn)發(fā)機制:

#import <objc/message.h>
#import <objc/runtime.h>

SEL selector = @selector(test);
Method method = class_getInstanceMethod(self.class, selector);
class_replaceMethod(self.class, selector, _objc_msgForward, method_getTypeEncoding(method)); // test的Method結(jié)構(gòu)體里面的imp替換成 _objc_msgForward
[self test]; // 這時候執(zhí)行,就會觸發(fā)消息轉(zhuǎn)發(fā)了

另外,IMP 設置_objc_msgForward 和nil 是有區(qū)別,當設置為nil的時候,lookUpImpOrNil會尋找其父類等,直至找不到方法才會執(zhí)行消息轉(zhuǎn)發(fā),如果父類有實現(xiàn)這個方法,那么會正常執(zhí)行函數(shù)。
設置IMP為_objc_msgForward之后,就會立即執(zhí)行消息轉(zhuǎn)發(fā),避免了其父類存在實現(xiàn)或者尋找IMP的過程消耗。
尋找IMP的主要源碼在下面(IMP == nil時的父類循環(huán)尋找):

objc-runtime-new.m
lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver){
....
    while ((curClass = curClass->superclass)) {
        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (imp) {
            if (imp != (IMP)_objc_msgForward_impcache) {
                // Found the method in a superclass. Cache it in this class.
                log_and_fill_cache(cls, imp, sel, inst, curClass);
                goto done;
            }
            else {
                break;
            }
        }

        // Superclass method list.
        meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
            imp = meth->imp;
            goto done;
        }
    }
.....
}

最后附上Method結(jié)構(gòu)體的源碼

// runtime.h
struct objc_method {
    SEL method_name           OBJC2_UNAVAILABLE;
    char *method_types        OBJC2_UNAVAILABLE;
    IMP method_imp            OBJC2_UNAVAILABLE;
}
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容