iOS动态调用类方法

作者: _人非草木_ | 来源:发表于2018-12-28 12:26 被阅读270次

    iOS动态调用类方法(不带参数)

    Class class = NSClassFromString(@"MyClass");
    if (class) {
        id obj = [[class alloc] init];
        SEL sel = NSSelectorFromString(@"myMethod");
        //检查是否有"myMethod"这个名称的方法
        if ([obj respondsToSelector:sel]) {
            [obj performSelector:sel];
        }
    }
    

    iOS动态调用类方法(带参数)

    Class class = NSClassFromString(@"MyClass");
    if (class) {
        id obj = [[class alloc] init];
        SEL sel = NSSelectorFromString(@"myMethod:"); //方法带有参数需要加冒号:
        //检查是否有"myMethod"这个名称的方法
        if ([obj respondsToSelector:sel]) {
            [obj performSelector:sel withObject:param]; //方法有多个参数时使用多个withObject传递参数
        }
    }
    

    iOS动态调用类方法(有返回值)

    Class class = NSClassFromString(@"MyClass");
    if (class) {
        id obj = [[class alloc] init];
        SEL sel = NSSelectorFromString(@"myMethod");
        //检查是否有"myMethod"这个名称的方法
        if ([obj respondsToSelector:sel]) {
            id = [obj performSelector:sel];
            //然后将id转换为类方法实际返回的数据类型。
            //假设为NSString *类型
            NSString *str = (NSString *)id;
            NSLog(@"str: %@", str);
        }
    }
    

    优点

    1. 弱化连接,因此并不会把没有的Framework也link到程序中。
    2. 不需要使用import,因为类是动态加载的,只要存在就可以加载。因此如果你的toolchain中没有某个类的头文件定义,而你确信这个类是可以用的,那么就可以用这种方法。

    问题

    采用这种方式,在Xcode中会报以下警告信息:

    "performSelector may cause a leak because its selector is unknown"(因为performSelector的选择器未知可能会引起泄漏)
    

    原因

    在ARC下调一个方法,runtime需要知道对于返回值该怎么办。返回值可能有各种类型:voidintcharNSString *id等等。ARC 一般是根据返回值的头文件来决定该怎么办的,一共有以下4种情况:

    1. 直接忽略(如果是基本类型比如 voidint这样的)。
    2. 把返回值先retain,等到用不到的时候再release(最常见的情况)。
    3. retain,等到用不到的时候直接release(用于 initcopy 这一类的方法,或者标注ns_returns_retained的方法)。
    4. 什么也不做,默认返回值在返回前后是始终有效的(一直到最近的release pool结束为止,用于标注ns_returns_autoreleased的方法)。

    而调performSelector:的时候,系统会默认返回值并不是基本类型,但也不会retainrelease,也就是默认采取第 4 种做法。所以如果那个方法本来应该属于前3种情况,都有可能会造成内存泄漏。

    对于返回void或者基本类型的方法,就目前而言你可以忽略这个warning,但这样做不一定安全。我看过Clang在处理返回值这块的几次迭代演进。一旦开着ARC,编译器会觉得从performSelector:返回的对象没理由不能retain,不能release。在编译器眼里,它就是个对象。所以,如果返回值是基本类型或者void,编译器还是存在会retainrelease它的可能,然后直接导致crash。

    解决办法

    1. 使用宏忽略警告(不推荐)

    #define SuppressPerformSelectorLeakWarning(Stuff) \
    do {\
        _Pragma("clang diagnostic push") \
        _Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") \
        Stuff; \
        _Pragma("clang diagnostic pop") 
    \} while (0)
    

    在产生警告也就是performSelector的地方用使用该宏,如:

    SuppressPerformSelectorLeakWarning([obj performSelector:sel]);
    

    1. 使用函数指针方式(推荐)

    Class class = NSClassFromString(@"MyClass");
    if (class) {
        id obj = [[class alloc] init];
        SEL sel = NSSelectorFromString(@"myMethod");
        IMP imp = [obj methodForSelector:sel];
        void (*function)(id, SEL) = (void *)imp;
        function(obj, sel);
    }
    

    这一堆代码在做的事情其实是向obj请求那个方法对应的C函数指针。所有的NSObject都能响应methodForSelector:这个方法,不过也可以用Objective-C runtime里的class_getMethodImplementation(只在protocol的情况下有用,id<SomeProto>这样的)。这种函数指针叫做IMP,就是typedef过的函数指针(id (*IMP)(id, SEL, ...))。它跟方法签名(signature)比较像,虽然可能不是完全一样。

    得到IMP之后,还需要进行转换,转换后的函数指针包含ARC所需的那些细节(比如每个OC方法调用都有的两个隐藏参数self_cmd)。这就是代码第6行干的事(右边的那个(void *)只是告诉编译器,不用报类型强转的warning)。

    最后一步,调用函数指针。

    如果selector接收参数,或者有返回值,代码就需要改改:

    Class class = NSClassFromString(@"MyClass");
    if (class) {
        id obj = [[class alloc] init];
        SEL sel = NSSelectorFromString(@"myMethod:");
        IMP imp = [obj methodForSelector:sel];
        NSString *(*function)(id, SEL, NSString *) = (void *)imp;
        NSString *result = function(obj, sel);
    }
    
    部分引用内容出处如下:

    链接:https://www.jianshu.com/p/a9569a9c9a63
    链接:https://www.jianshu.com/p/6517ab655be7

    相关文章

      网友评论

        本文标题:iOS动态调用类方法

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