OC到Swift的转变

tech2026-04-12  2

1、Selector

@selector是 Objective-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)

2、实例方法的动态调用

class MyClass { func method(number: Int) -> Int { return number + 1 } }

想要调用method方法的话,最普通的使用方式是生成MyClass的实例,然后用 .method 来调用它

let cls = MyClass() cls.method(number: 1)

我们还可以把刚才的方法该成下面这样

let f = MyClass.method let object = MyClass() let result = f(object)(1)

我们观察f类:alt+单击

let f: (MyClass) -> (Int) -> Int

其实对于 Type.instanceMethod 这样的取值语句,实际上刚才

let f = MyClass.method

做的事情类似于下面字面量的转换

let f = { (obj: MyClass) in obj.method }

3、条件编译

在 C 系语言中,可以使用#if或者 #ifdef之类的编译条件分支来控制哪些代码需要编译,而哪些代码不需要。Swift 中没有宏定义的概念,因此我们不能使用#ifdef的方法来检查某个符号是否经过宏定义。但是为了控制编译流程和内容,Swift 还是为我们提供了几种简单的机制来根据需求定制编译内容的。

首先是 #if 这一套编译标记还是存在的,#elseif 和#else 是可选的。

#if <condition> #elseif <condition> #else #endif

但是这几个表达式里的 condition 并不是任意的。Swift 内建了几种平台和架构的组合,来帮助我们为不同的平台编译不同的代码,具体地

方法可选参数os()macOS, iOS, tvOS, watchOS, Linuxarch()x86_64, arm, arm64, i386swift()>= 某个版本

 

 

 

 

 

如果我们统一我们在 iOS 平台和 Mac 平台的关于颜色的 API 的话,一种可能的方法就是配合 typealias 进行条件编译:

#if os(macOS) typealias Color = NSColor #else typealias Color = UIColor #endif #if arch(x86_64) #else #endif #if swift(>=14.0) #else #endif

对自定义符号进行编译

我们需要使用同一个 target 完成同一个 app 的收费版和免费版两个版本,并且希望在点击某个按钮时收费版本执行功能,而免费版本弹出提示的话,可以使用类似下面的方法

