理解Block(上)

前言

  整理一些概念  

环境

Xcode 8.2.1
macOS Sierra 10.12.3

是什么

Block objects are a C-level syntactic and runtime feature. They are similar to standard C functions, but in addition to executable code they may also contain variable bindings to automatic (stack) or managed (heap) memory. A block can therefore maintain a set of state (data) that it can use to impact behavior when executed.

Block是个C语言级别的语法与运行时特性。它和函数类似,能执行代码,但更重要的是能绑定堆栈中的变量,通过设置这些变量来改变代码的行为。

用在哪

You can use blocks to compose function expressions that can be passed to API, optionally stored, and used by multiple threads. Blocks are particularly useful as a callback because the block carries both the code to be executed on callback and the data needed during that execution.

Block常被用在多线程中,在应用开发时,大多数情况下不能因为网络请求而阻塞与用户的交互,待响应返回后,又需要根据响应的结果对页面进行调整。在这种场景下,可以让主线程可以提供一个Block到子线程。

优点

  • 主线程:实现异步,无需关注网络请求的细节部分,只要考虑请求结果对界面的影响,预先做好处理方式。
  • 子线程:负责网络请求,它清楚网络请求何时完成,由它来通知主线程进行页面刷新操作是比较合适的。

而它们的联结点就是Block,主线程提供Block,子线程负责在合适时机调用Block

如果没有Block

因为Block和函数类似,用函数来实现回调功能,同时因为OC是面向对象的编程语言,为了能与其他方法交互,更贴近现实情形,写了如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 主线程 .m 函数定义
void completedFunc(NSString *response) {
NSLog(@"Hello %@",response);
}
// 函数调用
SubViewController *subVC = [[SubViewController alloc] init];
[subVC setCompletedFunc:completedFunc];

sleep(3);

[self.navigationController pushViewController:subVC animated:true];

// 子线程.h
- (void)setCompletedFunc:(void (NSString *))r;

// 子线程.m
// 接收函数指针
@property (nonatomic,copy) NSString *httpResponse;
@property (nonatomic,assign) Response reponser;

- (void)setCompletedFunc:(void (NSString *))r {
_reponser = r;
// ...
}

// 在合适的时机调用回调方法比如viewDidLoad
NSLog(@"发送网络请求");
// 接收网络请求数据
_httpResponse = @"test,test,test";
// 执行回调
_reponser(_httpResponse);

如果用Block方式改写,可以将函数的定义与函数参数的传递结合在一起,和匿名函数差不多,因为主线程中并不关心函数的名称,主要还是其中执行的代码,子线程中是要属性引用来执行,还是直接执行,那是子线程需要考虑的事情。而httpResponse是子线程中的变量,被传递到主线程的函数中进行执行了。因此Block和通知,代理等一样都具有传递数据的能力。

这里所说的子线程和主线程只是为了区分文件,不是说有什么主线程的.m

怎么写

  • int是返回值
  • ^是声明block的语法
  • myBlockblock*变量的名称,它的值为{return num * multiplier;}*
  • int num是参数名
  • {return num \ multiplier;}block*的内容

Xcodeblock代码块提示为:inlineBlock

作属性

1
@property (nonatomic,copy) void (^responser)(NSString *res);

如果觉得不好看,可以typedef定义一个别名类型

1
2
typedef void (^Responser)(NSString *);
@property (nonatomic,copy) Responser req;

作参数

1
2
3
- (void)setReq:(void (^)(NSString *))req {

}
1
2
3
- (void)setReq:(Responser)req {

}

作返回值

1
2
3
- (void (^)(NSString *))req {
return nil;
}
1
2
3
- (Responser)req {
return nil;
}

Block的类型

属性:是OC的一项特性,用于封装对象中的数据。

  • 原子性([no]atomatic)
  • 读写权限(readwrite/readonly)
  • 内存管理语义(retain,copy,assign,weak,strong,unsafe_unretained)
  • 方法名(getter,setter)

strong:为该种属性设置新值时,设置方法会先保留新值并释放旧值,然后再将新值设置上去
copy:与strong类似,但是不保留新值,而是将其”拷贝”。当属性类型为NSString时,常用该特质来保护其封装性

