Objective-C Block篇(二) : Block捕获自动变量机制

带有(捕获)自动变量值

正确理解Block的定义:带有自动变量的匿名函数.

关于"本质是匿名函数"在上一篇中已经讲过。正确理解"带有自动变量值",也还是需要从Block的底层实现是函数出发

# 机制

Block会自动截获定义语法中所使用到的自动变量(全局变量、静态自动变量、静态全局变量不用截获,因为作用域的原因,可直接使用)的值,即保存该自动变量的瞬间值

  • 定义后(保存后)修改,对调用时没有任何影响。
  • 定义中修改自动变量值编译器报错

捕获非对象自动变量

  • 可以使用,但是不能赋值变量

捕获对象自动变量

  • 可以使用对象的任何方法(比如:mutableArray的addObject都可以),但是不能赋值变量
  • 可以使用、赋值修改对象的属性

注意:
Blocks中,截获自动变量的方法并没有实现对C语言数组的截获

const char text[] = "hello";
在Block中使用text,获取元素是会编译报错的
//可以使用指针
const char * text = "hello";

# 实现原理(跟C函数的值传递、地址传递结合理解)

带有自动变量的值的原因是:Block需要保证定义中使用的自动变量在外部随时可能释放,所以Block需要保留该变量(全局、静态因为不会被释放,所以Block对此类变量没有操作)

在上一篇中可以看到Block在没有拦截自动变量时的默认形式如下

// Block类型变量对应的结构体
struct __main_block_impl_0 {
    struct __block_impl imp1;   
    struct __main_block_desc_0* Desc;
    //默认构造函数,C++中的语法
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
        imp1.isa = &_NSConcreteStackBlock;
        imp1.Flags = flags  //不传默认=0,Reserved默认也是0
        imp1.FuncPtr = fp;
        Desc = desc:
    }
};

//该函数的参数__cself相当于C++实例方法中指向实例自身的变量this,或是Objective-C实例方法中指向对象自身的变量self
//即参数__cself为`指向Block值的变量`
static void __main_block_func_0(struct __main_block_impl_0* cself)
{
    printf("Block\n");
}

//今后版本升级所需的区域和Block的大小
static struct __main_block_desc_0{
    unsigned long reserved;
    unsigned long Block_size;
} __main_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0)   //block对应结构体的实例大小
};

    //定义
     void (*blk)(void) = (void(*)(void)) &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);  
    //调用
    ( (void (*)(struct __block_impl *)) ((struct __block_impl *)blk)->FuncPtr )((struct __block_impl *)blk);

Block将语法表达式中使用到的自动变量作为成员变量追加到了Block类型变量对应的结构体(__main_block_impl_0)中

## Block拦截非OC对象自动变量

反编译代码:

// Block类型变量对应的结构体
struct __main_block_impl_0 {
    struct __block_impl imp1;   
    struct __main_block_desc_0* Desc;
   "const char * fmt;
    int val;"
    //默认构造函数,C++中的语法
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, "const char * _fmt, int _val,"  int flags=0)" : fmt(_fmt), val(_val)"{
        imp1.isa = &_NSConcreteStackBlock;
        imp1.Flags = flags  //不传默认=0,Reserved默认也是0
        imp1.FuncPtr = fp;
        Desc = desc:
    }
};

//该函数的参数__cself相当于C++实例方法中指向实例自身的变量this,或是Objective-C实例方法中指向对象自身的变量self
//即参数__cself为`指向Block值的变量`
static void __main_block_func_0(struct __main_block_impl_0* cself)
{
    "
      const char * fmt = __cself->fmt;
      int val = __cself->val;
      printf(fmt, val);
    "
}

//今后版本升级所需的区域和Block的大小
static struct __main_block_desc_0{
    unsigned long reserved;
    unsigned long Block_size;
} __main_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0)   //block对应结构体的实例大小
};

    //定义
     void (*blk)(void) = (void(*)(void)) &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA ", fmt, val");  
    //调用
    ( (void (*)(struct __block_impl *)) ((struct __block_impl *)blk)->FuncPtr )((struct __block_impl *)blk);

## Block拦截OC对象自动变量

现状:

  1. Block将语法表达式中使用到的自动变量作为成员变量追加到了Block类型变量对应的结构体(__main_block_impl_0)中,
  2. C语言结构体不能含有附有__strong修饰符的变量,因为编译器不知道何时进行C语言结构体的初始化和废弃操作,不能很好的管理内存。

