iOS基础深入补完计划--Block相关原理探究

作者: kirito_song | 来源:发表于2017-12-05 15:13 被阅读323次

    前文地址:《iOS基础深入补完计划》

    在前文、我们提到了property中的关键字copy可以修饰block。
    那么、block究竟有哪些特殊。
    block该怎样声明
    __block、__weak、__strong是如何工作的呢。
    __block又是否可以解决循环引用呢?

    首先、关于block的作用域

    • 总的来说。block默认是存档在栈中(作用于被限制在当前上下文(函数, 方法...))、可能被随时回收(比如代码块执行完毕、会将块内变量内存回收)。通过copy操作可以使其在堆中保留一份,、相当于一直强引用着。以此保证在执行block对象时、程序不会因为执行一个空对象而崩溃。
    举个例子
       @interface ViewController ()
       
       @property (nonatomic, copy) void(^myblock)();
       
       @end
       
       @implementation ViewController
       
       - (void)viewDidLoad {
           [super viewDidLoad];
           // Do any additional setup after loading the view, typically from a nib.
       
           [self test];//当此代码结束时,test函数中的所有存储在栈区的变量都会被系统释放。
           self.myblock();
       
       }
       
       - (void)test {
           int n = 5;
           [self setMyblock:^{
               NSLog(@"%d",n);
           }];
       }
    
    之后、把copy声明换成assign。

    EXC_BAD_ACCESS、使用了一个野指针。


    屏幕快照 2017-12-04 下午5.53.09.png
    此外、我们还可以进一步确认崩溃的原因。
    屏幕快照 2017-12-05 上午10.12.01.png
    之后、到底是不是因为局部变量被释放、导致的崩溃?
    • 用成员变量试试。


      WechatIMG210.jpeg

      看来无论是否使用成员变量、内部的变量指针都会被释放


      WechatIMG211.jpeg
    结论:
    • copy声明会把变量在block保存一份指针变量、指向原对象地址。保证block内部所引用变量不会被自动释放而造成崩溃。
    • assign声明的block、内部引用的变量(无论是否是局部变量)在出了赋值当前作用域之后、会被释放。导致崩溃。
    除此之外:
    • block声明为成员变量、用strong修饰。也是没有问题的、作用和copy一样。
    • block声明为成员变量、不加任何修饰。效果也和copy/strong一样。
    • block声明为局部变量、不加任何修饰。效果也和copy/strong一样。

    关于copy之后block的一些特性

    会对其内部的使用对象引用计数+1

    这也是为什么需要用__weak去修饰self以防止循环引用的原因。
    更多的解释可以参阅《__weak与__block区别,深层理解两者区别》《深入分析 Objective-C block、weakself、strongself 实现原理》

    原理总结下来就是

    __weak会新开辟一个变量指针、指向被修饰的变量地址、但不会对该变量增加引用
    当被修饰变量被释放、__weak变量指针依旧会指向原有变量地址、而该地址则会返回空。
    所以需要用__strong在block内部修饰、让block内的变量不会在中途被释放掉、保证block内容一致性。

    WechatIMG204.jpeg

    默认情况下、内外部的变量数据不会互通。

       int a = 1;
       
       int (^backValue)(int) = ^(int num) {
           return num+a;
       };
       
       a += 1;//这次+=1不会被传递到block中a上。
       int b = backValue(1);
       NSLog(@"b-->%d", b);
      //输出结果b-->2
    
    • 当然、我们可以__block来修饰这个a、使其在block内部的指针也可以被修改


      屏幕快照 2017-12-04 下午6.11.00.png
    • 为什么用__block修饰就可以修改外部的变量、或者将外部变量的修改传递进block内了?

    先写结论、不愿意细看可以跳过。

    • 无论是普通变量、还是OC变量。在不使用__block的情况下、无法对其本身进行修改。

    因为二者、都只会在block执行代码中copy一份值/指向该对象地址的指针。即使修改了这个值/指针指向、也无法对外部造成影响。所以编译器干脆就禁止这个操作了(虽然你可以通过直接操作指针的方式修改、但是被修改的依旧只是block内部的指针变量而已)

    • OC对象在不使用__block修饰的情况下、可以修改其属性。

    自身的指针会被捕获到block代码块中、借此操作该指针指向地址所属对象的属性。(这也是为什么在block里可以修改self.xxx属性的原因)。

    • OC对象以及普通变量在使用__block修饰的情况下、可以对自身进行修改。

    会将原变量的指针从栈移动至堆区。由block对象进行捕获保存、并借此进行修改。


    以下是相关分析

    这里有一篇文章、借助编译后的block阐释了__block的工作原理
    《你真的理解__block修饰符的原理么》

    • 简而言之、block捕获普通变量其实将变量的值捕获到了block执行块中。这个已经与原变量无关。(由于无法直接获得原变量、技术上无法实现修改、所以编译器直接禁止了)。
      屏幕快照 2017-12-04 下午6.36.49.png
      block内部的指针指向、与外部完全分离
    • __block声明的变量、在被block捕获时。会被block以指针变量的形式保存在自身处于堆中结构体内。同时、原对象的指针地址也被修改到了堆中。既然变量指针内存地址相同、就代表是同一个指针、自然可以同步修改。
    __block之后的效果
    具体可见:《iOS中__block 关键字的底层实现原理》《一篇文章看懂iOS代码块Block》
    • 当然、还有个其他情况。就是OC对象的属性。

    当block执行代码需要捕获的是OC对象时、block会自动在block代码块中copy一个对象指针(普通对象是copy值)、指向该对象当前地址。
    就会出现如下情况


    屏幕快照 2017-12-04 下午6.57.07.png
    不过、并不允许你为这个存在栈中的变量指针直接重新指定一个新的指向。除非使用__block将这个对象的地址直接移动到堆中。

    此外、还做了另外一个实验。


    屏幕快照 2017-12-04 下午8.18.27.png
    这也证明、即便是OC对象也仅仅是捕获了当前指针的指向。如果新生成了一个对象修改了外部的对象的指针指向地址、里面继续修改也没用了。

    关于block的循环引用

    • __block是否可以防止循环引用。

    可以、只需要在block内部根据需要将变量置空。缺点是需要手动调用一次block

     __block id tmp = self;
     self.block = ^{
         tmp = nil;
     };
     self.block();
    
    需要注意的是。在tmp被block的捕获的时候、和__weak一样是不会增加tmp的引用计数的。之所以要手动释放、是因为在__block id tmp = self;时、tmp对self进行了一次强引用。需要消除的是这次引用。而且、当tmp被置nil之后、下次block调用将会得到一个空地址。但原对象不受此影响。
    WechatIMG207.jpeg

    最后

    本文主要是自己的学习与总结。如果文内存在纰漏、万望留言斧正。如果不吝赐教小弟更加感谢。

    相关文章

      网友评论

        本文标题:iOS基础深入补完计划--Block相关原理探究

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