网络请求中的block

场景

block和delegate是iOS开发者经常用到的技术,也常常出现在各种面试题里,你经常听到他们之间的对比。

我的态度是每个成熟的技术并没有明显的优劣,不应该用谁好谁劣来评判他们,而应该看谁更适合应用场景,在合适的场合选择合适的技术。

本篇文章将讨论在 网络层调用和回调 这个场景下的技术选择。

本文涉及代码

Block回调

一个常见的Block回调,通常是业务代码调用请求,然后在回调中获得返回的数据,然后执行业务逻辑,如下:

// 业务层代码
- (void)blockDemo {
    [self.service requestWithParms:nil WithResult:^(id data, NSError *error) {
        // 处理业务逻辑代码
        // ...
        NSLog(@"Result from block:%@", data);
    }];
}

考虑点1:内存

之所以会考虑这个问题,是因为用户使用时常常会刚进入一个页面,就立刻点返回,此时网络请求刚发出去,数据返回有可能还没回来,那么这个网络请求会怎么样呢?

当前页面controller能够被pop出栈,释放掉吗?毕竟网络请求还没结束

我们一一来验证

controller销毁

如何验证内存释放已经释放,很简单,首先我写的网络请求并不是真的网络请求,他只会延时5s返回一个假的数据,用来方便模拟网络请求

- (void)requestWithParms:(NSDictionary *)parms WithResult:(ResultBlock)result {

    int delay = NET_DELAY; // 默认是5s,可以传参数改变

    if ([parms valueForKey:@"delayTime"] != nil) {
        delay = (int)[parms valueForKey:@"delayTime"];
    }

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSString *resultData = @"This is a Mock Data!!";
        NSLog(@"Network Finish:%@",resultData);
        if (result) {
            result(resultData, nil);
        }
    });
}

再在viewcontroller的dealloc方法里打印log,就能知道dealloc是否被调用,如果调用说明可以释放

- (void)dealloc {
    NSLog(@"NextPageViewController has been dealloc!")
}

这样验证起来就是很简单,只需要执行blockDemo方法,然后立刻点返回,退出当前vc,等5秒后如果看结果

结果是vc可以释放的

2018-03-25 19:36:07.345523+0800 NetworkCallback[4580:3600834] NextPageViewController has been dealloc!

Block捕获外界变量销毁

我们再进一步想想,Block有个最大的特点是可以访问当前的作用域,我们随便创建一个数组,重复上面操作,是否能够销毁

- (void)blockDemo {
    NSArray *outsideArray = @[@1, @2, @3];

    [self.service requestWithParms:nil WithResult:^(id data, NSError *error) {
        // 处理业务逻辑
        // ...

        NSLog(@"Result from block:%@", data);
        NSLog(@"outsideArray :%@", outsideArray);
    }];
}

打印结果:

2018-03-25 19:55:40.997535+0800 NetworkCallback[4970:3641450] NextPageViewController has been dealloc!
2018-03-25 19:55:44.831721+0800 NetworkCallback[4970:3641450] outsideArray :(
    1,
    2,
    3
)

神奇不?!

注意,vc先销毁了,但是5s后,这个临时变量竟然还没有销毁。那么这个变量存储在哪里呢?留个悬念

考虑一下,如果你在Block代码里访问了一个超大的文件,这个文件必然是保存内存的,然后此时你遇上了网络慢,接口好久没有返回,那么这个超大的文件就会一直占用内存

继续,如果我在Block中访问self呢?此时的self就是当前的controller,这时候可以销毁吗?

- (void)blockDemo {
    NSArray *outsideArray = @[@1, @2, @3];
    [self.service requestWithParms:nil WithResult:^(id data, NSError *error) {
        // 处理业务逻辑
        // ...

        NSLog(@"Result from block:%@", data);
        NSLog(@"outsideArray :%@", outsideArray);
        NSLog(@"self :%@", self);
    }];
}

打印结果:

2018-03-25 20:04:21.012135+0800 NetworkCallback[5224:3659292] Network Finish:This is a Mock Data!!
2018-03-25 20:04:21.012476+0800 NetworkCallback[5224:3659292] Result from block:This is a Mock Data!!
2018-03-25 20:04:21.013186+0800 NetworkCallback[5224:3659292] outsideArray :(
    1,
    2,
    3
)
2018-03-25 20:04:21.013675+0800 NetworkCallback[5224:3659292] self :<NextPageViewController: 0x7f993662cf20>
2018-03-25 20:04:21.013858+0800 NetworkCallback[5224:3659292] NextPageViewController has been dealloc!