详情可以参考书籍Effective Objective C 2.0 第二章第6

通过这儿5道题可知

Block可根据存储的区域分为3种类型:NSGlobalBlock,NSStackBlock,NSMallocBlock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
typedef void (^dBlock)();

@interface MRCViewController ()
@property (nonatomic,copy) dBlock dB;
@end

@implementation MRCViewController

- (void)viewDidLoad {
[super viewDidLoad];

dBlock b = ^{
printf("666");
};
NSLog(@"b=%@",b);
char d = 'D';
printf("d的addr:%p\n", &d);
dBlock b2 = ^{
printf("d的addr:%p\n", &d);
};
NSLog(@"b2=%@",b2);

self.dB = b2;
NSLog(@"b2=%@",b2);
NSLog(@"dB=%@",self.dB);
self.dB();

// MRC环境下
dBlock dB2 = [[b2 copy] autorelease];
NSLog(@"dB2=%@",dB2);
dB2();
}
...

输出结果为

MRC

ARC

目前结论:
  不绑定变量时,MRCARC*环境下都是NSGlobalBlock类型。
  绑定非对象变量的情况下,被绑定的变量都是传值的,在MRC环境下,创建Block默认会存储在栈中,即为
NSStackBlock类型,生命周期为函数或方法体内有效,当使用copy修饰的属性对其进行引用或对Block调用copy + autorelease时,将会拷贝一个新的Block到堆中,即为NSMallocBlock类型,生命周期将会被延长。在ARC环境下,~~创建Block默认存储在堆中,即为NSMallocBlock类型~~,当使用copy*修饰的属性对其接收时,因为内存地址没变,所以只进行了浅拷贝。

但是,根据谈Objective-C block的实现中的评论,在ARC环境下照着操作后,输出确实如此。因此又进行了以下的代码测试

1
2
3
4
5
6
7
8
9
id arr;
NSLog(@"%@", ^(){NSLog(@"%@", arr); });

// 如果绑定的不是对象会有影响吗?
NSLog(@"%@",^{printf("d的addr:%p\n", &d);});

dBlock b3 = nil;
NSLog(@"%@",b3 = ^{printf("d的addr:%p\n", &d);});
b3();

输出结果:

不论Block*绑定的是对象还是非对象数据类型,结果都是一样的,直接输出都是NSStackBlock类型的,但是当进行赋值操作后,在上面的情况下,用b3去接收Block类型时,ARC发现这个Block的生命周期变了,在输出后,依然应该存在,所以对其进行拷贝一份到堆中,因此b3类型变为NSMallocBlock*。

通过b3执行输出的地址,也不是原来的地址了,因为非对象数据类型是传值的,那么如果换成对象,会有不同吗?再分别写了如下测试代码:

1
2
3
4
5
6
7
8
9
NSString *d2 = @"123";
printf("d2的addr%p\n",d2);
dBlock b4 = ^ {
printf("d2的addr:%p\n", d2);
};
b4();
NSLog(@"b4=%@",b4);
self.dB = b4;
NSLog(@"dB=%@",self.dB);

MRC环境下输出

ARC环境下输出

可以得到结果,对于对象的绑定在MRCARC都只是进行了浅拷贝

现在绑定的是局部变量,如果是对象的属性,会有变化吗?

进行如下代码测试:

1
2
3
4
5
6
7
8
9
@property (nonatomic,strong) NSArray *dBTestArr;

self.dBTestArr = @[@"456"];
printf("dBTestArr:%p",self.dBTestArr);
dBlock b5 = ^ {
NSLog(@"dBTestArr:%p",self.dBTestArr);
};
b5();
NSLog(@"b5=%@",b5);

MRC环境输出

ARC环境输出

同样也是对变量进行浅拷贝。

如果是静态变量呢,会有变化吗?

代码

1
2
3
4
5
6
7
8
9
static NSArray *dBTestArr2;

dBTestArr2 = @[@"789"];
printf("dBTestArr2:%p\n",dBTestArr2);
dBlock b6 = ^ {
NSLog(@"dBTestArr2:%p",dBTestArr2);
};
b6();
NSLog(@"b6=%@",b6);

