Objective-C中的Block

假设你有一间屋子需要刷漆,而你自己不会刷,而且还嫌麻烦。于是呢,你需要一个会刷漆的人替你完成任务,Worker类。

Worker是一个会刷漆的工人,有自己的名字name。在完成工作后,会通知雇主任务做完了。name是一个不能被外部改动的属性,毕竟被别人起外号也不是什么让人开心的事。因此在初始化时必须指明Worker对象的名字。然后就是开始刷漆的 paint 方法,还有一个对雇主的回馈Block,如果雇主不太在意完成进度, 那么这个Block可以什么都没有。那么我们的Worker应该具有下面的公有属性和方法。同时为了更好的跟踪Worker的行为,我们重写了dealloc和description两个方法,当工人完成工作离开我们的房间后,他会主动告诉我们一声。

Worker.h

@property (nonatomic, readonly, copy) NSString *name;
@property (nonatomic, copy) void(^complete)(void);
- (instancetype)initWithName:(NSString *)name;
- (void)paint;

Worker.m

- (instancetype)initWithName:(NSString *)name
{
    if (self = [super init]) {
        _name = name;
    }
    return self;
}

- (void)paint
{
    NSLog(@"%@正在工作", self);
    if (self.complete != nil) {
        self.complete();
    }
}

- (NSString *)description
{
    return [NSString stringWithFormat:@"worker%@", self.name];
}

- (void)dealloc
{
    NSLog(@"%@完成工作离开了", self);
}

接下来我们在新建工程的默认控制器里面雇佣一个Worker并让他粉刷我们的房间。

UIViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];
    Worker *workerA = [[Worker alloc] initWithName:@"A"];
    workerA.complete = ^{
        NSLog(@"完成了%@的委托工作", self);
    };
    [workerA paint];
}

接下来是工作进度的报告

workerA正在工作
完成了ViewController: 0x7fcfd1c57c50的委托工作
workerA完成工作离开了

看上去我们的workerA完美的完成了任务,并且离开了我们的屋子(调用了dealloc)。可能你会注意到,在workerA的Block里面使用了Self关键字,稍微考虑了一下下会不会发生循环引用。这个例子是没有发生循环引用的,因为workerA作为一个临时变量,并未有任何对象记得他的存在,仅仅只是在Block回调里面单向指向了ViewController。所以workA还是正常的完成任务并且释放了。

倘若我们在ViewController增加一个属性,并且对worker进行引用,那么循环引用还是会发生的。就像下面这样:

self.worker = workA;

以下是工作进度报告:
workerA正在工作
完成了ViewController: 0x7f9e40d90960的委托工作

我们发现workerA在完成工作后,并没有离开我们的屋子(调用dealloc),难道循环引用发生了,循环引用确实是发生了,但是这次没有像上次那样在完成工作后离开,只是因为ViewController一直没有释放workerA;
通过调用以下语句是可以完成workerA的正确释放。

self.worker = nil;

接下来我们考虑更深层次的功能结构:你需要雇佣很多工人同时给你刷漆,但是呢你发现你在追踪粉刷进度上出现了困扰(你需要给每一个工人都添加complete回调,或许你可以通过一个循环给他们添加,但是面对工人个体化的需求时,还是会出现更多的代码),于是你将任务委托给了一个工头:WorkerLeader。WorkerLeader负责管理所有工人的进度,并最后通过Block告诉雇主,任务全部完成了。那么WorkerLeader的头文件大概是这个样子的。

WorkerLeader.h

@property (nonatomic, strong) NSArray *workers;

- (void)letWorkerStartWorkWithComplete:(void(^)(void))complete;

具体实现部分是这样的,我用一个单线程的循环来模拟WorkerLeader指挥工人工作(实际中,可能是多线程,然后任务更加复杂),同样的为了追踪WorkerLeader,也重写了两个方法。

WorkerLeader.m

- (void)letWorkerStartWorkWithComplete:(void(^)(void))complete
{
    if (self.workers.count > 0) {
        for (Worker *worker in self.workers) {
            if (worker.complete == nil) {
                worker.complete = ^{
                    NSLog(@"完成了%@分配任务", self);
                };
            }
            [worker paint];
        }
        if (complete != nil) {
            complete();
        }
    }
}

- (NSString *)description
{
    return @"WorkerLeader";
}

- (void)dealloc
{
    NSLog(@"WorkerLeader离开了");
}

接下来我们只需要指挥WorkerLeader工作并且听他的汇报就可以了。

ViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];

    Worker *workA = [[Worker alloc] initWithName:@"A"];
    Worker *workB = [[Worker alloc] initWithName:@"B"];
    WorkerLeader *workerLeader = [[WorkerLeader alloc] init];
    workerLeader.workers = @[workA, workB];
    [workerLeader letWorkerStartWorkWithComplete:^{
        NSLog(@"全部任务完成了");
    }];}

以下为工作汇报

workerA正在工作
完成了WorkerLeader分配任务
workerB正在工作
完成了WorkerLeader分配任务
全部任务完成了