func someButtonPressed(sender: AnyObject!) { #if FREE_VERSION // 弹出购买提示,导航至商店等 #else // 实际功能 #endif }

在这里我们用 FREE_VERSION这个编译符号来代表免费版本。为了使之有效,我们需要在项目的编译选项中进行设置,在项目的 Build Settings中,找到 Swift Compiler - Custom Flags,并在其中的Other Swift Flags加上-D FREE_VERSION就可以了。

4、可选协议和协议扩展

Objective-C 中的 protocol里存在 @optional关键字,被这个关键字修饰的方法并非必须要被实现。我们可以通过协议定义一系列方法,然后由实现协议的类选择性地实现其中几个方法。最好的例子我想应该是 UITableViewDataSource 和 UITableViewDelegate。前者中有两个必要方法

-tableView:numberOfRowsInSection: -tableView:cellForRowAtIndexPath:

原生的 Swift protocol 里没有可选项,所有定义的方法都是必须实现的

protocol MyProtocol { func mustProtocolMethod() //必须实现方法 func mustProtocolMethod1() //必须实现方法 } class MyClass: MyProtocol { func mustProtocolMethod() { print("MyClass-->必须实现方法:mustProtocolMethod") } func mustProtocolMethod1() { print("MyClass-->必须实现方法:mustProtocolMethod1") } }

如果我们想要像 Objective-C 里那样定义可选的协议方法,就需要将协议本身和可选方法都定义为Objective-C 的,也即在 protocol定义之前以及协议方法之前加上 @objc。另外和 Objective-C 中的 @optional不同,我们使用没有 @符号的关键字 optional来定义可选方法

一个不可避免的限制是,使用 @objc 修饰的 protocol 就只能被 class 实现了,也就是说,对于 struct 和 enum 类型,我们是无法令它们所实现的协议中含有可选方法或者属性的

在 Swift 2.0 中,我们有了另一种选择,那就是使用 protocol extension。我们可以在声明一个 protocol 之后再用 extension 的方式给出部分方法默认的实现。这样这些方法在实际的类中就是可选实现的了

protocol MyProtocol2 { func optionalProtocolMethod1() //可选方法 func optionalProtocolMethod2() //可选方法 func mustProtocolMethod1() //必须实现方法 } extension MyProtocol2{ func optionalProtocolMethod1(){} func optionalProtocolMethod2(){} }

5、内存管理,weak 和 unowned

跟OC一样,Swift也是采用基于引用计数的ARC内存管理方案(针对堆空间)

Swift中ARC有3种引用

1、强引用:默认情况下,引用都是强引用2、弱引用(weak):通过weak定义弱引用 必须是可选类型的var,因为实例销毁后,ARC会自动将弱引用设置为nilARC自动给弱引用设置nil时,不会触发属性观察器3、无主引用(unowned):通过unowned定义无主引用 不会产生强引用,实例销毁后仍然存储着实例的内存地址(类似于OC中的unsafe_unretained)试图销毁后访问无主引用,会产生运行时错误(野指针)Fatal error: Attempted to read an unowned reference but object 0x10070a460 was already deallocated class Person { func eat() { } deinit { print("Person销毁") } } unowned var p = Person() p.eat()

这段代码就会产生运行时错误

循环引用

weak、unowned 都能解决循环引用的问题,unowned 要比weak 少一些性能消耗

生命周期中可能被置为nil使用weak初始化赋值以后不会被置为nil使用unowned

闭包的循环引用

闭包表达式默认会对用到的外层对象产生额外的强引用(对外层进行了retain操作) class Person { var fn:(() -> ())? func run() { print("run") } deinit { print("Person销毁") } } func test() { let p = Person() p.fn = { p.run() } } test()

上面这段代码就会造成循环引用,想要解决这个问题,可以使用weak或者unowned

func test() { let p = Person() p.fn = {[weak p] in p?.run() } } func test() { let p = Person() p.fn = {[unowned p] in p.run() } }

如果想在定义闭包属性的同时引用self,这个闭包必须是lazy的,因为在实例初始化完毕后才能引用self

class Person { lazy var fun:(() -> ()) = { [weak self] in self?.run() } func run() { print("run") } deinit { print("Person销毁") } }

闭包fn内部如果用到了实例成员,属性,方法,编译器会强制要求明确的写出self

【注】:编译器强制要求明确的写出self的时候有可能会导致循环引用,需要注意的

如果lazy属性是闭包调用的结果,那么不用考虑循环引用问题,(因为闭包调用后,闭包的声明周期就结束了)

class Person { var age: Int = 0 lazy var getAge: Int = { self.age }() deinit { print("Person销毁") } }

6、值类型与引用类型

内存(RAM)中有两个区域,栈区(stack)和堆区(heap)。在 Swift 中,值类型,存放在栈区;引用类型,存放在堆区。

值类型(Value Type)

值类型,即每个实例保持一份数据拷贝

在 Swift 中,典型的有 struct,enum,以及 tuple 都是值类型。而平时使用的 Int, Double,Float,String,Array,Dictionary,Set 其实都是用结构体实现的,也是值类型。

Swift 中,值类型的赋值为深拷贝(Deep Copy),值语义(Value Semantics)即新对象和源对象是独立的,当改变新对象的属性,源对象不会受到影响,反之同理。

struct CoordinateStruct { var x: Double var y: Double } var coordA = CoordinateStruct(x: 0, y: 0) var coordB = coordA coordA.x = 100.0 print("coordA.x -> \(coordA.x)") print("coordB.x -> \(coordB.x)")

如果声明一个值类型的常量,那么就意味着该常量是不可变的(无论内部数据为 var/let)

let coordC = CoordinateStruct(x: 0, y: 0)

在 Swift 3.0 中,可以使用 withUnsafePointer(to:_:) 函数来打印值类型变量的内存地址,这样就能看出两个变量的内存地址并不相同。

withUnsafePointer(to: &coordA) { print("\($0)") } withUnsafePointer(to: &coordB) { print("\($0)") } 0x0000000100007670 0x0000000100007680

在 Swift 中,双等号(== & !=)可以用来比较变量存储的内容是否一致,如果要让我们的 struct 类型支持该符号,则必须遵守Equatable协议。

extension CoordinateStruct: Equatable { static func ==(left: CoordinateStruct, right: CoordinateStruct) -> Bool { return (left.x == right.x && left.y == right.y) } } if coordA != coordB { print("coordA != coordB") }

引用类型(Reference Type)

引用类型,即所有实例共享一份数据拷贝

在 Swift 中,class 和闭包是引用类型。引用类型的赋值是浅拷贝(Shallow Copy),引用语义(Reference Semantics)即新对象和源对象的变量名不同,但其引用(指向的内存空间)是一样的,因此当使用新对象操作其内部数据时,源对象的内部数据也会受到影响。

class Dog { var height = 0.0 var weight = 0.0 } var dogA = Dog() var dogB = dogA dogA.height = 50.0 print("dogA.height -> \(dogA.height)") print("dogB.height -> \(dogB.height)") // dogA.height -> 50.0 // dogB.height -> 50.0

在 Swift 3.0 中,可以使用以下方法来打印引用类型变量指向的内存地址。从中即可发现,两个变量指向的是同一块内存空间。

print(Unmanaged.passUnretained(dogA).toOpaque()) print(Unmanaged.passUnretained(dogB).toOpaque()) //0x0000000100772ff0 //0x0000000100772ff0

在 Swift 中,三等号(=== & !==)可以用来比较引用类型的引用(即指向的内存地址)是否一致。也可以在遵守 Equatable 协议后,使用双等号(== & !=)用来比较变量的内容是否一致。

7、String 还是 NSString

简单来说:没有特别需要,尽可能的还是使用String,有以下三个原因:

1、虽然 String和 NSString有着良好的互相转换的特性,但是现在 Cocoa 所有的 API 都接受和返回 String类型。我们没有必要也不必给自己凭空添加麻烦去把框架中返回的字符串做一遍转换2、因为在 Swift 中 String是struct,相比起 NSObject 的 NSString 类来说,更切合字符串的 "不变" 这一特性。通过配合常量赋值 (let) ,这种不变性在多线程编程时就非常重要了,它从原理上将程序员从内存访问和操作顺序的担忧中解放出来。另外,在不触及 NSString 特有操作和动态特性的时候,使用 String 的方法,在性能上也会有所提升3、因为 String 实现了 Collection 这样的协议,因此有些 Swift 的语法特性只有 String 才能使用,而 NSString 是没有的。一个典型就是 for...in的枚举

8、GCD

GCD中Swift和OC都差不多,为了方便使用,我们可以简单封装以下GCD

typealias Task = (_ cancel : Bool) -> Void @discardableResult func delay(_ time: TimeInterval, task: @escaping ()->()) -> Task? { func dispatch_later(block: @escaping ()->()) { let t = DispatchTime.now() + time DispatchQueue.main.asyncAfter(deadline: t, execute: block) } var closure: (()->Void)? = task var result: Task? let delayedClosure: Task = { cancel in if let internalClosure = closure { if (cancel == false) { DispatchQueue.main.async(execute: internalClosure) } } closure = nil result = nil } result = delayedClosure dispatch_later { if let delayedClosure = result { delayedClosure(false) } } return result; } func cancel(_ task: Task?) { task?(true) }

9、自省

向一个对象发出询问,以确定他是不是属于某个类,这种操作就称为自省。

在OC中一个对象询问它是不是属于某个类。常用的方法有下面两类:

OC方法

[obj1 isKindOfClass:[ClassA class]]; [obj2 isMemberOfClass:[ClassB class]]; 1、-isKindOfClass:判断 obj1 是否是 ClassA 或者其子类的实例对象;2、isMemberOfClass: 则对 obj2 做出判断,当且仅当 obj2 的类型为 ClassB 时返回为真

Swift方法

class ClassA: NSObject {} class ClassB: ClassA {} let obj1 = ClassA() let obj2 = ClassB() print(obj1.isKind(of: ClassA.self)) print(obj2.isMember(of: ClassA.self)) //true //false

对于一个不确定的类型,我们现在可以使用 is来进行判断。is在功能上相当于原来的 isKindOfClass,可以检查一个对象是否属于某类型或其子类型。is和原来的区别主要在于亮点,首先它不仅可以用于 class类型上,也可以对 Swift 的其他像是 struct或enum类型进行判断

class ClassA { } class ClassB: ClassA { } let obj: AnyObject = ClassB() if (obj is ClassA) { print("属于 ClassA") } if (obj is ClassB) { print("属于 ClassB") }

10、KVO

在Swift中KVO仅限于NSObject的子类,我们还需要做额外的工作,那就是将想要观测的对象标记为 dynamic 和 @objc

在 Swift 4 之前的版本中,为一个 NSObject 的子类实现 KVO 的最简单的例子看起来是这样的

class MyClass: NSObject { @objc dynamic var date = Date() } private var myContext = 0 class Class: NSObject { var myObject: MyClass! override init() { super.init() myObject = MyClass() print("初始化 MyClass,当前日期: \(myObject.date)") myObject.addObserver(self, forKeyPath: "date", options: .new, context: &myContext) delay(3) { self.myObject.date = Date() } } override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if let change = change, context == &myContext { if let newDate = change[.newKey] as? Date { print("MyClass 日期发生变化 \(newDate)") } } } } let obj = Class() 初始化 MyClass,当前日期: 2020-04-08 07:26:22 +0000 MyClass 日期发生变化 2020-04-08 07:26:25 +0000

Swift 4 中 Apple 引入了新的 KeyPath 的表达方式,现在,对于类型 Foo 中的变量 bar: Bar,对应的 KeyPath 可以写为 \Foo.bar

class AnotherClass: NSObject { var myObject: MyClass! var observation: NSKeyValueObservation? override init() { super.init() myObject = MyClass() print("初始化 AnotherClass,当前日期: \(myObject.date)") observation = myObject.observe(\MyClass.date, options: [.new]) { (_, change) in if let newDate = change.newValue { print("AnotherClass 日期发生变化 \(newDate)") } } delay(1) { self.myObject.date = Date() } } }

使用Swift 4.0 KeyPath的好处有很多

1、设定观察和处理观察的代码被放在了一起,让代码维护难度降低很多;2、其次在处理时我们得到的是类型安全的结果,而不是从字典中取值;3、我们不再需要使用 context 来区分是哪一个观察量发生了变化,而且使用 observation 来持有观察者可以让我们从麻烦的内存管理中解放出来,观察者的生命周期将随着 AnotherClass 的释放而结束

Swift 中使用 KVO 还是有有两个显而易见的问题

1、在 Objective-C 中我们几乎可以没有限制地对所有满足 KVC 的属性进行监听,而现在我们需要属性有 dynamic 和 @objc进行修饰,有时候我们很可能也无法修改想要观察的类的源码,遇到这种情况,一个可行的方案是继承这个类,并且将需要观察的属性使用dynamic 和 @objc重写 class MyClass: NSObject { var date = Date() } class MyChildClass: MyClass { @objc dynamic override var date: Date { get { return super.date } set { super.date = newValue } } } 2、另一个大问题是对于那些非 NSObject 的 Swift 类型怎么办。我们可以通过属性观察器来处理

11、局部scope

C 系语言中在方法内部我们是可以任意添加成对的大括号 {} 来限定代码的作用范围的。这么做一般来说有两个好处,首先是超过作用域后里面的临时变量就将失效,这不仅可以使方法内的命名更加容易,也使得那些不被需要的引用的回收提前进行了,可以稍微提高一些代码的效率;另外,在合适的位置插入括号也利于方法的梳理,对于那些不太方便提取为一个单独方法,但是又应该和当前方法内的其他部分进行一些区分的代码,使用大括号可以将这样的结构进行一个相对自然的划分

OC代码

- (void)loadView { UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 480)]; { UILabel *titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(150, 30, 200, 40)]; titleLabel.textColor = [UIColor redColor]; titleLabel.text = @"Title"; [view addSubview:titleLabel]; } { UILabel *textLabel = [[UILabel alloc] initWithFrame:CGRectMake(150, 80, 200, 40)]; textLabel.textColor = [UIColor redColor]; textLabel.text = @"Text"; [view addSubview:textLabel]; } self.view = view; }