看到了吗?dealloc是最后打印出来的,也就是说Block不返回,controller就释放不了了!

看起来是在Block内访问谁,谁就无法释放啊!

有人会想这是不是就是循环引用呢?

请大家回忆一下:Block循环引用是self强引用Block,Block里面再强引用self,这里Block确实强引用了self,但是self并没有强引用Block,这个Block是一个参数传给了NetService,跟self并无关系

而且是循环引用的话,那么vc会一直释放不掉,但看上面的log,其实是可以释放的,只是释放的时机被延后了

但是,无论如何,我们试试换成 weakself 会怎么样呢? 打印结果:

2018-03-25 20:07:10.944557+0800 NetworkCallback[5294:3665537] NextPageViewController has been dealloc!
2018-03-25 20:07:15.228961+0800 NetworkCallback[5294:3665537] Network Finish:This is a Mock Data!!
2018-03-25 20:07:15.230836+0800 NetworkCallback[5294:3665537] Result from block:This is a Mock Data!!
2018-03-25 20:07:15.231074+0800 NetworkCallback[5294:3665537] outsideArray :(
    1,
    2,
    3
)
2018-03-25 20:07:15.231190+0800 NetworkCallback[5294:3665537] weakSelf :(null)

没问题,果然换成weakself就解决了

原因是什么?

为什么在Block内访问谁,谁就无法释放呢?为什么用weakself就解决了呢?

Block的本质是个对象

Block看起来像一个函数,其实在objectice-c中,它是个对象,之所以Block可以捕获外部变量,正是因为它是个对象,他有自己的属性,他用属性强引用了外部变量,导致外部变量(就是上面的self和outsideArray)的引用计数不为0,也就不能释放了

weakself做了什么

当Block中访问weakself的时候,强引用并没有指向self,而是指向weakself,所以self可以被释放

内存小结

  1. 使用Block无论是否有循环引用的可能,都要使用weakself,来防止vc被持有,而延迟释放
  2. Block会导致对象的生命周期被延长,特别是当某些大文件被Block访问时,有几率导致内存不足

考虑点2:代码安全

这是基于上面的考虑,我们已经知道要用weakself来保证controller被及时释放,也可以在上面log中看到weakself变成了nil,此时有可能导致crash,因为我们正在操作一个nil对象 想象一个业务场景:分页请求,你拉取了前面几页,比如page=3,然后去拉下一页数据,此时网络请求尚未返回,用户就退出当前页面,此时

  1. 如果页面能够被释放,那么Block中的业务逻辑代码被执行吗?
  2. 如果可以执行会有什么危险?

其实第一个问题上面的log已经回答了,log之所以被打印出来,其实就是Block中的代码被执行了嘛。

也就是说即使controller已经销毁,Block中的代码还是会被执行

第二个问题,执行了会有什么危险

  1. 通常这里会做json转model,会做某些数据转换,如果返回数据很大,比如是个三千多个元素的数组,那么势必浪费CPU去执行,注意,此时controller已经销毁了,执行代码是无意义的,这里的CPU是确确实实的浪费掉了。
  2. 想像一下,此时weakself是nil,如果是分页请求的数据,你通常是把新的数据加到某个数组里,然后你就crash了,因为你把nil加到数组去了
- (void)blockDemo {
    __weak typeof(self) weakSelf = self;
    NSArray *outsideArray = @[@1, @2, @3];
    [self.service requestWithParms:nil WithResult:^(id data, NSError *error) {
        // 处理业务逻辑
        // ...
        [data addObject:weakSelf.pageArray]; // weakSelf是nil
    }];
}

因此,你必须小心翼翼,写上的保护代码

- (void)blockDemo {
    __weak typeof(self) weakSelf = self;
    NSArray *outsideArray = @[@1, @2, @3];
    [self.service requestWithParms:nil WithResult:^(id data, NSError *error) {

        // 保护代码
        if (weakSelf == nil) {
            return;
        }

        // 处理业务逻辑
        // ...
    }];
}