你会发现这一次Worker和WorkerLeader都没有离开,都没有调用dealloc,如果你需要雇佣好几个WorkerLeader以及Worker的话,慢慢的你的房间里会充满WorkerLeader和Worker(你的内存占用太高了),这是循环引用发生后很严重的问题,尤其在Navigation导航模式下的UIViewController对象,常常会发生UIViewController被POP后,无法正确释放。内存占用会猛增。
现在我们修改WorkerLeader的letWorkerStartWorkWithComplete:方法。在if (worker.complete == nil) 下面添加 __weak WorkerLeader *weakSelf = self;
然后将NSLog里面的self换成weakSelf;
修改以后是这个样子的

if (self.workers.count > 0) {
        self.execWorker = [self.workers firstObject];
        for (Worker *worker in self.workers) {
            if (worker.complete == nil) {
                __weak WorkerLeader *weakSelf = self;
                worker.complete = ^{
                    NSLog(@"完成了%@分配任务", weakSelf);
                };
            }
            [worker paint];
        }
        if (complete != nil) {
            complete();
        }
    }

再次运行程序,你会发现这次,Worker和WorkerLeader在完成任务后都离开了屋子(调用了dealloc方法);

复杂一点儿要求

工人们完成工作后,你觉得他们做的不错,然后和workerA攀谈起来,忽然,你想问工人workerA,他们的领队是谁(反正不知道怎么地,你也忘记了你找来的WorkerLeader是谁了)为了实现这个要求,Worker还要记住他的领队是谁,于是增加下面的一个属性。workerLeader同样的,也是一个readonly属性。

Worker.h

@property (nonatomic, readonly, strong) WorkerLeader *workerLeader;

然后我们Worker增加一个私有方法。

Worker.m

- (void)signRelatinWithWorkerLeader:(WorkerLeader *)workerLeader
{
    _workLeader = workerLeader;
}

重写WorkerLeader的workers的set方法,同时调用worker的signRelatinWithWorkerLeader方法,这样,在外界浑然不知的情况下,worker记住了自己的workerLeader。

WorkerLeader.m

- (void)setWorkers:(NSArray *)workers
{
    _workers = workers;
    for (Worker *worker in _workers) {
        [worker performSelector:@selector(signRelatinWithWorkerLeader:) withObject:self];
    }
}

在上面的代码中会有一个警告,但是不影响程序的运行,可以通过事先声明方法,去除警告。
接下来,你就可以问一个工人,他的WorkerLeader是谁了。

NSLog(@"workerA的队长是%@", workA.workLeader);

但是此刻,新的问题还是出现了,Worker和WorkerLeader都没有离开。明明循环引用的问题已经解决了啊。

其实,如果细心的话,worker对workerLeader的引用是一个strong型的引用,而workerLeader又通过workers这个NSArray对worker进行了引用,是相互引用,导致了无法释放的问题。这也是大多数情况下,使用delegate模式时delegate这一属性都推荐使用weak,因为在delegate的模式下,任务派发者和执行任务者基本都是相互引用的,如果都是strong引用,发生循环引用是必然的结果。

因此我们将worker的workerLeader属性改为weak引用即可。

看到这你可能觉得,block 的循环引用似乎不是一个什么大问题嘛,用__weak引入临时变量即可了吗。

更深入的需求

WorkerLeader需要对已完成的工作进行统计,统计在单独的一个方法里面进行。大概是这个样子的。

- (void)recordTaskCount
{
    static NSInteger count = 0;
    count++;
    NSLog(@"完成了%@件任务", @(count));
}

然后在NSLog(@”完成了%@分配任务”, weakSelf);后面调用这个方法。修改以后是下面这个样子:

worker.complete = ^{
                    NSLog(@"完成了%@分配任务", weakSelf);
                    [self recordTaskCount];
                };

很不幸的是,这一次循环引用还是发生了。因为调用方法时使用了self关键字。

将self改成weakSelf:

[weakSelf recordTaskCount];

循环引用解决。
但是此时问题真的有那么那么乐观?

那么我们猜想如果在recordTaskCount中也使用self关键字,会不会造成循环引用呢?那么将recordTaskCount修改成下面的样子

- (void)recordTaskCount
{
    static NSInteger count = 0;
    count++;
    NSLog(@"%@的队伍完成了%@件任务", self, @(count));
}

最后发现,worker和workerLeader都是可以正确释放的,因为调用该方法的其实是__weak修改过后的self;

方法可以再复杂点,因为这次我们不光要统计完成了多少任务,还要统计是谁完成的。

- (void)recordTaskCountWithWorker:(Worker *)worker
{
    static NSInteger count = 0;
    count++;
    NSLog(@"%@的队伍中%@完成了第%@件任务", self, worker, @(count));
}

然后调用的时候换成一下方式。

[weakSelf recordTaskCountWithWorker:worker];

或许此刻XCode已经在提醒你,有循环引用的问题了。运行发现,workerLeader正确释放,但是worker没有释放。worker通过自己的block引用了自己。导致了无法释放的问题。
该问题也可以通过对方法参数中的worker对象用__weak修饰解决。但也可以通过修改block来解决,将worker的complete改为带参数的block,也就是这个样子void(^complete)(Worker *worker),在Worker内部调用时通过self.complete(self)调用。

然后在workerLeader 中worker的complete变成了下面的样子

worker.complete = ^(Worker *bWorker){
                        NSLog(@"完成了%@分配任务", weakSelf);
                        [weakSelf recordTaskCountWithWorker:bWorker];
                };

相对来说,我更喜欢后一种方法,这样设计的block会带来很多使用上的方便,而且不会引起循环引用的问题,更清晰。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值