Swift方法

在 Swift 中,直接使用大括号的写法是不支持的,因为这和闭包的定义产生了冲突。如果我们想类似地使用局部 scope 来分隔代码的话,一个不错的选择是定义一个接受 ()->() 作为函数的全局方法,然后执行它:

override func loadView() { let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 480)) view.backgroundColor = .white local { let titleLabel = UILabel(frame: CGRect(x: 150, y: 30, width: 200, height: 40)) titleLabel.textColor = .red titleLabel.text = "Title" view.addSubview(titleLabel) } local { let textLabel = UILabel(frame: CGRect(x: 150, y: 80, width: 200, height: 40)) textLabel.textColor = .red textLabel.text = "Text" view.addSubview(textLabel) } self.view = view }

我们还可以使用匿名闭包来实现

override func loadView() { let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 480)) view.backgroundColor = .white let titleLabel: UILabel = { let label = UILabel(frame: CGRect(x: 150, y: 30, width: 200, height: 40)) label.textColor = .red label.text = "Title" return label }() view.addSubview(titleLabel) let textLabel: UILabel = { let label = UILabel(frame: CGRect(x: 150, y: 80, width: 200, height: 40)) label.textColor = .red label.text = "Text" return label }() view.addSubview(textLabel) self.view = view }

12、关联对象

