美文网首页
28.iOS底层学习之block初学习

28.iOS底层学习之block初学习

作者: 牛牛大王奥利给 | 来源:发表于2022-01-20 17:06 被阅读0次

    本章提纲
    1、Block的几种类型
    2、Block常见试题
    3、Block的循环引用问题

    1、Block的几种类型

    Block分为三种类型,分别是GlobalBlockMallocBlockStackBlock

    • GlobalBlock
      位于全局区,在Block内部不使用外部变量,或者只使用静态变量或者全局变量。

    • MallocBlock
      位于堆区,在Block内部使用局部变量或者OC属性,并且赋值给强引用或者Copy属性变量。

    • StackBlock
      位于栈区,与MallocBlock一样,可以在内部使用强引用或者OC属性。但是不能赋值给强引用或者Copy修饰变量。

    1.1 GlobalBlock类型实例

    全局Block类型,不使用局部变量,或者内部使用的变量是全局的。

    Block内部无操作
     void (^block)(void) = ^{
        };
        NSLog(@"%@",block);
    

    打印结果:<__NSGlobalBlock__: 0x100b72100>

    Block内部引用静态变量
    static int jingtai = 3;
    void (^block)(void) = ^{
            NSLog(@"%d",jingtai);
        };
        NSLog(@"%@",block);
    

    打印结果:<__NSGlobalBlock__: 0x10430d100>

    1.2 MallocBlock类型实例

    Block,内部使用局部变量或者OC属性,并且赋值给强引用Block或者Copy变量。

    使用局部变量并且强引用
     int a = 2;
        void (^block)(void) = ^{
            NSLog(@"%d",a);
        };
        NSLog(@"%@",block);
    

    打印结果:<__NSMallocBlock__: 0x600001dc8b40>
    block,默认是强引用。

    Copy变量的情况
    int a = 10;
        void (__weak ^block)(void) = ^{
            NSLog(@"%d",a);
        };
        NSLog(@"%@",[block copy]);
    

    打印结果:<__NSMallocBlock__: 0x600002d307e0>
    如果我不进行[block copy]的操作,直接打印这个block的话,是一个栈Block,经过copy,打印就是堆Block。

    1.2 StackBlock类型实例

    Block和堆Block很相似,区别在于赋值的对象是强引用还是弱引用。还是上边那个例子🌰,如果我进行Copy操作,打印就是一个stackBlock

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

    打印结果:<__NSStackBlock__: 0x30d7e5548>

    2、Block常见试题

    • Block的引用计数考题
      下面代码输出的结果是。
    - (void)blockDemo{
        NSObject *objc = [NSObject new];
        NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)(objc))); 
       
        void(^strongBlock)(void) = ^{
            NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
        };
        strongBlock();
    
        void(^__weak weakBlock)(void) = ^{ // + 1
            NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
        };
        weakBlock();
        
        void(^mallocBlock)(void) = [weakBlock copy];
        mallocBlock();
    }
    

    输出结果是:1,3,4,5
    解析
    1、首先是创建一次,引用计数+1,所以第一次打印是引用计数是1。
    2、然后来到这个strongBlock关键的地方,它是一个堆Block,strongBlock本身引用了一次 底层实际执行了copy操作,所以又引用一次 +2,此时引用计数变成3。
    3、然后后面来到weakBlock这里,它是弱引用,是个栈Block,底层不会拷贝 所以+1,此时引用计数变成了4。
    4、最后mallocBlock是由weakBlock手动copy一次得到的,所以再加1,引用计数为5。

    截图
    • Block的浅拷贝
      下面代码运行结果
    - (void)blockDemo1{
        
        int a = 0;
        void(^__weak weakBlock)(void) = ^{
            NSLog(@"a:%d", a);
        };
        
        struct _LGBlock *blc = (__bridge struct _LGBlock *)weakBlock;
    
        id __strong strongBlock = weakBlock;
        NSLog(@"weakBlock:%@",weakBlock);
        NSLog(@"strongBlock:%@",strongBlock);
     
        blc->invoke = nil;
        void(^strongBlock1)(void) = strongBlock;
        NSLog(@"strongBlock1:%@",strongBlock1);
        
        strongBlock1();
    }
    

    运行结果程序crash
    解析weakBlock是一个栈类型的Block,blc的初始化是直接把自己的指针指向了这个weakBlock,他们指向的是同一块内存空间。然后后面的strongBlock赋值和blc的情况是相似的,也是指针指向了那块内存,所以
    blc->invoke置为空相当于把那块内存值为空了,所以此刻不管是blc的指向还是weakBlock的指向,或者是strongBlock的指向,他们都指向了一块nil空间,是不可以再去访问的,所以会崩溃了(EXC_BAD_ACCESS)。

    上述代码修复

    - (void)blockDemo1{
        int a = 0;
        void(^ __weak weakBlock)(void) = ^{
            NSLog(@"-----%d", a);
        };
        struct _LGBlock *blc = (__bridge struct _LGBlock *)weakBlock;
    
        id __strong strongBlock = [weakBlock copy];
        blc->invoke = nil;
        void(^strongBlock1)(void) = strongBlock;
        strongBlock1();
    }
    

    最后是调用strongBlock1,所以strongBlock1赋值时可以进行copy,让它在堆空间上,开辟了另外一块空间,原来的那块置为nil不会对新的空间有影响。

    • Block的堆栈释放差异
      下面代码的输出是什么:
    - (void)blockDemo3{
        int a = 8;
        void(^__weak weakBlock)(void) = nil;
        {
            // 栈区
            void(^__weak strongBlock)(void) = ^{
                NSLog(@"---%d", a);
            };
            weakBlock = strongBlock;
            NSLog(@"1 - %@ - %@",weakBlock,strongBlock);
        }
        weakBlock();
    }
    

    运行结果1 - <__NSStackBlock__: 0x308640520> - <__NSStackBlock__: 0x308640520>
    ---8

    解析

    • weakBlock是一个栈区的Block但是初始化为nil,一开始是空。
    • 然后下面的{}是一个代码块,这个主要影响部分代码的作用域。
    • strongBlock是栈区的Block,初始化正常。
    • 然后weakBlock = strongBlock;weakBlock指向了strongBlock指针指向的的内存。
    • 所以后面打印weakBlock和strongBlock他们的地址是一样的0x308640520
    • 然后出作用域,调用weakBlock(),因为还有指针weakBlock指向0x308640520,但是它是个弱引用,strongBlock出了整个函数才会被释放,所以后面weakBlock()还可以调用代码块,调用的时候代码块没有被释放。

    如果上述代码做简单的修改,把strongBlock前边的__weak去掉:

    - (void)blockDemo3{
        int a = 8;
        void(^__weak weakBlock)(void) = nil;
        {
            // 堆
            void(^strongBlock)(void) = ^{
                NSLog(@"---%d", a);
            };
            weakBlock = strongBlock;
            
            NSLog(@"1 - %@ - %@",weakBlock,strongBlock);
        }
    
        weakBlock();
    }
    

    那么运行就会崩溃了。原因就是,堆空间,出了第一个花括号的作用域,strongBlock就被释放掉了,而weakBlock是弱持有,不是强持有,所以就调用不到块了。花括号去掉就没问题了。截图看下花括号对比前后的结果。


    去掉花括号之前 去掉花括号之后

    通过血与泪的教育😭😭😭,这道题告诉我们了一个很重要的人生哲理!就是
    花括号不要乱写!!!!!!


    3、Block的循环引用问题

    正常情况下,一个对象A如果强持有对象B,那么B的引用计数retainCount会进行+1的操作,当A对象释放时会发送delloc给B,此时B的引用计数retainCount会进行-1,当B的引用计数为0的时候,B也会被释放掉。

    但是,总有那么些特殊的情况,A已经持有了B的时候,B非得又持有了A,他们相互持有了,都等着对方先放手,此时就形成了一个环,不靠外力是断不开的,这种情况,就发生了循环引用

    3.1 循环引用的形成

    下面来看一段经典的循环引用

    • 经典循环引用示例
    - (void)test1{
        self.name = @"ABC";
        self.block = ^{
            NSLog(@"%@",self.name);
        };
    }
    
    • 上述示例self->block ,block->self,相互持有,形成了环,delloc不走。
    3.2 解决循环引用

    通常我们解决循环引用的方式有三种

    • 进行_ weak弱引用,有时候要和 _strong配合使用
    • 强制断链,将其中一个置为nil。
    • 将持有者改为Block的参数进行传递使用。
    3.2.1 _ _weak弱引用

    上述代码,我们进行简单的改造,就可以解决循环引用的问题,通过用_ _weak修饰的方式。


    image.png

    有时候在block的内部会有需要进行异步线程的操作,为了避免使用的对象提前被释放,通常会在内部添加_ _strong来增加一次引用计数。

    3.2.2 强制断链

    强制断链就是在使用完对象后,强制设置为空。如下:


    强制断链

    但是也会有问题,这种情况必须block被调用,代码块中的代码被执行才会解决循环引用,容易引发bug,如果不调用block中的内容循环引用还是存在的。

    Block未被调用的情况

    所以这样dealloc是没有走的。

    3.2.3作为参数进行传递

    我们把上述的vc作为参数传递到block中再进行使用也是可以解决这个问题的。

    参数传递
    可以看到作为参数传进来是没有问题,参数就避免了Block对外部变量的捕获的问题。

    这篇文章对Block日常的一些使用情况做了一个简单概括,下一篇会深入研究Block的结构,底层原理等。

    相关文章

      网友评论

          本文标题:28.iOS底层学习之block初学习

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