【Objective-C 高级编程】—— 初恋Blocks

一、基础

1.定义:

Blocks是带有自动变量(局部变量)的匿名函数。

2.格式:

^ 返回值类型 参数列表 表达式
^int (int count){return count + 1;}

其中的返回值类型参数列表可以省略。

3.Block类型变量声明:

返回参数 (^变量名称)(接受参数);

该用法就和平时变量的用法相同了,也可以进行赋值等操作。

int (^blk)(int) = ^(int count) {return count + 1;};

int (^blk_t)(int);
blk_t = blk;

4.使用typedef重命名Block类型:

因为Block类型变量和平时的使用类型相同,为了方便我们使用,我们通常都是用typedef来重命名Block

typedef 返回参数 (^该Block的名称)(接收参数)

此时就可以用该Block的名称代表该Block了。

typedef int (^blk_t)(int)

blk_t blk = ^(int count) {return count + 1;};

5.Block截获自动变量值和__block说明符:

Block截获自动变量值就是说在定义Block的时候,其之前的变量都会被该Block所截获,该Block后再改变变量的值然后再调用,其Block中所捕获的值不会受到改变。

int main(int argc, const char * argv[]) {
    int val = 10;
    const char *fmt = "val = %d\n";
    void (^blk)(void) = ^{
        printf(fmt, val);
    };
    
    val = 2;
    fmt = "These values were changed. val = %d\n";
    
    blk();
    
    return 0;
}

4234
此时我们发现,在定义Block之后的这valfmt两个变量被改变了,但是再次调用该Block还是定义Block之前的值,这就是捕获。我的理解就是定义Block之前的变量都会被该Block捕获,你在定义Block后改变变量其值还是该Block之前的定义的值,就不会收到影响,当然若你在Block中改变之前变量的值,他就会报错,想要在其中改变变量的值,这里就的用到__block说明符了。

注意这里的定义Block和调用Block,这是俩个不同的概念,定义就是首次出现该Block的地方,而调用就是之后出现Block的地方。

上面说了Block有截获功能,这里的__block说明符可以说就是用来打破其自动截获功能的,并且还可以使变量可以在Block中进行改变。

int main(int argc, const char * argv[]) {
    __block int val = 10;
    __block const char *fmt = "val = %d\n";
    void (^blk)(void) = ^{
        printf(fmt, val);
        val = 20;
    };
    
    val = 2;
    fmt = "These values were changed. val = %d\n";
    
    blk();
    printf(fmt, val);
    
    return 0;
}

432432
之前说到的,定义Block后再改变变量的值,再次调用时不会改变值,但是使用__block说明符之后,你就会发现其当时的自动截获功能被打破了,不再是截获定义之前的了,而是开始截获调用Block之前的值了,并且你还可以在Block中对变量进行改变了。 这就是__block说明符的秒处。

但是要注意OC对象和C语言的数组!!!
先用OC中的NSMutableArray来说,截获它其实就是截获该类对象用的结构体实例指针,就是说你只要不改变其指针的指向和其指针的值,他就不会出错!!!

int main(int argc, const char * argv[]) {
    id array = [[NSMutableArray alloc] init];
    
    void (^blk)(void) = ^{
        id obj = [[NSObject alloc] init];
        [array addObject:obj];
    };
    
    blk();
    
    return 0;
}

这里因为你并没有改变其array指针的指向和值,而只是在其地址后边新加了一个值,所以他就不会报错!!!

int main(int argc, const char * argv[]) {
    id array = [[NSMutableArray alloc] init];
    
    void (^blk)(void) = ^{
        array = [[NSMutableArray alloc] init];
    };
    
    blk();
    
    return 0;
}

但是你这样写就改变了array的初始地址,所以他就会报错!!!

再说C语言中的数组,在截获时一定要小心使用其指针。

int main(int argc, const char * argv[]) {
    const char text[] = "hello";  //这里是定义一个数组
    
    void (^blk)(void) = ^{
        printf("%c\n", text[2]);
    };
    
    blk();
    
    return 0;
}

这里你看似没有什么错误,但是会出现下面的编译错误。
432423
因为在现在的Blocks中,截获自动变量的方法并没有实现对C语言数组的截获,这时我们可以使用指针来解决该问题。

int main(int argc, const char * argv[]) {
    const char *text = "hello";  //这里是使用指针引用
    
    void (^blk)(void) = ^{
        printf("%c\n", text[2]);
    };
    
    blk();
    
    return 0;
}

这样就没问题了。

二、Blocks实现

1.Blocks本质:

前面简单说了说Blocks,但是我们还是不太清楚其实质是什么东西,为了我们可以理解,我们通过“-rewrite-objc”选项就能将含有 Block 语法的源代码变换为C++的源代码。说是C++,其实也仅是使用了struct 结构,其本质是C语言源代码。

clang -rewrite-objc 源代码文件名

下面我们转换Block语法:

