Swift Tips:二、从Objective-C到Swift

Selector

selector是Objective-C runtime的概念,在调用一个selector前,要求selector方法加上@objc修饰。

实例方法的动态调用

我们可以做到在运行时才决定对哪个实例调用哪个方法。

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

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

条件编译

Swift依然可以使用条件编译。
Swift内建了几种平台和架构的组合,来帮助我们为不同的平台编译不同的代码:

方法 可选参数
ox OSX, iOS
arch() x86_64, arm, arm64, i386
#if os(OSX)
    typealias Color = NSColor
#else
    typealias Color = UIColor
#endif

也可以对自定义的符号进行条件编译,比如定义一个免费版本标记FREE_VERSION:

#if ok
    print("免费版本")
#else
    print("收费版本")
#endif

为了使之有效,还需要在项目的编译选项中进行设置,在项目的Build Settings中,找到Swift Compiler - Custom Flags,并在其中的 Other Swift Flags 加上 -D FREE_VERSION 就可以了。

编译标记

Xcode将在导航栏显示出编译标记

// MARK: 你的标记
// MARK: -
// TODO:
// FIXME:

@objc和dynamic

添加@objc修饰符并不意味着这个方法或者属性会变成动态派发,Swift依然可能会将其优化为静态调用。如果你确实需要动态调用的特性,就加上dynamic

weak 和 unowned

如果您是一直写 Objective-C 过来的,那么从表面的行为上来说 unowned 更像以前的 unsafe_unretained,而 weak 就是以前的 weak。用通俗的话说,就是 unowned 设置以后即使它原来引用的内容已经被释放了,它仍然会保持对被已经释放了的对象的一个 "无效的" 引用,它不能是 Optional 值,也不会被指向 nil。如果你尝试调用这个引用的方法或者访问成员属性的话,程序就会崩溃。而 weak 则友好一些,在引用的内容被释放后,标记为 weak 的成员将会自动地变成 nil (因此被标记为 weak 的变量一定需要是 Optional 值)。
关于两者使用的选择,Apple 给我们的建议是如果能够确定在访问时不会已被释放的话,尽量使用unowned,如果存在被释放的可能,那就选择用 weak。

值类型和引用类型

Swift 的值类型,特别是数组和字典这样的容器,在内存管理上经过了精心的设计。值类型的一个特点是在传递和赋值时进行复制,每次复制肯定会产生额外开销,但是在 Swift 中这个消耗被控制在了最小范围内,在没有必要复制的时候,值类型的复制都是不会发生的。也就是说,简单的赋值,参数的传递等等普通操作,虽然我们可能用不同的名字来回设置和传递值类型,但是在内存上它们都是同一块内容。
值类型被复制的时机是值类型的内容发生改变时。
如果确实需要引用类型的容器,可以使用Cocoa的NSMutableArrayNSMutableDictionary

UnsafePointer

为了与庞大的 C 系帝国进行合作,Swift 定义了一套对 C 语言指针的访问和转换方法,那就是 UnsafePointer 和它的一系列变体。
对于使用 C API 时如果遇到接受内存地址作为参数,或者返回是内存地址的情况,在 Swift 里会将它们转为 UnsafePointer<Type> 的类型,比如说如果某个 API 在 C 中是这样的话:

void method(const int *num) {
    printf("%d",*num);
}

其对应的 Swift 方法应该是:

func method(num: UnsafePointer<CInt>) {
    print(num.memory)
}

对于其他的 C 中基础类型,在 Swift 中对应的类型都遵循统一的命名规则:在前面加上一个字母 C 并将原来的第一个字母大写:比如 int,bool 和 char 的对应类型分别是 CInt,CBool 和 CChar。在上面的 C 方法中,我们接受一个 int 的指针,转换到 Swift 里所对应的就是一个 CInt 的 UnsafePointer 类型。

获取对象类型

使用type(of:)

let str = "hello"
let t = type(of: str)
debugPrint(t)
// Swift.String

自省

向一个对象发出询问,以确定它是不是属于某个类,这种操作就称为自省。
在以前的Objective-C项目中,我们用

[obj1 isKindOfClass: [ClassA class]];
[obj2 isMemberOfClass: [ClassB class]];

-isKindOfClass:判断obj1是否是ClassA或者其子类的实例对象;

-isMemberOfClass:判断obj2是否就是ClassB的实例。

在Swift中,用关键字is就可以起到isKindOfClass的作用,并且可以用在structenum类型上。

