iOS底层原理 - 探寻block本质 之 __block

面试题引发的思考:

Q: __block的作用是什么?有什么使用注意点?

  • __block用于解决block内部无法修改auto变量值的问题;
  • __block不能修饰全局变量、静态变量。

Q: block内部修改的NSMutableArray,是否需要添加__block

  • 因为block内部只是使用了array的内存地址添加数据,并没有修改array的内存地址,所以array不需要__block修饰;
  • 添加__block修饰符之后,系统会创建相应的结构体,占用一定的内存空间;所以要根据相应情况添加__block修饰符,避免内存浪费。

Q: 使用block有那些注意事项?

  • 注意循环引用问题。

iOS底层原理 - 探寻block本质(二)中介绍到block对对象类型的变量捕获,以及对象的销毁时机。

下面介绍如何实现在block内部修改变量的值。


1. 修饰符__block

需要在block内部修改变量的值,代码如下:

typedef void (^Block)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        Block block = ^{
            age = 20;  // error
        };
        block();
    }
    return 0;
}

以上代码会出现编译错误。

C++源码

通过源码可知:
age是在main函数内部声明的变量,存在于main函数的栈空间内部;
block内部实现是__main_block_func_0函数,其内部捕获age,新增一个参数存储外部的age变量的值,这个age存在于block的栈空间内部;
所以block内部无法修改main函数内部的auto变量。

Q: 那么该如何在block内部修改变量的值呢?

1> 方法一:使用static变量
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        static int age = 10;
        Block block = ^{
            age = 20;
            NSLog(@"------------ %d", age);
            // 打印结果:------------ 20
        };
        block();
    }
    return 0;
}

由前文可知:局部变量都会被block捕获,auto变量值传递,static变量指针传递。
block内部会新增一个参数存储age的指针,通过指针访问age变量的内存地址,就可以修改age的值。

2> 方法二:使用全局变量

全局变量在哪里都可以访问,所以block不用捕获全局变量,直接进行访问。

以上两种方法会使变量一直存在于内存中,占用内存地址。我们可以使用__block修饰符来解决这个问题。

3> 方法三:使用__block修饰变量
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int age = 10;
        Block block = ^{
            age = 20;
            NSLog(@"------------ %d", age);
            // 打印结果:------------ 20
        };
        block();
    }
    return 0;
}

转化成C++源码:

使用__block修饰变量的源码

由源码可知:

存值时:

  1. __block修饰的age变量会在block内部转化成名为age__Block_byref_age_0结构体,结构体包括:

    • __isa:说明__Block_byref_age_0本质也是对象
    • __flags:赋值为0
    • __forwarding:指向结构体自身的指针
    • __size:占用的内存空间
    • age:存储变量
  2. 然后__Block_byref_age_0结构体age存储在结构体__main_block_impl_0中。

取值时:

  1. 通过_cself->ageage赋值给 __Block_byref_age_0结构体;
  2. age->__forwarding->age通过结构体指针访问成员变量来改变成员变量的值;
  3. 通过age->__forwarding->age进行取值。
4> Q: 以下代码是否可以正确执行?
typedef void (^Block)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSMutableArray *array = [NSMutableArray array];
        Block block = ^{
            [array addObject: @"1"];
            [array addObject: @"2"];
            NSLog(@"array - %@", array);
        };
        block();
    }
    return 0;
}
  • 可以正确执行。
  • 因为block内部只是使用了array的内存地址添加数据,并没有修改array的内存地址;
  • 所以array不需要__block修饰;
  • 添加__block修饰符之后,系统会创建相应的结构体,占用一定的内存空间;所以要根据相应情况添加__block修饰符,避免内存浪费。

2. __block内存管理

(1) 分析一下代码:

typedef void (^Block)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        int age = 10;
        __block int weight = 60;

        NSObject *object = [[NSObject alloc] init];
        __weak NSObject *weakObject = object;
        __block NSObject *blockObject = object;
        __block __weak NSObject *blockWeakObject = object;

        Block block = ^{
            NSLog(@"%d", age); // 局部变量
            NSLog(@"%d", weight); // __block修饰的局部变量
            NSLog(@"%p", object); // 对象类型的局部变量
            NSLog(@"%p", weakObject); // __weak修饰的对象类型的局部变量
            NSLog(@"%p", blockObject); // __block修饰的对象类型的局部变量
            NSLog(@"%p", blockWeakObject); // __block、__weak修饰的对象类型的局部变量
        };
        block();
     }
    return 0;
}

使用命令行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m将代码转化成C++语言:

__main_block_impl_0函数

__main_block_impl_0函数可知:

  1. 未使用__block修饰的变量(objectweakObject),根据被block捕获的指针类型进行强引用或弱引用;
  2. 使用__block修饰的变量(weightblockObjectblockWeakObject),都是使用强指针引用生成的结构体。
结构体

由以上__block修饰的对象类型的变量生成的结构体可知:

  1. 其内部多了__Block_byref_id_object_copy__Block_byref_id_object_dispose两个函数,用来对对象类型的变量进行内存管理操作。
  2. block捕获的对象类型决定结构体对象的引用类型:
    a> blockObject是强指针,所以__Block_byref_ blockObject_1blockObject就是强引用;
    b> blockWeakObject是弱指针,所以__Block_byref_ blockWeakObject_1blockWeakObject就是弱引用。
__main_block_copy_0函数、__main_block_dispose_0函数

由以上C++代码可知:

  1. __main_block_copy_0函数根据变量的强弱指针及是否被__block修饰做出不同处理:
    a> 强指针在block内部产生强引用;
    b> 弱指针在block内部产生弱引用;
    c> 被__block修饰的变量最后的参数传入的是8
    d> 没有被__block修饰的变量最后的参数传入的是3
  2. __main_block_dispose_0函数会在block从堆中移除时释放这些变量。

(2) 总结:

block复制到堆上

由block复制到堆上的内存变化图可知:

  1. 将block复制到堆上时,block内部引用的__block变量也被复制到堆上,此时block持有__block变量;
  2. 若将block复制到堆上时,__block变量已经在堆上,则不会再次将其复制到堆上。
block从堆上移除

由block从堆上移除的内存变化图可知:

  1. 将block从堆中移除时,若有别的block持有__block变量,则不会将__block变量移除;
  2. 将所有的block从堆中移除时,此时没有block持有__block变量,__block变量被移除。

(3) __forwarding指针

typedef void (^Block)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int age = 10;
        Block block = ^{
            age = 20;
        };
        block();
     }
    return 0;
}

将代码转化成C++语言:

C++代码

由以上代码可知:

  1. 当block在栈上时,栈上的__Block_byref_age_0结构体内部__forwarding指针指向结构体自己;
  2. 当block复制到堆上时,栈上的__Block_byref_age_0结构体也会被复制到堆上,此时栈上的__Block_byref_age_0结构体内部__forwarding指针指向的是堆中的__Block_byref_age_0结构体,堆中__Block_byref_age_0结构体内的__forwarding指针依然指向自己。

以上结论可由下图展示:

__forwarding指针

3. __block修饰的对象类型的内存管理

(1) __block修饰对象类型

typedef void (^Block)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block Person *person = [[Person alloc] init];
        // __block __weak Person *person = [[Person alloc] init];
        Block block = ^ {
            NSLog(@"%p", person);
        };
        block();
     }
    return 0;
}

将代码转化成C++语言:

C++代码

由以上C++代码可知:
__Block_byref_person_0内部多了__Block_byref_id_object_copy__Block_byref_id_object_dispose两个函数,用来对对象类型的变量进行内存管理操作:

对于__Block_byref_id_object_copy函数:
a> __Block_byref_id_object_copy函数赋值为__Block_byref_id_object_copy_131函数;
b> __Block_byref_id_object_copy_131函数调用_Block_object_assign函数;
c> _Block_object_assign函数内部拿到dst指针(block对象的地址值)加上40个字节,即为person指针。

也就是说__Block_byref_id_object_copy函数会将person地址传入_Block_object_assign函数,_Block_object_assign中对Person对象进行强引用或者弱引用。