int main() {
	void (^blk)(void) = ^{
		printf("Block\n");
	};
	blk();
	return 0;
}

使用clang转换成源代码就是下面这样的:

//block类的结构体指针,每个block的这个都是相同的,存储一些block常用的标识等等
struct __block_impl {
  	void *isa; //指向所属类的指针,也就是Block类型,参考OC的self指针
  	int Flags; //标志变量,在实现block的内部操作时会用到
  	int Reserved; //保留变量
  	void *FuncPtr; //block执行时调用的函数指针,其指向block代码块中的地址
};

//block本体,block的构造函数,我们在定义blockd的时候其实就是定义了一个这样的结构体,其中存储了block的全部信息,捕获的变量、指向的代码块的地址、block本身的大小、block的类型等等
struct __main_block_impl_0 {
  	struct __block_impl impl; //定义一个block通用的数据,其中包括了block的类型和block中实现的代码块
  	struct __main_block_desc_0* Desc; //定义一个保存block结构体大小的变量,其中保存了定义的该block的大小
    
    //block的构造方法,及初始化方法
 	__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
 	    //block的类型,分为全局Block、栈Block、堆Block
    	impl.isa = &_NSConcreteStackBlock;
    	impl.Flags = flags;
    	impl.FuncPtr = fp; //令定义的新的block的函数指针指向你Block代码块中写的代码
    	Desc = desc; //保存该Block的所占内存大小
  	}
};

//block中的代码块
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
	printf("Block\n");
}
static struct __main_block_desc_0 {
  	size_t reserved; //保留字段
  	size_t Block_size; //block大小
} __main_block_desc_0_DATA = {
	0,
	sizeof(struct __main_block_impl_0)
}; //构造函数,对该结构体进行初始化处理,保存该block的大小

