[读书笔记]iOS与OS X多线程和内存管理 [Blocks部分-3]

2.3.2 截获自动变量
通过转换后的源码可以发现,Block语法中使用的自动变量被作为成员变量追加到__main_block_impl_0结构体中,Block中没有使用的自动变量不会被追加,所以Block的变量截获只针对Block使用的自动变量。
源码:
#include <stdio.h>//不导入库文件无法运行
int main() {
    int val1=0;
   
int val2=10;
   
void(^testBlock)(void)=^{
       
printf("i am testBlock %d",val1);
    };
    val1=
100;
    testBlock();
}
转换之后的代码与Block不使用变量时的代码区别如下:
struct __main_block_impl_0 {
 
struct __block_impl impl;
 
struct __main_block_desc_0* Desc;
  int val1;
 __main_block_impl_0(void*fp, struct __main_block_desc_0 *desc, int _val1, int flags=0) : val1(_val1) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct__main_block_impl_0 *__cself) {
 
int val1 = __cself->val1; // bound by copy
printf("i am testBlock %d",val1);
}
//调用构造函数
void(*testBlock)(void)=(void(*)())&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA, val1);
在初始化结构体实例时,根据传递给构造函数的参数对(自动变量val0追加的)成员变量进行初始化。这样就截获了自动变量的值。
总的来说,所谓截获自动变量的值意味着在执行Block语法时,Block语法所使用的自动变量的值被保存到Block的结构体实例(即Block自身这个对象)中。
如之前所说,Block不能使用c语言数组类型的自动变量。可能的原因如下:Block截获自动变量的方式是将自动变量赋值给成员变量,假设Block中使用了整形数组a[2],需要将a[2]赋值给成员变量b[2],即b[2]=a[2],而C语言规范不允许这种赋值,导致无法编译。当然有其他方法来截获自动变量,但Block似乎更遵循C规范。

2.3.3 __block说明符
前面说过,Block仅截获自动变量的值,相当于对这个值拷贝了一份。这导致一方面自动变量值的改变不会影响Block中使用的那个值,另一方面在Block中无法更改自动变量的值。这样就无法在Block中保存值了。有两种方法来解决这个问题:
     第一,C语言中的静态变量、静态全局变量、全局变量允许Block改写值。Block语法部分变换为了C语言函数,从这个函数访问静态全局变量、全局变量是没有问题的,但是转换后的语法在原Block语法所在的函数之外,超出了静态变量作用域。所以对于静态变量,将其静态变量指针传递给__main_block_impl_0结构体的构造函数并保存,这样就解决了超出作用域使用变量的问题。

转换前代码:
int global_val=1;//全局变量
static int static_global_val=2;//静态全局变量
int main() {
    static int static_val=3;//静态变量
    void(^testBlock)(void)=^{
        static_val=
4;
       
static_global_val=5;
        global_val=6;
printf("%d,%d,%d",global_val,static_global_val,static_val);
    };
    testBlock();
}












