Swift路由组件(二)路由的实现

6,847 阅读8分钟

上一篇请看这里: Swift路由组件(一)使用路由的目的和实现思想

这个为本人原创,转载请注明出处juejin.cn/post/703221…

方案的选择

具体一个路由,他真正要做什么事情呢,总结一下应该是这几个逻辑功能:

  1. 通过key来map到一个controller
  2. 实例化一个controller
  3. 解决好传参
  4. 页面跳转

下面讲讲具体代码如何实现每一个逻辑。

一、通过key来map到一个controller

方法有两种:

  1. 通过维护一个plist文件,然后开发的时候添加好路由表。
  2. 在运行的时候通过业务注册,每个业务把key注册到路由里面去,在内存中维护一个路由表。

先说1,这个的做法也简单。定义的plist如下:

image.png

这样运行的时候就可以通过把这个plist读进来,然后就可以做map了。

再说2.这个的简单做法是做一个入口函数,然后各个页面自己把注册的代码添加到这个入口函数里面。比如:

// 启动先初始化好
func onLaunch() {
	YYRouter.register(to: "native://to_home_page", routerClass: HomeViewController.self)
        YYRouter.register(to: "native://to_buy_page", routerClass: BuyViewController.self)
 	YYRouter.register(to: "http", routerClass: WebViewController.self)
 	YYRouter.register(to: "https", routerClass: WebViewController.self)
}

两种方式都需要有一个入口的地方,类似于集中存放。集中存放也是有好处的,方便维护,直观,一眼知道有多少定义,不好的地方就是不方便做组件化。而且组件化还是大家很迫切需要的。

那么这里的需求就是能不能分散定义。

首先我是使用方案2,然后这里我又想到了一种巧妙的方法,先定义一个总的YYRouter类,然后让各个业务自己去extension YYRouter的方法。然后在运行的时候,就可以通过遍历一个类有多少方法,然后再动态的去调用这些方法得到返回值,这样就知道有多少路由定义了。

如下,先定义YYRouter的扩展。

// 添加路由表
extension YYRouter {
    @objc func router_WebController() -> YYRouterModel {
        return YYRouterModel(to: "http", routerClass: WebController.self)
    }
    @objc func router_WebController_https() -> YYRouterModel {
        return YYRouterModel(to: "https", routerClass: WebController.self)
    }
}

这个多定义的方法的要求如下:

  1. 必须定义为@objc,依赖运行时。

  2. 必须router_开头的函数名,后面可以随便定义名字,建议用路由的类名。如:路由类WebController, 就可以定义函数router_WebController

  3. 必须函数的入参为空。

  4. 这个函数返回值要求是一个YYRouterModel,他的值有: 1)to: 路由名的定义,如:native://course,或者http等唯一的key, 做好分层命名。 2)routerClass: 路由目标类,如:TestClassName.self

运行时遍历和动态调用如下:

private static func configRoutersFromMethodList() -> [String: YYRouterModel] {
        var routerList: [String: YYRouterModel] = [:]
        var methodCount: UInt32 = 0
        let methodList = class_copyMethodList(YYRouter.self, &methodCount)
         
        if let methodList = methodList, methodCount > 0 {
            for i in 0..<Int(methodCount) {
                let selName = sel_getName(method_getName(methodList[i]))
                if let methodName = String(cString: selName, encoding: .utf8),
                   methodName.hasPrefix("router_") {
                    let selector: Selector = NSSelectorFromString(methodName)
                    if YYRouter.shared.responds(to: selector) {
                        if let result = YYRouter.shared.perform(selector).takeUnretainedValue() as? YYRouterModel {
                            routerList[result.to] = result
                        }
                    }
                }
            }
        }
        free(methodList)
        return routerList
    }
  1. 遍历一个类的所有方法
  2. 拿出以router_开头的那些方法。
  3. 动态调用router_开头的方法,获取他的返回值,保存为路由表。

二、实例化一个controller

从路由层面来看也是有两种方法,要么路由自动实例化。要么让实现路由的页面自己实例化。

如,路由里面自动做实例化, 那就是接收一个AnyClass,然后利用NSObject的init(), 方法自动实例化:

private static func getInstance(_ cls: AnyClass) -> YYRoutable? {
        if let instance = (cls as? NSObject.Type)?.init(), let routable = (instance as? YYRoutable) {
            return routable
        }
        return nil
    }