int main(int argc, const char * argv[]) {
    //定义Block
    //因为block相当于一个函数指针,所以下面这个是&,之后再通过数据内容和数据的大小信息创建一个block的结构体对象,并且这里调用__main_block_impl_0的构造函数,还将Block中的代码块和Block结构体所占的大小传给了该Block,使其初始化
	void (*blk)(void) = (void (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
	
	//调用Block
	//相当于函数指针的调用,将Block结构体传给block的代码块,让其执行代码块中的代码
	((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
    return 0;
}

2.截获自动变量值:

代码:

int main(int argc, const char * argv[]) {
    
    int dmy = 256;
    int val = 10;
    const char *fmt = "val = %d\n";

    void (^blk)(void) = ^{
        printf(fmt, val);
    };
    
    return 0;
}

源代码:

struct __block_impl {
  	void *isa;
  	int Flags;
  	int Reserved;
  	void *FuncPtr;
};

struct __main_block_impl_0 {
  	struct __block_impl impl;
  	struct __main_block_desc_0* Desc;
  	const char *fmt;
  	int val;
  	__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
    	impl.isa = &_NSConcreteStackBlock;
    	impl.Flags = flags;
    	impl.FuncPtr = fp;
    	Desc = desc;
  	}
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself {
  	const char *fmt = __cself->fmt;
  	int val = __cself->val; 
  
	printf(fmt, val);
}

static struct __main_block_desc_0 {
  	size_t reserved;
  	size_t Block_size;
} __main_block_desc_0_DATA = {
	0, 
	sizeof(struct __main_block_impl_0)
};

int main(int argc, const char * argv[]) {
	int dmy = 256;
    int val = 10;
    const char *fmt = "val = %d\n";
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));
    return 0;
}

通过上述两个源代码的对比发现,这里多了val,fmt这两个变量,这就是block截获的变量,其实截获的本质就是通过结构体将截获变量的值存储起来,就是因为在定义的时候已经将值存储起来了,所以你之后无论再怎么改变改变了的值他都不会受影响。要注意的是,Block语法表达式中没有使用的自动变量不会被追加,即代码块中没有涉及到的变量是不会被截获的,就像这里的变量dmy

我们之前说的,数组类型的数据Block目前暂时无法进行截获,这就是因为Block在进行截获的时候在结构体中对该数据的创建是和你当时在外部时创建的方法是完全相同的,并且其传递数据是使用值传递来传递给结构体的,下面来用例子来说明:
这里使用一个函数当作Block结构体:

void func(char array[10]) {
    char arrayCopy[10] = array;
    printf("%d\n", b[0]);
}

int main(int argc, const char * argv[]) {
    
    char array[10] = {2};
    func(array);
    
    return 0;
}

这里的func函数就相当于Block,我们在func(Block)前使用char array[10]的方式创建了这个数组,所以在func(Block)中就会以相同的方式创建一个该数据的临时变量即char arrayCopy[10](这里名称不同是为了进行区分,其实名称是相同的),然后再将你传输给函数(结构体)的值进行赋值,这样做当然是不可以的!!!所以Block现在没有办法捕获数组的值,我们只能用指针来实现。

3.__block说明符:

block其实就是超出之前作用域并且在新的函数中访问之前的变量,要实现这种方法变量,我们可以使用static进行修饰,但是他并不太好因为变量作用域结束的同时,原来的自动变量被废弃,因此Block 中超过变量作用域而存在的变量同静态变量一样,将不能通过指针访问原来的自动变量。

当然我们还可以使用__block修饰符,它类似于static、auto、和register说明符,用于指定将变量值设置到哪个存储域中。
下面我们就来看看:

int main(int argc, const char * argv[]) {
    
    __block int val = 10;
    void (^blk)(void) = ^{
        val = 1;
    };
    
    return 0;
}

源代码:

//__block说明符修饰后的变量的结构体
struct __Block_byref_val_0 {
	void *__isa;  //指向所属类的指针
	__Block_byref_val_0 *__forwarding;  //指向自己的内存地址的指针
	int __flags;  //标志性参数,暂时没用到所以默认为0
	int __size;  //该结构体所占用的大小
	int val;  //该结构体存储的值,即原变量的赋的值
};

//block本体
struct __main_block_impl_0 {
	struct __block_impl impl;  //block的主体
	struct __main block desc 0* Desc;  //存储该block的大小
	__Block_byref_val_0 *val;  //__block修饰的变量的值
	//构造函数
	__main_block_impl_0(void *fp, struct __main_block_desc 0 *desc, __Block_byrefval_0 *_val, int flags=0) : val(_val->__forwarding) {
	impl.isa = &_NSConcreteStackBlock;
	impl.Flags = flags;
	impl.FuncPtr = fp;
	Desc = desc;
};

//封装的block逻辑,存储了block的代码块
static void __main_block_func_0(struct__main_block_impl_0 *_cself) {
	__Block_byref_val_0 *val =__cself->val;
	
	(val->__forwarding->val) = 1;
}
static void_main_block_copy_0(struct __main_block_impl_0* dst, struct __main_block_impl_0* src) {
    //根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用
	_Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}

static void __main_block_dispose_0(struct __main_block_imp1_0* src) {
    //自动释放引用的auto变量(相当于release)
	_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}

static struct __main_block_desc_0 {
	unsigned long reserved;  //保留字段
	unsigned long Block_size;  //block大小
	void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);  //copy的函数指针,下面使用构造函数进行了初始化
	void (*dispose)(struct __main_block_impl_0*);  //dispose的函数指针,下面使用构造函数进行了初始化
}
    //构造函数,初始化保留字段、block大小及两个函数
    __main_block_desc_0_DATA = {
	0,
	sizeof(structmain_block_impl_0),
	__main_block_copy_O, 
	__main_block_dispose_0
};
int main() {
    //之前的 __block int val = 10;变成了结构体实例
	struct __Block_byref_val_0 val = {
		0,  //isa指针
		&val,  //指向自身地址的指针
		0,  //标志变量
		sizeof(__Block_byref_val_0),  //block大小
		10  //该数据的值
	};
	blk = &__main_block_impl_0(
__main_block_func_0, &__main_block_desc_0_DATA, &val, 0x22000000);

	return 0;
}

通过上述的代码和之前代码进行比对,发现使用__block说明符修饰的变量变成了一个结构体,它是利用这个结构体来进行存储数据的。

为什么会有成员变量__forwarding呢?

因为__block变量用结构体成员变量__forwarding可以实现无论__block变量配置在栈上还是堆上时都能够正确的访问__block变量。

__block变量的__Block_byref_val_0结构体并不在Block__main_block_impl_0结构体中,这是为了在多个Block中使用__block变量,即共用这个结构体。

4.Block存储域:

之前说过Block的本质,它是一个结构体,其中还有isa指针它指向的是所属类的指针,之前没有具体说过Block的所属类,下面就来浅谈一下:

(1)Block的所属类:
  • _NSConcreteStackBlock //栈
  • _NSConcreteGlobalBlock //全局(.data区)
  • _NSConcreteMallocBlock //堆

应用程序的内存分配如图:
5435345

(2)全局Block:

在记述全局变量的地方使用Block语法时,生成的Block为_NSConcreteGlobalBlock类对象。

void (^blk)(void) = ^{printf("Global Block\n");};

int main(void) {
    ······
}

isa指针初始化如下:

impl.isa = &_NSConcreteGlobalBlock;

该类Block用结构体实例设置在程序的数据区域中。因为在使用全局变量的地方不能使用自动变量,所以不存在对自动变量进行截获。只在截获自动变量时,Block 用结构体实例截获的值才会根据执行时的状态变化。也就是说,全局Block不可以截获自动变量,否则其就不可以设置为全局Block。

也就是说,即使在函数内而不在记述广域变量的地方使用Block语法时,只要Block不截获自动变量,就可以将Block用结构体实例设置在程序的数据区域。

总结:

  • 记述全局变量的地方有Block语法时
  • Block语法的表达式中不使用应截获的自动变量时

以上的这些情况下,Block为_NSConcreteGlobalBlock类对象。即Block配置在应用程序的数据区域中。

(3)栈Block:

isa指针初始化如下:

impl.isa = &_NSConcreteStackBlock;

除上述的设置两种全局Block之外的Block语法生成的Block为_NSConcreteStackBlock类对象,且设置在栈上。

配置在全局变量上的Block,从变量作用域外也可以通过指针安全地使用。但设置在栈上的Block,如果其所属的变量作用域结束,该Block就被废弃。由于__block变量也配置在栈上,同样地,如果其所属的变量作用域结束,则该__block变量也会被废弃。我们也就无法再访问到该变量了。

为了解决无法访问被废弃变量这个问题,就出现了堆block,同时也出现了将Block__block变量从栈上复制到堆上的方法。这样即使Block 语法记述的变量作用域结束,堆上的Block还可以继续存在。下面我们就来说说:

(4)堆Block:

isa指针初始化如下:

impl.isa = &_NSConcreteMallocBlock;

堆上的Block其实就是将栈上的Block复制过来而成的,有时我们在_block变量配置在堆上的状态下,也可以访问栈上的__block变量。在此情形下,只要栈上的结构体实例成员变量__forwarding指向堆上的结构体实例,那么不管是从栈上的__block 变量还是从堆上的__block 变量都能够正确访问。
232424

那么Blocks 提供的复制方法究竟是什么呢?

实际上当ARC有效时,大多数情形下编译器会恰当地进行判断,自动生成将Block从栈上复制到堆上的代码。这是为了防止其被废弃而导致我们访问错误。

下面这个例子就是堆Block,用它来说明:

typedef int (^blk_t)(int);

blk_t func(int rate) {
	return ^(int count) {return rate * count;};
}

源代码:

blk_t func(int rate) {
    //因为ARC处于有效的状态,所以blk_t tmp实际上与附有__strong 修饰符的blk_t __strong tmp 相同
    blk_t tmp = &__func_block_impl_0(__func_block_func_0,&__func_block_desc_0_DATA, rate);
    
    //通过 objc4运行时库的runtime/objc-arrmm可知,objc_retainBlock函数实际上就是_Block_copy 函数
    tmp = objc_retainBlock(tmp);
    //等同于 tmp = _Block_copy(tmp);
 
    //最后将tmp放入自动释放池中进行返回
    return objc_autoreleaseReturnValue(tmp);
}    

这个过程到底发生了什么?我们通过下列源代码中的注释来看看:

/*
 *将通过Block语法生成的Block,
 *即配置在栈上的Block用结构体实例
 *赋值给相当于Block类型的变量tmp中。
 */
tmp = _Block_copy(tmp);
/*
 *_Block_copy 函数
 *将栈上的Block复制到堆上。
 *复制后,将堆上的地址作为指针赋值给变量tmp。
 */

return objc_autoreleaseReturnValue(tmp);
/*
 *将堆上的Block作为Objective-c对象
 *注册到autoreleasepool中,然后返回该对象。
 */

说明,将 Block 作为函数返回值返回时,编译器会自动生成复制到堆上的代码。

前面讲到过“大多数情况下编译器会适当地进行判断”,不过在此之外的情况下需要手动生成代码,将 Block 从栈上复制到堆上。此时我们使用“copy实例方法”。这就是ARC 一章中大量出现的alloc/new/copy/mutableCopy方法中的一个方法,copy方法。那么编译器不能进行判断究竟是什么样的状况呢?如下所示:

  • 向方法或函数的参数中传递Block时

但是如果在方法或函数中适当地复制了传递过来的参数,那么就不必在调用该方法或函数前手动复制了。以下的方法或函数不用手动复制:

  • Cocoa 框架的方法且方法名中含有usingBlock 等时
  • Grand Central Dispatch 的 API

下面举例说明:

- (id)getBlockArray {
	int val =10;
	
	return [[NSArray alloc] initWithObjects:
		^{NSLog(@"blk0:%d",val);},
		^{NSLog(@"blk1:%d",val);}, nil];
}

这种情况因为你向initWithObjects方法中传递了两个Block,这种情况ARC就无法识别然后进行自动管理,此时就得需要我们手动进行copy了。

若不进行手动copy它就会因为栈上的Block被废弃的缘故,我们再次访问该数组中的元素就会出错。

id obj = getBlockArray();

typedef void (^blk_t)(void);

blk_t blk = (blk_t)[obj objectAtIndex:0];

blk();

因为没有对其进行copy到堆上处理,所以这里的blk();就会出错。

我们只需要这样更改代码就行了:

- (id)getBlockArray {
	int val = 10;
	
	return [[NSArray alloc] initWithObjects:
		[^{NSLog(@"blk:%d", val);} copy],
		[^{NSLog(@"blk1:%d",val);} copy], nil];
}

也就是让其进行一个copy操作就不会出错了。当然也可以对Block变量进行copy,就像这样:

typedef int (^blk_t)(int);

blk_t blk = ^(int count){return rate * count;};

blk = [blk copy];
(5)Copy Block:

534535

所以,不管Block配置在什么地方,用copy方法复制都不会出现问题,并且在ARC有效时,你连续copy也不会出现任何问题:

blk = [[[[blk copy] copy] copy] copy];
(6)__block变量存储域:

上节只对 Block 进行了说明,那么对__block变量又是如何处理的呢?使用__block变量Block 从栈复制到堆上时,__block 变量也会受到影响。总结如表所示。
3242343
就是说,不管你的__block变量原本在哪块存储区,只要使用该__block变量Block被复制到堆区时,其__block变量也就会被复制到堆区,并被该Block持有。

43244

那么多个Block中使用同一个__block变量会怎么样呢?

在多个 Block中使用__block 变量时,因为最先会将所有的 Block 配置在栈上,所以__block变量也会配置在栈上。在任何一个 Block 从栈复制到堆时,__block 变量也会一并从栈复制到堆并被该 Block所持有。当剩下的 Block 从栈复制到堆时,被复制的 Block 持有__block 变量,并增加__block变量的引用计数。

423424
这里的持有就和OC中的引用计数式内存管理完全相同,已经复制到堆上的Block就对堆上的从栈上复制过去的该__block变量成了持有状态,而没有复制到堆上的即还在栈上的Block它还是对之前在栈上的__block变量是一个使用的状态,即现在有了两个该Block和该__block变量,一个在栈上,一个在堆上。

但是因为你复制之后的Block会将__block变量也一并复制到堆中,所以此时出现了一个问题此时有了两个该__block变量,他们之间又是怎样的关系呢?
用下面的代码来说明:

int main(int argc, const char * argv[]) {
    
    __block int val = 10;
    void (^blk)(void) = [^{++val;} copy];
    
    ++val;
    blk();
    NSLog(@"%d", val);
    
    return 0;
}

432424
上述的代码打印出来的值为12,这就表示++val;blk();++的是同一个值,但是一个是在栈上一个是在堆上这又是怎么做到访问的为同一块内存区域的呢,其实上述的++val;都会转换成这样的代码

++(val.__forwarding->val);

也就是说,该__block变量转换成结构体后,结构体中的__forwarding指针发挥了作用,该指针指向的是同一块内存区域,所以才可以做到val的信息同步。如图解:
532525
通过该功能,无论是在 Block 语法中、Block语法外使用__block变量,还是__block变量配置在栈上或堆上,都可以顺利地访问同一个__block变量

(7)截获对象:

先看代码:

blk_t blk;
{
	id array = [[NSMutableArray alloc] init];  //使用strong修饰
	blk = [^(id obj) {
		[array addObject:obj];
		NSLog(@"array count = %ld", [array count]);
	} copy];
}

blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]); 
blk([[NSObject alloc] init]);

