OC和Swfit混编中一次有意思的Selector探索

735 阅读5分钟

Selector

@selectorObjective-C 时代的一个关键字,它可以将一个方法转换并赋值给一个 SEL 类型,它的表现很类似一个动态的函数指针。在 Objective-C 时 selector 非常常用,从设定 target-action,到自举询问是否响应某个方法,再到指定接受通知时需要调用的方法等等,都是由 selector 来负责的。在 Objective-C 里生成一个 selector 的方法一般是这个样子的

-(void) callMe {
    //...
}

-(void) callMeWithParam:(id)obj {
    //...
}

SEL someMethod = @selector(callMe);
SEL anotherMethod = @selector(callMeWithParam:);

// 或者也可以使用 NSSelectorFromString
// SEL someMethod = NSSelectorFromString(@"callMe");
// SEL anotherMethod = NSSelectorFromString(@"callMeWithParam:"); 

一般为了方便,很多人会选择使用 @selector,但是如果要追求灵活的话,可能会更愿意使用 NSSelectorFromString 的版本 -- 因为我们可以在运行时动态生成字符串,通过方法名来调用对应的方法

在 Swift 中没有 @selector 了,取而代之,从 Swift 2.2 开始我们使用 #selector 来从暴露给 Objective-C 的代码中获取一个 selector。类似地,在 Swift 里对应原来 SEL 的类型是一个叫做 Selector 的结构体

@objc func callMe() {
    //...
}
@objc func callMeWithParam(obj: AnyObject!) {
    //...
}
let someMethod = #selector(callMe)
let anotherMethod = #selector(callMeWithParam(obj:))  

【注】selector 其实是 Objective-C runtime 的概念。在 Swift 4中,默认情况下所有的 Swift 方法在 Objective-C 中都是不可见的,所以你需要在这类方法前面加上 @objc 关键字,将这个方法暴露给 Objective-C,才能进行使用

如果方法名字在方法所在域内是唯一的话,我们可以简单地只是用方法的名字来作为 #selector 的内容。相比于前面带有冒号的完整的形式来说,这么写起来会方便一些

let someMethod = #selector(callMe)
let anotherMethod = #selector(callMeWithParam)

如果同一个作用域里面存在同样名字的两个方法,但是参数不同,我们可以通过将方法强制转换来使用

@objc func commonFunc() {}

@objc func commonFunc(input: Int) -> Int {
    return input
} 

let method1 = #selector(commonFunc as ()->())
let method2 = #selector(commonFunc as (Int)->Int)

我项目中遇到的问题

我的新项目是使用的OC和Swift混编,主要是Swift项目,但是为了追求灵活,我在运行时动态生成字符串,通过方法名来调用对应的方法

/// 执行事件
/// @param methodsName 事件名
/// @param objectArg 参数
/// @param target target
+ (void)doAction:(NSString *)methodsName andArgObject:(nullable id)objectArg AndTarget:(id)target{
    SEL selector = NSSelectorFromString(methodsName);
    if ([target respondsToSelector:selector])
    {
        [target performSelectorOnMainThread:selector withObject:objectArg waitUntilDone:NO];
    }
}

再一次调用带参数的Swift方法是我遇到了一个问题,不知道字符串该写成什么?

为了节省这个问题,我写了一个小的Demo来探索一下。我们创建一个OC项目,然后在里面创建一个Swift类SwiftMethods

class SwiftMethods: NSObject {
    /// 测试方法1
    @objc
    func testMethods(){
        print("测试方法1:\(#function)")
    }
    ///测试方法2
    @objc
    func testMethods(_ text:String) {
        print("测试方法2:\(#function)")
    }
    ///方法3
    @objc
    func testMethods(by text:String) {
        print("测试方法3:\(#function)")
    }
    ///方法4
    @objc
    func testMethods(text:String) {
        print("测试方法4:\(#function)")
    }
}

为了能够在OC项目中调用类中方法,我们再需要调用类中添加项目名-Swift.h

SwiftMethods *methods = [SwiftMethods new];
[methods testMethods];
[methods testMethods:@"test2"];
[methods testMethodsBy:@"test3"];
[methods testMethodsWithText:@"test4"];

我们可以在项目名-Swift.h里面看到swift方法生成OC方法的


SWIFT_CLASS("_TtC12testSelector12SwiftMethods")
@interface SwiftMethods : NSObject
/// 测试方法1
- (void)testMethods;
/// 测试方法2
- (void)testMethods:(NSString * _Nonnull)text;
/// 方法3
- (void)testMethodsBy:(NSString * _Nonnull)text;
/// 方法4
- (void)testMethodsWithText:(NSString * _Nonnull)text;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end

再使用我们封装的doAction方法,根据字符串调用Swift方法时,我们需要一一对应

  • func testMethods()方法对应方法名字符串是testMethods
  • func testMethods(_ text:String)方法对应方法名字符串是testMethods:
  • func testMethods(by text:String)方法对应方法名字符串是testMethodsBy:
  • func testMethods(text:String)方法对应方法名字符串是testMethodsWithText:

但是没有任何文档说明func testMethods(text:String)方法对应方法名字符串一直都是testMethodsWithText:,我们不知道会不会随着Swfit的升级字符串变了。

@objc

为了解决上面的问题,我对@objc进行了一些探索。

  • 1、在 Swift 类型文件中,我们可以将需要暴露给 Objective-C 使用的任何地方 (包括类,属性和方法等) 的声明前面加上 @objc 修饰符
  • 2、@objc 修饰符的另一个作用是为 Objective-C 侧重新声明方法或者变量的名字

虽然绝大部分时候自动转换的方法名已经足够好用 (比如会将 Swift 中类似 init(name: String) 的方法转换成 -initWithName:(NSString *)name 这样),但是有时候我们还是期望 Objective-C 里使用和 Swift 中不一样的方法名或者类的名字,比如 Swift 里这样的一个类:

class 我的类: NSObject {
    func 打招呼(名字: String) {
        print("哈喽,\(名字)")
    }
}

我的类().打招呼("小明")

Objective-C 的话是无法使用中文来进行调用的,因此我们必须使用 @objc 将其转为 ASCII 才能在 Objective-C 里访问:

  @objc(MyClass)
    class 我的类 {
        @objc(greeting:)
        func 打招呼(名字: String) {
            print("哈喽,\(名字)")
        }
    }

这样,我们在 Objective-C 里就能调用 [[MyClass new] greeting:@"XiaoMing"] 这样的代码

根据上面特性,我们可以实践一下

探索1:

@objc (testMethods:)
func testMethods(_ text:String) {
     print("测试方法:\(#function)")
}
    
生成的OC方法
- (void)testMethods:(NSString * _Nonnull)text;

探索2:

@objc (testMethods:)
func testMethods(by text:String) {
     print("测试方法3:\(#function)")
}

生成的OC方法
- (void)testMethods:(NSString * _Nonnull)text;

探索3:

@objc (testMethods:)
func testMethods(text:String) {
     print("测试方法3:\(#function)")
}

生成的OC方法
- (void)testMethods:(NSString * _Nonnull)text;

好了,上面遇到的问题解决了

参考:

Swifter - Swift 必备 Tips:SELECTOR

Swifter - Swift 必备 Tips:@OBJC 和 DYNAMIC