如何在OC中没有显示声明的方法?

在OC中调用方法时,对于有声明的方法可以直接使用显式

//NSMutableArray调用array类方法
NSMutableArray *arr = [NSMutableArray array];
//NSMutableArray对象调用addObject方法
[arr addObject:@"element"];

经常会需要执行没有显示声明的方法,比如动态添加的方法,私有方法等,这些方法都不能使用这种方式进行调用.这时候如何调用这些确实存在的方法呢?

使用运行时为NSObject类添加方法为例,进行调用说明.

//使用block为NSObject添加appendString:withString:方法
    NSString *(^appendStringBlock)(id slf, NSString *, NSString *) = ^NSString *(id slf, NSString *a, NSString *b) {
      return  [NSString stringWithFormat:@"%@ %@", a , b];
    };

    IMP imp = imp_implementationWithBlock(appendStringBlock);
    SEL sel_appendString = sel_registerName("appendString:withString:");
    class_addMethod([NSObject class], sel_appendString, imp, "@@:@@");



    NSObject *obj = [[NSObject alloc] init];
    NSString *str1 = @"I love";
    NSString *str2 = @"China";
//obj如何调用appendString:withString:方法呢?

performSelector系列函数

系统在NSObject协议中定义了一系列performSelector方法

@protocol NSObject

...

- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
...
@end

用于执行不能直接调用的方法,可以根据参数个数的不同选择对应的方法.使用起来也比较简单,针对示例中的需求

#pragma clang push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    if ([obj respondsToSelector:sel_appendString]) {
        NSString *result = [obj performSelector:sel_appendString withObject:str1 withObject:str2];
        NSLog(@"result == %@", result);
        }

#pragma clang pop

这种调用方式操作简单使用起来非常方便,但是缺点也很明显:

  • 会出现"PerformSelector may cause a leak because its selector is unknown"信息,如果你是个"强迫症患者"或者大量使用这种方式进行调用的时候将是一件非常崩溃的事.尽管可以使用
  • #pragma clang push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        //your code here
    #pragma clang pop
        

    来消除警告⚠️.

  • 参数类型只能是id类型;
  • 这种方式最多只能传递两个参数当参数较多时,无法使用.

直接获取方法实现

既然可以调用那方法必定是存在的,所以只需要找到方法的实现,然后进行响应的类型转化就可以直接进行函数调用.对于如何查找方法的实现,大概有以下两种思路:

使用运行时方法

在objc/runtime.h中提供多种可以获取到方法实现的函数,可以借助于这些函数来获取方法的实现.

    IMP implement = class_getMethodImplementation([obj class], sel_appendString);
    //或者
    Method method = class_getInstanceMethod([obj class], sel_appendString);
    IMP implement  = method_getImplementation(method);

使用系统提供的示例

在NSObject中也定义了可以获取到对应方法实现的一些系统方法,利用这些方法也可以获取到对应方法的实现.

IMP implement = [NSObject instanceMethodForSelector:sel_appendString];
//或者
IMP implement = [obj methodForSelector:sel_appendString];

获取到方法实现之后就可以将方法原型转化为需要的方法类型进行调用.

    if (implement) {
        //定义类型
        typedef NSString *(*Function)(id, SEL, NSString *, NSString *);
        //进行强制转化并调用
        NSString *result = ((Function)implement)(obj, sel_appendString, str1, str2);
        NSLog(@"result== %@", result);
    }

如果对这个转化非常熟悉,也可直接进行转化使用.

    if (implement) {
        //进行强制转化并调用
        NSString *result = ((NSString*(*)(id, SEL, NSString *, NSString *))implement)(obj, sel_appendString, str1, str2);
        NSLog(@"result== %@", result);
    }

这种调用方式需要对原生的获取方法实现的途径非常清楚,能够准确定地获取方法的实现并进行方法类型的转化,而且代码写起来会更接近C语言的风格,如果习惯了使用OC的代码风格且没有C语言基础的话理解起来会比较困难.

objc_msgSend系列方法

在Runtime中定义这样一个转发消息的类C函数

OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

这个函数默认需要两个参数,第一个是方法调用者,第二个是对应的方法选择器.自定义的参数可以随意添加,只不过在使用的时候需要根据参数的个数和类型强制转化一下方法类型才能使用.例如当你需要调用一个没有参数且没有返回值的方法时

SEL sel = sel_registerName("viewDidLoad")
((void(*)(id,SEl))objc_msgSend)(viewController, sel);

而当需要调用一个返回类型为NSArray且参数为id的方法时:

SEL sel = sel_registerName("arrayByAddingObject:")
NSArray *oriArr = @[];
id element = [[NSObject alloc] init];
 NSArray *result = ((NSArray *(*)(id, SEl, id))objc_msgSend)(oriArr, sel, element);

所以针对示例中的需求,可以这样实现:

    if ([obj respondsToSelector:sel_appendString]) {
        NSString *result =  ((NSString *(*)(id, SEL, NSString *, NSString *))objc_msgSend)(obj, sel_appendString, @"I love", @" BeiJing");
        NSLog(@"result == %@", result);
        }

