在Cocoa框架中使用Swift的一些注意事项

虽然说Swift是作为一种全新的语言被推出的,但是不可避免的需要借助于Apple生态来对它进行推广,在推广的过程中,就不可避免的需要被使用在Cocoa框架中,所以我们今天来总结一下当Swift被使用在Cocoa框架中时需要注意的一些事项。

在我们开始讨论之前,我们先来了解一下Swift与Objective-C的一些不同点。

区别

我们通过使用Swift与Objective-C来编写具有一个存储属性、一个计算属性、一个实例方法的类:

Objective-C

@interface OCModel : NSObject

///存储属性
@property (nonatomic, copy) NSString *privateName;

///计算属性
@property (nonatomic, copy) NSString *publicName;

///实例方法
- (void)showName;

@end

@implementation OCModel

- (void)setPublicName:(NSString *)publicName {
    self.privateName = publicName;
}

- (NSString *)publicName {
    return self.privateName;
}

///实例方法
- (void)showName {
    NSLog(@"OCModel name is %@", self.privateName);
}

@end

Swift

class SwiftModel {
    ///存储属性
    var privateName: String?

    ///计算属性
    var publicName: String {
        get {
            return privateName ?? "none"
        }
        set {
            privateName = newValue
        }
    }

    ///实例方法
    func showName() -> Void {
        print("SwiftModel name is \(privateName ?? "none")")
    }
}

现在我们对两种语言定义的类通过Runtime来读取一下相关的内容,结果如下:

Objective-C

Objective-C

Swift

Swift

从上面结果可见,Swift中的属性即方法并不能通过Runtime机制读取出,这是因为Runtime的API是基于运行时机制的,而Swift本身是静态语言,在编译时就已经确定变量、方法等内容。

但我们在使用Objective-C进行iOS开发时,或多或少的都使用到了Runtime,同时Runtime也可以为我们解决许多问题,那么在Swift中就无法使用Runtime了吗?当然不是。

Swift中使用Runtime

我们了解了Swift为何无法使用Runtime的原因,要解决这个问题,最简单的方法就是将Swift中想要使用Runtime的内容桥接至Objective-C上,所幸苹果已经为我们做到了这一点,那就是@objc关键字。

我们将上述Swift中的类桥接至Objective-C之后看一下结果:

桥接之后

@objc class SwiftModel: NSObject {
    ///存储属性
    @objc var privateName: String?

    ///计算属性
    @objc var publicName: String {
        get {
            return privateName ?? "none"
        }
        set {
            privateName = newValue
        }
    }

    ///实例方法
    @objc func showName() -> Void {
        print("SwiftModel name is \(privateName ?? "none")")
    }
}

结果

Swift桥接至Objective-C

可见,在将Swift桥接至Objective-C之后,我们可以使用Runtime访问到所有的变量、属性以及方法。

注意

  1. 使用@objc可以将Swift编写的类桥接至Objective-C,但是必须保证该类的基类为NSObject。
  2. 在基于Swift3以及之前版本的Swift语言项目中,当Swift编写的类被标记为@objc时,编译器会自动为该类中所有非private访问级别的成员默认添加@objc关键字。但是在Swift4之后,该功能被关闭,我们需要为我们想要桥接至Objective-C的类、属性、方法进行手动显式地添加@objc标记。

现在,我们再来讨论一下在Cocoa框架中使用Swift,首先我们来看一下Target-Action模式。

Target-Action模式

首先我们看一下Target-Action模式中的两个关键点:Target以及Action。

我们使用Timer的API来查看一下:

public init(timeInterval ti: TimeInterval, target aTarget: Any, selector aSelector: Selector, userInfo: Any?, repeats yesOrNo: Bool)

首先,target为Any类型,其余没有特殊要求。

其次,我们来重点探讨一下action。

Action

从API中可以看出,Action的类型为Selector。在官方文档中,Selector的描述如下:The Objective-C SEL type.

