Flutter有局限,拥抱Swift!优酷Mac迁移Swift实践

1567次阅读  |  发布于4年以前

Flutter有局限,拥抱Swift!优酷Mac迁移Swift实践

不管从“明里”还是“暗里”来看,苹果都在大力推荐使用 Swift 这一门语言。作为苹果的“亲儿子”,相信 Swift 语言将会是开发 MacOS 和 iOS 的第一选择。  一、背景
随着 Swift 5.0 的发布,Swift 的 ABI 终于稳定下来了。如果是很早就拥抱 Swift 的开发者,一定经历过各 Swift 大版本发布时的痛苦。回想在前一家公司将 Swift 2.2 升级到 Swift 3.0,基本上是换了个语言,两个版本之间的差异非常之大,升级起来简直是苦不堪言。

另外 ABI 的稳定也让 Swift 运行时环境可以随着苹果系统(iOS, Mac OS, Watch OS, TV OS)一起发布,不用再将 Swift 加入应用包,减小了包的体积。

所以,如果以前不使用 Swift 的原因之一,是 Swift 不稳定造成的开发成本过大,那随着 Swift 5.0 的发布,终于可以抛开这个顾虑了。

二、为什么迁移 Swift

当然,仅仅就 ABI 稳定这一个原因,肯定不足以说服我们将 Objective-C(以下简称 OC)迁移到 Swift,我们还得看看使用 Swift 能够给我们带来什么东西是 OC 没有的。

安全编程

Swift 是一门静态语言,虽然没有 OC 动态特性的灵活,但是这也让 Swift 更加安全。当然,静态语言非常多,为何 Swift 会特意强调“安全”这一特性?因为 Swift 在语言层面做了很多工作。

1) 可选值。可选值的引入明确了这样一个问题:这个值是否存在。这使得我们在处理某个值的时候,能够很清楚的知道是否应该去判断这个值的有无,避免了不必要的 crash 问题;

2) 值类型。Swift 中的 Struct 是一个值类型,它和引用类型的最大区别就是,将一个值类型赋给另外一个变量时,是通过值拷贝完成的(当然 Swift 用了 Copy-on-Write 的技术保证性能),我们就不用担心拷贝之后使用的安全问题,不用担心新变量的值修改之后会影响到原来的值;

3) 更多安全的关键字。guard让我们在执行接下来的代码前保证某一个条件的成立,并且使程序可读性更高;defer避免我们忘记在代码块执行完毕后所需要执行的清理工作。

编程范式的丰富

1) 支持函数式编程。函数作为 Swift 中的一等公民,Swift 可以支持函数式编程。我们可以使用函数式编程的无状态性,不可变性,无副作用这些特性写出更健壮的代码;

2) 面向协议编程。Swift 中也有协议 protocol,不过 Swift 中的 protocol 比起 OC 中的 protocol 强大太多。我们可以扩展协议给协议中的方法给一个默认实现。这样我们就可以在不改动已有类或 Struct 的前提下添加能力,非常的方便;

3) 强大泛型。泛型的引入可以让我们编写一些更加通用的代码,使代码更加灵活,可用性更高。

其它

如没有头文件减轻了复杂性,让代码量更少;利用元组(tuple)支持多返回值减少了一些不必要的模型等等。这些都使代码更简洁,更清晰。

三、迁移实战从哪里开始迁移?

Swift 和 OC 可以相互调用,但是由于 Swift 新增了一些新的数据结构,如 Enum、 Struct 等,因此 OC 调用 Swift 时有一定的局限性,需要做的一些额外的工作。反过来,当 Swift 调用 OC 时则容易得多。

一般来说,大多数 iOS 工程都有如下结构:

从上图可以看出,UI View Controller 和 UI View 都是在最上层,很少有其它模块依赖它们。即使有,也是其它模块的 UI View Controller 或 UI View 对其有依赖。因此,我们在迁移的时候可以从 UI View Controller 和 UI View 相关类进行迁移。这样的话,将这些类迁移为 Swift 后,可以顺利的调用 OC 相关的类和 API。

另外,从稳定性的角度来看,从上到下的迁移也更安全。如果我们从下层的一些中间件或基础库开始迁移的话,由于上层的大多数业务模块都对中间件或者基础库有依赖,我们对下层模块的修改就会影响多个业务模块,而且通常不知道这些下层模块到底被上层的哪些模块所调用。如果修改出现问题就会影响到非常多的模块,更糟的是,如果是一些使用频率比较少的业务场景对这些下层模块有依赖,那么可能在开发过程中很难发现问题,不知不觉的就带到线上,造成比较大的影响。

