【AICon】开辟产业应用新天地,大模型重塑各行各业,精华内容上线58%!>>> 了解详情
写点什么

拥抱 Swift!优酷 Mac 迁移 Swift 实践

  • 2020-05-08
  • 本文字数:4773 字

    阅读完需:约 16 分钟

拥抱Swift!优酷Mac迁移Swift实践

一、背景

随着 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 没有的。


  1. 安全编程


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


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


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


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


  1. 编程范式的丰富


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


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


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


  1. 其它


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

三、迁移实战

  1. 从哪里开始迁移?


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


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



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


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


因此,如需要将 OC 迁移到 Swift,建议按照“从上到下”的原则进行迁移。这样既保证了工作量不会太大(不用去写一些 OC 调用 Swift 的适配代码)也保证了迁移后的稳定性,测试


只需回归一下迁移过的业务模块即可,还可以快速定位问题。


  1. 如何使用 Swift 的值类型


我们都知道 Swift 引入了两个重要的数据结构 Struct 和 Enum,这两个都是值类型。值类型我们都知道是非常安全的,例如下面这段代码:


var a = 1var b = ab = 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()
复制代码


非常的简洁明了。


  1. 混编问题


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


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


// method in OC file- (void)someMethod {   A *a = [A new];   [a doWork];}
// class in A.swiftclass 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.swiftclass A: NSObject {   @objc func doWork() {      // do something   }}
复制代码


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


必须说明的一点是,标记为 @objc 并不意味着这个方法就是动态派发的,它依然是静态调用。如果想要运行时相关的特性,必须使用 dynamic 关键字,这里不再赘述。


  1. 在 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 = <a href="">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() }}</a href="">
复制代码


我们利用 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,那我的答案是:现在。


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


2020-05-08 17:571318

评论

发布
暂无评论
发现更多内容

redis 在微服务领域的贡献,java制作微信小程序教程

Java 程序员 后端

Redis面试题汇总,mysql调优面试题

Java 程序员 后端

RocketMQ 5,学习linux系统管理

Java 程序员 后端

Servlet的Cookie和Session机制,面试谈谈对springboot的理解

Java 程序员 后端

RabbitMQ 可靠性、重复消费、顺序性,突围金九银十面试季

Java 程序员 后端

Redis该怎么学?其实很简单,这份学习路线,mybatis架构梳理

Java 程序员 后端

Redis:看完就比常人多会三种类型实战,可以拿去炫耀了

Java 程序员 后端

set集合,挑战华为社招

Java 程序员 后端

Redis高可用篇:Cluster集群能支持的数据量有多大?,再不了解你就out啦

Java 程序员 后端

Set集合无法去重相同内容的父类对象和子类对象的问题解决

Java 程序员 后端

Socket和ServerSocket的简单介绍及例子,mongodb教程导入外部数据

Java 程序员 后端

Redis-生产架构选型解决方案,java开发架构师

Java 程序员 后端

Redis到底能干什么?又不能干什么呢?,kettle面试题

Java 程序员 后端

SCA Sentinel 分布式系统的流量防卫兵,春招我借这份PDF的复习思路

Java 程序员 后端

SonarQube检测出的bug、漏洞以及异味的修复整理,mysql基础知识

Java 程序员 后端

RabbitMQ实现即时通讯居然如此简单!后端代码都省得写了

Java 程序员 后端

Redis-用的很溜,了解过它用的什么协议吗?,3天拿到网易Java岗offer

Java 程序员 后端

Redis哨兵原理,我忍你很久了!,java面试视频百度云

Java 程序员 后端

Redis总结,学Java必看书籍

Java 程序员 后端

RestFul API 统一格式返回 + 全局异常处理,linux系统编程视频教程

Java 程序员 后端

Sleuth服务跟踪大厂高频面试题:整合-Zipkin,java面向对象程序开发及实战答案

Java 程序员 后端

Spring Boot 谷粒学院、谷粒商城项目问题汇总,springboot源码视频

Java 程序员 后端

Redis不只是get set,八种数据类型及应用场景分析,java技术栈面试题

Java 程序员 后端

Redis分布式基石——主从复制技术详述,Java黑科技实现原理揭秘

Java 程序员 后端

Serverless Devs 的官网是如何通过 Serverless Devs 部署的

Java 程序员 后端

SonarQube,SonarLint检测代码修复问题汇总归纳,2021京东最新Java面试真题解析

Java 程序员 后端

pageHelper----Mybaits分页插件,mysql架构设计器没有显示

Java 程序员 后端

Redis 千万不要乱用KEYS命令,不然会挨打的,面试必问

Java 程序员 后端

Redis入门到五大类型实现,java基础知识点大全

Java 程序员 后端

Redis精通系列——LRU算法详述(Least Recently Used - 最近最少使用)

Java 程序员 后端

seata-golang 一周年回顾,java面试准备内容

Java 程序员 后端

拥抱Swift!优酷Mac迁移Swift实践_文化 & 方法_阿里巴巴文娱技术_InfoQ精选文章