NBus 的由来

一直使用 MonkeyKing 作为分享模块的底层实现,但 微信 SDK 1.8.6.1QQ SDK 3.3.6 开始改用 Universal Links 进行处理且校验请求合法性,而 MonkeyKing 在较晚的时候才提供支持

当时急着上线,微信和 QQ 直接改用官方 SDK,微博还是保留 MonkeyKing 的处理。从那之后我就在想,官方闭源也好,三方开源也罢,能快速接入上线才是最重要的。如果分享组件能提供一键切换的能力,那就方便了。除此之外,也受 nixzhu 的鼓励,萌生了写 NBus 的想法。


前言

一开始用的名字是「Bus」,但已经被其他人在 CocoaPods 注册了,因此库名改成了「NBus」,除此之外还是用「Bus」。

Bus 采用了注册 Handler 的机制,由 Handler 实现细节,Bus 只作为派发任务的管理者。这样做的好处是新增或者修改默认实现变得十分简单。

目前有 share / oauth / openURL / openUserActivity 四个方法,用于实现分享、登录以及回调。

Handler

1
2
3
4
public protocol HandlerType {

var isInstalled: Bool { get }
}

Handler 由 HandlerType 协议所约束, 声明了 isInstalled 属性,用于判断 Handler 是否安装对应 App。

在此之上根据具体功能分为 ShareHandlerType / OauthHandlerType / OpenURLHandlerType / OpenUserActivityHandlerType 四个子协议,同时还有一个 LogHandlerProxyType 用于记录日志。

ShareHandlerType

1
2
3
4
5
6
7
8
9
10
11
12
13
public protocol ShareHandlerType: HandlerType {

var endpoints: [Endpoint] { get }

func share(
message: MessageType,
to endpoint: Endpoint,
options: [Bus.ShareOptionKey: Any],
completionHandler: @escaping Bus.ShareCompletionHandler
)

func canShare(to endpoint: Endpoint) -> Bool
}
1
2
3
4
5
6
extension ShareHandlerType {

public func canShare(to endpoint: Endpoint) -> Bool {
endpoints.contains(endpoint)
}
}

Bus 通过判断 Handler 预先声明的 endpoints 是否包含本次分享的 endpoint 进行派发任务。

OauthHandlerType

1
2
3
4
5
6
7
8
9
10
11
public protocol OauthHandlerType: HandlerType {

var platform: Platform { get }

func oauth(
options: [Bus.OauthOptionKey: Any],
completionHandler: @escaping Bus.OauthCompletionHandler
)

func canOauth(with platform: Platform) -> Bool
}
1
2
3
4
5
6
extension OauthHandlerType {

public func canOauth(with platform: Platform) -> Bool {
self.platform == platform
}
}

Bus 通过判断 Handler 预先声明的 platform 是否等于本次登录的 platform 进行派发任务。

OpenURLHandlerType

1
2
3
4
5
6
7
8
public protocol OpenURLHandlerType: HandlerType {

var appID: String { get }

func openURL(_ url: URL)

func canOpenURL(_ url: URL) -> Bool
}
1
2
3
4
5
6
extension OpenURLHandlerType {

public func canOpenURL(_ url: URL) -> Bool {
appID == url.scheme
}
}

Bus 通过判断 Handler 预先声明的 appID 是否等于本次打开 URL 的 scheme 进行派发任务。

OpenUserActivityHandlerType

1
2
3
4
5
6
7
8
public protocol OpenUserActivityHandlerType: HandlerType {

var universalLink: URL { get }

func openUserActivity(_ userActivity: NSUserActivity)

func canOpenUserActivity(_ userActivity: NSUserActivity) -> Bool
}
1
2
3
4
5
6
7
8
extension OpenUserActivityHandlerType {

public func canOpenUserActivity(_ userActivity: NSUserActivity) -> Bool {
let lhs = userActivity.webpageURL?.absoluteString ?? ""
let rhs = universalLink.absoluteString
return lhs.hasPrefix(rhs)
}
}

Bus 通过判断 Handler 预先声明的 universalLink 是否是本次打开 NSUserActivity 的 webpageURL 的前缀进行派发任务。

LogHandlerProxyType

1
2
3
4
5
6
7
8
9
10
11
public protocol LogHandlerProxyType: HandlerType {

var logHandler: Bus.LogHandler { get }

func log(
_ message: String,
file: String,
function: String,
line: UInt
)
}
1
2
3
4
5
6
7
8
9
10
11
extension LogHandlerProxyType {

public func log(
_ message: String,
file: String = #file,
function: String = #function,
line: UInt = #line
) {
logHandler(message, file, function, line)
}
}