输出如下:

array count = 1
array count = 2
array count = 3

我们就会产生疑问,这个array对象不是超出作用域了吗,为什么它还可以正常的使用,程序还不会出错。

我们再将上述的代码转换为源代码来看看:

/*Block用结构体/函数部分*/
struct __main_block_impl_0 {
	struct __block_impl_impl;
	struct __main_block_desc_0* Desc;
	id __strong array;
	__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id __strong _array, int flags=0) : array(_array) {
		impl.isa = &_NSConcreteStackBlock;
		impl.Flags = flags;
		impl.FuncPtr = fp;
		Desc = desc;
	}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) {
	id strong array = __cself->array;
	[array addObject:obj];
	NSLog(@"array count = %ld", [array count]);
}

static void __main_block_copy_0(struct __main_block_impl_o *dst, struct __main_block_impl_0 *src) {
	_Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT);
}

static void_main_block_dispose_0(struct __main_block_impl_0 *src) {
	_Block_object_dispose(src->array, BLOCK_FIELD_IS_OBJECT);
}

static struct __main block desc_0 {
	unsigned long reserved;
	unsigned long Block_size;
	void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
	void (*dispose)(struct __main_block_impl_0*);
} __main block_desc_0_DATA = {
	0,
	sizeof(struct __main_block_impl_0),
	__main_block_copy_0,
	__main_block_dispose_0
};
/*Block语法,使用Block部分 */
blk_t blk;
{
	id __strong array = [[NSMutableArray alloc] init];
	blk = &_main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, array, 0x22000000);
	blk = [blk copy];
}