拦截NSArray对象自动变量反编译代码:

"=========OC代码========="
id array = [[NSMutableArray alloc] init];
blk = [^(id obj) {
    [array addObject: obj];
    NSLog(@“array count = %ld”, [array count]);
} copy];

blk([NSObject new]);

"========反编译代码========"
// Block类型变量对应的结构体
struct __main_block_impl_0 {
    struct __block_impl imp1;   
    struct __main_block_desc_0* Desc;
   "id __strong array;"
    //默认构造函数,C++中的语法
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, "id __strong _array,"  int flags=0)" : array(_array)"{
        imp1.isa = &_NSConcreteStackBlock;
        imp1.Flags = flags  //不传默认=0,Reserved默认也是0
        imp1.FuncPtr = fp;
        Desc = desc:
    }
};

//该函数的参数__cself相当于C++实例方法中指向实例自身的变量this,或是Objective-C实例方法中指向对象自身的变量self
//即参数__cself为`指向Block值的变量`
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]);
    "
}

/*
使用_Block_object_assign函数将 对象类型 对象赋值给Block结构体的成员变量array,并持有该对象
_Block_object_assign函数调用相当于retain实例方法的函数
*/
"static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 * src)
{
  _Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT);
}"

/*
 Block_object_dispose函数调用相当于release实例方法的函数,释放Block结构体成员变量array中的对象
*/
"static void __main_block_dispost_0(struct __main_block_impl_0 * src)
{
  _Block_object_dispose(src->array, BLOCK_FIELD_IS_OBJECT);
}"

//今后版本升级所需的区域和Block的大小
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),   //block对应结构体的实例大小
   "__main_block_copy_0,
    __main_block_dispose_0"
};

    //定义
     void (*blk)(void) = (void(*)(void)) &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA ", fmt, val");  
    //调用
    ( (void (*)(struct __block_impl *)) ((struct __block_impl *)blk)->FuncPtr )((struct __block_impl *)blk);

内部实现:
__main_block_desc_0结构体中增加的成员变量copy和dispose , 以及作为指针赋值给该成员变量__main_block_copy_0函数和__main_block_dispose_0函数。

  • 用途:在上面代码段中有注明
  • 调用时机:在上面转换的源码中,没有发现调用。在Block从栈赋值到堆上(即_Block_copy函数)以及堆上的Block被废弃时才会调用这些函数
  • 在__block变量时,也是这两个方法,不过其中有些不同,对象是BLOCK_FIELD_IS_OBJECT, __block变量是BLOCK_FIELD_IS_BYREF,通过这两个参数区分两种类型。不过持有、释放时机与机制都是一样的。

这是为什么堆上的Block捕获的对象与__block类型变量能超出变量作用域而存在

__block存储域说明符

# 机制

为什么Block拦截的自动变量为什么默认不能修改?
C函数对参数的处理机制是:对自动变量-非对象是值传递,对自动变量-对象,是复制栈上的指针变量(对象名)
Block被定义为匿名函数,是借鉴了C函数的机制,其实是拿不到实质的自动变量,修改没有意义,对外部没有影响。
为什么Block要如C一样对其进行复制?
因为如果直接使用最初的自动变量,当变量作用域结束的同时,原来的自动变量也会被废弃。将不能通过指针(地址)访问原来的自动变量。Block如同C一样,不做额外操作,是可以直接访问,修改静态变量、静态全局变量、全局变量,不用担心作用域问题

使用附有__block说明符的自动变量可在Block中修改、赋值,此类变量称为__block变量

# 实现原理

C语言有以下存储域类说明符:

  • typedef
  • extern
  • static 表示作为静态变量存储在数据区中
  • auto 表示作为自动变量存储在栈中
  • register

__block说明符类似于static、auto和register,用于指定将变量值设置到哪个存储域中

"block拦截__block变量与拦截全局、静态全局变量的代码分析"
1. 拦截全局、静态全局变量
    没有形态改变,在源码中也是以全局变量、静态全局变量形态存在
    原处定义相同的代码,__main_block_func_0中直接使用变量
    "其他与默认的Block的源码一样"

