iOS Block

Block的分类

Block有三种类型:全局Block,堆区Block,栈区Block

全局Block

当Block没有引用到局部变量时或者Block里面使用的是全局变量,静态变量时为全局Block

    int a = 10;
    void (^block)(void) = ^{
         NSLog(@"hello world");
    };

    NSLog(@"block:%@", block);

输出结果:block:<__NSGlobalBlock__: 0x104e580f8>
    static int a1 = 20;
    void (^block)(void) = ^{
        NSLog(@"hello - %d",a1);
    };

    NSLog(@"block:%@", block);
输出结果:block:<__NSGlobalBlock__: 0x100cec0f8>

堆区Block

当Block里有引用到局部变量时为堆区block

    int a = 10;
    void (^block)(void) = ^{
        NSLog(@"hello - %d",a);
    };

    NSLog(@"block:%@", block);
输出结果:block:<__NSMallocBlock__: 0x281065950>
    __block int a = 10;
    void (^block)(void) = ^{
        NSLog(@"hello - %d",a);
    };

    NSLog(@"block:%@", block);
输出结果:block:<__NSMallocBlock__: 0x281aad5c0>

栈区Block

在Block名前面加个__weak就是栈区block

    __block int a = 10;
    static int a1 = 20;
    void (^__weak block)(void) = ^{
        NSLog(@"hello - %d",a);
        NSLog(@"hello - %d",a1);
    };

    NSLog(@"block:%@", block);
输出结果:block:<__NSStackBlock__: 0x16f7ccfb0>

堆Block和栈Block的区别

接下来看看这两种block有什么区别呢?
先看个示例:

    __block int a = 10;
    __block int b = 20;
    NSLog(@"a:%p---b:%p", &a, &b);
    void (^__weak block)(void) = ^{
        NSLog(@"hello - %d---%p",a, &a);
        a++;
    };
    void (^block1)(void) = ^{
        NSLog(@"hello - %d---%p",b, &b);
        b++;
    };
    block();
    block1();
    NSLog(@"block:%@---block1:%@", block, block1);
    NSLog(@"a:%d---b:%d", a, b);
    NSLog(@"a:%p---b:%p", &a, &b);
输出结果:
a:0x16bb7cfe8---b:0x16bb7cfc8
hello - 10---0x16bb7cfe8
hello - 20---0x283e0b1b8
block:<__NSStackBlock__: 0x16bb7cf70>---block1:<__NSMallocBlock__: 0x28307d9b0>
a:11---b:21
a:0x16bb7cfe8---b:0x283e0b1b8

通过结果我们看到,首先block的地址是在栈区,而block1的地址是在堆区,而栈block引用的变量a的地址并没有变化,而堆block1引用的变量b的地址也相应变成了堆区0x283e0b1b8,并且后面使用的b的地址都是堆区上的。
总结:栈block存放在栈区,对局部变量引用只拷贝局部变量的地址,而堆block存放在堆区,并且直接将局部变量拷贝了一份到堆空间。
接下来我们再来看个示例:

NSObject *objc = [NSObject new];
    NSLog(@"%@---%ld",objc, CFGetRetainCount((__bridge CFTypeRef)(objc)));// 1
    // block 底层源码
    // 捕获 + 1
    // 堆区block
    // 栈 - 内存 -> 堆  + 1
    void(^strongBlock)(void) = ^{ // 1 - block -> objc 捕获 + 1 = 2
        NSLog(@"%@---%ld",objc, CFGetRetainCount((__bridge CFTypeRef)(objc)));
    };
    strongBlock();

    void(^__weak weakBlock)(void) = ^{ // + 1
        NSLog(@"%@---%ld",objc, CFGetRetainCount((__bridge CFTypeRef)(objc)));
    };
    weakBlock();
    
    void(^mallocBlock)(void) = [weakBlock copy];
    mallocBlock();
输出结果:
<NSObject: 0x28292ff20>---1
<NSObject: 0x28292ff20>---3
<NSObject: 0x28292ff20>---4
<NSObject: 0x28292ff20>---5

奇怪为什么堆区block里面的对象引用计数加2呢?而后面的mallocBlock只加1呢?
首先objcstrongBlock里面必然会拷贝一份到堆区,所以会加1,但是他是从当前函数的栈区拷贝吗?并不是,对于堆区Block一开始编译时是栈block这时候objc对象地址拷贝了一份引用计数加1,后面从栈block变成堆block,又拷贝了一份引用计数又加1,所以这时候是3
weakBlock栈block仅拷贝了一份,所以引用计数加1,这时候是4
mallocBlockweakblock拷贝了一份,所以引用计数再加1,这时候是5
相当于strongBlock = weakblock + void(^mallocBlock)(void) = [weakBlock copy];

再来看个示例:

    NSObject *a = [NSObject alloc];
    NSLog(@"1---%@--%p", a, &a);
    void(^__weak weakBlock)(void) = nil;
    {
        // 栈区
        void(^__weak strongBlock)(void) = ^{
            NSLog(@"2---%@--%p", a, &a);
        };
        weakBlock = strongBlock;
        strongBlock();
        NSLog(@"3 - %@ - %@",weakBlock,strongBlock);
    }
    weakBlock();
    NSLog(@"4---%@--%p", a, &a);
输出结果:
1---<NSObject: 0x2820337a0>--0x16bcf4fd8
2---<NSObject: 0x2820337a0>--0x16bcf4fc0
3 - <__NSStackBlock__: 0x16bcf4fa0> - <__NSStackBlock__: 0x16bcf4fa0>
2---(null)--0x16bcf4fc0
4---<NSObject: 0x2820337a0>--0x16bcf4fd8