(*b1k->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*b1k->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*b1k->impl.FuncPtr)(blk, [[NSObject alloc] init]);

我们发现,之前截获的array它是使用strong修饰的,在Objective-C中,C语言结构体不能含有附有strong修饰符的变量。因为编译器不知道应何时进行C语言结构体的初始化和废弃操作,不能很好地管理内存。

但是Obiective-C的运行时库能够准确把握Block从栈复制到堆以及堆上的Block 被废弃的时机,因此Block 用结构体中即使含有附有__strong修饰符或__weak 修饰符的变量,也可以恰当地进行初始化和废弃。为此需要使用在__main_block_desc_0结构体中增加的成员变量copydispose,以及作为指针赋值给该成员变量的__main_block_copy_0函数和_main_block_dispose_0函数。

copy和dispose函数:

_Block_object_assign函数调用相当于retain实例方法的函数,将对象赋值在对象类型的结构体成员变量中。
_Block_object_dispose函数调用相当于release实例方法的函数,释放赋值在对象类型的结构体成员变量中的对象。

但是我们发现在转换后的源代码中,这些函数(copy和dispose)包括使用指针全都没有被调用。那么这些函数是从哪调用呢?

在Block从栈复制到堆时以及堆上的Block被废弃时会调用这些函数。

432424

