Block由浅入深(4):Block修改局部变量

Block可以修改的变量

我们可能都知道,不使用__block关键字,我们不能在Block内修改变量的值。但是严格说来,只有不使用__block修饰的局部变量是不能在Block内修改的。全局变量、静态变量和对象的属性都可以不使用__block而在Block内部被修改,下面这段代码是可以正常编译通过的,但是如果我们将main函数中的变量前面的static修饰符去掉,在编译的时候就会报错:error: variable is not assignable (missing __block type specifier),错误就定位在static_val++这行代码。

int global_val = 1;
static int static_global_val = 2;

int main()
{
    static int static_val = 3;

    void (^blk)(void) = ^{
        global_val++;
        static_global_val++;
        static_val++;
    };

    return 0;
}

Block为什么可以修改非局部变量

为什么修改全局变量和静态变量可以不使用__block修饰符呢?
我们需要了解各种不同变量存放的内存区域才能解答这个问题。
Objective-C程序运行的时候,内存空间会被分为数据区、堆区和栈区。数据区在程序加载的时候静态分配,堆区在程序运行的时候由内存分配函数malloc、calloc等或者NSObject的alloc方法动态分配。数据区的内存空间不受程序的控制,只有在程序运行结束后才能被释放;堆区的内存空间,需要由程序自行释放,否则会导致内存泄漏。简而言之,数据区的空间在整个程序运行过程中都是可读写的,堆区的空间什么时候可以读写我们是可以控制的。
栈空间不需要我们手动管理,在程序运行过程中(主要发生在函数调用时)系统会自动进行pop和push操作来管理栈空间。
堆空间和栈空间不同的地方在于,栈空间上存储的信息,一旦过了作用域就会被系统自动pop出去而变得不可合法的读写,而动态分配的堆空间上的数据,就算过了作用域,我们还是可以通过一定的方式能够合法读写到(比如将这块空间的地址存储下来)。

在ARC配置下,很多Objective-C对象的释放已经由编译器自行添加了,但是如果我们使用Core Foundation对象依然需要手动释放。

在Block的使用过程中,给我们的主观感受是,Block内部引用的变量跟Block外部定义的变量是同一个变量。根据我们之前的分析,显然内部实现不是同一个变量。所以我们的主观期望是,如果我们在Block内部修改了某个变量的值,那么Block外部的变量值也会被修改。如何实现这个期望呢?

上述代码中,全局变量global_val、静态全局变量static_global_val和静态局部变量static_val都是存储在数据区的,内存空间不会被释放,所以Block在执行的时候可以直接读写。但是实现方式有所不同,拿上面的例子来说,转化后的主要代码如下:

int global_val = 1;
static int static_global_val = 2;


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

        global_val++;
        static_global_val++;
        (*static_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()
{
    static int static_val = 3;

    void (*blk)(void) = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val));

    return 0;
}

我们发现global_valstatic_global_val都不是Block结构体的成员,而static_val虽然是Block结构体的成员,但是是通过指针的方式实现的。
为什么会有这样的区别实现呢?
在这个例子中,Block是一个局部数据,而global_valstatic_global_val这两个变量的作用域都是文件级作用域,超过了Block的作用域,所以Block内部可以无障碍的使用global_valstatic_global_val变量名来读写这两个变量,所以无需在Block内部再存有这两个变量的引用了。
但是static_val不同,因为Block在执行的时候,有可能已经超出了static_val的作用域,所以这个例子中使用了指针的方式实现,指针是最简单的读写超出作用域的变量的方法了。

那么Block为什么可以修改成员变量呢?道理跟static_val的访问方式类似,因为成员变量是随着self对象在堆空间动态分配的,在Block的内部有一个self指针,通过self指针就可以正常的读写self的成员变量了。

Block为什么不可以修改局部变量

