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的NSMutableArray
和NSMutableDictionary
。
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
的作用,并且可以用在struct
或enum
类型上。
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有两个问题:
- 属性必须要有
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
}
}
}
- 对于非
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
网友评论