SRouter

SRouter是为了解决模块和模块间的引用,根据符号导出(Symbol Export)实现的一个路由方案

背景

我们在把项目进行组件化的过程中,会将不同业务进行模块化,甚至将某些业务模块制作成动态或者静态库来提供给不同项目,然后每个同学主要负责不同的业务模块
但是某个业务一般不会单一存在的,往往是业务A有个入口进入到业务B,业务B又有几个入口到业务C,D等等。。。
这个时候就会出现模块A import B,模块B import C,D… 这样某个同学在制作自己的模块时,还得导入其他同学的模块,否则就编译GG了。这样就增加了模块的耦合性了

为此,根据动态库编译链接原理,带来了这个无需import(避开了编译的链接,在runtime时链接), 无需注册的路由方案 SRouter

霜神的iOS组件化-路由设计思路分析这篇文章将目前业界大部分的路由方案都分析了一下。这些方案都是前辈们的智慧结晶,可以根据自己的项目进行可以参考选取

实现效果

先看下其中一个功能实现效果吧~

Swift

在登录业务模块(Login)定义一个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
@_silgen_name("Login://login")
public func LoginRouterInterface(with params: [String: Any]) -> [String: Any]? {
guard let navi = params["navi"] as? UINavigationController else {
return nil
}

let loginController = LoginViewController()
if let title = params["title"] as? String {
loginController._title = title
}
navi.pushViewController(loginController, animated: true)
return nil
}

在其他任意模块路由到该登录模块接口

1
2
3
import SRouter

SRouterManager.default.routeTo("Login://login")?(navi: navigationController, title: "登录 🚀🚀🚀")

@_silgen _name这个后面解释

OC

在业务OC模块(OCModule)定义一个接口

1
2
3
4
5
6
7
8
9
// .m

OBJC_EXPORT
NSDictionary* OCControllerInterface(NSDictionary* params) {
UINavigationController *naviController = params[@"navi"];
OCViewController *oc_controller = [[OCViewController alloc] init];
[naviController pushViewController:oc_controller animated:true];
return nil;
}

其他任意模块路由到OC模块的OCControllerInterface

1
2
3
import SRouter

SRouterManager.default.ocRouteTo("OCModule://OCControllerInterface")?(navi: naviController)

OBJC_EXPORT 这个后面解释

接口输入参数和输出参数默认都是字典,支持自定义

如下自定义输入输出

在登录模块定义一个注册接口

1
2
3
4
5
@_silgen_name("Login://registered")
public func RegisteredRouterInterface(with param: String) -> UIViewController {
let registeredController = RegisteredViewController(title: "Registered 🚀🚀🚀")
return registeredController
}

其他任意模块路由到登录模块的注册接口

1
2
3
4
5
typealias RegisteredRouterSILFunctionType = @convention(thin) (_ input: String) -> UIViewController

if let registeredController = SRouterManager.default.routeTo("Login://registered", routerSILFunctionType: RegisteredRouterSILFunctionType.self)?("注册 🚀🚀🚀") {
self.present(UINavigationController(rootViewController: registeredController), animated: true, completion: nil)
}

实现原理

原理

主要利用了编译链接的(Symbol Export)符号导出来实现的

如图所示,是Xcode的Linking配置

Exported Symbols File: 符号导出
Order File: 符号重排 (之前头条用于启动优化)
UnExported Symbols File: 符号不导出

对于符号导出,当你在导出文件中加上该符号时,即使你声明的函数为internal,你的函数符号也会导出

对于符号重排,当你在重排文件中加上该符号时,你的函数实现会在代码段的代码区的最前面

对于符号不导出,当你在不导出文件中加上该符号时,即使你声明的函数为public,你的函数符号也不会导出,其他动态库调用你public声明的函数,在链接期间也会报符号未定义的错误(别拿来坑队友喔)

Exporting Your Framework Interface
Minimizing Your Exported Symbols

实现

导出的符号会在动态库(Mach-O)的Dynamic Loader Info段 的 Export Info区,这个区的数据结构是根据导出的符号名生成的一棵多叉数,每一条路径代表一个符号,最终可以找到对应符号的信息

这样的话,只需要将路由接口符号导出,在其他模块通过SRouter动态链接”路由”过去,从而解决模块引用问题(无需import)

实现问题

当然手动的将每个路由符号添加到符号导出文件中比较麻烦,可以利用以下两个编译属性来实现。

OC(OBJC_EXPORT)

OBJC_EXPORT 关键字包括 extern c 和 attribute ((visibility(“default”)))

Swift(@_ silgen_name)

@_ silgen_name是Swift中间语言SIL的一个属性

@_ silgen_name有两个目的,以下是原话

  1. To specify the symbol name of a Swift function so that it can be called from Swift-aware C. Such functions have bodies.
  2. To provide a Swift declaration which really represents a C declaration. Such functions do not have bodies.

第一个是指定函数符号,为了让C调用
第二个可以只声明一个函数符号,没有函数体,真正的函数实现是对应C的函数符号实现

我利用了第一个目的,可以避免Swift mangling(Swift符号重整),以及可以让函数符号external,这样其他模块就能动态链接该符号

当然@_ silgen_name编译成Swift中间语言后函数类型是@convention(thin), 所以我在定义函数类型时会加上@convention(thin). 有兴趣的可以看下SIL Function Type