LogHandlerProxyType 协议声明了 logHandler 属性以及 log 函数用于记录日志。这个协议只是用来约束命名,可以不用实现。

自定义日志记录行为,可以用 delegate 或者 block,这里为了方便选择了 block。只需要在初始化 Handler 时修改 logHandler,即可把日志记录转到自有的日志系统。

Endpoint

1
2
3
4
5
6
7
8
9
10
public struct Endpoint: RawRepresentable, Hashable {

public typealias RawValue = String

public let rawValue: Self.RawValue

public init(rawValue: Self.RawValue) {
self.rawValue = rawValue
}
}

Endpoint 是一个只有 rawValue 的结构体类型,同时实现了 Hashable 协议。它表示的是分享行为的目标。

Endpoints

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public enum Endpoints {

public enum QQ {

public static let friend = Endpoint(rawValue: "com.nuomi1.bus.endpoint.qq.friend")

public static let timeline = Endpoint(rawValue: "com.nuomi1.bus.endpoint.qq.timeline")
}

public enum Wechat {

public static let friend = Endpoint(rawValue: "com.nuomi1.bus.endpoint.wechat.friend")

public static let timeline = Endpoint(rawValue: "com.nuomi1.bus.endpoint.wechat.timeline")

public static let favorite = Endpoint(rawValue: "com.nuomi1.bus.endpoint.wechat.favorite")
}
}

通过枚举类型的嵌套,把各个 endpoint 放到 Endpoints 集中访问。如果把这些常量直接放到 Endpoint,不容易做平台的区分,这里通过中间的枚举类型分开了不同平台。同时枚举类型没有显式的初始化方法,在代码提示中可以避免不必要的展示。写法参考了 Combine 的 Publishers

Platform

1
2
3
4
5
6
7
8
9
10
public struct Platform: RawRepresentable, Hashable {

public typealias RawValue = String

public let rawValue: Self.RawValue

public init(rawValue: Self.RawValue) {
self.rawValue = rawValue
}
}

Platform 是一个只有 rawValue 的结构体类型,同时实现了 Hashable 协议。它表示的是登录行为的平台。

Platforms

1
2
3
4
5
6
public enum Platforms {

public static let qq = Platform(rawValue: "com.nuomi1.bus.platform.qq")

public static let wechat = Platform(rawValue: "com.nuomi1.bus.platform.wechat")
}

Endpoints 类似,把各个 platform 放到 Platforms 集中访问。平台下面没有子平台,因此不需要嵌套来区分。

Message

1
2
3
4
5
6
7
8
9
10
public struct Message: RawRepresentable, Hashable {

public typealias RawValue = String

public let rawValue: Self.RawValue

public init(rawValue: Self.RawValue) {
self.rawValue = rawValue
}
}

Message 是一个只有 rawValue 的结构体类型,同时实现了 Hashable 协议。它表示的是分享行为中所分享事物的类型。

Messages

1
2
3
4
5
6
public enum Messages {

public static let text = Message(rawValue: "com.nuomi1.bus.message.text")

public static let image = Message(rawValue: "com.nuomi1.bus.message.image")
}

Endpoints 类似,把各个 message 放到 Messages 集中访问。

MessageType

1
2
3
4
public protocol MessageType {

var identifier: Message { get }
}
1
2
3
4
5
6
7
8
public protocol MediaMessageType: MessageType {

var title: String? { get }

var description: String? { get }

var thumbnail: Data? { get }
}

MessageType 协议声明了 identifier 属性用于区别分享事物的类型。MediaMessageTypeMessageType 的子协议,额外声明了 title / description / thumbnail 等共有属性。

TextMessage

1
2
3
4
5
6
public struct TextMessage: MessageType {

public let identifier = Messages.text

public let text: String
}

TextMessage 是一个实现了 MessageType 协议的结构体类型。它表示的是分享事物为纯文本。

ImageMessage

1
2
3
4
5
6
7
8
9
10
11
12
public struct ImageMessage: MediaMessageType {

public let identifier = Messages.image

public let data: Data

public let title: String?

public let description: String?

public let thumbnail: Data?
}

ImageMessage 是一个实现了 MediaMessageType 协议的结构体类型。它表示的是分享事物为图片。

Messages-func

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
extension Messages {

public static func text(
text: String
) -> TextMessage {
TextMessage(
text: text
)
}

public static func image(
data: Data,
title: String? = nil,
description: String? = nil,
thumbnail: Data? = nil
) -> ImageMessage {
ImageMessage(
data: data,
title: title,
description: description,
thumbnail: thumbnail
)
}
}