我们经常会遇到给分类添加成员变量的问题,对于这类问题,OC的写法大家都是耳熟能详了。譬如给UIView添加一个viewId的成员变量

#import <objc/runtime.h> static const void *RunTimeViewID = @"RunTimeViewID"; @implementation UIView (JHExtension) - (NSString *)viewID{ NSString *ID = objc_getAssociatedObject(self, &RunTimeViewID); return ID; } - (void)setViewID:(NSString *)viewID{ objc_setAssociatedObject(self, &RunTimeViewID, viewID, OBJC_ASSOCIATION_COPY_NONATOMIC); }

Swift

在 Swift 中这样的方法依旧有效,只不过在写法上可能有些不同。两个对应的运行时的 get 和 set Associated Object 的 API 是这样的

func objc_getAssociatedObject(object: AnyObject!, key: UnsafePointer<Void> ) -> AnyObject! func objc_setAssociatedObject(object: AnyObject!, key: UnsafePointer<Void>, value: AnyObject!, policy: objc_AssociationPolicy) struct RunTimeViewKey { static let RunTimeViewID = UnsafeRawPointer.init(bitPattern: "RunTimeViewID".hashValue) } extension UIView { var ViewID: String? { set { objc_setAssociatedObject(self, RunTimeViewKey.RunTimeViewID!, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC) } get { return objc_getAssociatedObject(self, RunTimeViewKey.RunTimeViewID!) as? String } } }