转换后部分代码:
struct __main_block_impl_0 {
  struct __block_impl impl;
 
struct __main_block_desc_0* Desc;
 
int *static_val;//静态变量被加到结构体中
  __main_block_impl_0(
void*fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct__main_block_impl_0 *__cself) {
  int *static_val = __cself->static_val;// bound by copy
        (*static_val)=4;
        static_global_val=
5;//静态全局变量和全局变量使用和转换前一致。
        global_val=
6;//
        printf(
"%d,%d,%d",global_val,static_global_val,(*static_val));
    }

看起来自动变量也可以使用此方法来进行超出作用域的访问,实际情况是在变量作用域结束的时候自动变量会被废弃(静态变量虽然也有作用域的限制,但是生命周期与程序相同),不能通过指针访问原来的变量。相关内容下节说明
     第二,使用“__block说明符”更准确的表达方式是”__block存储域类说明符(__block storage-class-specifier)”。在C语言中,有以下存储域类说明符:typedef、extern、static、auto、register。__block说明符类似于static、auto和register说明符,用于指定将变量值设置到哪个存储域中,如auto表示作为自动变量存储在栈中,static表示作为静态变量存储在数据区中。

原代码:
//__block修饰符
int main() {
   
__block int val=0;
   
void(^tetsBlock)(void)=^{val=2;};
}
转换之后:




struct __Block_byref_val_0 {
 
void *__isa;
__Block_byref_val_0 *__forwarding;
 
int __flags;
 
int __size;
 
int val;
};
struct __main_block_impl_0 {
 
struct __block_impl impl;
 
struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val;
// by ref
  __main_block_impl_0(
void*fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val,int flags=0) : val(_val->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct__main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val;
// bound by ref
(val->__forwarding->val)=
2;}
static void __main_block_copy_0(struct__main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val,8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct__main_block_impl_0*src) {_Block_object_dispose((void*)src->val,8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t 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};
int main() {
   
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0,sizeof(__Block_byref_val_0),0};
   
void(*tetsBlock)(void)=(void(*)())&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val,570425344);
}

可以看到,__block变量val在转换之后的代码中成为了结构体实例(__Block_byref_val_0 *val;)的自动变量,即栈上生成的__Block_byref_val_0结构体实例。__Block_byref_val_0实例和__block变量赋值的代码如下:
struct __Block_byref_val_0 {
 
void *__isa;
__Block_byref_val_0 *__forwarding;
 
int __flags;
 
int __size;
 
int val;
};
static void __main_block_func_0(struct__main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val;
// bound by ref
(val->__forwarding->val)=2;
}
Block的__main_block_impl_0结构体实例持有(指向__block修饰变量的)__Block_byref_val_0结构体实例的指针。__Block_byref_val_0的成员变量__forwarding持有指向该实例自身的指针。赋值的时候通过__forwarding访问变量val.成员变量__forwarding的作用在下节说明。另外__Block_byraf_val_0结构体并不在__main_block_impl_0 中定义,见图:


这样做是为了在多个Block中使用同一__block变量。如下:
转换前代码:
int main() {
   
__block int val=0;
   
void(^tetsBlock1)(void)=^{val=2;};
   
void(^testBlock2)(void)=^{val=8;};
}
转换后关键代码:
int main() {
   
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0,sizeof(__Block_byref_val_0),0};
   
void(*tetsBlock1)(void)=(void(*)())&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val,570425344);//同时使用了val这个变量
   
void(*testBlock2)(void)=(void(*)())&__main_block_impl_1((void*)__main_block_func_1, &__main_block_desc_1_DATA, (__Block_byref_val_0 *)&val,570425344);//
}
同理,如果想要在一个Block中使用多个__block变量,只需要增加Block的结构体成员变量与构造函数参数。

2.3.4 Block存储域
此节需说明两个问题:
  • Block超出变量作用域存在的理由(例如函数返回Block,返回后Block应当被释放/废除,却仍能够使用)
  • __block变量的结构体成员变量__forwarding的作用
Block的实质是栈上Block的结构体实例,__block变量的实质是栈上__block变量的结构体实例。从另一方面来说,Block又是OC的对象,其类为_NSConcreteStackBlock.与此相关的类还有 _NSConcreteGlobalBlock、 _NSConcreteMallocBlock.对应的存储区域如下:


之前使用Block的例子出现的都是_NSConcreteStackBlock,在使用全局变量的地方使用Block语法时,生成的Block为_NSConcreteGlobalBlock类对象。如下
void(^tetsBlock1)(void)=^{};
int main() {    }
该Block的类: 
impl.isa = &_NSConcreteGlobalBlock;
在使用全局变量的地方使用Block语法时,因为Block不截获自动变量,Block用结构体实例的内容不依赖于执行时的状态,整个程序中只需一个实例,因此可将Block存储在与全局变量相同的数据区域。实际上,即使在函数中, 当Block不截获自动变量时,也可将Block用结构体实例存储在程序的数据区域。总结:在Block语法位于全局变量位置时或Block语法不截获自动变量时,Block为_NSConcreteGlobalBlock类对象,存储在程序的数据区域。除此之外生成的Block为_NSConcreteStackBlock对象,存储在栈上。还有一个_NSConcreteMallocBlock类没有用到,正是本节要解决的两个问题的答案。
配置在全局变量上的Block从变量作用域外通过指针也可以安全地使用。但是设置在栈上的Block,如果其所属变量作用域结束,该Block就被废弃,由于__block变量也在栈上,__block变量也会被废弃。Blocks提供了将Block和__block变量复制到堆上来解决此问题。复制到堆上的Block就属于_NSConcreteMallocBlock类的对象,而__block变量的结构体成员变量__forwarding可以实现无论__block存储在栈上还是堆上都能访问__block变量。下节说明
Blocks如何让将Block复制到堆上呢?一般情况下,如果ARC处于开启状态,编译器会自动将Block从栈上覆制到堆上,可以看下面的返回Block的函数:

typedef int (^type_block)(int);
type_block func(int rate){
   
return ^(int count){return rate*count;};//在非ARC环境下会报错:Returning block that lives on the local stack,在ARC下正常。
}
大致流程:
源码在对应的ARC编译器下转换为 
使用objc_retainBlock函数处理生成的Block对象tmp_block=objc_retainBlock(tmp_block)
objc_retainBlock实际就是_Block_copy函数,
/**
 * 
将通过Block语法生成的Block赋值给变量tmp_block
 */
// _Block_copy函数将栈上的Block复制到堆上,复制后将堆上的地址作为指针赋值给变量tmp_block
tmp_block=_Block_copy(tmp_block);
// 将堆上的Block作为OC对象注册到autoreleasepool中并返回该对象。
return objc_autoreleaseReturnValue(tmp_block);
特殊情况下编译器并不会自动复制Block,我们需要使用copy实例方法。这个特殊情况就是向方法或函数的参数中传递Block时。有时候方法中已经进行了相应处理,就不用手动复制了,如
  • Cocoa框架的方法且方法名中含有usingBlock时
  • Grand Central Dispatch的API
需要手动复制的例子:
-(id)getBlockArray{
   
int val=10;
   
return [[NSArray alloc]initWithObjects:^{NSLog(@"%d",val);},^{NSLog(@"%d",val);},nil];
}
使用:
//由于getBlockArray函数执行结束,栈上的Block被废除,程序崩溃
   NSArray*array= [selfgetBlockArray];
   void (^aBlock)(void)=array[0];
   aBlock();
修改后代码:
-(id)getBlockArray{
   
int val=8;
    return [[NSArrayalloc]initWithObjects:[^{NSLog(@"%d",val);}copy],[^{NSLog(@"%d",val);}copy],nil];
}

对不同类型的Block调用copy方法效果如下:
Block类
原存储位置
复制效果
_NSConcreteStackBlock
栈区
从栈复制到堆
_NSConcreteGlobalBlock
程序的数据区域
无任何变化
_NSConcreteMallocBlock
堆区
引用计数增加
在不确定是否需要拷贝时进行拷贝。
在ARC中多次调用copy方法是不会造成内存泄露的,可以使用,原因如下:

源代码:
    typedef void (^aBlock)(void);
    aBlock oneBlock;
    oneBlock =[[[oneBlock copy]copy]copy];
该代码可理解为:
    {
       
aBlock tmp=[oneBlock copy];
        oneBlock=tmp;
    }
    {
       
aBlock tmp=[oneBlock copy];
        oneBlock=tmp;
    }
    {
       
aBlock tmp=[oneBlock copy];
        oneBlock=tmp;
    }


提示:

ARC模式下赋值时系统会自动增加强引用
当对象被赋值为nil,对象相关的强引用将被销毁
加入注释:
    oneBlock =[[[oneBlockcopy]copy]copy];
    {
       
//将堆上的Block赋值给tmptmp持有强引用的Block
       
aBlock tmp=[oneBlock copy];//执行copy后从栈到堆
       
//由于此时为ARC模式,赋值后oneBlock持有堆上的Block的强引用,此时Block的持有者为oneBlocktmp。(在非ARC模式下为简单赋值,oneBlock并未持有强引用,需手动增加引用计数,否则如果tmp被释放,oneBlock将不可使用);
        oneBlock=tmp;
    }
    //超出tmp变量作用域,tmparc自动释放,但Block仍被oneBlock引用,未被释放
    {
       
//tmp持有堆上的Block的强引用
       
aBlock tmp=[oneBlock copy];
       
//赋值给oneBlock,oneBlock原先指向的堆上的Block强引用被释放,原先指向的堆上的Block并未废弃(tmp强引用)。oneBlock现在持有与tmp相同的强引用。现在堆上的BlockoneBlocktmp持有。
        oneBlock=tmp;
    }
   
//tmp指向的强引用被销毁,堆上的Block由于被oneBlock持有,仍然存在。
   
//下面过程同上
    {
       
aBlock tmp=[oneBlock copy];
        oneBlock=tmp;
    }

示意图:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章