由此我们可以看出,该Selector必须是桥接到Objective-C的方法。

Selector的初始化方式有两种,其中有一些注意点如下:

1.#selector()

我们可以使用#selector()来初始化一个Selector。

#selector

由Xcode的提示我们可知,所需要使用到的方法也必须是经过@objc标记的方法。

2.init(_ str: String)

使用Selector的初始化方法来进行创建,通过传入一个方法名称的字符串来创建Selector。

在使用该方法创建时,同样需要注意使用到的方法也必须是进过@objc标识的方法。

同时,在使用该方法创建时,方法名称字符串必须是Objective-C语法类型的方法名。

例如,我们使用如下方法创建Selector:

@objc func repeatFunction(timer: Timer) -> Void {
    print(timer)
}

那么对应的方法应该是:

Selector("repeatFunctionWithTimer:")

接下里我们再看一下Cocoa中的另一个模式。

Key-Value Observing模式

首先我们查看一下与KVO相关的几个方法:

extension NSObject {

    open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)
}

extension NSObject {

    open func addObserver(_ observer: NSObject, forKeyPath keyPath: String, options: NSKeyValueObservingOptions = [], context: UnsafeMutableRawPointer?)

    @available(iOS 5.0, *)
    open func removeObserver(_ observer: NSObject, forKeyPath keyPath: String, context: UnsafeMutableRawPointer?)

    open func removeObserver(_ observer: NSObject, forKeyPath keyPath: String)
}

由方法定义我们可知,想要使用KVO,那么被观察的对象需要是NSObject的子类。那好,我们定义一个NSObject的子类:

class SwiftModel: NSObject {
    var name: String?
}

接下来我们创建一个该类的实例,并对该实例进行 name 变量的观察:

override func viewDidLoad() {
    super.viewDidLoad()

    let model = SwiftModel()
    model.addObserver(self, forKeyPath: "name", options: [.new], context: nil)
    model.name = "new name"
    model.removeObserver(self, forKeyPath: "name")

}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if object is SwiftModel, keyPath == "name" {
        print(change!)
    }
}

接下来运行,我们发现并没有触发KVO观察的回调,这是为什么呢?

我们知道,KVO机制是在建立观察时动态的为被观察的类创建一个子类,同时重写被观察键的setter方法(关于KVO的探讨,可以查看该篇博客)。

此时我们没有触发KVO的观察回调,是由于@objc虽然将属性桥接至Objective-C上,但是Swift编译器还是无法实现动态调用,我们需要将被观察的属性设置为动态调用,即使用dynamic关键词来标识属性:

class SwiftModel: NSObject {
    @objc dynamic var name: String?
}

至此,我们实现了在Swift中使用KVO,下面是在使用KVO时的几点总结:

1.被观察的类需要继承自NSObject。

2.被观察的属性/变量需要制定动态调用,即使用dynamic来标识。

3.在使用dynamic标识时,需要将属性/变量桥接至Objective-C,即同时使用@objc dynamic标识。

其他一些相关注意事项

在实际开发中,我们需要对方法进行Swizzle,此时我们一般使用下面的方法:

func swizzle(instanceMethod: Selector, method: Selector) -> Void {
    let cls = type(of: self)
    let origMethodOptional = class_getInstanceMethod(cls, instanceMethod)
    let newMethodOptional = class_getInstanceMethod(cls, method)
    guard let origMethod = origMethodOptional, let newMethod = newMethodOptional else {
        return
    }
    if class_addMethod(cls,
                       instanceMethod,
                       method_getImplementation(newMethod),
                       method_getTypeEncoding(newMethod)) {
        class_replaceMethod(cls,
                            method,
                            method_getImplementation(origMethod),
                            method_getTypeEncoding(origMethod))
    } else {
        class_replaceMethod(cls,
                            method,
                            class_replaceMethod(cls,
                                                instanceMethod,
                                                method_getImplementation(newMethod),
                                                method_getTypeEncoding(newMethod))!,
                            method_getTypeEncoding(origMethod))
    }
}