当需要调用当前类的父类实现,需要使用objc_msgSendSuper,当返回值为结构体时需要使用objc_msgSendSuper_stret等.

使用objc_msgSend系列方法实现方法调用,操作简便执行效率更高,不会手动处理警告.但是

  • 对类型强转的书写要求比较高;
  • 需要处理默认的参数(self,_cmd);
  • 是类C的实现可读性较差.

NSInvocation类

在OC有一个专门用来存储和转发消息的类,就是NSInvocation. NSInvocation对象包含Objective-C消息的所有元素:目标,选择器,参数和返回值.利用NSInvocation对象可以直接设置每个参数并且在分派NSInvocation对象时自动设置返回值,也可动态修改参数或者分派到不同的目标.使用起来更加灵活,功能非常强大,在Aspects和JSPatch等知名的第三方框架中都利用了NSInvocation进行消息处理.

针对示例中的需求,可以这样做实现:

    NSMethodSignature *signature = [obj methodSignatureForSelector:sel_appendString];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    [invocation setTarget:obj]; //index=0位置的参数
    [invocation setSelector:sel_appendString]; //index=1处的参数
    NSString *a = @"I love";
    NSString *b = @" China";

    //设置除了self和_cmd之外的其他参数,根据方法的需要依次设置参数
    [invocation setArgument:&str1 atIndex:2];
    [invocation setArgument:&str2 atIndex:3];

    //执行
    [invocation invoke];
    
    NSString *result;
    [invocation getReturnValue:&result];
    NSLog(@"result == %@", result);

之所以可以这样处理结果是因为已经知道了返回类型就是NSString*对象类型.如果事先并不知道返回值的类型,那么就需要根据返回值的类型进行动态处理.

以下实现以常见数据类型进行返回值处理,可以根据实际需求进行更加细滑的处理.

    //获取实际的返回值类型编码
    char returnType[255];
    strcpy(returnType, [signature methodReturnType]);

    switch (returnType[0] == 'r' ? returnType[1] : returnType[0]) {
            //处理数字类型包括BOOL
            #define RETURN_TYPE_NUMBER(_typeChar, _type) \
            case _typeChar: { \
                _type ret; \
                [invocation getReturnValue:&ret]; \
                break; \
            }
            RETURN_TYPE_NUMBER('c', char);
            RETURN_TYPE_NUMBER('C', unsigned char);
            RETURN_TYPE_NUMBER('i', int);
            RETURN_TYPE_NUMBER('I', unsigned int);
            RETURN_TYPE_NUMBER('s', short);
            RETURN_TYPE_NUMBER('S', unsigned short);
            RETURN_TYPE_NUMBER('l', long);
            RETURN_TYPE_NUMBER('L', unsigned long);
            RETURN_TYPE_NUMBER('q', long long);
            RETURN_TYPE_NUMBER('Q', unsigned long long);

        case 'v': {
            //返回值为void
            break;
        }
        case '@': {
            //返回值为对象类型
            __autoreleasing id ret;
            [invocation getReturnValue:&ret];
            NSLog(@"result == %@", ret);
            break;
        }

        case '{': {
            //结构体常见结构体
            /*
             由于结构体中有符合结构体,例如{CGRect={CGPoint=dd}{CGSize=dd}},所以只获取第一个结构

             */
            NSString *type = [NSString stringWithUTF8String:returnType];
            NSError *_error = nil;
            NSRegularExpression *reg = [NSRegularExpression regularExpressionWithPattern:@"[a-zA-Z]+(?=\\=)" options:(0) error:&_error];
            NSAssert(!_error, _error.debugDescription);
            NSTextCheckingResult *result = [reg firstMatchInString:type options:(NSMatchingReportCompletion) range:(NSRange){0, type.length}];
            NSAssert(result, @"result不存在");
            NSString *realStruct = [type substringWithRange:result.range];



    #define RETURN_TYPE_STRUCT(_type, realStruct) \
        if([realStruct rangeOfString:@#_type].location != NSNotFound) { \
                _type ret;\
                [invocation getReturnValue:&ret]; \
        }

            RETURN_TYPE_STRUCT(CGPoint, realStruct);
            RETURN_TYPE_STRUCT(CGSize, realStruct);
            RETURN_TYPE_STRUCT(CGRect, realStruct);
            RETURN_TYPE_STRUCT(NSRange, realStruct);
            break;
        }

        case '*':
        case '^': {
            //指针类
            void *ret;
            [invocation getReturnValue:&ret];
            break;
        }

        default:
            break;
    }

所以对于NSInvocation来讲,单次使用的并不划算.但是对于系统性的封装和调用来说,使用NSInvocation进行封装就会具有更加充分的灵活性,更加考验开发者对于消息转发的理解和使用能力.

你可能感兴趣的:(iOS,开发,开发小技巧)