美文网首页
Swift Tips:二、从Objective-C到Swift

Swift Tips:二、从Objective-C到Swift

作者: 囧书 | 来源:发表于2017-12-05 15:47 被阅读61次

    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

    相关文章

      网友评论

          本文标题:Swift Tips:二、从Objective-C到Swift

          本文链接:https://www.haomeiwen.com/subject/sajcixtx.html