iOS中block的使用、实现底层、循环引用、存储位置

一、整体介绍

  • 定义:C语言的匿名函数,􏰀提前准备一段代码,在需要的时候调用。
  • 底层:是一个指针结构体,在终端下可以通过clang -rewrite-objc 文件名(会在当前目录生成.cpp文件)指令看看c++代码,它的实现底层。

注意:容易造成循环引用,经常是在 block 里面使用了 self.,然后形成强引用,我们打断循 环链即可,如果 MRC 下用__block,ARC 下用__weak(下文会有详细介绍)。

二、内存位置(ARC情况)

block块的存储位置(block块入口地址):可能存放在2个地方:代码区(NSConcreteGlobalBlock)、堆区(NSConcreteMallocBlock),程序分5个区,还有常量区、全局区和栈区,对于MRC情况下代码还可能存在栈区(NSConcreteStackBlock)。关于内存分区详细参考:http://www.jianshu.com/p/d85a5e56c505

  • 情况1:代码区

不访问处于栈区的变量(例如局部变量),且不访问处于堆区的变量(例如alloc创建的对象)。也就是说访问全局变量也可以。

/**
  没有访问任何变量
 */
int main(int argc, char * argv[]) {
    void (^block)(void) = ^{
        NSLog(@"===");
    };
    block();
}
/**
  访问了全局(静态)变量
 */
int  iVar = 10;
int main(int argc, char * argv[]) {
    void (^block)(void) = ^{
        NSLog(@"===%d",iVar);
    };
    block();
}
  • 情况2:堆区

如果访问了处于栈区的变量(例如局部变量),或处于堆区的变量(例如alloc创建的对象)。都会存放在堆区。(实际是放在栈区,然后ARC情况下自动又拷贝到堆区)

/**
  访问局部变量
 */
int main(int argc, char * argv[]) {
    int iVar = 10;
    void (^block)(void) = ^{
        NSLog(@"===%d",iVar);
    };
    block();
}

总结下:

  • 代码区:不访问处于栈区的变量(例如局部变量),且不访问处于堆区的变量(例如alloc创建的对象)。也就是说访问全局变量(静态变量)也可以,或者是什么变量都不访问
  • 堆区:如果访问了处于栈区的变量(例如局部变量),或处于堆区的变量(例如alloc创建的对象),即便也访问了全局变量

三、注意事项

1 block为空

代码存放在堆区时,就需要特别注意,因为堆区不像代码区不变化,堆区是不断变化的(不断创建销毁)。因此代码有可能会被销毁(当没有强指针指向时),如果这时再访问此段代码则会程序崩溃。因此,对于这种情况,我们在定义一个block属性时应指定为strong,或copy:

  • @property (nonatomic, strong) void (^myBlock)(void); // 这样就有强指针指向它
  • @property (nonatomic, copy) void (^myBlock)(void); // 并不会在堆区copy一份,原因见 四

而对于block代码存在代码区,使用strong,copy(不会复制一份到堆区)也可以。因此定义block时最好指定为strong(推荐)或copy。我们在使用时最后判断下block是否为空,例如:

- (void)blockTest {
    // 如果为空则返回
    if (!block) {
        NSLog(@"block is nil");
        return;
    }
    block();
  
}

2 当不在使用block时,将其置空

当有类成员.block时:

一方面是调用方,调用.block调用完成后,应将.block置为nil;另一方面是block内部使用到self时要__weak声明。下面是一个经典例子(是正确的写法):

@property (nonatomic, copy) dispatch_block_t block;

----