因此,如需要将 OC 迁移到 Swift,建议按照“从上到下”的原则进行迁移。这样既保证了工作量不会太大(不用去写一些 OC 调用 Swift 的适配代码)也保证了迁移后的稳定性,测试只需回归一下迁移过的业务模块即可,还可以快速定位问题。
如何使用 Swift 的值类型
我们都知道 Swift 引入了两个重要的数据结构 Struct 和 Enum,这两个都是值类型。值类型我们都知道是非常安全的,例如下面这段代码:

var a = 1
var b = a
b = 2

我们可以随意更改 b 的值而不用担心 a 的值会受任何影响,因为值类型的赋值都是通过拷贝来进行的(并使用 Copy-on-Write 的技术来保证性能)。另外,值类型相较于引用类型来说,减少了堆上的内存分配和回收次数。理论上来说,如果能用 Struct 类型就尽量使用 Struct 类型。

@interface Video: NSObject

@property (nonatomic, copy) NSString *videoId;
@property (nonatomic, copy) NSString *videoTitle;
@property (nonatomic, copy) NSString *videoSubtitle;

@end

例如我们有一个 Model 叫 Video,然后我们用 Struct 可以写成这样:

struct Video {
    let videoId: String
    let videoTitle: String
    let videoSubtitle: String
}

这样改写不仅将我们的 Model 改成更加安全的值类型 Struct,还利用了 let 关键字将里面的属性改为不可变的,使代码更加安全。

哪些又需要改成 Enum 类型呢?Enum 类型特别适合那种有明显种类区别的场景。例如以下代码:

typedef NS_ENUM(NSUInteger, TradeType) {
    TradeTypeVip = 0,
    TradeTypeSingleVideo
};

@interface TradeManager: NSObject

- (void)buyWithType:(TradeType)type;

@end

比如我们的支付场景分为购买 VIP 会员 ,购买单片。在 OC 中我们会定义一个 enum 来区分不同的购买类型,然后通过TradeManager相关 API 来进行购买。

[[TradeManager sharedManager] buyWithType:TradeTypeVip]

而在 Swift 中我们可以把它改成这样:

enum Trade {
    case VIP(userId: String)
    case singleVideo(videoID: String)

    func buy() {
        switch self {
        case .VIP(let userId):
            // buy vip with user Id

        case .singleVideo(let videoId):
            // buy single video with video id
        }
    }
}

我们把购买的逻辑全部放到 enum 中,利用 enum 的关联值来进行相关参数的传递,而且 Swift 中的 enum 类型可以添加方法,所以我们可以把购买的业务逻辑都放到一起,通过 switch 来进行判断。

然后我们就可以这样使用:

Trade.VIP(userId: "349951").buy()

非常的简洁明了。

混编问题

虽然我们可以按照“从上到下”的原则开始迁移,但是即使是这样也免不了需要 OC 去调用 Swift 的代码,有一些地方还是得处理一下。

我们都知道,OC 是一门动态语言,所有对象都是基于运行时的。而 Swift 则是一门静态语言,除了某一些特性可能需要在运行时完成(如反射),绝大部分的工作都是在编译时就确定了的(例如 Swift 类型的成员变量或方法)。我们来看一段代码:

// method in OC file
- (void)someMethod {
    A *a = [A new];
    [a doWork];
}

// class in A.swift
class A: NSObject {
    func doWork() {
        // do something
    }
}

上面的代码中,能编译通过吗?

当然不能,因为 Swift 的类型缺少一些运行时所需要信息,会导致失败,编译器会报出No visible @interface for 'CMSFilterViewController' declares the selector ‘reloadWith:channelName:'的错误。解决方法也很简单,在所需要使用到的前添加@objc即可。

需要注意的是,Swift 的 class 类型必须继承 NSObject 才能被 OC 所调用。

// class in A.swift
class A: NSObject {
    @objc func doWork() {
        // do something
    }
}

这样,OC 就能够找到 Swift 类型中相应的方法 (属性同理)。

必须说明的一点是,标记为 @objc 并不意味着这个方法就是动态派发的,它依然是静态调用。如果想要运行时相关的特性,必须使用 dynamic 关键字,这里不再赘述。
在 Swift 那些消失的东西
在迁移到 Swift 的过程中,我们会发现某些代码并不能在 Swift 中找到对应类或者方法来处理,下面是一些典型的例子。
 1) @synchronized