此时如果我们不对需要交换的方法进行特殊的处理,那么可能会造成交换失败,如下例:

class SuperClass: NSObject {
    @objc func sayHello() -> Void {
        print("Super Say Hello")
    }
}

class SubClass: SuperClass {

    override init() {
        super.init()
        self.swizzle(instanceMethod: #selector(sayHello), method: #selector(hookSayHello))
    }

    @objc func hookSayHello() -> Void {
        print("Sub Say hello")
    }
}

接下来我们初始化一个SubClass对象,然后分别调用sayHello()和hookSayHello()方法,理论上两个方法进行了交换,打印结果也应该进行交换,但结果并没有交换。

这是为什么呢?

其实不难想象,KVO中要将观察的属性设置为dynamic,目的就是为了在调用时动态访问到重写之后的setter方法。而在此处,两个方法并没有指定dynamic,虽然两个方法进行了调换,但是在调用时并没有动态调用,而仅仅是直接访问,所以并没有达到我们想要的效果。

我们可以通过以下步骤进行验证:

1.验证方法是否被交换

我们改动一下Runtime交换方法的函数,在其中打印一下交换前与交换后的方法指向:

添加代码

然后我们运行代码,此时方法的打印结果没有交换,但是方法的指向发生了改变:

交换结果

2.验证单个dynamic标识效果
  • 我们为sayHello方法添加dynamic,运行代码,发现两次打印都是”Sub Say Hello”。
  • 我们为hookSayHello方法添加dynamic,允许代码,发现两次打印都是”Super Say Hello”。

不难理解,当其中一个方法被指定为dynamic时,当访问该方法时,会采用动态调用的方式,进而访问到调换之后的实现。而没有指定dynamic的方法,还是按照直接调用的方法,访问到它自己本身的实现上。

3.验证两个dynamic标识效果

当两个方法都被指定为dynamic时,打印结果发生了交换,证明方法调换成功。

接下来我们继续对方法调换进行探索,假设hookSayHello方法是在SubClass的Extension中,那么对于dynamic的使用有和不同:

class SuperClass: NSObject {
    @objc func sayHello() -> Void {
        print("Super Say Hello")
    }
}

class SubClass: SuperClass {

    override init() {
        super.init()
        self.swizzle(instanceMethod: #selector(sayHello), method: #selector(hookSayHello))
    }
}

extension SubClass {
    @objc func hookSayHello() -> Void {
        print("Sub Say hello")
    }
}

我们依旧使用上述步骤进行验证:

1.验证方法是否被交换

我们运行代码,此时方法的指向发生了变化,同时两次打印结果均为”Super Say Hello”。

交换结果

2.验证单个dynamic标识效果
  • 我们为sayHello方法添加dynamic,运行代码,发现两次分别是”Sub Say Hello”,”Super Say Hello”。
  • 我们为hookSayHello方法添加dynamic,允许代码,发现两次打印都是”Super Say Hello”。
3.验证两个dynamic标识效果

当两个方法都被指定为dynamic时,打印结果发生了交换,证明方法调换成功。

从上述结果,我们可以总结出,sayHello方法的表现与之前验证结果一直,而hookSayHello的表现无论有没有指定dynamic,都进行了动态调用,由此可见,通过Extension新增的方法,均表现为dynamic。

至此,我们可以进行一个总结:

1.Runtime可以将Swift中的@objc方法进行调换。

2.Swift调用方法时,并不依赖于Runtime,即使方法对应的IMP已经改变,但Swift依旧可以使用直接调用方式调用到原始的IMP。

3.可以使用dynamic关键字将Swift中的方法指定为动态调用,此时的Swift方法与Objective-C中方法表现一致,会受到Runtime的影响。

4.通过Extension新增的方法,默认为dynamic的,这些方法受Runtime影响。

发布了71 篇原创文章 · 获赞 34 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/TuGeLe/article/details/82693536