其实Block不可以直接修改局部变量的原因上面跟上面的分析类似:

  1. Block执行的时候有可能它引用的局部变量已经超出作用域了;
  2. 局部变量超出作用域会被系统自动释放掉,即使保存了它的指针也无法合法的访问了;
  3. Block内部对局部变量的修改需要反映到外部的局部变量上。
    基于上面的几个原因,Block内部读写全局变量和静态变量的方式已经不再适用于读写局部变量了。

__block的实现

为什么被__block修饰的变量可以在Block内部被修改呢,我们先看一个简单的例子:

int main(int argc, const char * argv[]) {
    __block int val = 10;
    printf("1. %d, address 0x%lx\n", val, &val);
    void (^blk)(void) = ^{
        val++;
        printf("3. %d, address 0x%lx\n", val, &val);
    };
    
    printf("2. %d, address 0x%lx\n", val, &val);
    blk();
    
    return 0;
}

为了便于分析,我们先给出这个例子的执行结果:


执行结果

val的值我们很容易理解,我们重点关注val的地址。从结果我们发现,在Block表达式之后,val的地址竟然发生了变化!
为了分析原因,我们同样把上面的代码转化一下:

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)++;
        printf("3. %d, address 0x%lx\n", (val->__forwarding->val), &(val->__forwarding->val));
    }
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(int argc, const char * argv[]) {
    __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
    printf("1. %d, address 0x%lx\n", (val.__forwarding->val), &(val.__forwarding->val));
    void (*blk)(void) = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));

    printf("2. %d, address 0x%lx\n", (val.__forwarding->val), &(val.__forwarding->val));

    blk->FuncPtr(blk);

    return 0;
}

我们首先看main函数,发现经过转化后,val已经不是一个普通的int型变量了,而是成为了__Block_byref_val_0结构,这个结构的定义在最开头。
另外一个很大的不同点在于struct __main_block_desc_0多了一个copy和一个dispose成员。copy函数会调用_Block_object_assign,在本例中这个函数使用的参数是Block里的val成员和main函数里的val变量。虽然没有显式调用,但是我们可以猜测在Block实现之前,系统会调用这个函数将val从栈上拷贝到堆上,拷贝到堆上后,Block就可以通过指针来读取val了。

可以通过查阅GNUStep上的开源代码获取一点信息,_Block_object_assign实现在blocks_runtime.m文件内。我们猜测的_Block_object_assign会被调用,可以通过在Xcode中加Symbolic Breakpoint并将Symbol设置为_Block_object_assign来验证。

那么怎么实现在Block内部修改val变量的值,能够影响到外部呢?答案就在__Block_byref_val_0结构的forwarding成员上,我们也发现,经过转化后,所有对val变量的访问都是通过forwarding来中转的,我们通过下图描述一下在main函数里Block实现代码前后内存里究竟发生了什么:

内存变化

在Block未实现时,val存储在栈上,forwarding指向的是它本身;但是当Block实现后,会在堆上分配一个相同的val的对象,同时将栈上val的forwarding指向堆上的val,Block内部持有的是堆上的对象,后续对val的修改都是修改了堆上的对象。因为我们获取val对象里的val值是通过forwarding指针获取的,其指向的对象都是堆上的,所以能保证获取的都是修改后的值。
即使栈上的val超过了作用域,堆上的对象也依然存在,我们也可以正常的访问。

Block修改超出作用域的局部变量

下面是一个Block修改超出作用域的局部变量的例子:

typedef void (^blk_t)(void);

blk_t getBlk() {
    __block int val = 1;
    printf("Begin block, val = %d, address of val = 0x%lx\n", val, &val);
    blk_t blk = ^{
        ++val;
        printf("In block, val = %d, address of val = 0x%lx\n", val, &val);
    };
    ++val;
    printf("In function, val = %d, address of val = 0x%lx\n", val, &val);
    blk();
    
    return [blk copy];
}

int main(int argc, const char * argv[]) {
    blk_t blk = getBlk();
    printf("In main\n");
    blk();
    
    return 0;
}

执行结果见下图:


执行结果

这段代码的原理与上面的是一样的,各位看官可以自行分析理解。

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

推荐阅读更多精彩内容