Objective-C之block详细介绍

本文详细介绍了Objective-C中的Block,包括其定义、语法结构、捕获自由变量的机制,特别是__block修饰符的使用。同时,探讨了Block的内存管理,解析了栈块、堆块和全局块的区别,并且讲解了如何避免Block导致的保留环问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

1.定义

闭包是一个允许访问自由变量(局部变量)的匿名函数。自由变量:跟block声明在同一个作用域内的局部变量。

2.语法结构

返回类型 (^块名称)(参数列表)

int (^myblock)(int a, int b) ;         //声明一个块类型和块名,myblock为块名,块类型为int (^)(int a, int b);
myblock= ^(int a, int b) { return a+b;};   //定义块
int c = myblock(1,2);  //c = 3;     //调用块

从上面代码,块跟一个变量一样,先声明,定义,访问。
块变量与一般的C语言变量完全相同,可以作为以下用途。
局部变量
函数参数
静态变量
静态全局变量
全局变量

例1.块作为属性变量
@interface EOCClass : NSObject
@property ( nonatomic , strong ) int (^variableName)( int a, int b
);
@end

@implementation EOCClass
- (
id )init {
   if((self= [superinit])) {
       _variableName= ^(inta,intb) {    //定义一个块,但块里面的代码还没被调用
            return a + b;
        };
       
    }
   
return self ;
}

- (
void )fun {
   
int c = _variableName ( 1 , 2 );    //block 被调用,即执行上面定义的代码 a+b
}
@end

声明定义好一个块之后,就可以像传递一个变量一样,把block里面的代码传递到其他地方运行。这是块其中一个强大之处。

3.捕获自由变量

块另外一个强大之处是:在声明它的范围里,所有变量都可以为其捕获。例2
@implementation EOCClass
- (
id )init {
   
if (( self = [ super init ])) {
       
int additional = 5 ;
       
_variableName = ^( int a, int b) {    // 定义一个块
           
return a + b + additional;  //additional 被块捕获
        };
    }
   
return self ;
}

- (
void )fun {
   
int c = _variableName ( 1 , 2 );
}
在fun方法调用块变量_variableName,这个块变量却能访问了声明在init方法里面的additional,这是因为块在声明时捕获了additional。需要注意的是,如果实例变量被块捕获了,那么也会自动把self一并捕获了,因为在对象内直接访问实例变量和通过self来访问是等效的:
self->anInstanceVariable = @“something”;

__block修饰符

默认情况下,为块所捕获的变量,是不可以在块里修改的。在上例中,假如块内的代码改动了additional的值,那么编译器就会报错。在声明变量时加上__block修饰符,这个变量就可以在块内修改了。
__block int additional = 5 ;
_variableName = ^( int a, int b) {    // 定义一个块
    additional =
3 ;
   
return a + b + additional;  //additional 被块捕获
};

注意:以下情况变量不需加修饰符__block也能在块修改。
1.静态变量
2.全局变量
3.实例变量