class A {
    
}

class A1: A {
    
}

class B: NSObject {
    
}

class B1: B {
    
}

enum E1 {
    case ok
}

let aaa = A1()
print(aaa is A)

let bbb = B1()
print(bbb is B)

let eee = E1.ok
print(eee is E1)

KVO

在Swift中我们也是可以使用KVO的,但是仅限于在NSObject的子类中。这是可以理解的,因为KVO是基于KVC(Key-Value Coding)以及动态派发技术实现的,而这些东西都是Objective-C运行时的概念。

由于Swift为了效率,默认禁用了动态派发,我们想要让KVO正常工作,需要在被观测属性前加dynamic,在 Swift 4 后,需要同时加@objc dynamic

举个栗子:

class MyClass: NSObject {
    @objc dynamic var date = Date()
}

private var myContext = 0

class ViewController: UIViewController {
    
    var myObject: MyClass!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        myObject = MyClass()
        print("当前日期:\(myObject.date)")
        
        myObject.addObserver(self, forKeyPath: "date", options: .new, context: &myContext)
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
            print("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 date = change[.newKey] {
                print("日期发生变化:\(date)")
            }
        }
    }
}

打印结果:

当前日期:2017-12-04 06:18:52 +0000
3秒后
日期发生变化:2017-12-04 06:18:55 +0000

在Swift中使用KVO有两个问题:

  1. 属性必须要有dynamic才能被观察,而有的类我们可能无法修改其源码。这种情况下,一个可能可行的方案是继承这个类,并将需要观察的属性使用dynamic进行重写。
class MyClass: NSObject {
    var date = Date()
}

class MyChildClass: MyClass {
    @objc dynamic override var date: Date {
        get {
            return super.date
        }
        set {
            super.date = newValue
        }
    }
}
  1. 对于非NSObject的类型,Swift暂时还没有类似KVO的观察机制。我们可能只能通过属性观察来实现一套自己的类似替代了。

局部scope

在Objective-C中,我们有时会在方法内使用一对大括号来创创建临时的作用域,以此分隔不相关联的代码,这在手写视图布局时特别有用。
但是在Swift中,直接写大括号与闭包的定义冲突,这时可以定义一个接受() -> ()作为参数的全局方法,然后执行它:

func local(closure: () -> ()) {
    closure()
}

class ViewController: UIViewController {
    override func loadView() {
        local {
            // ...
        }
        
        local {
            // ...
        }
    }
}

在Swift 2.0 中,为了处理异常,Apple加入了do这个关键字来作为捕获异常的作用域。这一功能恰好为我们提供了一个完美的局部作用域,现在我们可以简单地使用do来分隔代码了:

class ViewController: UIViewController {
    override func loadView() {
        do {
            // ...
        }
        
        do {
            // ...
        }
    }
}

判等

对字符串的内容判等,我们可以简单地使用==操作符来进行。
Equatable里声明了这个操作符的接口方法:

public protocol Equatable {
    public static func ==(lhs: Self, rhs: Self) -> Bool
}

实现了Equatable的类型就可以使用==以及!=来进行相等判定了。!=由标准库自动取反实现。

Swift的基本类型都重载了自己对应版本的==,而对于NSObject的子类来说,如果我们使用 == 并且没有对于这个子类的重载的话,将转为调用这个类的-isEqual:方法,如果子类没有实现-isEqual:方法,则会使用NSObject的实现,直接比较对象的内存地址。

如果要进行对象指针的判定,在Swift中是使用另一个操作符===

Swizzle

Swizzle是Objective-C运行时的黑魔法之一。
在Swift中也可以使用它,前提要把方法声明为@objc
比如,置换UIButton的事件发送方法,以统计全局点击:

extension UIButton {
    
    class func jx_swizzleSendAction() {
        let cls: AnyClass! = UIButton.self
        let originalSelector = #selector(sendAction(_:to:for:))
        let swizzledSelector = #selector(jx_sendAction(_:to:for:))
        
        let originalMethod = class_getInstanceMethod(cls, originalSelector)
        let swizzledMethod = class_getInstanceMethod(cls, swizzledSelector)
        
        method_exchangeImplementations(originalMethod!, swizzledMethod!)
    }
    
    @objc func jx_sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) {
        print("swizzle tap!")
        jx_sendAction(action, to: target, for: event)
    }
}