如,让各个页面自己实例话好,再传给路由层。

// 实现路由协议,让自己支持路由跳转。
extension WebController: YYRoutable {
    // 返回一个路由协议的实例
    static func createInstance(params: [String : Any]) -> YYRoutable {
        let vc = WebController()
        vc.url = params["to"] as? String ?? "" // 路由传参数
        vc.value = params["value"] as? String ?? "" // 路由传参数
        return vc
    }
}

这里两种方式都可以,我选择了后面这种,让业务自己实例化好再给路由层,感觉合理些。包括他还可以处理好参数再给回来。

解决好传参

通过上面的方案就能把参数都搞定了。

页面跳转

这个就更简单了。无非就是拿到navigationController,然后push。为了方便,我这里写了一个全局的获取navigationController来做push。看代码

	/// 当前的导航控制器
    public static func currentNavigationController() -> UINavigationController? {
        return currentController().navigationController
    }
    
	public static func currentController() -> UIViewController {
        if let root = delegate().window??.rootViewController {
            return getCurrent(controller: root)
        } else {
            print("异常问题, 还没有rootVC不应该调用")
            assert(false, "异常问题, 还没有rootVC不应该调用")
            return UIViewController()
        }
    }

    
    /// 通过递归拿到当前显示的UIViewController
    public static func getCurrent(controller: UIViewController) -> UIViewController {
        if controller is UINavigationController {
            let naviController = controller as! UINavigationController
            return getCurrent(controller: naviController.viewControllers.last!)
        } else if controller is UITabBarController {
            let tabbarController = controller as! UITabBarController
            return getCurrent(controller: tabbarController.selectedViewController!)
        } else if controller.presentedViewController != nil {
            return getCurrent(controller: controller.presentedViewController!)
        } else {
            return controller
        }
    }

有以上的代码,可以这样获取一个全局的navigationController

YYRouterUtil.currentNavigationController()

那么push就简单了。

YYRouterUtil.currentNavigationController()?.pushViewController(controller, animated: animated)

具体实现

定义路由协议

通过定义一个路由协议,让实现路由协议的页面都能有跳转的能力。定义一个YYRoutable的协议如下:

/// 路由协议, 需要是AnyObject,其他如struct和enum等不能实现这个协议
public protocol YYRoutable: AnyObject {
    /// 路由传参,接收者负责解析自己的参数并返回一个路由实例
    static func createInstance(params: [String: Any]) -> YYRoutable

    /// 路由逻辑处理
    func executeRouter(params: [String: Any], navRootVC: UIViewController?)
}
  1. 让实现路由的页面,要返回一个实例,实现createInstance方法返回。
  2. 路由要执行的具体跳转逻辑,通过实现executeRouter方法实现。也可以不实现这个,路由组件做了默认的路由跳转。

默认跳转如下:

/// 路由协议的默认实现
public extension YYRoutable {
    /// 默认路由跳转
    func executeRouter(params: [String: Any] = [:], navRootVC: UIViewController? = nil) {
        guard let controller = self as? UIViewController else {
            assert(false, "默认路由跳转,需要routable继承UIViewController")
            return
        }
        
        defaultPush(to: controller, params: params, navRootVC: navRootVC)
    }
}
/// 路由里面的私有方法
public extension YYRoutable {
    /// native://my?userId=1&token=jdfsakbfjkafbf
    ///
    /// - Parameters:
    ///   - controller: 跳转VC
    ///   - params: 额外参数
    ///   - navRootVC 有的时候不需要取currentVC
    func defaultPush(to controller: UIViewController, params: [String: Any] = [:], navRootVC: UIViewController? = nil) {
        let animated = (params["animated"] as? Bool) ?? true

        if navRootVC?.navigationController != nil {
            navRootVC?.navigationController?.pushViewController(controller, animated: animated)
        } else {
            YYRouterUtil.currentNavigationController()?.pushViewController(controller, animated: animated)
        }

    }
}

注册路由表

前面说过添加路由表通过extension YYRouter方法来实现,然后扩展的方法里面统一返回一个路由表模型。模型定义如下

public class YYRouterModel: NSObject {
    /// 路由名,可以任意定义一个唯一的key
    /// 如:native://course/detail,这样的一个规则表示本地的某个页面。
    /// 或者http,或者https,这两个表示是url网页,直接用webview去响应。
    public var to: String = ""
    public var routerClass: AnyClass = YYRouterModel.self // 路由目标类
    
