这是我参与「第四届青训营 」笔记创作活动的的第20天
前言
今天学习到的是【iOS 客户端专场 学习资料一】第四届字节跳动青训营的第五节:闭包与 Objective-C block:学习OC有接触到一个新词Block,但不是新的概念,不是新的东西。学过Javascript的小伙伴对闭包应该不陌生吧,学过PHP的应该也不陌生,在PHP5.3版本以后也支持闭包, 也就是OC中所提到的Block。 到底什么是闭包或者block呢?用大白话说就是匿名函数,也就是在函数中可以包含这函数。就是在函数中可以定义匿名函数然后在函数中调用。学习OC中的block之前也小担心一下,Block在OC中属于高级的部分,心里有又有个疑问:学起来难不难?看过Block的部分,感觉Block挺好理解的,用起来也挺顺手的,Block没我想象中的那么难理解。
闭包与 Objective-C block
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
});
推荐阅读
引用参考:
课外补充引用:
文章学习来源:
- iOS 客户端专场 学习资料二】第四届字节跳动青训营(第五节:闭包与 Objective-C block )
感谢以上作者的文章,今天的学习收获满满!!Thanks and HappyCoding!