热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

虽然说Swift是作为一种全新的语言被推出的,但是不可避免的需要借助于Apple生态来对它进行推广,在推广的过程中,就不可避免的需要被使用

虽然说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影响。


推荐阅读
author-avatar
施工的公司_534
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有