    /// 路由模块的路由表定义
    /// - Parameters:
    ///   - to: 路由名的定义
    ///   - routerClass: 路由目标类
    public convenience init(to: String, routerClass: AnyClass) {
        self.init()
        self.to = to
        self.routerClass = routerClass
    }
}

校验

路由的定义

通过上面,让一个页面实现路由,那么只需要实现一下路由协议,按路由协议实现对应的方法就能够有路由的功能。再加上一个extension方法去添加一个路由表就行。 整体定义少的话就两个方法,如下:

// 正常的路由定义
extension TestClassName: YYRoutable {
    static func createInstance(params: [String : Any]) -> YYRoutable {
        return TestClassName()
    }
}
// 多定义一个extension方法
// 添加路由表
extension YYRouter {
    @objc func router_TestClassName() -> YYRouterModel {
        return YYRouterModel(to: "native://testTest", routerClass: TestClassName.self)
    }
}

路由的使用

YYRouter.pushTo(jumpParams: ["to": "native://testTest", "name": "1"])

自动校验

看上面的定义,非常简单,就两个方法。 不过这两个方法的定义都是有要求的。那么假如使用者没有按规则定义怎么办。

  1. 比如,类已经实现了YYRoutable协议,但是忘记加extension的router_xxx方法添加路由表的,会路由失败。
  2. 比如,添加了router_xxx方法添加了路由表,但是忘记实现YYRoutable协议的。
  3. 比如,添加了router_xxx方法添加了路由表,但是添加的router_xxx方法,没有按规定,导致方法不生效。(@objc,函数命名,函数入参,函数返回值等不规范)
  4. 比如,添加的路由表的key重复了,怎么办。

为了解决这些遗漏的问题,我写了一个自动校验的函数。通过路由初始化的时候,把上面的问题全部检查一遍,有问题直接中断,然后控制住这函数只在Debug里面才运行就行,不影响线上。函数如下:

#if DEBUG
    /// 自动检查所有的路由设置是否符合规范,当发现不符合规范的路由设置,直接中断
    private static func checkRoutableClassesSettingIsConform() {
        guard !isCheck else { return } // 只检查一次
        let expectedClassCount = objc_getClassList(nil, 0)
        let allClasses = UnsafeMutablePointer<AnyClass>.allocate(capacity: Int(expectedClassCount))
        let autoreleasingAllClasses = AutoreleasingUnsafeMutablePointer<AnyClass>(allClasses)
        let actualClassCount: Int32 = objc_getClassList(autoreleasingAllClasses, expectedClassCount)

        for i in 0 ..< actualClassCount {
            let currentClass: AnyClass = allClasses[Int(i)]
            if (class_getInstanceMethod(currentClass, NSSelectorFromString("methodSignatureForSelector:")) != nil),
               (class_getInstanceMethod(currentClass, NSSelectorFromString("doesNotRecognizeSelector:")) != nil),
               let cls = currentClass as? YYRoutable.Type {
                var isSet = checkList["\(cls)"]
                if isSet == nil {
                    var curCls: AnyClass = cls as AnyClass
                    // 只要有一个父亲添加了路由表,就表示ok,因为路由那边是不允许子类实现路由协议的,子类只能继承,只能override,或者换别的类去实现路由
                    while let superCls = curCls.superclass() {
                        if checkList["\(superCls)"] != nil {
                            isSet = true
                            break
                        }
                        curCls = superCls
                    }
                }
                assert(isSet != nil, "\(cls)有实现YYRoutable协议,但是没有添加路由表,或者路由表配置没有按规范,请检查:\(cls)。")
                checkList["\(cls)"] = true
            }
        }
        for (key, value) in checkList where value == false {
            assert(false, "\(key)有添加路由表,但是没有实现YYRoutable协议,请检查:\(key)。")
        }
        isCheck = true
    }
#endif

函数做两个事情

  1. 有添加路由表,但是没有实现YYRoutable协议
  2. 有实现YYRoutable协议,但是没有添加路由表,或者路由表配置没有按规范定义。

还缺少一个路由表重复。这个可以在添加路由表的时候加多一个判断就能解决,

具体看源码:YYRouter

希望点Star支持。