美文网首页
[7]iOS的block介绍

[7]iOS的block介绍

作者: 默然走一生 | 来源:发表于2019-04-30 08:54 被阅读0次

    一、整体介绍

    定义: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

    内存分区:可以分为5个区

    说到内存分区,内存即指的是RAM

    (高地址)

    • 栈区(stack): 这个一般由编译器操作,或者说是系统管理,会存一些局部变量,函数跳转跳转时现场保护(寄存器值保存于恢复),这些系统都会帮我们自动实现,无需我们干预。 所以大量的局部变量,深递归,函数循环调用都可能耗尽栈内存而造成程序崩溃
    • 堆区(heap): 一般由程序员管理,比如alloc申请内存,free释放内存。我们创建的对象也都放在这里
    • 全局区(静态区 static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放。注意:在嵌入式系统中全局区又可分为未初始化全局区:.bss段 和初始化全局区:data段。举例:int a;未初始化的。int a = 10;已初始化的。
    • 常量区:常量字符串就是放在这里的,还有const常量
    • 代码区:存放代码,app程序会拷贝到这里
      (最下面的是低地址)

    情况1:代码区 NSConcreteGlobalBlock

    1,全局的静态block,不会访问外部的变量。就是说如果你的block没有调用其他 的外部变量,那你的block类型就是这种。例如:你仅仅在你的block里面写一个NSLog("hello world");
    不访问处于栈区的变量(例如局部变量),且不访问处于堆区的变量(例如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:栈区NSConcreteStackBlock

    2.保存在栈中的 block,当函数返回时会被销毁。这个block就是你声明的时候不用copy修饰,并且你的block访问了外部变量。

    情况3:堆区 (ARC情况下会自动拷贝到堆区、因此ARC下只有两个地方:代码区和堆区)。

    保存在堆中的 block,当引用计数为 0 时会被销毁。(实际是放在栈区,然后ARC情况下自动又拷贝到堆区)
    如果访问了处于栈区的变量(例如局部变量),或处于堆区的变量(例如alloc创建的对象)。都会存放在堆区。

    /**
      访问局部变量
     */
    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的指针时,将其置空

    当有类对象的成员变量pBlock指向block时,一方面是调用方,调用pBlock调用完成后,应将pBlock置为nil;另一方面是被调用方即block函数内部使用到self时要__weak声明。其实__weak声明有很多注意事项,下面是一个经典例子(是正确的写法):

    block本身是像对象一样可以retain,和release。但是,block在创建的时候,它的内存是分配在栈上的,而不是在堆上。他本身的作于域是属于创建时候的作用域,一旦在创建时候的作用域外面调用block将导致程序崩溃。因为栈区的特点就是创建的对象随时可能被销毁,一旦被销毁后续再次调用空对象就可能会造成程序崩溃,在对block进行copy后,block存放在堆区.
    使用retain也可以,但是block的retain行为默认是用copy的行为实现的,
    因为block变量默认是声明为栈变量的,为了能够在block的声明域外使用,所以要把block拷贝(copy)到堆,所以说为了block属性声明和实际的操作一致,最好声明为copy。

    // 弱声明,防止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还经常出现循环引用,如下:

    @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中所说即可。

    四、关于捕获变量

    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是不会释放的,那内存的泄露可就随处可见了。。。

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

    相关文章

      网友评论

          本文标题:[7]iOS的block介绍

          本文链接:https://www.haomeiwen.com/subject/dpxmnqtx.html