MRC环境输出

ARC环境输出

变量是浅拷贝的,同时Block*统一都是NSGlobalBlock*,这也很好理解,因为静态变量在内存中是分配在全局区的。

OC没有类属性,但是有关联属性,如果关联到类对象上,用Block进行绑定,会怎么样呢?

代码

1
2
3
4
5
6
7
8
9
#import <objc/message.h>
static const char *dBTest3 = "dbTest3"; // static const char *dBTest4 = "dbTest4";
objc_setAssociatedObject([self class], dBTest3, @[@"0"], OBJC_ASSOCIATION_RETAIN);
printf("dBTest3:%p\n",objc_getAssociatedObject([self class], dBTest3));
dBlock b7 = ^ {
NSLog(@"dBTest3:%p",objc_getAssociatedObject([self class], dBTest3));
};
b7();
NSLog(@"b7=%@",b7);

MRC环境

ARC环境

同样也是对变量浅拷贝,MRC*下是NSStackBlock,ARC下是NSMallocBlock*

总结一下,大概如下表:

绑定变量类型 MRC ARC
不绑定 NSGlobalBlock NSGlobalBlock
局部变量 NSStackBlock,copy*属性赋值修饰拷贝一个NSMallocBlock*对象,非对象传值,对象传址 NSStackBlock,赋值后拷贝一个为NSMallocBlock对象,非对象传值,对象传址
属性 NSStackBlock,非对象传值,对象传址 NSStackBlock,赋值后拷贝一个为NSMallocBlock对象,非对象传值,对象传址
静态变量 NSGlobalBlock,变量地址 NSGlobalBlock,变量地址
关联对象 NSStackBlock,对象传址 NSMallocBlock,对象传址

到这里,先停一下,说明两个问题

在5道练习题中的Example B

1
2
3
4
5
6
7
8
9
10
11
12
13
void exampleB_addBlockToArray(NSMutableArray *array) {
char b = 'B';
[array addObject:^{
printf("%cn", b);
}];
}

void exampleB() {
NSMutableArray *array = [NSMutableArray array];
exampleB_addBlockToArray(array);
void (^block)() = [array objectAtIndex:0];
block();
}

能把Block添加到数组中,那么Block应该是一个对象吧。

JaviSoto/iOS10-Runtime-Headers工程中,查找发现Block对象的继承关系为

NSStackBlock => __NSStackBlock => NSBlock => NSObject
NSGlobalBlock => __NSGlobalBlock => NSBlock => NSObject
NSMallocBlock => __NSMallocBlock => NSBlock => NSObject

可知OC中的Block确实是一个对象

还有一个小问题就是存在的到底是NSConcreteGlobalBlock,NSConcreteStackBlock,还是NSMallocBlock这种类型的?


在13年发表的,猜测可能苹果对Block应该进行修改了,又根据clang-do

又觉得偏向于NSConcreteGlobalBlock,我暂时认为在底层实际上是NSConcreteGlobalBlock这种的,然后在OC*上表现为NSMallocBlock*

__Block

1
2
3
4
5
6
NSString *d2 = @"123";
printf("d2的addr%p\n",d2);
dBlock b4 = ^ {
printf("d2的addr:%p\n", d2);
d2 = @"777";
};

以上代码将会报Variable is not assignable (missing __block type specifier)

为什么呢?

同样根据clang-doc,里面有这样的说明

因为如果变量不用__block修饰,那么默认在拷贝时,会添加const进行修饰

Block的循环引用

更新: 170402

5个动画,理解常用垃圾回收算法的执行过程 中有这样一段话

引用计数算法存在很多问题。最大的一个问题是,它无法解决循环引用问题。这种情况很常见,父对象和反向引用会形成引用环


原来的

先构造一个简单的循环引用

1
2
3
4
@property (nonatomic,copy) dBlock dBCycle;
self.dBCycle = ^ {
NSLog(@"%@",self);
};

  正常情况下,selfVC,被导航控制器的viewControllers属性持有,而self持有dBCycle这个Block属性,当pop控制器后,self的引用计数为0,被释放,然后dBCycle计数也为0,也能被顺利释放
  