2. 拦截静态自动变量(与上面看到的拦截自动变量形式一样)
    //拦截静态自动变量:static int static_auto_val = 3;
    - 在原处定义static int static_auto_val = 3;
    - 在__main_block_impl_0中增加了成员变量 int * static_auto_val; 指向该静态变量的指针
    "其他没变化"

3. 拦截__block变量
    拦截 
        __block int val = 10;
        __block id __strong(默认) obj = [[NSObject alloc] init];
//源码:
    //__block变量如同Block一样变为了结构体__Block_byref_val_0实例、__Block_byref_obj_0实例
struct __Block_byref_val_0 {
  void * _isa;
  __Block_byref_val_0 * __forwarding;  //指向自身,确保__block是配置在堆上还是在栈上,都可以通过这个指针正确访问栈上的__block变量和堆上的__block变量
  int __flags;
  int __size;
  int val;  //原先变量值,意味着该结构体持有相当于原自动变量的成员变量
}

static void __main_block_copy_0 (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_dispost_0(struct __main_block_impl_0 * src)
{
  _Block_object_dispose(src->array, Block_FIELD_IS_BYREF);
}

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* src)
{
  _Block_object_dispose(*(void * *)   ((char *)src+40), 131);
}

__main_block_impl_0结构体新增成员变量__Block_byref_val_0 *val;
static void_main_block_func_0(struct __main_block_impl_0 * __cself){
  __Block_byref_val_0 * val = __cself->val;
  (val->__forwarding->val);  //具体使用
}

//今后版本升级所需的区域和Block的大小
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),   //block对应结构体的实例大小
   "__main_block_copy_0,
    __main_block_dispose_0"
};


    //定义
    __Block_byref_val_0 val = {
        0,
        &val,
        0,
        sizeof(__Block_byref_val_0),
        10  //原先变量值
    }
    __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];
    }
     void (*blk)(void) = (void(*)(void)) &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA ", &val, 0x22000000");  
    //调用
    ( (void (*)(struct __block_impl *)) ((struct __block_impl *)blk)->FuncPtr )((struct __block_impl *)blk);

由此可见,block对__block修饰的OC对象,与未用__block修饰的,在内存管理上几乎是一致的,_Block_object_assign持有,_Block_object_disposes释放. _block修饰的OC对象,只要_block变量还在堆上存在,就不会释放。只不过,__block修饰的OC对象,不增加引用计数,block不retain变量

以上对__block作用域修饰符的底部实现,做了反编译查看,那么__block变量具体是怎么运作的呢?

  • 在block复制到堆上的时候,__block变量也会复制到堆上,在block已经被复制到堆上的时候,再复制,对__block变量没有任何影响。当block被废弃的时候,__block变量也会被释放。
  • 当多个block引用__block变量时,引用计数增加。与OC的引用计数内存管理完全相同。从编译的源码来看,与block拦截OC自动变量时除了类型Block_FIELD_IS_BYREF不一样,其他都一样。
  • __forwarding成员变量指向自身,确保__block是配置在堆上还是在栈上,都可以通过这个指针正确访问栈上的__block变量和堆上的__block变量
  • 栈上的__block变量,在__block变量从栈上复制到堆上的时候,会将成员变量__forwarding的值替换为复制到的目标堆上的__block变量的地址 这也能解释为什么__block修饰过的指针变量(对象名),在block定义后,再打印对象名的地址,发现变为了堆上的地址。
  • 栈上的__block变量,在block不发生复制,一直在栈上的时候,也不会发生复制,会仍然在栈上,不过此时__forwarding指向自身,会将自身传入block,不跟默认一样,复制指针变量传入。
    最后两条也是为什么:__block修饰的变量可以修改?的原因。可以保证__block变量在block中的修改,对外部真实有效

# __block与其他所有权修饰符结合

上面,讲的都是__block与默认的__strong修饰的现象。

__weak与block的结合使用
block不管赋值、copy怎么操作,对外部__weak修饰的对象,不持有,这是避免循环引用的机制之一。

__block与__weak同时使用
现在与__weak单独使用是一致的

__block与__autoreleasing不能同时使用

# __block避免循环引用

根本机制是:

    1. __block修饰时,引用计数不会增加,block不retain变量
    1. __block修饰时,可以在block中修改,置为nil,手动解除循环引用,但是比__weak的缺点是必须要执行block,要不然不触发置nil行为,还是会循环引用
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容