Messages 除了各个 message,还定义了静态函数来初始化各个分享事物,并且把非必须的属性自动赋值 nil 方便初始化。

Bus

share

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
extension Bus {

public struct ShareOptionKey: RawRepresentable, Hashable {

public typealias RawValue = String

public let rawValue: Self.RawValue

public init(rawValue: Self.RawValue) {
self.rawValue = rawValue
}
}

public typealias ShareCompletionHandler = (Result<Void, Bus.Error>) -> Void

public func share(
message: MessageType,
to endpoint: Endpoint,
options: [Bus.ShareOptionKey: Any] = [:],
completionHandler: @escaping ShareCompletionHandler
) {
let handlers = self.handlers.compactMap { $0 as? ShareHandlerType }

guard
let handler = handlers.first(where: { $0.canShare(to: endpoint) })
else {
assertionFailure()
completionHandler(.failure(.missingHandler))
return
}

handler.share(
message: message,
to: endpoint,
options: options,
completionHandler: completionHandler
)
}
}

ShareOptionKey 是实现了 Hashable 协议的结构体类型,作用是传递数据时做标识符。

ShareCompletionHandler 是分享回调的函数签名,分享不需要处理返回值,因此 Success 被设计为 Void

share 中,先找出实现了 ShareHandlerType 协议的 handlers,再向各个 handler 询问是否可以处理本次分享行为。找不到任何 handler 就执行 assertionFailure 提示调用方应注册所需的 Handler,找到就给对应的 Handler 派发任务。

oauth

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
extension Bus {

public struct OauthOptionKey: RawRepresentable, Hashable {

public typealias RawValue = String

public let rawValue: Self.RawValue

public init(rawValue: Self.RawValue) {
self.rawValue = rawValue
}
}

public struct OauthInfoKey: RawRepresentable, Hashable {

public typealias RawValue = String

public let rawValue: Self.RawValue

public init(rawValue: Self.RawValue) {
self.rawValue = rawValue
}
}

public typealias OauthCompletionHandler = (Result<[OauthInfoKey: String], Bus.Error>) -> Void

public func oauth(
with platform: Platform,
options: [Bus.OauthOptionKey: Any] = [:],
completionHandler: @escaping OauthCompletionHandler
) {
let handlers = self.handlers.compactMap { $0 as? OauthHandlerType }

guard
let handler = handlers.first(where: { $0.canOauth(with: platform) })
else {
assertionFailure()
completionHandler(.failure(.missingHandler))
return
}

handler.oauth(
options: options,
completionHandler: completionHandler
)
}
}

oauth 中,处理同 share 方法,但是登录需要返回登录凭据和用户信息,因此 OauthCompletionHandlerSuccess 被设计为 [OauthInfoKey: String]

openURL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extension Bus {

public func openURL(_ url: URL) -> Bool {
let handlers = self.handlers.compactMap { $0 as? OpenURLHandlerType }

guard
let handler = handlers.first(where: { $0.canOpenURL(url) })
else {
return false
}

handler.openURL(url)
return true
}
}

openURL 中,处理同 share 方法,但是不需要执行 assertionFailure,因为这个方法一般在 AppDelegateapplication(_:open:options:) 方法中使用 || 调用,无法处理 url 是正常情况。

openUserActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extension Bus {

public func openUserActivity(_ userActivity: NSUserActivity) -> Bool {
let handlers = self.handlers.compactMap { $0 as? OpenUserActivityHandlerType }

guard
let handler = handlers.first(where: { $0.canOpenUserActivity(userActivity) })
else {
return false
}

handler.openUserActivity(userActivity)
return true
}
}

openUserActivity 中,处理同 openURL 方法,在 AppDelegateapplication(_:continue:restorationHandler:) 方法中使用 || 调用。

总结

Bus 中大量使用带 rawValue 的结构体类型用于标识符。虽然本质上都是字符串,但实际代表的事物却不一样,应加以区分。另一方面,如果使用 String 作为别名,那么实现 description 来描述具体的常量将是个灾难,不得不为每一个别名分别实现 xxxDescription,现在只需要给各个结构体单独实现 CustomStringConvertible 协议即可。

Bus 中还大量使用枚举类型作为常量集合的入口,这样既可以统一管理,也方便调用方增加自己的常量。如果直接使用枚举类型,除了修改源码,没法增加常量。

限于篇幅,没有逐一介绍 Bus 的源码,感兴趣的可以到 NBus 查看,顺便给个 star 或者 fork。

参考