另外,块所捕获的变量的值,是块定义时变量的值,如果块定义完之后再修改变量,块内变量值仍是修改前的。但若变量是静态变量、全局变量、实例变量或__block修饰的变量,在块的值是其最新的值。(来源: http://www.cnblogs.com/kesalin/archive/2013/04/30/ios_block.html
- ( void )testAccessVariable
{
   NSInteger outsideVariable = 10 ;
  //__block NSInteger outsideVariable = 10;
    NSMutableArray * outsideArray = [[ NSMutableArray alloc ] init ];
   
   
void (^blockObject)( void ) = ^( void ){
       
NSInteger insideVariable = 20 ;
       
NSLog ( @"  > member variable = %lu" , ( unsigned long ) self . memberVarible );
       
NSLog ( @"  > outside variable = %ld" , ( long )outsideVariable);
       
NSLog ( @"  > inside variable = %ld" , ( long )insideVariable);
       
        [outsideArray
addObject : @"AddedInsideBlock" ];
    };
   
    outsideVariable =
30 ;
   
self . memberVarible = 30 ;
   
    blockObject();
   
   
NSLog ( @"  > %lu items in outsideArray" , ( unsigned long )[outsideArray count ]);
}

输出结果为:
  > member variable =  30
  > outside variable =  10
  > inside variable =  20
  >  1 items  in outsideArray

在块内 outsideArray添加了一个字符串,因为并不是改变 outsideArray本身。

4.块的内部结构


块本身也是对象:
isa:指向Class对象的指针,即指向块所属的类。
invoke:是一个函数指针,指向块的实现代码。
descriptor:是指向结构体的指针,其中声明了块对象的大小,还声明了copy与dispose这两个函数所对应的函数指针。函数会在拷贝及丢弃块对象时运行。
块还会把它所捕获的所有变量都拷贝一份,拷贝的并不是对象本身,而是指向这些对象的指针变量。这是块为什么能访问自由变量的原因。

5.全局块、栈块及堆块

5.1栈块NSConcreteStackBlock

定义块的时候,其所占得内存区域是分配在栈中,这就是说,块只在定义它的那个范围内有效。例如,下面这段代码就有危险:
void (^block)( );
if(/* some condition */) {
    block = ^{
        NSLog(@“block A”);
    };
} else {
    block = ^{ 
        NSLog(@“block B”);
}
block( );
编译器会给每个块分配好栈内存,然而等离开了相应了的范围之后,编译器有可能把分配给块的内存覆写掉。于是,这两个块只能保证在对应的if或else语句范围内有效。

5.2堆块NSConcreteMallocBlock

为了在块的作用域之外使用块,需要把块从栈复制到堆。而且,一旦复制到堆上块就成了带引用计数的对象了。如果不再使用这个块,那就应将其释放,在ARC环境下会自动释放,而手动管理引用计数时需要调用release方法。当引用计数将为0后,“分配在堆上的块”会像其他对象,为系统所回收。
上述改进:
void (^block)( );
if(/* some condition */) {
    block = [^{
        NSLog(@“block A”) copy];
    };

5.3全局块NSConcreteGlobalBlock

这种块不会捕获任何变量,运行时也无须任何变量参与。块所使用的整个内存区域,在编译期已经确定了。这种块实际上相当于单例。
void (^block)( )  = ^{
    NSLog(@"This is a block”);
}

6.保留环

在例1中的代码稍作修改,使得块捕获self,就会出现保留环。
- ( id )init {
   
if (( self = [ super init ])) {
       
int additional = 5 ;
       
_variableName = ^( int a, int b) {    // 定义一个块
           
NSLog ( @"%@" , self );
           
return a + b + additional;  //additional 被块捕获
        };
       
    }
   
return self ;
}
self被块所捕获,即self被保留了一次。同时块作为self的属性,也被self保留了一次。所以它们互相保留,产生了保留环。使用week声明块属性就可以破除保留环了。

另外一个典型的保留环例子:使用网络获取器EOCNetworkFetcher获取数据,当任务完成时,调用EOCNetworkFetcherCompletionHandler。
#import <Foundation/Foundation.h>
typedef void (^EOCNetworkFetcherCompletionHandler)( NSData *data);

@interface EOCNetworkFetcher : NSObject
@property ( nonatomic , strong , readonly ) NSURL *url;
- (
id )initWithURL:( NSURL *)url;
- (
void )startWithCompletionHandler:( EOCNetworkFetcherCompletionHandler )completion;
@end

#import "EOCNetworkFetcher.h"
@interface EOCNetworkFetcher ()
@property ( nonatomic , copy ) EOCNetworkFetcherCompletionHandler completionHandler;
@property ( nonatomic , strong ) NSData *downloadedData;
@property ( nonatomic , strong , readwrite ) NSURL *url;
@end

@implementation EOCNetworkFetcher
- (
id )initWithURL:( NSURL *)url {
   
if (( self = [ super init ])) {
       
_url = url;
    }
   
return self ;
}

- (
void )startWithCompletionHandler:( EOCNetworkFetcherCompletionHandler )completion {
   
self . completionHandler = completion;
   
// 请求执行完后调用 p_requestCompleted
}

- (
void )p_requestCompleted {
   
if ( _completionHandler ) {
       
_completionHandler ( _downloadedData );
    }
}
@end

某个类会创建这种网络数据获取器对象,并用其从URL中下载数据:
@implementation EOCClass {
   
EOCNetworkFetcher *_networkFetcher;
   
NSData *_fetchedData;
}

- ( void )downloadData {
   
NSURL *url = [[ NSURL alloc ] initWithString : @"www.example.com/something.dat" ];
   
_networkFetcher = [[ EOCNetworkFetcher alloc ] initWithURL :url];
    [
_networkFetcher startWithCompletionHandler :^( NSData *data) {
       
NSLog ( @"Request URL %@ finished" , _networkFetcher . url );
       
_fetchedData = data;
    }];
}
@end
因为handler要设置_fetchedData实例变量,所以它必须捕获self变量,handler块保留了创建网络获取器的那个EOCClass实例。而EOCClass实例则通过strong实例变量保留了获取器,最后获取器对象又保留了handler块。


打破保留环:要么令_networkFetcher实例变量不再引用获取器,要么令获取器的completionHandler属性不再持有handler块。
- ( void )downloadData {
   
NSURL *url = [[ NSURL alloc ] initWithString : @"www.example.com/something.dat" ];
   
_networkFetcher = [[ EOCNetworkFetcher alloc ] initWithURL :url];
    [
_networkFetcher startWithCompletionHandler :^( NSData *data) {
       
NSLog ( @"Request URL %@ finished" , _networkFetcher . url );
        _fetchedData = data;
        _networkFetcher = nil;  //不推荐使用这种方法,它责任推给API调用者了
    }];
}
- ( void )p_requestCompleted {
   
if ( _completionHandler ) {
       
_completionHandler ( _downloadedData );
    }
   
self . completionHandler = nil ;
}

总结:
1.块是一个可以访问自由变量的匿名函数。
2.它的一个强大之处是,声明定义好一个变量之后,可以作为参数传递块,那么块内的代码就可以传递到其他地方运行了。
3.块另外一个强大之处是,它可以捕获在它声明范围内的所有变量,需要注意的是,如果在块内捕获实例变量也会把self一并捕获,因为在对象内直接访问实例变量和通过self访问是等效的,这容易导致保留环。
4.默认情况下,所捕获的变量是只读的,如果想在块内修改所捕获的变量,需要在声明变量时加上__block修饰符。但静态变量、全局变量、实例变量不需要加__block修饰符,也能在块修改(跟作用域有关)。
5.块是一个轻量级的结构体,在块的内部第一个变量是isa,指向块所属的类。还有一个函数指针,指向块的实现代码。这是块为什么能被当做变量传递的原因。这个结构还保存着块所捕获的变量,这是块为什么能访问自由变量的原因。
6.块有栈块、堆块及全局块。在定义块时,块所占内存是分配在栈内的,也就是说,离开了定义块所在范围后,块就被释放了。若要想在定义块的范围之外使用块,需要把块从栈中拷贝到堆中,这时,块就变成了带有引用计数的对象了。不使用块时,需要释放块。在ARC环境下会自动释放。
7.使用块需要注意的是,保留环,比如把块声明为属性,self又被块所捕获,这时self跟块就互相保留了,导致内存泄露。 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值