美文网首页
使用runtime给类动态添加方法并调用 - class_add

使用runtime给类动态添加方法并调用 - class_add

作者: 白色天空729 | 来源:发表于2019-04-22 14:21 被阅读0次

    原文链接:https://www.cnblogs.com/chipmuck/p/5807190.html

    /** 
     * Adds a new method to a class with a given name and implementation.
     * 
     * @param cls The class to which to add a method.
     * @param name A selector that specifies the name of the method being added.
     * @param imp A function which is the implementation of the new method. The function must take at least two arguments—self and _cmd.
     * @param types An array of characters that describe the types of the arguments to the method. 
     * 
     * @return YES if the method was added successfully, otherwise NO 
     *  (for example, the class already contains a method implementation with that name).
     *
     * @note class_addMethod will add an override of a superclass's implementation, 
     *  but will not replace an existing implementation in this class. 
     *  To change an existing implementation, use method_setImplementation.
     */
    OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp, 
                                     const char *types) 
         __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);
    

    大意翻译一下,这个方法的作用是,给类添加一个新的方法和该方法的具体实现。分析一下这个方法需要的参数:

    Class cls
    cls 参数表示需要添加新方法的类。

    SEL name
    

    name 参数表示 selector 的方法名称,可以根据喜好自己进行命名。

    IMP imp
    

    imp 即 implementation ,表示由编译器生成的、指向实现方法的指针。也就是说,这个指针指向的方法就是我们要添加的方法。

    const char *types
    

    最后一个参数 *types 表示我们要添加的方法的返回值和参数。

    简要介绍了 class_addMethod 中所需要的参数以及作用之后,我们就可以开始利用这个方法进行添加我们所需要的方法啦!在使用之前,我们首先要明确 Objective-C 作为一种动态语言,它会将部分代码放置在运行时的过程中执行,而不是编译时,所以在执行代码时,不仅仅需要的是编译器,也同时需要一个运行时环境(Runtime),为了满足一些需求,苹果开源了 Runtime Source 并提供了开放的 api 供开发者使用。

    首先,既然要给某个类添加我们的方法,就应该继承或者给这个类写一个分类,这里我新建一个名为「myCar」的类,作为「Car」类的分类。

    #import "Car+myCar.h"
    
    @implementation Car (myCar)
    
    @end
    

    我们知道,在 Objective-C 中,正常的调用方法是通过消息机制(message)来实现的,那么如果类中没有找到发送的消息方法,系统就会进入找不到该方法的处理流程中,如果在这个流程中,我们加入我们所需要的新方法,就能实现运行过程中的动态添加了。这个流程或者说机制,就是 Objective-C 的 Message Forwarding

    这个机制中所涉及的方法主要有两个:

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

    两个方法的唯一区别在于需要添加的是静态方法还是实例方法。我们就拿前者来说,既然要添加方法,我们就在「myCar」类中实现它,代码如下:

    #import "Car+myCar.h"
    
    void startEngine(id self, SEL _cmd) {
        NSLog(@"my car starts the engine");
    }
    
    @implementation Car (myCar)
    
    @end
    

    至此,我们实现了我们要添加的 startEngine 这个方法。这是一个 C 语言的函数,它至少包含了 self 和 _cmd 两个参数(self 代表着函数本身,而 _cmd 则是一个 SEL 数据体,包含了具体的方法地址)。如果要在这个方法中新增参数呢?见如下代码:

    #import "Car+myCar.h"
    
    void startEngine(id self, SEL _cmd, NSString *brand) {
        NSLog(@"my %@ car starts the engine", brand);
    }
    
    @implementation Car (myCar)
    
    @end
    

    只要在那两个必须的参数之后添加所需要的参数和类型就可以了,返回值同样道理,只要把方法名之前的 void 修改成我们想要的返回类型就可以,这里我们不需要返回值。

    接着,我们重载 resolveInstanceMethod: 这个函数

    #import "Car+myCar.h"
    #import <objc/runtime.h>
    
    void startEngine(id self, SEL _cmd, NSString *brand) {
        NSLog(@"my %@ car starts the engine", brand);
    }
    
    @implementation Car (myCar)
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        if (sel == @selector(drive)) {
            class_addMethod([self class], sel, (IMP)startEngine, "v@:@");
            return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
    
    @end
    

    解释一下,这个函数在 runtime 环境下,如果没有找到该方法的实现的话就会执行。第一行判断的是传入的 SEL 名称是否匹配,接着调用 class_addMethod 方法,传入相应的参数。其中第三个参数传入的是我们添加的 C 语言函数的实现,也就是说,第三个参数的名称要和添加的具体函数名称一致。第四个参数指的是函数的返回值以及参数内容。

    至于该类方法的返回值,在我测试的时候,无论这个 BOOL 值是多少,并不会影响我们的执行目标,一般返回 YES 即可。

    如果觉得用 C 语言风格写新函数比较不适应,那么可以改写成以下的代码:

    @implementation Car (myCar)
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        if (sel == @selector(drive)) {
            class_addMethod([self class], sel, class_getMethodImplementation(self, @selector(startEngine:)), "v@:@");
            return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
    
    - (void)startEngine:(NSString *)brand {
        NSLog(@"my %@ car starts the engine", brand);
    }
    
    @end
    

    其中 class_getMethodImplementation 意思就是获取 SEL 的具体实现的指针。
    然后创建一个新的类「DynamicSelector」,在这个新类中我们实现对「Car」的动态添加方法。

    #import "DynamicSelector.h"
    #import "Car+myCar.h"
    
    @implementation DynamicSelector
    
    - (void)dynamicAddMethod {
        Car *c = [[Car alloc] init];
        [c performSelector:@selector(drive) withObject:@"bmw"];
    }
    
    @end
    

    注意,在这里就不能使用 [self method:] 进行调用了,因为我们添加的方法是在运行时才执行,而编译器只负责编译时的方法检索,一旦对一个对象没有检索到它的 drive 方法,就会报错,所以这里我们使用 performSelector:withObject: 来进行调用,保存,运行。

    2016-08-26 10:50:17.207 objc-runtime[76618:3031897] my bmw car starts the engine
    Program ended with exit code: 0
    

    这里可能会有人不明白

     class_addMethod([self class], sel, class_getMethodImplementation(self, @selector(startEngine:)), "s@:@");
    

    注意到上面代码有这样一个字符串"v@*,它表示方法的参数和返回值,详情请参考Type Encodings

    image.png
    image.png

    参考链接:
    https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100-SW1

    https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtForwarding.html#//apple_ref/doc/uid/TP40008048-CH105-SW1

    https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtDynamicResolution.html#//apple_ref/doc/uid/TP40008048-CH102-SW1

    相关文章

      网友评论

          本文标题:使用runtime给类动态添加方法并调用 - class_add

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