Swift路由-LARouter远端服务调用

2023-07-06  本文已影响0人  AKyS佐毅

1. 背景

image.png

基于客户端已经实现了Swift版本的路由动态注册能力,为了扩展路由能力,结合服务模块对路由进行改造,使其具有动态调用服务的能力。可通过WebSocket、MQTT、JSBridge等调起服务。

2. 实现方式

如何让LARouter也具有调用远端服务的能力

类似整个dubbo服务的注册调用流程

image.png

通过路由,将路由对服务的调用转发到具体的服务进行调用,并返回结果

核心实现

 // 服务调用
// 服务调用
public class func routerService(_ uriTuple: (String, [String: Any])) -> Any? {
    let request = LARouterRequest.init(uriTuple.0)
    let queries = request.queries
    guard let protocols = queries["protocol"] as? String,
            let methods = queries["method"] as? String else {
        assert(queries["protocol"] != nil, "The protocol name is empty")
        assert(queries["method"] != nil, "The method name is empty")
        shareInstance.routerLogHandle?(uriTuple.0, .logError, "protocol or method is empty,Unable to initiate service")
        return nil
    }
    // 为了使用方便,针对1个参数或2个参数,依旧可以按照ivar1,ivar2进行传递,自动匹配。对于没有ivar1参数的,但是方法中必须有参数的,将queries赋值作为ivar1。
    shareInstance.routerLogHandle?(uriTuple.0, .logNormal, "")
  
    if let functionResultType = uriTuple.1[LARouterFunctionResultKey] as? Int {
        if functionResultType == LARouterFunctionResultType.voidType.rawValue {
            self.performTargetVoidType(protocolName: protocols,
                                                   actionName: methods,
                                                   param: uriTuple.1[LARouterIvar1Key],
                                                   otherParam: uriTuple.1[LARouterIvar2Key])
            return nil
        } else if functionResultType == LARouterFunctionResultType.valueType.rawValue {
            let exectueResult = self.performTarget(protocolName: protocols,
                                                   actionName: methods,
                                                   param: uriTuple.1[LARouterIvar1Key],
                                                   otherParam: uriTuple.1[LARouterIvar2Key])
            return exectueResult?.takeUnretainedValue()
        } else if functionResultType == LARouterFunctionResultType.referenceType.rawValue {
            let exectueResult = self.performTarget(protocolName: protocols,
                                                   actionName: methods,
                                                   param: uriTuple.1[LARouterIvar1Key],
                                                   otherParam: uriTuple.1[LARouterIvar2Key])
            return exectueResult?.takeRetainedValue() 
        }
    }
    return nil
}
 //实现路由转发协议
public class func performTarget(protocolName: String,
                                 actionName: String,
                                 param: Any? = nil,
                                 otherParam: Any? = nil,
                                 classMethod: Bool = false) -> Unmanaged<AnyObject>? {
    if classMethod {
        let serviceClass = LARouterServiceManager.default.servicesCache[protocolName] as? AnyObject ?? NSObject()
        assert(LARouterServiceManager.default.servicesCache[protocolName] != nil, "No corresponding service found")
        let selector  = NSSelectorFromString(actionName)
        guard let _ = class_getClassMethod(serviceClass as? AnyClass, selector) else {
            assert(class_getClassMethod(serviceClass as? AnyClass, selector) != nil, "No corresponding class method found")
            shareInstance.routerLogHandle?("\(protocolName)->\(actionName)", .logError, "No corresponding class method found")
            return nil
        }
        return serviceClass.perform(selector, with: param, with: otherParam)
    } else {
        let serviceClass = LARouterServiceManager.default.servicesCache[protocolName] as? AnyObject ?? NSObject()
        let selector = NSSelectorFromString(actionName)
        guard let _ = class_getInstanceMethod(type(of: serviceClass), selector) else {
            assert(class_getInstanceMethod(serviceClass as? AnyClass, selector) != nil, "No corresponding instance method found")
            shareInstance.routerLogHandle?("\(protocolName)->\(actionName)", .logError, "No corresponding instance method found")
            return nil
        }
        return serviceClass.perform(selector, with: param, with: otherParam)
    }
}

使用路由调用服务

异步带Block调用

let callBack: @convention(block) (Bool, [AnyHashable: Any]?) -> Void = { success, orderDetailInfo in
    print("Completed \(success) \(String(describing: orderDetailInfo))")
}
let userInfo: [String: Any] = ["ivar1": "164030287699771401711", "ivar2": callBack]
let result = LARouterBuilder().buildService(OrderServiceProtocol.self, methodName: LARouterOrderServiceApi.requestOrderDetailCallBack).buildDictionary(param: userInfo).fetchService()
// 效果同上
let result2 =  LARouter.openURL(("scheme://services?protocol=OrderServiceProtocol&method=requestOrderDetail:callBack:",userInfo))

同步调用

let result1 = LARouterBuilder().buildService(OrderServiceProtocol.self, methodName: LARouterOrderServiceApi.hasProcessOrder).fetchService() as? String
print("同步服务调用 \(String(describing: result1?.xl.bool))")
// 效果同上
let result2 =  LARouter.openURL("scheme://services?protocol=OrderServiceProtocol&method=hasProcessOrder")

服务按照字符串方式调用

struct XLTSLAlertServiceDebug: XLDebugable {
    var name = { "特斯拉弹框服务" }()

