借鉴Alamofire解决异步回调问题(Swift)

来源:互联网 发布:印度人在中国 知乎 编辑:程序博客网 时间:2024/05/17 22:56

遇到的问题

  今天在做一个swift练习demo时遇到了这样一个问题,我需要实现一个gps定位功能,于是封装一个LocationManager类去处理定位相关的逻辑,外部调用者需要获取定位信息时,直接调用LocationManager提供的getCurrentGpsInfo方法即可获取到当前的gps信息。

  getCurrentGpsInfo函数内部实现大概是这样的

        .......        //clManager是一个CLLocationManager对象        clManager.startUpdatingLocation()        .......
  主要就是调用CLLocationManager的startUpdatingLocation方法获取gps信息。但是现在问题来了,这个startUpdatingLocation是个异步方法,它的gps信息是通过代理(<CLLocationManagerDelegate>)的locationManager(manager:locations:)方法反馈的,我应该如何把gps信息返回给外部调用者呢?

可能的解决方式

  这种情况其实在开发中会经常遇到,一般我们可以用如下几种方式去处理

1.通知:

  结果返回后,通过通知将结果抛出去。呃,理论上没问题,但这种设计实在不是很合适,既不存在一对多的关系,也不存在跨层反馈的情况,信息取到了满世界喊,调用者还要维护通知的管理......,pass

2.代理:

  代理不错,CLLocationManager通过代理把结果反馈给了LocationManager,我再让LocationManager通过代理将信息传递出去就可以了。实现起来很简单,但这也不是我想要的,代理会造成逻辑的割裂,对调用者不是十分友好,pass

3.block:

  在oc中,通常情况下,我优先会选择使用block回调方式将结果返回,大概的实现应该是这样的

  为LocationManager定义一个block属性

//为LocationManager定义一个block属性@property (nonatomic,copy) void (^returnGpsInfoBlk)(GpsInfo *gpsInfo);

  getCurrentGpsInfo函数定义一个block参数,在函数体中,将block参数通过returnGpsInfoBlk属性保持住

