iOS 客户端-闭包与 Objective-C block | 青训营笔记

99 阅读6分钟

这是我参与「第四届青训营 」笔记创作活动的的第20天

前言

今天学习到的是【iOS 客户端专场 学习资料一】第四届字节跳动青训营的第五节:闭包与 Objective-C block:学习OC有接触到一个新词Block,但不是新的概念,不是新的东西。学过Javascript的小伙伴对闭包应该不陌生吧,学过PHP的应该也不陌生,在PHP5.3版本以后也支持闭包, 也就是OC中所提到的Block。 到底什么是闭包或者block呢?用大白话说就是匿名函数,也就是在函数中可以包含这函数。就是在函数中可以定义匿名函数然后在函数中调用。学习OC中的block之前也小担心一下,Block在OC中属于高级的部分,心里有又有个疑问:学起来难不难?看过Block的部分,感觉Block挺好理解的,用起来也挺顺手的,Block没我想象中的那么难理解。

image.png

闭包与 Objective-C block

image.png

Block基础

基础概念

  • block是闭包在Objective-C中的实现
  • block可以接受参数也可以有返回值
  • block可以分配在栈和堆上,也可以是全局的。分配到栈上的块可以拷贝到堆中,同标准的Objective-C对象一样,具备引用计数(这里大家可以简单回顾下上节课内容,Objective-C中是如何利用引用计数实现ARC的,会有关联的内容)

标准格式

// block声明
returnType (^blockName)(parameters);
// block赋值
^returnType(parameters) {
  // do something;
};
// 示例
int (^sumBlock)(int a, int b) = ^int(int a, int b) {
  return a + b;
};
int sum = sumBlock(1, 1);

block在声明时有几个组成部分,返回值,block的名称以及block传入的参数;而在给block赋值时,没有block名称这一部分,在对应的位置用返回值代替。

Block内存管理

基础分类

类型描述环境
NSGlobalBlock全局Block,保存在数据区(.data段)定义在全局区或者没有访问自动局部变量
NSStackBlock栈Block,保存在栈区访问了自动局部变量
NSMallocBlock堆Block, 保存在堆区__NSStackBlock__调用了copy

Objective-C中block大致分为表中3类,分别存储在不同的内存区域中:从表中可以看出对应内存区域block的产生条件,下面可以看下实际的代码

int main(int argc, char * argv[]) {
    @autoreleasepool {
        // __NSGlobalBlock__
        void(^globalBlock)(void) = ^{
            NSLog(@"Hello, World!");
        };
        NSLog(@"%@", [globalBlock class]);

        // __NSStackBlock__
        int age = 18;
        void(^stackBlock)(void) = ^{
            NSLog(@"Hello, World! %d", age);
        };
        NSLog(@"%@", [stackBlock class]);

        // __NSMallocBlock__
        void(^mallocBlock)(void) = [stackBlock copy];

        NSLog(@"%@", [mallocBlock class]);
    }
    return 0;
}

第4行声明的globalBlock因为没有访问任何自动变量,会被存储在.data段,所以是一个 NSGlobalBlock ;第11行声明的stackBlock引用了临时变量age,所以会被存储在栈上,是一个

NSStackBlock;第17行的mallocBlock因为手动调用了copy方法,所以被存储在了堆上,是一个__NSMallocBlock__。

  • block作为函数返回值
  • 将block赋值给strong指针
  • block作为某些系统方法参数

变量捕获

- (void)changeValue {
    int value = 1;

    void (^oneBlock)(void) = ^{
        NSLog(@"value = %d", value); // value1:?
    };

    value = 2;
    oneBlock();
    NSLog(@"value = %d", value); // value2:?
}

这里是由于oneBlock在初始化的时候,按值捕获了value,而后,当value更改后,block内捕获的值并没有跟着一起变化;而value2处则是使用的改变后的变量。

在ARC下,系统会在某些场景默认对block执行copy操作,使其变为__NSMallocBlock__,此时block也会有自己的内存引用计数。因此,在上文代码中,VC和Block产生了循环引用,导致了内存泄漏

