美文网首页
iOS【调用IMP/objc_msgSend】

iOS【调用IMP/objc_msgSend】

作者: NJ_墨 | 来源:发表于2020-06-01 14:01 被阅读0次

    摘录:晨寂

    objc_msgSend

    [self addSubviewTemp:[UIView new] with:@"temp"];
    
    - (NSString*) addSubviewTemp:(UIView *)view with:(id)obj
    {
        return @"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);
    

    objc_msgSend来调用

    #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这句话就知道,这属于编译选项!那么我们就可以在修改设置为NO
    Target -> BuildSetting ->Enable Strict Checking of objc_msgSend Calls->NO

    image

    修改配置后的调用

    #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
    
    

    这里有几种方法获取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):

    2020-06-01 13:52:33.581375+0800 ZFNSStringUtils[8066:4420900] Time1 ===== 0.136684
    2020-06-01 13:52:33.634366+0800 ZFNSStringUtils[8066:4420900] Time2 ===== 0.052801
    2020-06-01 13:52:33.634598+0800
    

    附加

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

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

    因为 methodForSelector 内部是用 class_getMethodImplementation 实现的,所以接下来就直接用 class_getMethodImplementation 进行分析。

    // 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【调用IMP/objc_msgSend】

          本文链接:https://www.haomeiwen.com/subject/dpnjzhtx.html