// 弱声明,防止block强引用self,造成循环引用
    __weak __typeof(self) weakSelf = self;
    self.observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"blockTest" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
        // 多线程情况下(假设发出通知的代码在另一线程下),strong强引用防止后面调用strongSelf时:前面的strongSelf正常,后面的strongSelf已在其它线程被释放,造成很奇怪的结果,虽然这种情况很少发生
        __strong __typeof(self) strongSelf = weakSelf;
        //if (strongSelf == nil) {
        //    return;
        //}
        // 下面再对strongSelf进行访问
        // 防止block为空
        if (!strongSelf.block) {
            return;
        }
        strongSelf.block();
        // 如果不用应置空,养成好习惯
        strongSelf.block = nil;
        NSLog(@"%@",strongSelf);
    }];
  • 1)我们都知道在使用通知中心时,应在dealloc函数中释放通知,如果上面没有使用__weak声明,那么:通知中心持有self.observer,observer又强引用 usingBlock(参数),usingBlock又强引用self,self就不会被释放,那么dealloc就不会被调用(即使在dealloc中写了[[NSNotificationCenter defaultCenter] removeObserver:self.observer]也不会调用,因为dealloc没有被调用),就造成内存泄露;

  • 2)另外,我们在第5行看到又使用了__strong声明,是否瞬间凌乱?下面给出解释:在多线程情况下,有可能在usingBlock调用时,执行if (!strongSelf.block)时strongSelf还没有释放,而执行到strongSelf.block()的时候strongSelf就被释放(现在没有强引用了,又开始担心self被释放,真是操碎了心。。。),造成调用失败(最大的问题是不统一,造成不可预知的错误。用__strong操作后保证要么都访问成功,要么都访问失败或者判断为空后直接return退出)。

而使用了__strong声明后:

  • 如果执行usingBlock时self已经被释放则后面的strongSelf均为nil,因为对weakSelf引用计数为0再retain一次也不会有变化;

  • 如果执行usingBlock时self没有释放,则strongSelf会使self引用计数+1,那么self在其它线程被release -1也不会有影响,只有到usingBlock全部执行完毕后,strongSelf释放,然后self引用计数-1,self才会释放(weak–strong dance)。

上面的例子是通知中心可能造成的内存泄露,而使用block还经常出现循环引用,如下:

3 最常出现的循环引用

@interface BlockViewController ()
@property (nonatomic, strong) void (^block)(void);
@property (nonatomic, copy) NSString *str;
@end

@implementation BlockViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.block = ^{
        self.str = @"123";
    };
}
@end

上面的代码,self.block强引用block,而block中又使用了self.str,所以block强引用self,造成强引用,解决方法使用2中所说即可。

关于引用计数(http://www.jianshu.com/p/28b074919df3)

四、关于捕获变量

block里面捕获的变量,都是副本。看下面一段代码

int val = 10;
void (^block)(void) = ^{
    NSLog(@"val = %d",val);
    // val = 1; //不允许
};
val = 5;
block();

它的打印结果是10,而不是5。

上面代码中val = 1是不允许的,如果想实现写操作,可以使用__block来修饰val,之后val会被拷贝(移动,便于理解)到堆上,之后无论是在block里面还是在val之前所处的作用域,访问的都是出于堆区的val。

为什么非要__block呢,因为如果不用__block,如果出了val所在的“}”,那么val就会被释放,而block的调用时机是不定的,可能调用时机已经超出了block和val本身所处的"{}",再访问val就可能坏地址访问(val已经被释放)。所以这样做是合理的。

但是在block里面,类似self.name = xxx,self->_val,却是很常见的,self也没有用__block修饰呀!你是否有过这样的迷惑?

self.name = xxx——>[self setName:xxx];是发送消息,函数调用,很好理解。那self->_val呢?因为_val本身是处于堆区的。

五、指定为copy后是否会拷贝一份呢?(或者说是浅拷贝还是深拷贝)

  • 1 copy可变变量:在赋值指针的同时也会复制指针指向的内存区域。深拷贝,例如NSMutableString对象。

  • 2 copy不可变变量:等同于strong,还是浅拷贝,例如NSString对象。

  • 因为block是一段代码,即不可变的,所以并不会深拷贝。

六、一些思考

block也是属于“函数”的范畴,即一段代码。为什么要将其放在堆区呢,而不是直接在代码区呢?

试想一下,如果不放到堆区,而放在代码区,那么block捕获的self对象将永远不会释放,因为代码区的block是不会释放的,那内存的泄露可就随处可见了。。。

所以苹果这么做也是有原因的

参考:http://www.cnblogs.com/mddblog/p/4754190.html

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

推荐阅读更多精彩内容