iOS 调用IMP/objc_msgSend详细说明


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;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,635评论 0 9
  • 本文转载自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex阅读 719评论 0 1
  • 消息发送和转发流程可以概括为:消息发送(Messaging)是 Runtime 通过 selector 快速查找 ...
    lylaut阅读 1,744评论 2 3
  • 如果要统计iOS开发代码,包括头文件的,终端命令进入项目目录下,命令如下: 列出每个文件的行数: find . -...
    柳爷在深圳阅读 844评论 1 6
  • 对吃货们来说,肥瘦相间、香气四溢的五花肉的吸引力简直是无穷大,但你知道吗,当吃下这块肉之后,体内会发生什么?对心血...
    心卫士阅读 315评论 0 0