    var action: DebugableClosure = {

        let resultArray = LARuntimeUtils.instanceMethodsSelectors(from: OrderServiceProtocol.self)
        print("resultArray: \(resultArray)")
        var data: LANeuronData = LANeuronData()
        data.title = "特斯拉弹框服务"
        data.content = " 基于客户端已经实现了Swift版本的路由动态注册能力,为了扩展路由能力,结合服务模块对路由进行改造,使其具有动态调用服务的能力。可通过WebSocket、MQTT、JSBridge等调起服务。"
        data.leftButtonText = "取消"
        data.leftButtonPath = ""
        data.rightButtonText = "进入订单列表"
        data.leftButtonPath = ""
        data.rightButtonPath = "scheme://order/list?jumpType=1"

       let remoteDict = ["title": data.title ?? "",
                    "content": data.content ?? "",
                    "leftButtonText": data.leftButtonText ?? "",
                    "rightButtonText": data.rightButtonText ?? "",
                    "rightButtonPath": data.rightButtonPath ?? "",
                    "leftButtonPath": data.leftButtonPath ?? ""]
        let dict = ["ivar1": remoteDict]

        let url = "scheme://services?protocol=OrderServiceProtocol&method=showTeslaAlertWithInfo:"
        LARouter.openURL((url, dict))
    }
}

struct XLWalletServiceDebug: XLDebugable {
    var name = { "钱包路由服务-保证金弹框" }()

    var action: DebugableClosure = {

        let resultArray = LARuntimeUtils.instanceMethodsSelectors(from: WalletServiceProtocol.self)
        print("resultArray: \(resultArray)")

        LARouterBuilder().buildService(WalletServiceProtocol.self, methodName: LARouterWalletServiceApi.showDepositAlertView).buildString(key: "ivar1", value: "保证金到期了请立即续费吧").fetchService()

        let dict = ["ivar1": "保证金到期了请立即续费吧"]
        LARouter.openURL(("scheme://services?protocol=WalletServiceProtocol&method=showDepositAlertViewWithMsg:", dict))
    }
}

硬编码问题解决

为了防止硬编码的问题,在LARouter中,增加了服务的构造器方法,传入ProtocolName\MethodName即可构造出服务路由,具体构造方法如下

@discardableResult
public func buildService<ServiceProtocol>(_ protocolInstance: ServiceProtocol.Type, methodName: String) -> Self {
    let protocolName = String(describing: protocolInstance)
    buildResult.0 = "\(LASerivceHost)protocol=\(protocolName)&method=\(methodName)"
    return self
}

public func fetchService() -> Any? {
   let result = LARouter.generate(buildResult.0, params: buildResult.1, jumpType: .push)
   return LARouter.openURL(result)
}

注意点: 我们将方法名的调用使用常量字符串的形式

//let resultArray = LARuntimeUtils.instanceMethodsSelectors(from: OrderServiceProtocol.self)
//print("resultArray: \(resultArray)")

// MARK: - 订单服务方法常量定义,使用runtime方式获取服务方法
/// 钱包服务
public struct LARouterWalletServiceApi {
    public static let showDepositAlertView = "showDepositAlertViewWithMsg:"
}

3. 实现过程中遇到的问题

这里需要注意点是

  1. 方法名称出入问题
 /// 展示特斯拉弹框
 func showTeslaAlert(info: [String: Any])

showTeslaAlert 在服务调用中的真实方法是 showTeslaAlertWithInfo:

小工具: 获取协议的方法列表

+ (NSArray *)instanceMethodsSelectorsFromProtocol:(Protocol *)aProtocol {
    NSMutableArray *tmp = [NSMutableArray array];
    unsigned int numMethods;
    struct objc_method_description *methods = protocol_copyMethodDescriptionList(aProtocol, NO, YES, &numMethods);
    if (methods) {
        for (int i = 0; i < numMethods; i++) {
            NSString *methodName = NSStringFromSelector(methods[i].name);
            [tmp addObject:methodName];
        }
        free(methods);
    }
    methods = protocol_copyMethodDescriptionList(aProtocol, YES, YES, &numMethods);
    if (methods) {
        for (int i = 0; i < numMethods; i++) {
            NSString *methodName = NSStringFromSelector(methods[i].name);
            [tmp addObject:methodName];
        }
        free(methods);
    }

    return [NSArray arrayWithArray:tmp];
}
  1. 支持服务调用的返回值不能是常用的数据类型,比如Bool 、Int、CGFlocat等类型,会崩溃,目前解决方案是转为指针类型
@objc
public protocol OrderServiceProtocol: ServiceProtocol {
    /// 是否有行程中的订单
    var hasProcessOrder: String { get }

   //  var hasProcessOrder: Bool { get }

    /// 展示特斯拉弹框
    func showTeslaAlert(info: [String: Any])
} 

4. 限制条件

  1. func perform(_ aSelector: Selector!, with object1: Any!, with object2: Any!) -> Unmanaged<AnyObject>! 最多支持两个参数,需要我们对现有的服务接口进行改造,多于两个参数的使用字典进行包装,解析时根据key赋值即可。

  2. 协议的方法、类的方法,无法以selector的形式传入,必须传入字符串

5. 支持如下调用方式

  1. 支持直接使用路由进行服务调用

  2. service调用也可以做到远端调用

  3. 通过路由可以拿到对应页面调转事件结果,可以拿到对应service的返回结果,可以通过远端下发做到某些特定的操作,例如打开某个service的计时器,改变某个页面的颜色等。

上一篇 下一篇

猜你喜欢

热点阅读