__Block_byref_id_object_copy函数同理。

(2) __block__weak同时修饰对象类型

使用__block__weak同时修饰变量同理。

block内部对__block修饰变量生成的结构体都是强引用;
结构体内部对外部变量的引用取决于传入block内部的变量是强引用还是弱引用。

(3) 总结

  • 当block在栈上时,不会对__block变量产生强引用;

  • 当block被copy到堆时:
    a> 会调用block内部的copy函数;
    b> copy函数内部会调用_Block_object_assign函数;
    c> _Block_object_assign函数会根据所指向对象的修饰符(__strong__weak__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain)。

  • 当block从堆中移除时:
    a> 会调用block内部的dispose函数;
    b> dispose函数内部会调用_Block_object_dispose函数;
    c> _Block_object_dispose函数会自动释放指向的对象(release)。


4. 循环引用

(1) 循环引用原理

// TODO: -----------------  Person类  -----------------
typedef void (^Block)(void);

@interface Person : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, copy) Block myBlock;
@end

@implementation Person
- (void)dealloc {
    NSLog(@"------------ %s", __func__);
}
@end

// TODO: -----------------  main  -----------------
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        person.age = 10;
        person.myBlock = ^{
            NSLog(@"%d", person.age);
        };
     }
    NSLog(@"大括号已经结束");
    return 0;
}

// 打印结果
Demo[1234:567890] 大括号已经结束

由打印结果可知:
大括号已经结束,person没有被释放,产生了循环引用。

循环引用原理

循环引用原理如上图所示:
大括号结束后引用1被断开,引用2引用3没有断开,形成循环引用,进而造成内存泄漏。

(2) 循环引用解决方法 - ARC

解决循环引用问题,还需要保证block在person销毁前不被销毁,解决方案是:
Person对block的引用(引用2)为强引用;block内部对Person的引用(引用3)为弱引用。

1> 使用__weak__unsafe_unretained修饰符
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        person.age = 10;

        // __weak:不会产生强引用,指向对象销毁时,会自动让指针置为nil
        // __unsafe_unretained:不会产生强引用,不安全,指向对象销毁时,指针存储的地址值不变

        //  __weak Person *weakPerson = person;
        // __weak typeof(person) weakPerson = person;
        __unsafe_unretained typeof(person) weakPerson = person;
        person.myBlock = ^{
            NSLog(@"age - %d", weakPerson.age);
        };
    }
    NSLog(@"大括号已经结束");
    return 0;
}
2> 使用__block修饰符
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block Person *person = [[Person alloc] init];
        person.age = 10;
        person.myBlock = ^{
            NSLog(@"age - %d", person.age);
            person = nil;
        };
        person.myBlock();
    }
    NSLog(@"大括号已经结束");
    return 0;
}

使用__block修饰符打破循环引用原理如下:

__block修饰符打破循环引用

由上文可知:
__block修饰person变量,会生成__Block_byref_person_0结构体,其内部包含的person对象才是block内部使用的变量。

那么将block内部的person置为nil,三角循环引用就会断开。

此方法要求执行block,并且在block内部将person对象置为nil

(2) 循环引用解决方法 - MRC

1> 使用__unsafe_unretained修饰符
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __unsafe_unretained Person *person = [[Person alloc] init];
        person.age = 10;
        person.myBlock = [^{
            NSLog(@"age - %d", person.age);
        } copy];
        [person release];
    }
    NSLog(@"大括号已经结束");
    return 0;
}

MRC环境下不支持__weak修饰符,使用__unsafe_unretained修饰符原理同ARC环境下相同,不再赘述。

2> 使用__block修饰符
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block Person *person = [[Person alloc] init];
        person.age = 10;
        person.myBlock = [^{
            NSLog(@"age - %d", person.age);
        } copy];
        [person release];
    }
    NSLog(@"大括号已经结束");
    return 0;
}

由上文可知:
MRC环境下,当block被copy到堆时,__block结构体不会对person产生强引用,所以也可以解决循环引用问题。

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

推荐阅读更多精彩内容