-(void)getCurrentGpsInfo:(void(^)(GpsInfo *gpsInfo))result{    ...........    //维持住block回调    self.returnGpsInfoBlk = result;    [_clManager startUpdatingLocation];    ..........}

  CLLocationManager回调中,回调returnGpsInfoBlk将信息反馈给外部调用者

- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations{    ........    _returnGpsInfoBlk(gpsInfo);    .......}

swift中尝试解决

  OC中我们可以通过block的方式实现我们的需求,Swift中我们有闭包,有函数,函数更是成为了一等公民,类比实现应该不难.
  getCurrentGpsInfo在Swift中的定义
func getGpsInfo(result:(GpsInfo)->Void) -> Void{}
 然后我们可以为LocationManager定义一个函数类型属性,但是......

  哦,对,我们要初始化这个属性,但是......





  难道只能这样?


  给它初始化一个空实现,这样确实没问题,但不是我想要的,设计上感觉很别扭,这个回调是我用来让外部调用者去实现的,为什么我要去给它实现一个空方法?理论上,可以把这个空实现类比成nil,但它毕竟不是nil。如此一来,我无法区分出returnGpsInfo到底有没有真正被调用者实现,感觉这个是很危险的事情。

寻求办法:

  既然我一时想不到满意的解决方式,于是就想到了看看别人是如何处理的,上来想到的就是Alamofire,网络请求肯定也会遇到和我类似的问题,让我看看大牛是如何处理的.

  api调用方式如下,request发出请求,responseString是个异步回调得到结果


  看看responseString内部具体细节,来到了response方法


  delegate.queue是个OperationQueue


  可以看到,这个函数将主体实现(拿到dataResponse,通过completionHandler将response反馈出去)全部加到了operation队列中.

  似乎明白点儿什么,网络请求是异步的,请求的结果也是在别处(网络请求相关代理方法)返回的,之前我遇到的问题也是一样的,结果是在别处(定位相关代理方法)返回的,所以我们试图保持住回调闭包,以便在代理方法中可以访问到被保持的闭包,将结果反馈出去,但是最终发现没有一个合理的方案去保持这个闭包。

  换个思路,闭包不好保持,但结果数据好保持啊,具体到这里就是网络请求回来的数据对象,我们在网络请求代理回调里,可以把拿到的数据对象保持住,然后再告知上层去获取解析这个数据对象,于是问题就从如何保持一个闭包转化为了如何协调:

  调用者发起请求->调用者等待结果->异步请求,拿到结果,通过对象保持住结果->告知调用者(通过对象)取结果,并将结果进一步反馈。调用者等待的时候完全不用离开发起请求的函数体,唤醒后继续执行当前函数体,于是也就规避了闭包的保持问题.

  问题从保持闭包转化为了流程协调,Alamofire的解决方式就是通过OperationQueue,具体一点就是借助OperationQueue的suspend.



大概流程是这样的:

1.发起请求,建立一个队列

2.将队列暂停住

3.把调用者的回调处理加入队列

4.在网络数据返回后,通过对象保持住返回的数据,取消队列暂停

5.调用者的回调处理被队列执行,调用者此时可以借助保持对象拿到返回的数据结果,进行后续的处理.

问题解决:

  OK,解决的思路学会了,下面在我的demo中尝试一下,不过我个人更习惯使用GCD,于是最终实现基于的是GCD的
  大概实现(Demo还没写完,所以很多逻辑都没补全,也不是十分严谨,仅为描述最终的解决方式)
    //定义一个队列    let queue:DispatchQueue    //用于保持结果的变量    var gpsInfo:GpsInfo?
func getGpsInfo(result:@escaping (GpsInfo)->Void) -> Void{        //挂起队列        queue.suspend()        //将回调处理加入队列        queue.async{            result(self.gpsInfo!)        }        //开始定位        clManager.startUpdatingLocation()    }
//CLLocationManager回调    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {        //停止定位        clManager.stopUpdatingLocation()        //停止后还会返回几次回调,临时简单加个保护措施,仅为实验能正常跑完        guard gpsInfo != nil else {            //保持结果,随便写个假数据            gpsInfo = GpsInfo(longitude: 0, latitude: 0, country: "", province: "", city: "")            //恢复队列            queue.resume()            return        }    }
//外部调用,一个button的点击    func butClick() -> Void {        LocationManager.shareLocationManager.getGpsInfo { (gpsInfo) in            print(gpsInfo)        }        print("go on")    }
运行结果:

  go on先被打印,异步回调print(gpsInfo)也在数据返回后被正确回调。

AFNetworking是如何处理的?

  问题解决了,但是这么费波折有点儿出乎了我的意料,于是我不禁想起,OC中常用的AFNetworking库(二者的作者是同一个人)是如何处理这个问题的呢?会不会也有一些出乎我意料的地方?




  看了一下,结果还好,并没有出人意料,使用的方式就是block保持

另一种解决方式与思考

  就在我折腾半天终于把问题解决了之后,在一篇介绍逃逸闭包的博文中看到了另一种特别简单的解决方式,截图:

  

   呃,好简单,用个数组维护就好了,似乎是个很好的方法,但是

1.从设计层面上想,还是觉得有点儿别扭,我的回调只有一个,为什么要用数组?

2.这种方式会不会有什么逻辑隐患?后期数组的维护会涉及到哪些问题?

3.为什么alamofire没有采取这种方式?使用OperationQueue仅仅是为了解决和我相同的问题吗,还是另有别的好处或别的原因?

4.如果这种方式是好的,既然要引入了一个包装对象,我也不想用数组。借助泛型,自己定义实现一个结构,专门用于维持函数回调,而不借助数组

......

  想不到写个练习小demo,最后牵扯出这么多问题,后面还需要日后继续思考总结

修正:在后续的工作实践中发现,其实当初还是对swift语法不熟悉导致了弯路,正确的写法应该是var returnGpsInfo:((GpsInfo)->Void)?,少写了个括号,可选型的修饰给了返回值,而不是函数变量本身,所以产生了错误。不过弯路绕的也值得,也学到了一些新的知识。

0 0