当前是栈区strongBlock的赋值给外面的栈区weakBlock,因为都是存放在栈空间的,只有当前函数结束才会被销毁,随意这边weakBlock调用并不会有什么问题。如果换成堆区block就不一样了
注意:这边的a对象weakBlock()调用时是nil,通过上面打印可以看出a对象在进入到strongblock里,&a拷贝了一份,拷贝的这一份地址指向的跟外面一样,但是当strongblock出了{},尽管strongblock对象不再了,但是其指向的内存空间还在,销毁之前给了外面的weakBlock,同理a也一样,对象(此时a指向的内容)不在了,但是内存空间却还在。

NSObject *a = [NSObject alloc];
//    NSLog(@"1---%@--%p", a, &a);
    void(^__weak weakBlock)(void) = nil;
    {
        // 堆区
        void(^strongBlock)(void) = ^{
            NSLog(@"2---%@--%p", a, &a);
        };
        weakBlock = strongBlock;
        strongBlock();
        NSLog(@"3 - %@ - %@",weakBlock,strongBlock);
    }
    weakBlock();
//    NSLog(@"4---%@--%p", a, &a);
输出结果:
2---<NSObject: 0x281218810>--0x281e7f6e0
3 - <__NSMallocBlock__: 0x281e7f6c0> - <__NSMallocBlock__: 0x281e7f6c0>
调用weakBlock时崩溃

为什么呢?因为在{}里面的堆区strongBlock出了大括号就会被销毁,此时你去调用这个block就会崩溃
注意:这边weakBlock为什么也是__NSMallocBlock__,其实weakBlock相当于是指针,此时指向的是一个堆上的内存所以是__NSMallocBlock__

Block的循环引用

内存泄漏一个主要原因就是block的循环引用。那么如何解决循环引用呢?

- (void)viewDidLoad {
    [super viewDidLoad];
    // 循环引用
    self.name = @"hongfa";
    self.block = ^{
        NSLog(@"%@", self.name);
    }
    
    self.block();
}

这边vc 引用了block ,block引用了vc,最后面造成了循环引用,那么如何解决呢?
方案一:通过weak来解决

- (void)viewDidLoad {
    [super viewDidLoad];
    // 循环引用
    self.name = @"hongfa";
    __weak typeof(self) weakself = self;
    self.block = ^{
        NSLog(@"%@", weakself.name);
    };
    
    self.block();
}
直接使用weakself来代替self,weakself是弱引用,这样不会导致引用计数+1。
这边也有一个问题如下:
    self.name = @"hongfa";
    __weak typeof(self) weakself = self;
    self.block = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@", weakself.name);
        });
    };
    
    self.block();

如果当我们延迟使用weakself的话,这时候的weakself可能已经被销毁了,这时候就需要用到__strong typeof(weakself) strongself = weakself;

    self.name = @"hongfa";
    __weak typeof(self) weakself = self;
    self.block = ^{
        __strong typeof(weakself) strongself = weakself;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@", strongself.name);
        });
    };
    
    self.block();

__strong可以让对象暂时在存活一段时间,用完就会销毁,这样也不会带来内存泄漏。
以上就是通过__weak__strong来解决block的循环引用。
方案二:通过临时变量来解决

    __block ViewController *vc = self;
    self.block = ^(void){
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",vc.name);
            vc = nil;
        });
    };
    self.block();

定义一个跟self一样类型的vc = self,然后block引用vc,用完之后再把vc=nil,这样引用链:self -> block -> vc -> self这样也可以解决循环引用问题

方案三:通过参数将self传进去,传参的话,参数是在栈区,函数运行好栈区销毁参数也就跟着销毁,所以也可以解决循环引用问题

   // 通讯 参数 block 通知
    self.hfblock = ^(ViewController *vc){
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",vc.name);
        });
    };
    self.hfblock(self);

以上就是目前掌握的三种解决循环引用的方案。

block的本质

接下来看看block通过xcrun后究竟是什么?

int main(int argc, char * argv[]) {
    int a = 9;
    __block int b = 10;
    NSObject *objc = [NSObject alloc];
    void(^Block)(void) = ^{
        NSLog(@"a:%d", a);
        NSLog(@"b:%d", b);
        NSLog(@"objc:%@", objc);
    };
    return 0;
}

xcrun -sdk iphonesimulator clang -rewrite-objc main.m
int main(int argc, char * argv[]) {
    int a = 9;
    __attribute__((__blocks__(byref))) __Block_byref_b_0 b = {(void*)0,(__Block_byref_b_0 *)&b, 0, sizeof(__Block_byref_b_0), 10};

    NSObject *objc = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc"));

    void(*Block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, objc, (__Block_byref_b_0 *)&b, 570425344));
    return 0;
}

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  NSObject *objc;
  __Block_byref_b_0 *b; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, NSObject *_objc, __Block_byref_b_0 *_b, int flags=0) : a(_a), objc(_objc), b(_b->__forwarding) { // 构造函数
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

struct __Block_byref_b_0 {
  void *__isa;
__Block_byref_b_0 *__forwarding;
 int __flags;
 int __size;
 int b;
};

xcrun后看到的__block b,底层是变成了结构体b,里面保存了b的值10,整个block也变成了__main_block_impl_0结构体对象,把a, b, objc作为参数传进去。而在block结构体里定义了三个成员变量来保存a,b,objc
通过上面的例子我们就很清楚block的底层结构以及他是如何对引用局部变量

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

推荐阅读更多精彩内容