在性能要求不是太高的情况下,我们通常会使用@synchronized来为一个对象加上锁,而 Swift 已经没有相关的关键字了,所以我们需要做一些额外的工作。

@synchronized本质上来讲是一个互斥锁,背后其实是调用了 objc_sync_enterobjc_sync_exit 方法来实现的,所以,我们可以自己写一个类似的方法:

func synchronized(_ lock: AnyObject, block: () -> Void) { 
 objc_sync_enter(lock)
 block()
 objc_sync_exit(lock)
}

在使用时我们利用 Swift 的 Trailing Closure 可以写出类似 OC 的代码,非常的优美:

func addObject(obj: AnyObject) {
 synchronized(self) { 
  // do something
 }
}

 2) 单例

在 OC 中,我们的单例基本都是这样写的:

+ (instancetype)sharedInstance {
    static id sharedInstance = nil;
    static dispatch_once_t onceToken = 0;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

而在 Swift 中,我们直接定义一个静态常量就可以定义一个单例:

static let shared = YourObject()

不仅代码量更少,意义也更加明确。
 3) dispatch_once
就像上面 OC 代码那样,一般是使用 dispatch_once 来实现一个单例。但是 Swift 中已经没有 dispatch_once 这个方法了,那如果非要要使用的话应该怎么办呢?我们可以这样定义:

public extension DispatchQueue {

    private static var _onceTokens = [String]()

    public class func once(token: String, block:()->Void) {
        objc_sync_enter(self)
        defer { objc_sync_exit(self) }

        if _onceTokens.contains(token) {
            return
        }

        _onceTokens.append(token)
        block()
    }
}

我们利用 Swift 的extension给 DispatchQueue 添加一个类方法,然后可以这样使用:

DispatchQueue.once(token: "oncetoken") {``  // do something``}

当然,OC 和 Swift 区别远远不止于此,包括 Swift 对 C 的调用,日志的打印等等都有很多可以深究的点,限于篇幅原因就不再赘述。
四、计划和展望
目前优酷 Mac 端还在继续迁移中,一方面需要进行正常的业务迭代,并不能投入太多的人力专门进行迁移,目前的做法是新的需求使用 Swift 进行开发,如果有依赖到原来 OC 的相关模块,根据工作量来进行一部分的迁移;另一方面就是考虑到项目的稳定性,也不会直接把所有 OC 代码迁移到 Swift 上,逐步迁移也方便测试人员进行针对性的回归。

Swift 所带来的肯定不只是语言层面的这些优点。WWDC 2019 年发布的 Swift UI 不仅可以使用更加简洁的语法来进行 UI 开发,最重要的是可以使用同一个 UI 组件库来开发 Mac OS 和 iOS 上的界面,让一套代码在 Mac 和 iOS 设备上运行提供了可能性。

另外,Swift 作为一个跨平台语言不只是在苹果相关的平台上运行,目前 Swift 还支持 Linux 系统,我们可以在 Linux 系统上将 Swift 作为开发语言进行开发。目前已经有跨 Android 和 iOS 的 UI 库 SCADE,可以让我们同一套代码来开发 Android 和 iOS 的界面。

可能也有人会问,跨平台现在有了 Flutter,我们还学习 Swift 干嘛呢?确实 Flutter 作为一个非常优秀的跨平台方案,它有着优秀的渲染性能,并且支持非常多的平台(iOS、Android、Mac  OS 甚至是 Windows)。但是我们也知道,Flutter 是一个 UI 组件库,它可以帮助我们解决一部分 UI 问题,但是再往下呢?还是得使用 OC 或者 Swift。有人也会说直接用 OC 不就好了。可是我们可以看到一个现象,现在苹果的官方文档上面,基本上都是使用 Swift 来编写相关的代码示例,苹果也是在慢慢的“抛弃”OC 这一门语言。

不管从“明里”还是“暗里”来看,苹果都是在大力推荐使用 Swift 这一门语言。作为苹果的“亲儿子”,相信 Swift 语言将会是开发 MacOS 和 iOS 的第一选择。

所以,如果有人问我什么时候可以开始学习 Swift,那我的答案是:现在。

作者丨阿里文娱高级无线开发工程师 大斗
来源 | InfoQ

Copyright© 2013-2019

京ICP备2023019179号-2