循环引用的常规解决方案就是打破引用环,使用weak,回忆下上节课的内容,想下可以怎么解决这个问题;有了想法的同学可以和下面的代码对下,检查下自己是否掌握了内存管理的相关知识

// ViewController.m

#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) void (^completionBlock)(void);

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf = self;
    self.completionBlock = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        NSLog(@"%@", strongSelf.name);
    };
}

@end

Block的应用

数组遍历

// block作为入参
[object doSomethingWithBlock:^returnType (varType varName) {
    // do something
}];
// 获取数组中大于2的数字的个数
__block NSInteger count = 0;
NSArray<NSNumber *> *array = @[@0, @1, @2, @3, @4, @5];
NSRange range = NSMakeRange(0, array.count);
[array enumerateObjectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:range] options:NSEnumerationReverse usingBlock:^(NSNumber * _Nonnull number, NSUInteger idx, BOOL * _Nonnull stop) {
    if (number.integerValue >= 2) {
        ++count;
    }
}];
NSLog(@"%ld", count); // count:4

首先,简单回顾下block作为入参的时候该如何书写;后面再看下Objective-C中是如何做数组遍历操作的,NSArray支持特定的遍历操作,该方法支持传入指定遍历的位置,遍历顺序,以及对每个元素执行的操作。相比于for-in循环,这里将常见的操作进行了封装,代码可读性高一些;option可以选择逆序和并发,在对每个数组元素执行耗时操作时,NSEnumerationConcurrent耗时有所优化

网络请求中数据传递

// AFHTTPSessionManager.h

- (nullable NSURLSessionDataTask *)GET:(NSString *)URLString
                            parameters:(nullable id)parameters
                              progress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgress
                               success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
                               failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure;

上图是iOS常用网络库AFNetWorking中的一个接口,它传入url、网络参数以及进度、成功和失败的回调block,这三个block就是主要负责数据传递的。

AFHTTPSessionManager *manager = AFHTTPSessionManager.manager;
    manager.responseSerializer = [AFHTTPResponseSerializer serializer];
manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"image/jpeg", nil];

__weak typeof(self) weakSelf = self;
NSString *url = @"<http://www.pptbz.com/pptpic/UploadFiles_6909/201203/2012031220134655.jpg>";

// 开始下载
[manager GET:url parameters:nil progress:^(NSProgress *downloadProgress) {
    NSLog(@"progress:%lld",downloadProgress.completedUnitCount);
} success:^(NSURLSessionDataTask *task, id responseObject) {
    NSLog(@"图片下载完成");
    __strong typeof(weakSelf) strongSelf = weakSelf;
    strongSelf.imgView.image = [UIImage imageWithData:(NSData *)responseObject];
} failure:^(NSURLSessionDataTask *task, NSError *error) {
    if (error) {
        NSLog(@"%@",error.userInfo);
    }
}];

上面是一段通过对应接口下载图片的函数,可以看到对应代码逻辑集中在了一块,可读性高。如果换用Objective-C中的其它方式,比如delegate,那这里就需要在调用的地方实现一大堆扰人的delegate方法

实现链式调用

主要还是为了简化代码,提升可读性。假设我们想要实现简单的四则运算,那么下面两种方式,后者可读性明显更高一些。这里因为是演示代码,比较简单,日常调用时会比这个场景更复杂,非链式调用的冗余代码会更多。

看下链式调用,名称加括号的调用方式非常类似于block的调用,那么我们唯一需要考虑的就是如何在调用一个block后能够继续调用下一个。

// 常规调用
float a = 0;
a = a + 2;
a = a - 4;
a = a * 5;
a = a / 3;
// 优化调用
Calculator *calculator = Calculator.new;
calculator.add(2).subtract(4).multiply(5).devideBy(3);
NSLog(@"%f", calculator.result);

实现异步操作

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.7 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    // do something
});

推荐阅读

用大白话🙌带你掌握闭包 - 掘金

OC Block - 掘金

引用参考:

课外补充引用:

文章学习来源:

感谢以上作者的文章,今天的学习收获满满!!Thanks and HappyCoding!