那么什么时候栈上的 Block 会复制到堆呢?
  • 调用Block的copy实例方法时
  • Block作为函数返回值返回时
  • 将Block 赋值给附有__strong修饰符id类型的类或Block类型成员变量时
  • 在方法名中含有usingBlockCocoa框架方法Grand Central Dispatch的 API 中传递 Block 时

其实,我们了解过上述的复制时机时,就会发现在上面这些情况下栈上的Block 被复制到堆上,可归结为其实可归结为_Block_copy函数被调用时 Block从栈复制到堆。

相对的,在释放复制到堆上的Block后,谁都不持有Block而使其被废弃时调用dispose函数。这相当于对象的dealloc实例方法。

有了这种构造,通过使用附有__strong修饰符的自动变量,Block中截获的对象就能够超出其变量作用域而存在。

虽然这种使用copy函数和dispose函数的方法在前面没做任何说明,但实际上在使用 block变量时已经用到了。

static void __main_block_copy0(
	struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
	_Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
	_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}

我们发现函数中所传的最后一个参数我们并不知道是什么意思,其实它是用于区分copy函数和 dispose函数的对象类型是对象还是__block变量
423424

由此可知,Block中使用的赋值给附有__strong修饰符的自动变量的对象和复制到堆上的__block变量由于被堆上的Block所持有,因而可超出其变量作用域而存在。

那么在刚才的源代码中,如果不调用Block的copy实例方法又会如何呢?

blk_t blk;
{
	id array =[[NSMutableArray alloc] init];
	blk = ^(id obj) {
		[array addObject:obj];
		NSLog(@"array count = %ld", [array count]);
	};
}

blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);

执行该源代码后,程序会强制结束。
因为只有调用_Block_copy函数才能持有截获的附有__strong修饰符的对象类型的自动变量值,所以像上面源代码这样不调用_Block_copy函数的情况下,即使截获了对象,它也会随着变量作用域的结束而被废弃。
因此,Block中使用对象类型自动变量时,除以下情形外,推荐调Block的copy实例方法。

  • Block作为函数返回值返回时
  • 将Block 赋值给类的附有__strong修饰符的id类型或Block类型成员变量时。
  • 向方法名中含有usingBlock的Cocoa 框架方法或Grand Central Dispatch 的API中传递 Block 时
虽然书上说了不使用copy修饰符即使截获了对象,它也会随着变量作用域的结束而被废弃,但是你实际进行操作其还是会保留,并没有废弃,不太理解。
(8)__block变量和对象:

__block变量说明符可指定任何类型的自动变量。
如下:

__block id obj = [[NSObject alloc] init];

等同于:

__block id __strong obj = [[NSObject alloc] init];

ARC有效时,id类型以及对象类型变量必定附加所有权修饰符,缺省为附有__strong修饰符的变量。该代码可通过clang转换如下:

/*_block变量用结构体部分*/
struct __Block byref_obj_0 {
	void *__isa;
	_Block_byref_obj_0 *_forwarding;
	int __flags;
	int __size;
	void (*__Block_byref_id_object_copy)(void*,void*);
	void (*__Block_byref_id_object_dispose)(void*);
	__strong id obj;
};

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
	_Block_object_assign((char*)dst + 40, *(void * *)((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *sre) {
	_Block_object_dispose(*(void * *)((char*)src + 40), 131);
}
/*_block变量明部分*/
__Block_byref_obj_0 obj = {
	0,
	&obj,
	0x2000000,
	sizeof(__Block_byref_obj_0),
	__Block_byref_id_object_copy_131,
	__Block_byref_id_object_dispose_131,
	[[NSObject alloc] init]
};

我们可以发现,这里__block变量和之前使用copy后的Block很像,都具有copydispose函数,这里的__block变量其实和之前的copy后的Block一样,当__block变量从栈复制到堆时,使用_Block_object_assign函数,持有赋值给__block变量的对象。当堆上的__block变量被废弃时,使用_Block_object_dispose函数,释放赋值给Block变量的对象。

由此可知,即使对象赋值复制到堆上的附有__strong修饰符的对象类型__block变量中,只要__block变量在堆上继续存在,那么该对象就会继续处于被持有的状态。这与Block中使用赋值给附有__strong修饰符的对象类型自动变量的对象相同。

如果使用__weak修饰符会如何呢?
blk_t blk;
{
	id array = [[NSMutableArray alloc] init];
	id __weak array2 = array;
	
	blk = [^(id obj) {
		[array2 addObject:obj];
		NSLog(@"array2 count = %ld", [array2 count]);
	} copy];
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);

423442
我们发现这与之前的输出完全不同,这是由于附有__strong修饰符的变量array在该变量作用域结束的同时被释放、废弃,nil被赋值在附有__weak修饰符的变量array2中。

若同时指定__block说明符__weak =修饰符会怎样呢?
blk_t blk;
{
	id array = [[NSMutableArray alloc] init];
	__block id __weak array2 = array;
	
	blk = [^(id obj) {
		[array2 addObject:obj];
		NSLog(@"array2 count = %ld", [array2 count]);
	} copy];
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);

423424
执行结果与之前相同,这是因为即使附加了__block说明符,附有__strong修饰符的变量array也会在该变量作用域结束的同时被释放废弃,nil被赋值给附有__weak修饰符的变量array2中。

另外,由于附有__unsafe_unretained修饰符的变量只不过与指针相同,所以不管是在Block中使用还是附加到__block变量中,也不会像__strong修饰符__weak修饰符那样进行处理。因此在使用附有__unsafe_unretained修饰符的变量时,注意不要通过悬垂指针访问已被废弃的对象。

因为并没有设定__autoreleasing修饰符Block同时使用的方法,所以没必要使用autoreleasing修饰符。另外,它与__block说明符同时使用时会产生编译错误。

__block id __autoreleasing obj = [[NSObject alloc] init];

423424

(9)Block循环引用:

如果在Block中使用附有__strong修饰符的对象类型自动变量,那么当Block从栈复制到堆时,该对象为Block所持有。这样容易引起循环引用。我们来看看下面的源代码:

typedef void (^blk_t)(void);
@interface MyObject : 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实例方法一定没有被调用,因为其中出现了Block循环引用。

MyObject类持有Block,而Block中又调用了self,即持有了MyObject类,这就形成了一个循环引用。
3123132
编译器在编译该源代码时能够查出循环引用,因此编译器能正确地进行警告。
423424

为避免此循环引用,可声明附有__weak修饰符的变量,并将self赋值使用。

- (id)init {
	self = [super init];
	id __weak tmp = self;
	blk_ = ^{
		NSLog(@"self = %@",tmp);
	};
	return self;
}

5324543
在该源代码中,由于Block存在时,持有该BlockMyObject类对象即赋值在变量tmp中的self必定存在,因此不需要判断变量tmp的值是否为nil
在面向iOS4Snow Leopard的应用程序中,必须使用__unsafe_unretained修饰符代替__weak修饰符。在此源代码中也可使用__unsafe_unretained修饰符,且不必担心悬垂指针。

- (id)init {
	self = [super init];
	id __unsafe_unretained tmp = self;
	blk_ = ^{
		NSLog(@"self = %@",tmp);
	};
	return self;
}

另外,并不是说你使用了self才会发生循环引用,你在Block中使用类对象的时候,这个类对象其实就相当于引用了self

@interface MyObject : NSObject
{
    blk_t blk_;
    id obj_;
}
@end

@implementation MyObject

- (id)init {
    self = [super init];
    blk_ = ^{
        NSLog(@"obj_ = %@", obj_);
    };
    return self;
}

这里的obj_其实就相当于self->obj_,这就是间接引用了self,造成了循环引用。当然这里也可以使用__weak修饰符__unsafe_unretained修饰符来解决问题。

- (id)init {
    self = [super init];
    id __weak obj = obj_;
    blk_ = ^{
        NSLog(@"obj_ = %@", obj_);
    };
    return self;
}

另外,还可以使用__block变量来避免循环引用。

typedef void (^blk_t)(void);
@interface MyObject : 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(void) {
	id o = [[MyObject alloc] init];
	[o execBlock];
	return 0;
}

但是一定要调用execBlock实例方法,如果不调用execBlock实例方法,即不执行赋值给成员变量blk_Block,便会循环引用并引起内存泄漏。在生成并持有MyObject类对象的状态下会引起以下循环引用。
tmp持有selfself持有BlockBlock持有tmp,造成了循环引用。
524253
但是你执行execBlock实例方法后,Block被实行,nil被赋值在__block变量tmp中。
5345
这样就打破了循环引用。

使用__block变量的优点如下:
  • 通过__block变量可控制对象的持有期间
  • 在不能使用__weak修饰符的环境中使用__unsafe_unretained 修饰符即可(不必担心悬垂指针)
  • 在执行Block时可动态地决定是否将nil或其他对象赋值在__block变量中。
使用__block变量的缺点如下:
  • 为避免循环引用必须执行Block

存在执行了Block语法,却不执行Block的路径时,无法避免循环引用。若由于Block引发了循环引用时,根据Block的用途选择使用__block变量__weak修饰符__unsafe_unretained修饰符来避免循环引用。

(10)copy/release

ARC无效时,一般需要手动将Block从栈复制到堆。另外,由于ARC无效,所以肯定要释放复制的Block。这时我们用copy实例方法用来复制,用release实例方法来释放。

void (^blk_on_heap)(void) = [blk_on_stack copy];
[blk_on_heap release];

只要Block有一次复制并配置在堆上,就可通过retain实例方法持有。

[blk_on_heap retain];

但是对于配置在栈上的Block调用retain实例方法则不起任何作用。

[blk_on_stack retain];

该源代码中,虽然对赋值给blk_on_stack的栈上的Block调用了retain实例方法,但实际上对此源代码不起任何作用。因此推荐使用copy实例方法来持有Block

另外,ARC无效时,__block说明符被用来避免Block中的循环引用。 这是由于当Block从栈复制到堆时,若Block使用的变量为附有__block说明符id类型或对象类型的自动变量,不会被retain;若Block使用的变量为没有__block说明符id类型或对象类型的自动变量,则被retain。例如下面的源代码中,不管ARC有效无效都会引起循环引用,Block持有self,且self持有Block

typedef void (^blk_t)(void);
@interface MyObject : NSObject {
    blk_t blk_;
}
@end
@implementation MyObject
- (id)init {
    self = [super init];
    
    blk_ = ^{
        NSLog(@"self = %@", self);
    };
    return self;
}
- (void)execBlock {
    blk_();
}
- (void)dealloc {
    NSLog(@"dealloc");
}
@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        id o = [[MyObject alloc] init];
        
        [o execBlock];
    }
    return 0;
}

