iOS开发读书笔记:Objective-C高级编程 iOS与OS X多线程和内存管理-上篇(自动引用计数)
iOS开发读书笔记:Objective-C高级编程 iOS与OS X多线程和内存管理-中篇(Blocks)
iOS开发读书笔记:Objective-C高级编程 iOS与OS X多线程和内存管理-下篇(GCD)
阅读完Block此篇后,可以与iOS开发经验(25)-Block一块阅读,主要是可以加深对__forwarding的理解。
目录
- 2.1 Blocks概要
- 2.1.1 什么是Blocks
- 2.2 Blocks模式
- 2.2.1 Blocks语法
- 2.2.2 Blocks类型变量
- 2.2.3 截获自动变量值
- 2.2.4 __block说明符
- 2.2.5 截获的自动变量
- 2.3 Blocks的实现
- 2.3.1 Block的实质
- 2.3.2 截获自动变量值
- 2.3.3 __block说明符
- 2.3.4 Block存储域
- 2.3.5 __block变量存储域
- 2.3.6 截获对象
- 2.3.7 __block变量和对象
- 2.3.8 Block循环引用
- 2.3.9 copy/release
2.1 Blocks概要
2.1.1 什么是Blocks
Blocks是C语言的扩充功能。可以用一句话来表示Blocks的扩充功能:带有自动变量(局部变量)的匿名函数。
顾名思义,所谓匿名函数就是不带有名称的函数。
C语言的标准函数如下:
int func(int count);//声明函数
int result = func(10);//调用函数
如果像下面这样,使用函数指针来代替直接调用函数,必须使用该函数的名称func。
int result = (*funcptr)(10);
这样以来,函数func的地址就能赋值给函数指针类型变量funcptr中了。
但其实使用函数指针也仍然需要知道函数名称。若不使用想赋值的函数的名称,就无法取得该函数的地址。
int (*funcptr)(int) = &func;
int result = (*funcptr)(10);
通过Blocks,源代码中就能够使用匿名函数,而不带名称的函数。
到这里我们知道了"带有自动变量值的匿名函数"中"匿名函数"的概念。那么“带有自动变量值”究竟是什么呢?
首先回顾下函数中可能使用的变量:
- 自定变量(局部变量)
- 函数的参数
- 静态变量(静态局部变量)
- 静态全局变量
- 全局变量
虽然这些变量的作用域不同,但在整个程序当中,一个变量总保持在一个内存区域。
另外,“带有自动变量值的匿名函数”这一概念并不仅指Blocks,它还存在于其他许多程序语言中。在计算机科学中,此概念也称为闭包。
2.2 Blocks模式
2.2.1 Blocks语法
与一般的函数定义相比,仅有两点不同
- 没有函数名
- 带有“^”记号(插入记号):因为OS X、iOS应用程序的源代码中将大量使用Block,所以插入该记号便于查找。
以下为Block语法的BN范式
^ 返回值类型 参数列表 表达式
^ int (int count) {
return count + 1;
}
该源代码可省略胃如下形式
^ {
return count + 1;
}
2.2.2 Blocks类型变量
在Block语法下,可将Block语法赋值给声明为Block类型的变量中。既源代码中一旦使用Block语法就相当于生成了可赋值给Block类型变量的“值”。
int (^blk)(int);
与前面的使用函数指针的源代码对比可知,声明Block类型变量仅仅是将声明函数指针类型变量的“*”变为“^”。该Blcok类型变量与一般的C语言变量完全相同,可作为以下用途使用 :
- 自定变量(局部变量)
- 函数的参数
- 静态变量(静态局部变量)
- 静态全局变量
- 全局变量
那么,下面我们就试着使用Block语法将Block赋值为Block类型变量:
int (^blk) (int) = ^ (int count) {
return count + 1;
}
也可以:
int (^blk1)(int) = blk;
但是此记述方式极为复杂。这时,我们可以像使用函数指针类型时那样,使用typedef来解决问题。
typedef int (^blk_t) (int);
如上所示,通过使用typedef可声明“blk_t”类型变量。这样函数定义就变得更容易理解了。
另外,将赋值给Block类型变量中的Block方法像C语言通常的函数调用那样使用,这种方法与使用函数指针类型变量调用函数的方法几乎完全相同。
2.2.3 截获自动变量值
通过以上说明,我们已经理解了“带有自动变量值的匿名函数”中的“匿名函数”。而“带有自动变量值”究竟是什么呢?“带有自动变量值”在Block中表现为“截获自动变量值”。截获自动变量值的实例如下:
int main() {
int val = 10;
void (^blk)(void) = ^ {
printf(val);
}
val = 2;
blk();
//打印结果为10;
}
Block中,Block表达式截获所使用的自动变量的值,既保持该自动变量的瞬间值。这就是自动变量值的截获。
2.2.4 __block说明符
实际上,自动变量值截获指南保持秩序Block语法瞬间的值。保存后就不能改写该值。如果尝试在Block中改写截获的自动变量值,会发生编译错误。
若想在Block语法的表达式中将值赋值在Block语法外声明的自动变量,需要在该自动变量上添加__block说明符。该变量称为__block变量。
int main() {
__block int val = 10;
void (^blk)(void) = ^ {
val = 1;
}
val = 2;
blk();
//打印结果为10;
}
2.2.5 截获的自动变量
截获OC对象,调用变更该对象的方法不会产生编译错误,而向截获的变量array赋值则会产生编译错误。
//编译正常
id array = [[NSMutableArray alloc] init];
void (^blk) (void) = ^ {
id obj = [[NSObject alloc] init];
[array addObject:obj];
}
//编译错误
id array = [[NSMutableArray alloc] init];
void (^blk) (void) = ^ {
array = [[NSMutableArray alloc] init];
}
以上的第二段代码需要给截获的自动变量附加__block说明符。
2.3 Blocks的实现
2.3.1 Block的实质
Block上“带有自动变值的匿名函数”,但Block究竟是什么呢?
它实际上是作为极普通的C语言源代码来处理的,通过支持Block的编译器,含有Block的编译器,含有Block语法的源代码转换为一般C语言编译器能够处理的源代码,并作为极普通的C语言源代码被编译。
clang(LLVM编译器)具有转换为我们可读源代码的功能。通过“-rewrite-objc”选项就能将含有Block语法的源代码变换为C++的源代码。
clang -rewrite-objc xxx.m
其实,所谓Block就是Objective-C对象。Block指针赋值给Block的结构体成员变量isa。
struct _main_block_impl_0 {
void *isa;
int flags;
int Reserved;
void *FuncPtr;
};
此_main_block_impl_0结构体相当于基于objc_object结构体的Objective-C类对象的结构体。另外,对其中的成员变量isa进行初始化,具体如下:
isa = &_NSConcreteStackBlock;
既_NSConcreteStackBlock相当于class_t结构体实例。在将Block作为Objective-C的对象处理时,关于该类的信息放置于_NSConcreteStackBlock中;
2.3.2 截获自动变量值
本节主要讲解如何截获自动变量值。将截获自动值的源代码用过clang进行转换(源代码略)。
我们注意到,Block语法表达式中使用的自动变量被作为成员变量追加到了_main_block_impl_0结构体中。
struct _main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
int var;
};
_main_block_impl_0结构体内声明的成员变量类型与自动变量类型完全相同。请注意,Block语法表达式中没有使用的自动变量不会被追加。Block的自动变量截获只针对Block中使用的自动变量。
总的来说,所谓“截获自动变量值”意味着在执行Block语法时,Block语法表达式所使用的自动变量值被保存到Block的结构体实例(既Block自身)中。
2.3.3 __block说明符
以上的截获自动变量的代码例子,在Block的结构体实例中重写该自动变量也不会改变原先截获的自动变量。因为在实现上不能改写被截获自动变量的值,所以会发生编译错误。
不过这样以来,无法在Block中保存值了,极为不便。但是有两个方法:
- 有如下几个变量,允许Block改写值:
- 静态变量
- 静态全局变量
- 全局变量
- 使用__block修饰变量 :__block 存储域类说明符
C语言有如下存储域类说明符:
- typedef
- extern
- static:表示作为静态变量存在在数据区中
- auto:表示作为自动变量存储在栈中
- register
__block说明符类似于static、auto和register说明符,它们用于指定将变量值设置到哪个存储域中。
个人笔记
2.3.4 Block存储域
通过前面说明可知,Block转换为Block的结构体类型的自动变量,__block变量转换为__block变量的结构体类型的自动变量。所谓结构体类型的自动变量,既栈上生成的该结构体的实例。
另外通过之前的说明可知Block也是Objective-C对象,该Block的类为_NSConcreteStackBlock。有很多与之类似的类,如:
- _NSConcreteStackBlock,既该类的对象Block设置在栈上
- _NSConcreteGlobalBlock,设置在程序的数据区域(.data)中
- _NSConcreteMallocBlock,设置在由malloc函数分配的内存块(既堆)中
在记述全局变量的地方使用Block语法时,生成的Block为_NSConcreteGlobalBlock类对象。例如
void (^blk)(void) = ^ {
printf("Global Block");
}
此源代码通过声明全局变量blk来使用Block语法。如果转换该源代码,Block用结构体的成员变量isa的初始化如下:
impl.isa = & _NSConcreteGlobalBlock;
该Block的类为_NSConcreteGlobalBlock类。此Block既该Block用结构体实例设置在程序的数据区域中。
在以下情况下,Block为_NSConcreteGlobalBlock类对象
- 记述全局变量的地方有Block语法时
- Block语法的表达式中不使用应截获的自动变量时
除此之外的Block语法生成的Block为_NSConcreteStackBlock类对象,且设置在栈上。
配置在全局变量上的Block,从变量作用域外也可以通过指针安全的使用。但设置在栈上的Block,如果其所属的变量作用域结束,该Block就被废弃。由于__block变量也配置在栈上,同样的,如果其所属的变量作用域结束,则该__block变量也会被废弃。
Block提供了将Block和__block变量从栈上复制到堆上的方法来解决这个问题。将配置在栈上的Block复制到堆上,这样即使Block语法记述的变量作用域结束,堆上的Block还可以继续存在。
复制到堆上的Block将_NSConcreteMallocBlock类对象写入Block用结构体实例的成员变量isa。
impl.isa = & _NSConcreteMallocBlock;
而__block变量用结构体成员变量_forwarding可以实现无论__block变量配置在栈上还是堆上时都能够正确的访问__block变量。在此情形下,只要栈上的结构体实例成员变量__forwarding指向堆上的结构体实例,那么不管是从栈上的__block变量还是从堆上的__block变量都能够正确的访问。
那么Block提供的复制方法是什么呢?当ARC时,大多数情形下编译器会恰当的判断,自动生成将Block从栈上复制到堆上的代码。
当Block作为函数返回值返回时,执行objc_retainBlock方法,实际上是copy函数。
那么少数情形有几种呢?
- XXXX
另外,对于已配置在堆上的Block以及配置在程序的数据区域的Block,调用copy方法又会如何呢?
- _NSConcreteMallocBlock:引用计数增加
- _NSConcreteStackBlock:从栈复制到堆
- _NSConcreteGlobalBlock:什么也不做
不管是Block配置在何处,用copy方法复制都不会引起任何问题。在不确定时调用copy方法即可
2.3.5 __block变量存储域
上节只对Block的copy进行了说明,使用__block变量的Block从栈复制到堆时,使用的所有__block变量也必定配置在栈上。这些__block变量也全部从栈复制到堆。此时,Block持有__block变量。
如果配置在堆上的Block被废弃,那么它所使用的__block变量也就被释放。
此思考方式与OC的引用计数内存管理完全相同。使用__block变量的Block持有__block变量。日光Block被废弃,它所持有的__block变量也就被释放。
那么在理解了__block变量的存储域之后,在回顾下之前讲过的使用__block变量用结构体成员变量__forwarding的原因。“不管__block变量配置在栈上还是在堆上,都能够正确的访问该变量”。正如这句话所述,通过Block的复制,__block变量也从栈复制到堆。此时可同时访问栈上的__block变量和堆上的__block变量。
源代码可转换为如下形式:
(val.__forwarding->val);
在变换Block语法的函数中,该变量val为复制到堆上的__block变量用结构体实例,而使用的与Block无关的变量val,为复制前栈上的__block变量用结构体实例。
但是栈上的__block变量用结构体实例在__block变量从栈复制带堆上时,会将成员变量__forwarding的值替换为复制目标堆上的__block变量用结构体实例的地址。
通过该功能,无论上在Block语法中、block语法外使用__block变量,还是__block变量配置在栈上或堆上,都可以顺利的访问同一个__block变量。
2.3.6 截获对象
以下源代码生成并持有NSMutableArray类的对象,由于附有__strong修饰符的赋值目标变量的作用域立即结束,因此对象被立即释放并废弃。
{
id array = [[NSMutableArray alloc] init];
}
下面我们看一下在Block语法中使用该变量array的代码:
//运行正常
blk_t blk;
{
id array = [[NSMutableArray alloc] init];
blk = [^(id obj) {
[array addObject:obj];
} copy];
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
该代码运行正常,执行结果如下
array count = 1;
array count = 2;
array count = 3;
请注意被赋值NSMutableArray类对象并被截获的自动变量array。我们可以发现它是Block用的结构体中附有__strong修饰符的成员变量。
struct _main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
id __strong array;
};
在OC中,C语言结构体不能含有附有__strong修饰的变量。因为编译器不知道应何时进行C语言结构体的初始化和废弃操作,不能很好的管理内存。
但是OC的运行时库能够很准确把握Block从栈复制到堆以及堆上的Block被废弃的时机,因此Block用结构体中即使含有附有__strong修饰符或__weak修饰符的变量,也可以恰当的进行初始化和废弃。为此需要使用在__main_block_desc_0结构体中增加的成员变量copy和dispose,以及作为指针赋值给该成员变量的_main_block_copy_0函数和_main_block_dispose_0函数。
恰当管理赋值给变量array的对象:__main_block_copy_0函数使用_Block_object_assign函数将对象类型对象复制给Block用结构体的成员变量array中并持有该对象。
_Block_object_assign函数调用相当于retain实例方法的函数,将对象赋值在对象类型的结构体成员变量中。
另外,__main_block_dispose_0函数使用_Block_object_dispose函数,释放赋值在Block用结构体成员变量array中的对象。
_Block_object_dispose函数调用相当于release实例方法的函数,释放赋值在对象类型的结构体成员变量中的对象。
虽然此__main_block_copy_0函数(以下简称copy函数)和__main_block_dispose_0函数(以下简称dispose函数)指针被赋值在__main_block_desc_0结构体成员变量copy和dispose。在Block从栈复制到堆时以及堆上的Block被废弃时会调用这些函数。
- copy函数:栈上的Block复制到堆时;
- dispose函数:堆上的Block被废弃时;
那么什么时候栈上的Block会复制到堆呢?
- 调用Block的copy实例方法时;
- Block作为函数返回值返回时;
- 将Block赋值给附有__strong修饰符id类型的类或Block类型成员变量时;
- 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递Block时;
在上面这些情况下栈上的Block赋值到堆上,其实可归结为_Block_copy函数被调用时Block从栈复制到堆。相对的,在释放复制到堆上的Block后,谁都不持有Block而使其被废弃时调用dispose函数。这相当于对象的dealloc实例方法。
有了这种构造,通过使用附有__strong修饰符的自动变量,因而Block中截获的对象就能够超出其变量作用域而存在。
2.3.7 __block变量和对象
__block说明符可指定任何类型的自动变量。
__block id obj = [[NSObject alloc] init];
其实该代码等同于
__block id __strong obj = [[NSObject alloc] init];
ARC有效时,id类型以及对象类型变量必定附加所有权修饰符,缺省为附有__strong修饰符的变量。
在Block中使用附有__strong修饰符的id类型或对象类型自动变量的情况下,当Block从栈复制到堆时,使用Block_object_assign函数,持有Block截获的对象。当堆上的Block被废弃时,使用_block_object_dispose函数,释放Block截获的对象。
在__block变量为附有_strong修饰符的id类型或对象类型自动变量的情形下会发生同样的过程。当__block变量从栈复制到堆时,使用_Block_object_assign函数,持有赋值给__block变量的对象。当堆上的__block变量被废弃时,使用_Block_object_dispose函数,释放赋值给__block变量的对象。
由此可知,即使对象赋值复制到堆上的附有_strong修饰符的对象类型__block变量中,只要__block变量在堆上继续存在,那么该对象就会继续处于被持有的状态。这与Block中使用赋值给附有__strong修饰符的对象类型自动变量的对象相同。
另外,我们前面用到的只有附有__strong修饰符的id类型或对象类型自动变量。如果使用__weak修饰符会如何呢?首先是在Block中使用附有__weak修饰符的id类型变量的情况。
blk_t blk;
{
id array = [[[NSMutableArray alloc] init];
id __weak array2 = array;
blk = [^(id obj) {
[array2 addObject:obj];
} copy];
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
该代码可正常执行。 执行结果,这与以上代码的结果不同:
array2 count = 0;
array2 count = 0;
array2 count = 0;
这是由于附有__strong修饰符的变量array在该变量作用域结束的同时被释放、废弃,nil被赋值在附有__weak修饰符的变量array2中。
2.3.8 Block循环引用
如果在Block中使用附有__strong修饰符的对象类型自动变量,那么当Block从栈复制到堆时,该对象为Block所持有。这样容易引起循环利用。我们来看看下面的源代码:
typedef void (^blk_t)(void);
@interface MOyObject : NSObject
{
blk_t blk_;
}
@end
@implementation MyObject
- (id)init {
self = [super init];
blk_ = ^ {
NSLog(@"self = %@",self);
} ;
return self;
}
- (void)dealloc {
NSLog(@:dealloc:);
}
@end
int main() {
id o = [[MyObject alloc] init];
NSLog(@"%@",o);
return 0;
}
该源代码中MyObject类的dealloc实例方法一定没有被调用。
MyObject类对象的Block类型成员变量blk_持有赋值为Block的强引用。既MyObject类对象持有Nlock。init实例方法中执行的Block语法使用附有_strong修饰符的id类型变量self。并且由于Block语法赋值在了成员变量blk中,因此通过Block语法生成在栈上的Block此时由栈复制到堆,并持有所使用的self。self持有Block,Block持有self。这正是循环引用。
另外,编译器在编译该源代码是能够查处循环引用,因此编译器能正确的进行警告。
为避免此循环引用,可声明附有__weak修饰符的变量,并将self赋值使用。
- (id)init {
self = [super init];
id __weak tmp = self;
blk_ = ^ {
NSLog(@"self = %@",tmp);
} ;
return self;
}
在该源代码中,由于Block存在时,持有该Block的MyObject类对象赋值在变量tmp中的self必须存在,因此不需要判断tmp的值是否为nil。
在面相iOS4(MRC),必须使用__unsafe_unretained修饰符代替__weak修饰符。在此源代码中也可使用__unsafe_unretained修饰符,且不必担心悬挂指针。
- (id)init {
self = [super init];
id __unsafe_unretained tmp = self;
blk_ = ^ {
NSLog(@"self = %@",tmp);
} ;
return self;
}
另外在以下源代码中Block内没有使用self也同样截获了self,引起了循环引用。
typedef void (^blk_t)(void);
@interface MOyObject : NSObject
{
blk_t blk_;
id obj_;
}
@end
@implementation MyObject
- (id)init {
self = [super init];
blk_ = ^ {
NSLog(@"obj_ = %@",obj_);
} ;
return self;
}
既Block语法内使用的obj_实际上截获了self。对编译器来说,obj_只不过是对象用结构体的成员变量。
blk_ = ^ {
NSLog(@"obj_ = %@",self->obj_);
};
该源代码也基本与前面一样,声明附有_weak修饰符的变量并赋值obj使用来避免循环引用。在此源代码中也可安全的使用__unsafe_unretained修饰符,原因同上。
- (id)init {
self = [super init];
id __weak obj = obj_;
blk_ = ^ {
NSLog(@"obj = %@",obj);
} ;
return self;
}
在为避免循环引用而使用__weak修饰符时,虽说可以确认使用附有__weak修饰符的变量时是否为nil,但更有必要使之生成以使用赋值给附有__weak修饰符变量的对象。
另外,还可以使用__block变量来避免循环引用。
typedef void (^blk_t)(void);
@interface MOyObject : NSObject
{
blk_t blk_;
}
@end
@implementation MyObject
- (id)init {
self = [super init];
__block id tmp = self;
blk_ = ^ {
NSLog(@"self = %@",tmp);
tmp = nil;
} ;
return self;
}
- (void)execBlock {
blk();
}
- (void)dealloc {
NSLog(@:dealloc:);
}
@end
int main() {
id o = [[MyObject alloc] init];
[o execBlock];
return 0;
}
该源代码没有循环引用。原因:通过执行execBlock实例方法,Block被实行,nil被赋值在__block变量tmp中。因此,_block变量tmp对MyObject类对象的强引用失效。但是如果不调用execBlock实例方法,既不执行赋值给成员变量blk的Block,便会循环引用并引起内存泄漏。
在生成并持有MyObject类对象的状态下会引起以下循环引用:
- MyObject类对象持有Block;
- Block持有__block变量;
- __block变量持有MyObject类对象;
下面我们对使用__block变量避免循环引用的方法和使用__weak 修饰符及__unsafe_unretained修饰符避免循环引用的方法做个比较。
使用__block变量的优点如下:
- 通过__block变量可控制对象的持有期间
- 在不能使用__weak修饰符的环境中不使用__unsafe_unretained修饰符即可(不必担心悬垂指针)
在执行Block时可动态的决定是否将nil或其他对象赋值在__block变量中。
使用__block变量的缺点如下:
- 为避免循环引用必须执行Block
存在执行了Block语法,却不执行Block的路径时,无法避免循环引用。若有雨Block引发了循环引用时,根据Block的用途选择使用__block变量、__weak修饰符或__unsafe_unretained修饰符来避免循环引用。
2.3.9 copy/release
ARC无效时,一般需要手动将Block从栈复制到堆。另外,由于ARC无效,所以肯定要释放赋值的Block。这时我们用copy实例方法用来赋值,用release实例方法来释放。
[blk_ release];
只要Block有一次复制并配置在堆上,就可通过retain实例方法持有。
[blk_ retain];
但是对于配置在栈上的Block调用retain实例方法则不起任何作用。
[blk_ retain];
该源代码中,虽然堆赋值给blk_的栈上的Block调用了retain实例方法,但实际上对此源代码不起任何作用。因此推荐使用copy实例方法来持有Block。
另外,ARC无效时,__block说明符被用来避免Block中的循环引用。这是由于当Block从栈复制到堆时,若Block使用的变量为附有__block说明符的id类型或对象类型的自动变量,不会被retain;若Block使用的变量为没有__block说明符的id类型或对象类型的自动变量,则被retain。
注意:正好在ARC有效时能够同__unsafe_unretained修饰符一样来使用。由于ARC有效时和无效时__block说明符的用途有很大的区别,因此在编写源代码时,必须知道该源代码是在ARC有效情况下编译还是在ARC无效情况下编译。