13、Lock

无并发,不编码。而只要一说到多线程或者并发的代码,我们可能就很难绕开对于锁的讨论。简单来说,为了在不同线程中安全地访问同一个资源,我们需要这些访问顺序进行

OC方法

- (void)myMethod:(id)anObj { @synchronized(anObj) { // 在括号内持有 anObj 锁 } }

Swift方法

在Swift中去掉了synchronized方法,其实 @synchronized在幕后做的事情是调用了objc_sync 中的 objc_sync_enter和 objc_sync_exit方法,并且加入了一些异常判断。因此,在 Swift 中,如果我们忽略掉那些异常的话,我们想要 lock一个变量的话

//定义一个闭包 func synchronized(_ lock: AnyObject, closure: () -> ()) { objc_sync_enter(lock) closure() objc_sync_exit(lock) } func myMethodLocked(anObj: AnyObject!) { synchronized(anObj) { // 在括号内持有 anObj 锁 } }

举一个具体的使用例子,比如我们想要为某个类实现一个线程安全的 setter,可以这样进行重写

class Obj { var _str = "123" var str: String { get { return _str } set { synchronized(self) { _str = newValue } } // 下略 } }

14、性能方面

相比于 Objective-C,Swift 最大的改变就在于方法调用上的优化。

 OC方法调用

在 Objective-C 中,所有的对于 NSObject 的方法调用在编译时会被转为 objc_msgSend方法。这个方法运用 Objective-C 的运行时特性,使用派发的方式在运行时对方法进行查找。因为 Objective-C 的类型并不是编译时确定的,我们在代码中所写的类型不过只是向编译器的一种“建议”,不论对于怎样的方法,这种查找的代价基本都是同样的

这个过程的等效的表述可能类似这样 (注意这只是一种表述,与实际的代码和工作方式无关)

methodToCall = findMethodInClass(class, selector); // 这个查找一般需要遍历类的方法表,需要花费一定时间 methodToCall(); // 调用

Swift方法调用

Swift 因为使用了更安全和严格的类型,如果我们在编写代码中指明了某个实际的类型的话 (注意,需要的是实际具体的类型,而不是像 Any 这样的抽象的协议),我们就可以向编译器保证在运行时该对象一定属于被声明的类型因为有了更多更明确的类型信息,编译器就可以在类型中处理多态时建立虚函数表 (vtable),这是一个带有索引的保存了方法所在位置的数组。在方法调用时,与原来动态派发和查找方法不同,现在只需要通过索引就可以直接拿到方法并进行调用了,这是实实在在的性能提升。这个过程大概相当于:

let methodToCall = class.vtable[methodIndex] // 直接使用 methodIndex 获取实现 methodToCall(); // 调用

更进一步,在确定的情况下,编译器对 Swift 的优化甚至可以做到将某些方法调用优化为 inline 的形式。比如在某个方法被 final 标记时,由于不存在被重写的可能,vtable 中该方法的实现就完全固定了。对于这样的方法,编译器在合适的情况下可以在生成代码的阶段就将方法内容提取到调用的地方,从而完全避免调用。

总结

1、文章是读王巍 (onevcat). “Swifter - Swift 必备 Tips (第四版)总结所得2、文章中代码的demo地址

 

 

 

 

 

最新回复(0)