在上面的循环引用代码中,当pop控制器后,selfdBCycle属性持有,所以不能被回收,dBCycle指向的Block的内存地址,在执行完Block后,因为它的引用计数还是至少为1(self持有),所以也不会被释放,有点类似死锁的概念,双方互相依赖对象。dealloc方法不会被调用。

知道原理之后,关注一下AFNetworking中的success回调Block

进入

1
2
3
4
- (nullable NSURLSessionDataTask *)GET:(NSString *)URLString
parameters:(nullable id)parameters
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure DEPRECATED_ATTRIBUTE;

再进入

1
2
3
4
5
- (NSURLSessionDataTask *)GET:(NSString *)URLString
parameters:(id)parameters
progress:(void (^)(NSProgress * _Nonnull))downloadProgress
success:(void (^)(NSURLSessionDataTask * _Nonnull, id _Nullable))success
failure:(void (^)(NSURLSessionDataTask * _Nullable, NSError * _Nonnull))failure
1
2
3
4
5
6
7
- (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(id)parameters
uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgress
downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgress
success:(void (^)(NSURLSessionDataTask *, id))success
failure:(void (^)(NSURLSessionDataTask *, NSError *))failure
1
2
3
4
5
6
7
8
9
10
11
12
13
14
dataTask = [self dataTaskWithRequest:request
uploadProgress:uploadProgress
downloadProgress:downloadProgress
completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *error) {
if (error) {
if (failure) {
failure(dataTask, error);
}
} else {
if (success) {
success(dataTask, responseObject);
}
}
}];

success回调的Block作参数传递到completionHander

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request
uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock
downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgressBlock
completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject, NSError * _Nullable error))completionHandler {

__block NSURLSessionDataTask *dataTask = nil;
url_session_manager_create_task_safely(^{
dataTask = [self.session dataTaskWithRequest:request];
});

// 添加代理对象
[self addDelegateForDataTask:dataTask uploadProgress:uploadProgressBlock downloadProgress:downloadProgressBlock completionHandler:completionHandler];

return dataTask;
}

传递到

1
2
3
4
5
6
7
8
9
- (void)addDelegateForDataTask:(NSURLSessionDataTask *)dataTask
uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock
downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgressBlock
completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler
{
AFURLSessionManagerTaskDelegate *delegate = [[AFURLSessionManagerTaskDelegate alloc] init];
delegate.manager = self;
delegate.completionHandler = completionHandler;
...

delegate.completionHandlerblock持有了我们传递的success回调的Block

delegateself(AFHTTPSessionManager)对象的mutableTaskDelegatesKeyedByTaskIdentifier属性持有

大致理解如下

AFHTTPSessionManager_Obj.mutableTaskDelegatesKeyedByTaskIdentifier.delegate.completionHandler = ^ {
  succcess()
}

若你在success()中使用了VCself,因为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
+ (instancetype)manager {
return [[[self class] alloc] initWithBaseURL:nil];
}

- (instancetype)initWithBaseURL:(NSURL *)url {
return [self initWithBaseURL:url sessionConfiguration:nil];
}

- (instancetype)initWithBaseURL:(NSURL *)url
sessionConfiguration:(NSURLSessionConfiguration *)configuration
{
self = [super initWithSessionConfiguration:configuration];
if (!self) {
return nil;
}

// Ensure terminal slash for baseURL path, so that NSURL +URLWithString:relativeToURL: works as expected
if ([[url path] length] > 0 && ![[url absoluteString] hasSuffix:@"/"]) {
url = [url URLByAppendingPathComponent:@""];
}

self.baseURL = url;

self.requestSerializer = [AFHTTPRequestSerializer serializer];
self.responseSerializer = [AFJSONResponseSerializer serializer];

return self;
}

AFHTTPSessionManager_Obj等价于局部变量,当方法执行完后,变量会被释放,紧接着Block执行完后,会被回收,对VC的self持有会被释放,当pop后,self对象也会被释放。

参考

FFmpeg学习笔记01(环境搭建) UIView的init与initWithFrame:的调用关系
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×