jx_swizzleSendAction是我们定义用来设置方法置换的代码,由于新版本Swift已经不能重写+load+initialize方法了,所以我们只好在一个比较早的时机手动调用它,让置换生效。

补充一点,可能你会疑惑为什么jx_sendAction方法中又调用了jx_sendAction,这不就死循环了吗?
不会的。在A和B方法发生置换以后,你可以想象成两个方法的方法名与方法体已经被相互交换了,当调用A方法时,实际执行的是B的实现。

输出格式化

如果想使用像%.2f这样的方式取得格式化字符串,可以这样:

let n = 1.23456789
let format = String.init(format: "%0.2f", n)
print(format)
// 1.23

数组enumerate

在Swift中,可以用for循环配合enumerated()取代OC的enumerateObjectsUsingBlock了。

let arr = [1, 2, 4, 5]
var result = 0
for (idx, num) in arr.enumerated() {
    result += num
    if idx == 2 {
        break
    }
}

sizeof和sizeofValue

Swift3以后,sizeof功能由MemoryLayout类封装。
求类型所占内存大小,使用它的计算属性size

let stringSize = MemoryLayout<String>.size
print("string size: \(stringSize)")
print("Uint16 size: \(MemoryLayout<UInt16>.size)")
// string size: 24
// Uint16 size: 2

求值(变量)的所占内存大小,用size(ofValue value: T) -> Int方法

let numArray: [UInt16] = [1, 2, 3, 4, 5]
print("numArray size: \(MemoryLayout.size(ofValue: numArray))")
// numArray size: 8

上例的numArray被sizeofValue后,得到结果为8,这其实是64位系统一个引用的长度。由此可见sizeofValue所返回的是这个值实际的大小,而非其意义内容的大小。
以下对枚举做个测试,可体会一下:

enum MyEnum: UInt16 {
    case A = 0
    case B = 65535
}
print("MyEnum size: \(MemoryLayout<MyEnum>.size)")
print("MyEnum.A size: \(MemoryLayout.size(ofValue: MyEnum.A))")
print("MyEnum.B size: \(MemoryLayout.size(ofValue: MyEnum.B))")
print("MyEnum.A.rawValue size: \(MemoryLayout.size(ofValue: MyEnum.A.rawValue))")
// MyEnum size: 1
// MyEnum.A size: 1
// MyEnum.B size: 1
// MyEnum.A.rawValue size: 2

delegate

Cocoa 开发中接口-委托 (protocol-delegate) 模式是一种常用的设计模式,它贯穿于整个 Cocoa 框架中,为代码之间的关系清理和解耦合做出了不可磨灭的贡献。

一般我们希望delegate引用是weak的,但在Swift中如果直接这么写的话,编译器会报错:

protocol MyDelegate {
    func method()
}

class AClass {
    weak var delegate: MyDelegate?
}
// 'weak' may only be applied to class and class-bound protocol types, not 'MyDelegate'

这是因为Swift的protocol除了可以被class遵守外,还可以被struct或enum这样的非class遵守的,它本身不通过引用计数来管理内存,所以也不能用weak修饰。

想要在Swift中使用weak delegate,我们就需要将protocol限制在class内,在声明后加上class关键字以限制:

protocol MyDelegate: class {
    func method()
}

class AClass {
    weak var delegate: MyDelegate?
}

Associated Object

Swift仍然不能通过Category向已有类添加成员变量,但我们还是可以使用OC运行时,将一个对象关联到已有的要扩展的对象上。

class MyClass: NSObject {
}

private var key: Void?

extension MyClass {
    var title: String? {
        get {
            return objc_getAssociatedObject(self, &key) as? String
        }
        set {
            objc_setAssociatedObject(self, &key, newValue, .OBJC_ASSOCIATION_RETAIN)
        }
    }
}

这样title在使用起来就像普通的属性一样。

synchronized

现版本的Swift是没有@synchronized的,如果我们想保护一个对象在某个作用域内不被其它线程改变,可以这么做:

var anObject: Any = ""
objc_sync_enter(anObject)

// 在 enter 和 exit 之间,anObject 不会被其它线程改变

objc_sync_exit(anObject)

当然,也可以写个全局方法,封装起来,这样就和以前的@synchronized的很像了:

func synchronized(_ object: Any, closure: () -> ()) {
    objc_sync_enter(object)
    closure()
    objc_sync_exit(object)
}

synchronized(anObject) {
    // 在括号内,anObject不会被其它线程改变
}

参考

  • Swifter - 100个 Swift 必备 Tips
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271