这时我们使用__block变量来避免该问题。

- (id)init {
	self = [super init];
	__block id tmp = self;
	blk_ = ^{
		NSLog(@"self=%@", tmp);
	};
	return self;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
中文名: Objective-C基础教程 作者: Mark Dalrymple Scott Knaster译者: 高朝勤 杨越 刘霞图书 分类: 软件 资源格式: PDF 版本: 扫描版 出版社: 人民邮电出版社 书号: ISBN: 9787115208774 发行时间: 2009年08月 地区: 大陆 语言: 简体中文 简介: 内容简介 Objective-C是扩展C的面向对象编程语言,也是iPhone开发用到的主要语言。本书结合理论知识与示例程序,全面而系统地讲述Objective-C编程的相关内容,包括Objective-C在C的基础上引入的特性和Cocoa工具包的功能及其中的框架,以及继承、复合、源文件组织等众多重要的面向对象编程技术。附录中还介绍了如何从其他语言过渡到Objective-C。   本书适合各类开发人员阅读。 内容截图 目录: 第1章 启程.1 1.1 预备知识1 1.2 历史背景1 1.3 内容简介2 1.4 小结3 第2章 对C的扩展4 2.1 最简单的Objective-C程序4 2.2 解构HelloObjective-C程序7 2.2.1 #import7 2.2.2 NSLog()和@"字符串"8 2.3 布尔类型10 2.3.1 BOOL强大的实用功能11 2.3.2 比较13 2.4 小结14 第3章 面向对象编程基础知识15 3.1 间接15 3.1.1 变量与间接16 3.1.2 使用文件名的间接18 3.2 在面向对象的编程中使用间接24 3.2.1 过程式编程24 3.2.2 实现面向对象编程29 3.3 学习有关的术语33 3.4 Objective-C中的OOP34 3.4.1 @interface部分34 3.4.2 @implementation部分38 3.4.3 实例化对象40 3.4.4 扩展Shapes-Object41 3.5 小结43 第4章 继承45 4.1 为何使用继承45 4.2 继承语法48 4.3 继承的工作机制51 4.3.1 方法调度51 4.3.2 实例变量53 4.4 重写方法55 4.5 小结57 第5章 复合58 5.1 什么是复合58 5.1.1 Car程序58 5.1.2 自定义NSLog()59 5.2 存取方法62 5.2.1 设置发动机的属性64 5.2.2 设置轮胎的属性64 5.2.3 跟踪汽车的变化66 5.3 扩展CarParts程序67 5.4 复合还是继承68 5.5 小结69 第6章 源文件组织70 6.1 拆分接口和实现部分70 6.2 拆分Car程序73 6.3 使用跨文件依赖关系75 6.3.1 重新编译须知75 6.3.2 让汽车开动77 6.3.3 导入和继承79 6.4 小结80 第7章 深入了解Xcode82

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值