iOS 调用IMP/objc_msgSend详细说明

96
晨寂
2.2 2017.06.30 18:11* 字数 1624

objc_msgSend

在iOS中我们调用一个函数,一般是[self handle]这种方式,在Runtime里面,这种也是通过发送消息的方式执行函数,那如果在一个大量循环的地方需要执行方法,有没有更高效的方法?
首先写一个示例方法(整篇文章都用这个方法做测试)

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

如果在代码里面直接写

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

那么会被编译成objc_msgSend的方式发送消息。那么我们可以尝试直接写成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.
这句话意思是:这些函数要调用的话必须要转换为适当的函数指针类型
简单说我们需要把objc_msgSend转换成我们对应的函数指针类型,那么需要这么写:

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

其中id和SEL参数是固定在最前面的,在源码里面的注释也说得很明白,其中self是需要执行方法的对象,op就是objc_msgSend需要调用的方法,...不定式就是说另外如果我们的方法如果需要其他参数,就可以按照函数参数的顺序写在后面。

接下来我们就可以用objc_msgSend来调用我们的方法 addSubviewTemp: with:

#import <objc/runtime.h>
#import <objc/message.h>
- (void) temp
{
    SEL sel = @selector(addSubviewTemp:with:); // 先获取方法编号SEL
    // 这样就可以成功执行方法,相当于[self addSubviewTemp:[UIView new] with:@"Temp"];
    NSString *str = ((id (*)(id, SEL, UIView*, NSString*))objc_msgSend)(self, sel, [UIView new], @"Temp"); 
}

这里面需要强转函数指针,有点麻烦,那有没有更好的写法呢?
我们再来看看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
    // 这样就可以成功执行方法,相当于[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调用

直接调用objc_msgSend会稍微减少一些步骤,但系统还是需要发送消息并找到对应的方法去执行,那么有没有更快的方法呢?
首先看下objc_msgSend的大概流程,objc_msgSend发送消息之后,系统需要根据sel名去查找类方法列表,找到对应的方法结构method_t。点击查看方法结构objc-runtime-new.h ,以及点击查看具体IMP获取的过程

struct method_t {
    SEL name;  // 方法名
    const char *types;  // // 参数和返回类型的描述字串 
    IMP imp; // 方法的函数指针
};

找到method_t后呢?当然是获取函数指针IMP啦!那如果我们直接获取到方法的IMP指针并调用就不完啦,还需要发送什么消息!!!
先来看看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"];
}

至于这几个方法的区别,在文章结尾再说明。

测试效率

现在我们来测试一下 objc_msgSend直接执行IMP 两种方法的时间效率

- (void) methodForTest{  / /乱写的,不用在意,就是让函数里面有事情干
    int i =0;
    i += 1;
    i ++;
    i -= 3;
    i = 6;
}
// 测试代码
- (void) test{
    const int count = 10000000; // 一千万的循环
    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);
}

首先输出在模拟器上的测试结果(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

看着效率没什么区别呀,额,不过模拟器CPU用的是x86_64架构,还是用真机试试吧(arm64架构),真机测试结果输出(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

咦,这时候区别就出来了,相差了大概一半的时间。


附加

那么现在看看这几种IMP获取的方法区别。

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

因为 methodForSelector 内部是用 class_getMethodImplementation 实现的,所以接下来就直接用 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效率高,而且当找不到实现函数Imp时(执行函数不存在), class_getMethodImplementation 会返回消息转发机制的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就返回结果,否则返回nil,对具体实现有兴趣的可以去 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 就是用来执行消息转发的,receiver是转发消息的对象,这时候就需要 receiver 对象里面已经实现好消息转发的机制,不然会报错 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里面没有实现 test ,所以imp 返回了 _objc_msgForward */

((void(*)(id, SEL))imp)(self, sel); // 执行 _objc_msgForward

上面的示例代码在执行imp之后,即执行_objc_msgForward,会首先触发 self 对象的 resolveInstanceMethod的方法,接下来就是执行消息转发机制,整个消息转发机制有这几个方法:

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

- (id)forwardingTargetForSelector:(SEL)aSelector;

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

不明白消息转发机制的话,那就网上查一下呗,现在有很多资料说的很清楚呢,这里就不说明啦。

既然知道有_objc_msgForward这个,那就可以实现一个功能啦,修改某个指定的函数方法,在执行这个函数的时候立即触发消息转发机制:

#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结构体里面的imp替换成 _objc_msgForward
[self test]; // 这时候执行,就会触发消息转发了

另外,IMP 设置_objc_msgForward 和nil 是有区别,当设置为nil的时候,lookUpImpOrNil会寻找其父类等,直至找不到方法才会执行消息转发,如果父类有实现这个方法,那么会正常执行函数。
设置IMP为_objc_msgForward之后,就会立即执行消息转发,避免了其父类存在实现或者寻找IMP的过程消耗。
寻找IMP的主要源码在下面(IMP == nil时的父类循环寻找):

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结构体的源码

// runtime.h
struct objc_method {
    SEL method_name           OBJC2_UNAVAILABLE;
    char *method_types        OBJC2_UNAVAILABLE;
    IMP method_imp            OBJC2_UNAVAILABLE;
}
iOS