代码安全总结

  1. Block会有执行无意义代码的可能,浪费CPU
  2. Block会有操作nil对象导致crash的可能,因此要写保护代码

Delegate回调

经过上面的验证,看起来好像Block有挺多麻烦了,那么delegate怎么样呢?我们也来试一试

首先是模拟网络请求,然后通过delegate回调

- (void)requestWithParms:(NSDictionary *)parms {
    int delay = NET_DELAY;

    if ([parms valueForKey:@"delayTime"] != nil) {
        delay = (int)[parms valueForKey:@"delayTime"];
    }

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

        // 判断成功
        // 判断失败

        NSString *resultData = @"This is a Mock Data!!";
        NSLog(@"Network Finish:%@",resultData);

        // ***这里加了判断***
        if (self.delegate && [self.delegate respondsToSelector:@selector(networkFinishWithSuccess:AndError:)]) {
            [self.delegate networkFinishWithSuccess:resultData AndError:nil];
        }
    });
}

注意我用***标注的注释,这里有个delegate的判断,这里保证了Block考虑点2不会出现,因为当delegate为nil的时候,绝对不会执行delegate的方法.

演示一下,代码如下

#pragma mark - Delegate request
- (void)delegateDemo {
    self.service.delegate = self;
    [self.service requestWithParms:nil];
}

#pragma mark - NetWorkDelegate
- (void)networkFinishWithSuccess:(id)data AndError:(NSError *)error {
    NSLog(@"Result from Delegate:%@", data);
}

验证block存在的问题

同样的,一进入Nextpage就立刻点返回,等5s看看代码会不会执行

2018-03-25 21:08:23.422103+0800 NetworkCallback[6399:3784145] NextPageViewController has been dealloc!

只有一条log,说明内存释放没有问题,而且在回调前对delegate的判断,使得我们非常方便的得知业务层是否还存在了,而如果用Block来实现就很麻烦,在网络回调前是无法得知的,一定要在Block里面加判断代码

总结:

  1. 在业务层delegate比Block更加优雅,可以在网络层回调前就中断逻辑,把错误发生的可能提前中断,而不必进入业务层才做判断,这是一个很好的隔断
  2. 没有延长某个对象生命周期,代码更加清晰,易于管理

delegate自己的问题

那么难道delegate就没有缺点了吗?

多个业务层请求

之前的demo中只有一个业务层,工程中绝对不会只有一个,而NetService的delegate只能指向一个对象,岂不是只有一个请求能够拿到回调,这岂不是滑天下之大稽?

当然不能这样,如果使用delegate,就必须对每个请求封装成一个对象,而不能统一的用一个NetService

@interface RequestAPI : NSObject

@property (nonatomic, weak, nullable) id<NetWorkDelegate> delegate;

/**
 模拟Delegate请求方法

 @param parms 请求参数
 */
- (void)requestWithParms:(NSDictionary *)parms;

可是每个对象都去实现一篇请求逻辑岂不是很傻?!

所以底层还是调用NetService


#import "RequestAPI.h"

@implementation RequestAPI

- (void)requestWithParms:(NSDictionary *)parms {
    __weak typeof(self) weakSelf = self;
    [[NetService alloc] requestWithParms:parms WithResult:^(id  _Nonnull data, NSError * _Nonnull error) {
        if (weakSelf && [weakSelf respondsToSelector:@selector(networkFinishWithSuccess:AndError:)]) {
            [self.delegate networkFinishWithSuccess:resultData AndError:nil];
        }
    }];
}

总结

所以其实,使用delegate和Block的结合使用,由此我们可以看出

  1. Block适合做集约型调用,每个业务逻辑不一样,但是我们可以通过把代码封装在Block中,然后发给统一的方法来处理,实现了统一方法处理不同的逻辑
  2. delegate适合离散型调用,每次返回是同样的逻辑
  3. 网络层调用要delegate和Block结合使用,在业务层回调适合delegate,在底层网络处理适合Block

引用于

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,001评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,210评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,874评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,001评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,022评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,005评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,929评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,742评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,193评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,427评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,583评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,305评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,911评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,564评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,731评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,581评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,478评论 2 352

推荐阅读更多精彩内容