作者:王浙剑(柘剑)
手淘新版商品评估列表在经验一个半月的技术重构,几个月的迭代和放量,最终在 2021 年的双十一上,以 100% 的流量稳固的跑完了整个过程。咱们不仅在业务上有了比拟明确的晋升,同时还积淀了不少技术摸索,比方积淀基于 DinamicX + 事件链编排的轻模式研发框架、推动原生语言升级成 Swift/Kotlin,最终使得整体研发效率和稳定性有一个比拟大的晋升。(注:DinamicX 为外部自研动态化 UI 框架)
这篇文章,我会重点议论对于 Swift 的局部。如果你想理解对于 Swift 如何晋升研发效率 / 品质、现有我的项目 / 模块是否须要 Swift 作为原生语言如何选型、在商品评估落地 Swift 过程中咱们遇到了哪些问题以及最初有哪些收益和论断的一些问题,心愿这篇文章能够给你带来一些帮忙。
首先是,我为什么会抉择学习 Swift?
技术改革,将来已来
因为,我心田非常动摇,相比拟于 OC,Swift 更能承载将来。
刚强后盾
最次要的起因就是它有一个 刚强的后盾,Swift 作为 Apple 将来最重要的开发语言,光对外输入的 WWDC 内容就曾经高达 73 个,包含但不限于语法、设计、性能、开发工具链等,具体内容如图所示:
回过头来看 Swift 这几年的倒退,从 2014 年开始正式对外公布,到当初曾经经验了 7 个年头了,在整个过程中,Apple 投入了大量精力建设 Swift,尤其是 Swift Only 框架的呈现,也意味着 Apple 正在踊跃提倡各位投入到 Swift 开发中来。
三大劣势
其次,Swift 有三个比拟明确的劣势: 更快、更平安且更具备表白性。
更快 是指 Swift 在执行效率上做了很多优化。比方,Swift 零碎库自身就采纳了很多不须要援用计数的根底类型,无论是内存调配大小、援用计数损耗、办法派发动态剖析等方面的问题都失去了一个无效的晋升。具体细节这里就不开展剖析,感兴趣的能够移步 Understanding Swift Performance 理解细节。
所谓的 平安 不等于不产生 Crash,而是指任何的输出都有一个比拟明确的体现定义。Swift 设计初衷是心愿开发者无需任何不平安的数据结构就能编写代码,因而 Swift 领有一个非常强壮的类型零碎,开发者简直不须要思考指针的问题,就能实现所有的开发工作。同时还提供了一系列前缀为 Unsafe 的类型或函数,用于与不平安语言(例如 C 语言)的高性能交互、操作原始内存等绝对不平安的操作,一方面以 Unsafe 警觉开发者应用这些 API,另外一方面是辨别类型以保障大部分开发场景应用的都是平安的类型。
这里能够分享一个数据,我之前参加的一个 App 我的项目,是用 Pure Swift 编写的(99%+),咱们的线上 crash 率长年继续在十万分之 8 左右,这对于一个小型团队(单端 4 人)来说,是一个非常可观的后果。咱们简直不应用 Unsafe 的 API,使得咱们的大部分问题都能在编译期间防止,可选类型及可选绑定的设计强制开发者须要去思考如何解决值为空的场景,使得在软件公布之前就把开发者的谬误扼杀在萌芽之中。
更具备表白性 简略点说就是用更少的代码来表白一段残缺的逻辑。在 Swift Evolution 我的项目中曾经有 330 个提案来加强 Swift 的表白性,也得益于这些个性,使得 Swift 的代码量比 OC 少了大略 30% – 50% 左右。咱们举几个理论例子
Builder Pattern
当咱们定义了一个有很多属性的简单 model 时,咱们不心愿这个 model 的属性在初始化实现后能够被变更。咱们就须要通过 builder 模式来解决,代码如下:
// OCDemoModelBuilder.h
@interface OCDemoModelBuilder : NSObject
@property (nonatomic, copy, nonnull) NSString *a;
@property (nonatomic, copy, nonnull) NSString *b;
@property (nonatomic, copy, nonnull) NSString *c;
@property (nonatomic, copy, nonnull) NSString *d;
@property (nonatomic, copy, nonnull) NSString *e;
@property (nonatomic, copy, nonnull) NSString *f;
@property (nonatomic, copy, nonnull) NSString *g;
@property (nonatomic, copy, nonnull) NSString *h;
@end
// OCDemoModelBuilder.m
@implementation OCDemoModelBuilder
- (instancetype)init {if (self = [super init]) {
_a = @"a";
_b = @"b";
_c = @"c";
_d = @"d";
_e = @"e";
_f = @"f";
_g = @"g";
_h = @"h";
}
return self;
}
@end
// OCDemoModel.h
@interface OCDemoModel : NSObject
@property (nonatomic, readonly, nonnull) NSString *a;
@property (nonatomic, readonly, nonnull) NSString *b;
@property (nonatomic, readonly, nonnull) NSString *c;
@property (nonatomic, readonly, nonnull) NSString *d;
@property (nonatomic, readonly, nonnull) NSString *e;
@property (nonatomic, readonly, nonnull) NSString *f;
@property (nonatomic, readonly, nonnull) NSString *g;
@property (nonatomic, readonly, nonnull) NSString *h;
- (instancetype)initWithBuilder:(void(^)(OCDemoModelBuilder *builder))builderBlock;
@end
// OCDemoModel.m
@implementation OCDemoModel
- (instancetype)initWithBuilder:(void(^)(OCDemoModelBuilder *builder))builderBlock {if (self = [super init]) {OCDemoModelBuilder * builder = [[OCDemoModelBuilder alloc] init];
if (builderBlock) {builderBlock(builder);
}
_a = builder.a;
_b = builder.b;
_c = builder.c;
_d = builder.d;
_e = builder.e;
_f = builder.f;
_g = builder.g;
_h = builder.h;
}
return self;
}
@end
// Usage
OCDemoModel *ret = [[OCDemoModel alloc] initWithBuilder:^(OCDemoModelBuilder * _Nonnull builder) {builder.b = @"b1";}];
// ret = a,b1,c,d,e,f,g
然而 Swift 的 Struct 反对属性默认值和初始化结构器,使得 builder pattern 意义并不是很大,代码如下:
struct SwiftDemoModel {
var a = "a"
var b = "b"
var c = "c"
var d = "d"
var e = "e"
var f = "f"
var g = "g"
var h = "h"
}
// Usage
let ret = SwiftDemoModel(b: "b1")
// ret = a,b1,c,d,e,f,g
State Pattern
当一个函数的执行后果可能存在多种不同的状态时,咱们通常会采纳状态模式来解决问题。
例如咱们定义一个函数执行后果可能存在 finish\failure\none 三种状态,因为存在一些关联值,咱们不能应用枚举来解决。须要定义三个不同的具体类型,具体代码如下所示:
/// Executable.h
@protocol Executable <NSObject>
- (nullable NSDictionary *)toFormattedData;
@end
/// OCDemoExecutedResult.h
@interface OCDemoExecutedResult: NSObject<Executable>
/// 结构一个空返回值
+ (OCDemoNoneResult *)none;
/// 结构胜利返回值
+ (OCDemoFinishedResult *)finishedWithData:(nullable NSDictionary *)data
type:(nullable NSString *)type;
/// 结构一个谬误返回值
+ (OCDemoFailureResult *)failureWithErrorCode:(nonnull NSString *)errorCode
errorMsg:(nonnull NSString *)errorMsg
userInfo:(nullable NSDictionary *)userInfo;
@end
/// OCDemoExecutedResult.m
@implementation OCDemoExecutedResult
/// 结构一个空返回值
+ (OCDemoNoneResult *)none {return [OCDemoNoneResult new];
}
+ (OCDemoFinishedResult *)finishedWithData:(nullable NSDictionary *)data type:(nullable NSString *)type {return [[OCDemoFinishedResult alloc] initWithData:data type:type];
}
+ (OCDemoFailureResult *)failureWithErrorCode:(nonnull NSString *)errorCode errorMsg:(nonnull NSString *)errorMsg userInfo:(nullable NSDictionary *)userInfo {return [[OCDemoFailureResult alloc] initWithErrorCode:errorCode errorMsg:errorMsg userInfo:userInfo];
}
- (nullable NSDictionary *)toFormattedData {return nil;}
@end
/// OCDemoNoneResult.h
@interface OCDemoNoneResult : OCDemoExecutedResult
@end
/// OCDemoNoneResult.m
@implementation OCDemoNoneResult
@end
/// OCDemoFinishedResult.h
@interface OCDemoFinishedResult: OCDemoExecutedResult
/// 类型
@property (nonatomic, copy, nonnull) NSString *type;
/// 关联值
@property (nonatomic, copy, nullable) NSDictionary *data;
/// 初始化办法
- (instancetype)initWithData:(nullable NSDictionary *)data type:(nullable NSString *)type;
@end
/// OCDemoFinishedResult.h
@implementation OCDemoFinishedResult
- (instancetype)initWithData:(nullable NSDictionary *)data type:(nullable NSString *)type {if (self = [super init]) {_data = [data copy];
_type = [(type ?:@"result") copy];
}
return self;
}
- (NSDictionary *)toFormattedData {
return @{
@"type": self.type,
@"data": self.data ?: [NSNull null]
};
}
@end
/// OCDemoFailureResult.h
@interface OCDemoFailureResult: OCDemoExecutedResult
/// 错误码
@property (nonatomic, copy, readonly, nonnull) NSString *errorCode;
/// 错误信息
@property (nonatomic, copy, readonly, nonnull) NSString *errorMsg;
/// 关联值
@property (nonatomic, copy, readonly, nullable) NSDictionary *userInfo;
/// 初始化办法
- (instancetype)initWithErrorCode:(NSString *)errorCode errorMsg:(NSString *)errorMsg userInfo:(nullable NSDictionary *)userInfo;
@end
/// OCDemoFailureResult.m
@implementation OCDemoFailureResult
- (OCDemoFailureResult *)initWithErrorCode:(NSString *)errorCode errorMsg:(NSString *)errorMsg userInfo:(nullable NSDictionary *)userInfo {if (self = [super init]) {_errorCode = [errorCode copy];
_errorMsg = [errorMsg copy];
_userInfo = [userInfo copy];
}
return self;
}
- (NSDictionary *)toFormattedData {
return @{
@"code": self.errorCode,
@"msg": self.errorMsg,
@"data": self.userInfo ?: [NSNull null]
};
}
@end
然而如果咱们应用 Swift 的 enum 个性,代码就会变的简洁很多很多:
public enum SwiftDemoExecutedResult {
/// 正确返回值
case finished(type: String, result: [String: Any]?)
/// 谬误返回值
case failure(errorCode: String, errorMsg: String, userInfo: [String: Any]?)
/// 空返回值
case none
/// 格式化
func toFormattedData() -> [String: Any]? {
switch self {case .finished(type: let type, result: let result):
var ret: [String: Any] = [:]
ret["type"] = type
ret["data"] = result
return ret
case .failure(errorCode: let errorCode, errorMsg: let errorMsg, userInfo: let userInfo):
var ret: [String: Any] = [:]
ret["code"] = errorCode
ret["msg"] = errorMsg
ret["data"] = userInfo
return ret
case .none:
return nil
}
}
}
Facade Pattern
当咱们定义一个入参须要合乎多个协定类型时,咱们通常会应用 Facade Pattern 来解决问题。
例如咱们有四个协定 JSONDecodable、JSONEncodable、XMLDecodable、XMLEncodable 以及一带有两个入参的办法,入参 1 为 json 要求同时满足 JSONDecodable、JSONEncodable 两个协定,入参 2 为 xml 同时满足 XMLDecodable、XMLEncodable。当咱们应用 OC 来解决问题时通常会这么写:
@protocol JSONDecodable <NSObject>
@end
@protocol JSONEncodable <NSObject>
@end
@protocol XMLDecodable <NSObject>
@end
@protocol XMLEncodable <NSObject>
@end
@protocol JSONCodable <JSONDecodable, JSONEncodable>
@end
@protocol XMLCodable <XMLDecodable, XMLEncodable>
@end
- (void)decodeJSON:(id<JSONCodable>)json xml:(id<XMLCodable>)xml {}
额定定义了两个协定 JSONCodable、XMLCodable 来解决这个问题。然而在 Swift 中咱们能够应用 & 来解决这个问题,不再须要定义额定的类型,代码如下:
protocol JSONDecodable {}
protocol JSONEncodable {}
protocol XMLDecodable {}
protocol XMLEncodable {}
func decode(json: JSONDecodable & JSONEncodable, xml: XMLDecodable & XMLEncodable) {}
以上是 Swift 在 更具备表白性 方面的一些内容,当然劣势也远不止这些,然而篇幅无限这里不再开展。
总而言之,得益于 Swift 的高表白性,使得开发者能够通过更少的代码能够表白一段残缺的逻辑,在肯定水平上缩小了开发成本,同时也升高了问题的产生。
势不可挡
Swift 除了有一个刚强的后盾以及三大劣势以外,这几年的发展趋势也比拟好。
首先依据 Githut 显示,Swift 语言在 Github 的活跃度(Pull request) 曾经超过了 OC 了,如下图所示:(数据截止至 2021/10/25)
同时,国内 Top 100 的 Swift 混编利用也有明显增加,从 19 年的 22% 曾经回升到了 59%:(数据截止至 2021/04/22)
这里的晋升,一方面是国内许多一线互联网公司都开始布局,另外一方面是 WidgetKit 等 Swift Only 的框架呈现也在促使大家开始建设 Swift 基础设施。
当然,国外数据更加亮眼,曾经达到了 91%,简直能够说是全副都曾经用上了,为什么这么说呢?因为美版前 100 中 Google 系有 8 个利用都没有应用上 Swift。
这里再和大家分享一个数据,在业余时间组织《WWDC 内参》作者招募的时候,咱们收集了作者的技术栈和趣味点,最终发现有超过一半的作者有比拟丰盛的 Swift 开发教训,还有 2/3 的人对 Swift 这个专题的内容比拟感兴趣(总共 180 人样本)。能够看得出社区对于 Swift 的激情还是非常高的,久远角度看,是否应用 Swift 进行开发也会成为大家抉择工作的起因之一。
为什么抉择商品评估列表?
兴许很多人在看到第一局部之后,会有一种我得马上在咱们我的项目中用上 Swift 的激动。为了防止你为你的“激动”买单,上面我分享一下「手淘商品评估列表」抉择 Swift 的心路历程。
先简略讲下本人来手淘的经验,起初我退出的是手淘基础架构组,次要工作职责之一就是建设 Swift 基础设施,然而起初因为组织须要,我退出到了一个新的业务架构组,工作重心也由原来的从 Swift 根底降级驱动业务,转变成业务试点驱动根底技术升级。在这个过程中,咱们次要经验了三次技术决策:
- 一. 团队最开始接手的我的项目:手淘订单协定降级为新奥创
- 二. 基于对业务研发的畛域了解,团队提出新的事件链编排能力,并与 DX 共建
- 三. 商品评估重构,包含评估列表、交互等
每个阶段我都有思考过我是否要应用 Swift,但最终前两次我都放弃了应用我本人比拟善于的 Swift,次要出于上面几点思考:
须要具备应用 Swift 的前提
订单新奥创我的项目之所以没有采纳 Swift 为次要开发语言,最大的问题就是过后的根本基础设施还不够齐备。依赖的大部分模块简直都不反对 Module,如果要硬上 Swift 简直是不可能的事件,会减少很多的工作量,对于一个工期较赶的我的项目来说,不是一个明智之举,衡量之下,临时放弃了应用 Swift 的念头。
什么样的业务更适宜应用 Swift 重构
在根本条件都很齐备的状况下,对于一个业务重构我的项目来说,Swift 会是一个更好的抉择。无论是大环境的趋势,还是 Swift 独有的劣势来说,曾经不太适宜持续应用 OC 去重构一个业务模块了。
对于想尝试 Swift 的大型项目来说,倡议能够优先思考包袱小、株连小的业务做试点。过后咱们在订单新奥创我的项目放弃应用 Swift 的另外一个重要起因就是因为奥创整体架构较为简单,搭建和数据混合在一起、部分改变老本过高会导致牵一发而动全身的问题,整体对端侧新技术交互的凋谢容纳无限。然而手淘商品评估就没有这类问题,能够抉择的空间比拟多,因而咱们就比拟动摇的抉择了 Swift 作为端侧次要开发语言。
既要就地取材、又要获取反对
当我的项目具备应用 Swift 的条件之后,肯定要联合本身团队现状进行综合思考。
首先,团队须要提前造就或者装备一位有 Swift 开发教训的人,来保障简单问题的攻坚以及代码品质的把控。尤其是代码品质,大部分最后从 OC 接触 Swift 的人,都会经验一段“不适”期,在这段期间,很容易写出「OC 味」的 Swift 代码,所以特地须要一位有激情、有相干教训和技术能力的人来实际并表率。
同时,咱们还须要取得主管的反对,这点很要害,光有技术酷爱很难把一件事件继续做上来。须要联合我的项目状况继续与主管放弃沟通,并且在交换过程中一直降级本人对一个技术的思考,让主管从最后的质疑到最初的反对,也是一个非常乏味的过程。
须要有肯定技术根底撑持
首先,在基础设施齐备性上,咱们做了一次大范畴的 Module 适配工作,解决了混编的外围问题。同时降级了 DevOps,将包管理工具 tpod 降级到了 1.9.1 反对了源码级别的动态库版本 framework 工程,同时还提供了 tpodedit 模式解决头文件依赖问题,以及在公布链路新增了一些外围卡口查看避免工程劣化。
其次,咱们基于手淘已有的技术计划,衡量性能与效率之类的问题之后,最终咱们联合对业务研发的痛点了解,发展基于事件链编排的研发模式降级摸索,并从老本上思考初期在 DX 外部共建、并输入到新奥创,整体架构如下所示:
在 UI 层,咱们应用 XML 作为 DSL 保障双端一致性的同时升高了双端的开发成本。
在逻辑编排上,咱们设计了事件链技术计划尽可能的原子化每一个端侧根底能力,从而保障端侧能力开发者能够聚焦在能力的开发上。
基于上述框架反对下,开发者能够自行决定单个根底能力所应用的开发语言, 对于老手应用 Swift 的上手老本,能够降落一个品位,不再须要和简单的环境做奋斗。
遇到了哪些问题?
坦白说,尽管咱们在技术决策的时候做了深度思考,但当真的执行起来的时候,仍旧遇到了不少问题。
根底库 API 并未适配 Swift
尽管 Xcode 提供了“主动”生成桥接文件的能力,但因为 OC 和 Swift 语法差别过大,大部分主动生成的 Swift API 并不遵循“API Design Guidelines”,这会导致目前接入的 Swift 业务库写出很多可读性差且不好保护的代码。
同时,因为 Swift 的可选值设计,使得 OC SDK 提供给 Swift 应用时须要梳理分明每一个对外的 API 入参和出参的可选设定。商品评估重度依赖的一个根底 SDK 就没有很好的做到这一点,以至于咱们遇到了不少问题。
谬误推导导致的不必要兼容
咱们先看下,上面这段代码:
// DemoConfig.h
@interface DemoConfig : NSObject
/* 此处已省略无用代码 */
- (instancetype)initWithBizType:(NSString *)bizType;
@end
// DemoConfig.m
@implementation DemoConfig
- (instancetype)initWithBizType:(NSString *)bizType {if (self = [super init]) {_bizType = bizType;}
return self;
}
/* 此处已省略无用代码 */
@end
因为 DemoConfig 这个类并没有注明初始化办法返回值是否可选,以至于 Xcode 默认推导的 API 变成了。
// 主动生成的 Swift API
open class DemoConfig : NSObject {
/* 此处已省略无用代码 */
public init!(bizType: String!)
}
开发者就不得不去思考如何解决初始化为空的场景,这显然是多余的。
除了 SDK 做可选语义适配以外,咱们也能够新增一个分类,提供一个返回值不为空的 OC 办法,代码如下:
/// DemoConfig+SwiftyRateKit.h
NS_ASSUME_NONNULL_BEGIN
@interface DemoConfig (SwiftyRateKit)
- (instancetype)initWithType:(NSString *)bizType;
@end
NS_ASSUME_NONNULL_END
/// DemoConfig+SwiftyRateKit.m
#import <SwiftyRateKit/DemoConfig+SwiftyRateKit.h>
@implementation DemoConfig (SwiftyRateKit)
- (instancetype)initWithType:(NSString *)bizType {return [self initWithBizType:bizType];
}
@end
不平安 API
没有写分明可选设定的 OC API 被桥接到 Swift 实质上都是不平安的。为什么这么说呢?
咱们拿一个线上 Crash 实在案例来举例,堆栈如下:
Thread 0 Crashed:
0x0000000000000012 Swift runtime failure: Unexpectedly found nil while implicitly unwrapping an Optional value DemoEventHandler.swift
0x0000000000000011 handle DemoEventHandler.swift
0x0000000000000010 handle <compiler-generated>
0x0000000000000009 -[XXXXXXXXXX XXXXXXXXXX:XXXXXXXXXX:XXXXXXXXXX:] XXXXXXXXXX.m
0x0000000000000008 -[XXXXXXXX XXXXXXXX:XXXXXXXX:XXXXXXXX:] XXXXXXXX.m
0x0000000000000007 +[XXXXXXX XXXXXXX:XXXXXXX:XXXXXXX:] XXXXXXX.m
0x0000000000000006 -[XXXXXX XXXXXX:] XXXXXX.m
0x0000000000000005 -[XXXXX XXXXX:] XXXXX.m
0x0000000000000004 -[XXXX XXXX:] XXXX.m
0x0000000000000003 -[XXX XXX:XXX:] XXX.m
0x0000000000000002 -[XX XX:]
0x0000000000000001 -[X X:]
客户端的实现代码如下:
class DemoEventHandler: SwiftyEventHandler {override func handle(event: DemoEvent?, args: [Any], context: DemoContext?) {
guard let ret = context?.demoCtx.engine.value else {return}
/// 此处省略无用代码
}
}
导致 Crash 的起因是 context?.demoCtx.engine.value 这段代码。
实质起因是 demoCtx 未注明可选语义,导致 OC 桥接到 Swift 的时候默认应用了隐式解包。在读取过程中,如果值并没有值,会因为强制解包而间接产生 Unexpectedly found nil while implicitly unwrapping an Optional value 的 Crash。
要解决这个问题,除了 SDK 做可选语义适配以外,咱们还能够能够把调用代码都改成可选调用防止强制解包的问题:
破坏性继承
在应用下面这个根底 SDK 遇到最大的问题就是 DemoArray 的破坏性继承。
DemoArray 继承自 NSArray 并且重写了不少办法,其中就有 objectAtIndex: 这个办法。
在 NSArray 头文件中分明的定义了
objectAtIndex: 这个办法的返回值肯定不为空,然而 SDK 在 DemoArray 这个子类实现 objectAtIndex: 这个办法时竟然返回了 nil,代码如下所示:
这使得应用 Swift 开发 SDK 自定义 EventHandler 压根无奈进行。
外围起因是实现一个 SDK 自定义 EventHandler 首先要合乎 DemoEventHandler 协定,合乎协定必须实现 – (void)handleEvent:(DemoEvent )event args:(NSArray )args context:(DemoContext *)context; 这个办法,因为协定上约定的是 NSArray 类型,因而转换成 Swift API args 就变成了 [Any] 类型,如下图所示:
然而 SDK 传给 DemoEventHandler 的类型实质上是一个 DemoArray 类型:
假使 DemoArray 外面存在 [Null null] 对象,就会导致 attempt to insert nil object from objects[0] 的 Crash,如下图所示:
具体起因是在调用 handleEvent(_:args:context:) 时候,Swift 外部会调用 static Array._unconditionallyBridgeFromObjectiveC(_:) 把 args 入参由 NSArray 转变成 Swift 的 Array,而在调用 bridge 函数的时候,会先对原数组进行一次 copy 操作,而在 NSArray Copy 的时候会调用 -[__NSPlaceholderArray initWithObjects:count:],因为 DemoArray 的 NSNull 被转变成了 nil,初始化会失败,间接 Crash。
要防止这个问题,让 SDK 批改 DemoArray 显然是不事实的,因为调用方切实是过多,无论是影响面还是回归测试老本短期内都无奈评估。所以只能减少一个中间层来解决这个问题。咱们首先设计了一个 OC 的类叫 DemoEventHandlerBox 用于包装和桥接,代码如下:
/// DemoEventHandlerBox.h
@class SwiftyEventHandler;
NS_ASSUME_NONNULL_BEGIN
@interface DemoEventHandlerBox : NSObject<DemoEventHandler>
-(instancetype)initWithHandler:(SwiftyEventHandler *)eventHandler;
@end
NS_ASSUME_NONNULL_END
/// DemoEventHandlerBox.m
#import <SwiftyRateKit/DemoEventHandlerBox.h>
#import <SwiftyRateKit/SwiftyRateKit-Swift.h>
@interface DXEventHandlerBox ()
/// 处理事件对象
@property (nonatomic, strong) SwiftyEventHandler *eventHandler;
@end
@implementation DemoEventHandlerBox
-(instancetype)initWithHandler:(SwiftyEventHandler *)eventHandler {self = [super init];
if (self) {_eventHandler = eventHandler;}
return self;
}
- (void)handleEvent:(DemoEvent *)event args:(NSArray *)args context:(DemoContext *)context {[self.eventHandler handle:event args:args context:context];
return;
}
@end
DemoEventHandlerBox 中有个类型为 SwiftyEventHandler 类用于逻辑解决,代码如下:
@objcMembers
public class SwiftyEventHandler: NSObject {
@objc
public final func handle(_ event: DemoEvent?, args: NSArray?, context: DemoContext?) {var ret: [Any] = []
if let value = args as? DemoArray {ret = value.origin} else {ret = args as? [Any] ?? []}
return handle(event: event, args: ret, context: context)
}
func handle(event: DemoEvent?, args: [Any], context: DemoContext?) {return}
}
SwiftyEventHandler 裸露给 OC 的办法设置为 final 同时做好将 DemoArray 转回 NSArray 的逻辑兼容。最初 Swift 这边的所有 EventHandler 实现类都继承自 SwiftyEventHandler 并重写 handle(event:args:context) 办法。这样就能够完满防止因为破坏性继承导致的问题了。
Clang Module 构建谬误
第二大类问题次要和依赖无关,尽管前文有提到,目前的根本基础设施曾经齐备,但仍旧存在一些问题。
依赖更新不及时
很多人刚开始写 Swift 的时候,常常会遇到一个问题 Could not build Objective-C module,个别状况下的起因是因为你所依赖的模块并没有适配 Module,但因为手淘基础设施根本曾经齐备,大部分库都曾经实现 Module 化适配,所以你可能只须要更新一下模块依赖就能够很好的解决这类问题。
例如 STD 这个库,手淘目前依赖的版本是 1.6.3.2,但当你的 Swift 模块须要依赖 STD 的时候,应用 1.6.3.2 会导致无奈编译通过。这时候你的 Swift 模块可能须要降级到 1.6.3.3 能力解决这个问题。实质上 1.6.3.3 和 1.6.3.2 的区别就是模块化适配,因而你也不必放心会产生什么副作用。
混编导致的依赖问题
前文提到的 Module 适配尽管解决了大部分问题,然而还是存在一些异样 case,这里开展说下。
咱们在商品评估重构的过程中,为了保障我的项目能够循序渐进的放量,咱们做了代码的物理隔离,新创建了一个模块叫 SwiftyRateKit 是一个 Swift 模块。然而评估列表的入口类都在一个叫 TBRatedisplay 的 OC 模块。因而为了做切流,TBRatedisplay 须要依赖 SwiftyRateKit 的一些实现。但当咱们将 TBRatedisplay 依赖了 SwiftyRateKit 开始编译之后,就遇到了上面这么一个问题:
Xcode 将裸露给 OC 的 Swift 类 ExpandableFastTextViewWidgetNode 的头文件申明写到了 SwiftyRateKit-Swift.h 中,ExpandableFastTextViewWidgetNode 是继承自 TBDinamic 的类 DXFastTextWidgetNode 的。
因为过后 TBRatedisplay 并没有开启 Clang Module 开关(CLANG_ENABLE_MODULES),导致 SwiftyRateKit-Swift.h 的上面这么一段宏定义并没有失效,因而就不晓得 ExpandableFastTextViewWidgetNode 这个类是在哪里定义的了:
但当咱们开启 TBRatedisplay 的 Clang Module 开关之后,更恐怖的事件产生了。因为 TBDinamic 没有开启 Clang Module 开关,导致 @import TBDinamic 无奈编译通过,进入了一个“死循环”,最初不得不长期移除了所有没有反对 Clang Module 开关的 OC 模块导出。
这里概念比拟形象,我用一张图来示意一下依赖关系:
首先,对于一个 Swift 模块来说,只有模块开启了 DEFINES_MODULE = YES 且提供了 Umbrella Header 就能够通过 import TBDinamic 的形式导入依赖。因而 SwiftyRateKit 能够在 TBDinamic 没有开启 Clang Module 开关的时候就显示依赖,并能够编译通过。
但对于一个 OC 模块来说,导入另外一个模块分两种状况。
- 第一种是开启了 DEFINES_MODULE = YES 的模块,咱们能够通过 #import <TBDinamic/TBDinamic_Umbrella.h> 导入。
- 第二种是开启了 Clang Module 开关的时候,咱们能够通过 @import TBDinamic 导入
因为 TBRatedisplay 依赖了 SwiftyRateKit,Xcode 主动生成的 SwiftyRateKit-Swift.h 头文件采纳的是 @import TBDinamic 的形式来导入模块的,因而就造成了下面的问题。
所以我集体倡议现阶段要尽量避免或者缩小将一个 Swift 模块的 API 提供给 OC 应用,不然就会导致这个 Swift 对外 API 须要依赖的 OC 模块都须要开启 Clang Module,同时依赖了这个 Swift 模块的 OC 模块也须要开启 Clang Module。而且,因为 Swift 和 OC 语法不对等,会让 Swift 开发进去的接口层能力十分受限,从而导致 Swift 对外的 API 变得相当不协调。
类名与 Module 同名
实践上 Swift 模块之间相互调用是不会存在问题的。但因为手淘模块泛滥,历史包袱过重,咱们在做商品评估革新的时候遇到了一个「类名与 Module 同名」的苦逼问题。
咱们个 SDK 叫 STDPop,这个 SDK 的 Module 名也叫 STDPop,同时还有一个工具类也叫 STDPop。这会导致什么问题呢?所有依赖 STDPop 的 Swift 模块,都无奈被另外一个 Swift 模块所应用的,会报一个神奇的谬误:’XXX’ is not a member type of class ‘STDPop.STDPop’ 次要起因是因为依赖 STDPop 的 Swift 模块生成的 .swiftinterface 文件时会给每个 STDPop 的类加一个 STDPop 的前缀。例如 PopManager 会变成 STDPop.PopManager 但因为 STDPop 自身就一个类叫 STDPop 会就导致编译器无奈了解 STDPop 到底是 Module 名还是类名。
而能解决这个问题的惟一方法就是须要 STDPop 这个模块移除或者批改 STDPop 这个类名。
具体有哪些方面的收益?
咱们在一次三思而行之后,踏上了乘风破浪的 Swift 落地之路,尽管在整个过程中遇到了很多前所未有的挑战,但当初回过来看,咱们当初的技术选型还是比拟正确的。次要体现在上面几个方面:
代码量减少,Coding 效率进步
得益于 Swift 的强表白性,咱们能够用更少的代码去实现一个本来用 OC 实现的逻辑,如下图所示,咱们不再须要写过多的防御性编程的代码,就能够清晰的表白出咱们要实现的逻辑。
同时,咱们对原有 13 个用 OC 实现的表达式,用 Swift 从新写了一遍,整体代码量的变动如下:
代码量的变少意味着须要投入开发的工夫变少了,同时产生 bug 的机会也就变少了。
大幅升高穿插 Review 的老本
OC 奇异的语法使得大部分其余开发压根无奈看懂具体的逻辑,从而导致 iOS 和 Android 双端穿插 Review 的老本相当之高,也会使得很多库常常存在双端逻辑不一致性。
当初在做订单迁徙新奥创时,面对较多双端 API 不统一,且局部代码逻辑的滋味较简单,我的项目上产生过多起长期问题排查影响节奏的事件。
因而,咱们另辟蹊径,采纳 Swift & Kotlin 的模式进行开发,因为 Swift 和 Kotlin 的语法极度类似,使得咱们穿插 Review 毫无压力。
同时,得益于商品评估应用的脚手架,后续需要迭代也大幅降落。咱们以「评估 Item 新增分享按钮」为例:
如果采纳 OC & Java 模式,因为双端代码都看不懂。所以需要评审都双端须要各派 1 名,加上探讨等各种事宜大略须要 0.5 人日。而后双端探讨计划后一个人进行模板开发,须要 1 人日左右。最初双端各自实现原生分享原子能力,须要各 2 人日左右(其中有 1 人日须要调研如何接入分享 SDK),总计 2 0.5 + 1 + 2 2 = 6 人日。
然而如果采纳 Swift & Kotlin 的模式,咱们只须要有 1 人取加入需要 Review,0.5 人日。单人实现技术调研、模板开发 3 人日左右。最初再把写好的代码给另外一端看,另外一端能够间接 copy 代码并依据本人端的特点进行适配 1 人日左右。总计 0.5 + 3 + 1 = 4.5 人日左右。大概节俭 25% 的工夫。
我的项目稳定性有所提高
因为没有比拟好的量化指标,只能谈谈感触。
首先,因为编码问题导致的提测问题显著降落,基本上的异样分干流得益于 Swift 的可选值设计,都曾经在开发阶段思考分明了,总体提测问题显著比应用 OC 时少了很多。
其次,线上问题也显著降落,除了上文提到的 Crash 问题。商品评估重构我的项目基本上没有产生线上问题。
优先享受技术红利
无论是 WidgetKit 还是 DocC,能够很显著的看得出来,苹果外部对于新个性以及开发工具链的降级肯定是 Swift 优先于 OC,因而所有应用 Swift 的同学都能很疾速的应用上所有苹果新开发的个性和工具。
同时,也得益于 Swift 的开源,咱们不仅能够通过源码去学习一些不错的设计模式,还能够定位一些疑难杂症,不再须要和生涩难懂的汇编代码作奋斗。
总结与瞻望
以上算是咱们在手淘摸索 Swift 业务落地的一个总结,心愿能够给大家在技术选型或者摸索避坑的时候给到一点帮忙,当然,这只是一个开始,还有很多事件值得去做。首先,咱们须要一起去欠缺和标准 Swift 的编码标准,甚至积淀一系列最佳实际去疏导大家更低成本的从 OC 转型到 Swift;其次,咱们也须要针对前文提到的混编问题,推动根底 SDK 做 Swift Layer 建设以及持续优化现有 Swift 工具链;最初,咱们还须要引入一些优良的开源库防止反复造轮子,以及利用好 Apple 提供的能力(例如 DocC),并最终找到一个 Swift 在手淘的最佳实际。
最初,如果你对咱们做的事件也比拟感兴趣,欢送退出咱们一起共建 Swift/Kotlin 生态,我的联系方式是:zhejian.wzj@alibaba-inc.com,期待你的退出。
【文献参考】
- https://mp.weixin.qq.com/s/pQ…
- https://github.com/apple/swif…
- https://mp.weixin.qq.com/s/5S…
- https://www.swift.org/documen…
- https://www.jianshu.com/p/0d3…
- https://developer.apple.com/v…
- https://madnight.github.io/gi…
- https://mp.weixin.qq.com/s/O5…
关注【阿里巴巴挪动技术】微信公众号,每周 3 篇挪动技术实际 & 干货给你思考!