美文网首页
iOS底层day6 - 探索block

iOS底层day6 - 探索block

作者: 宁夏灼雪__ | 来源:发表于2018-09-28 11:35 被阅读0次

    什么是block

    1.block本身也是一个OC对象,它里面也有isa指针
    2.block是封装了函数调用(存储函数调用地址,函数访问变量)和函数调用环境的OC对象
    3.block的底层结构如下图所示:

    AC2F3939-84BF-4073-A536-0BE55453989F.png

    证明:
    我们在main中写入一个block:

            // 带参数的block
            int c = 15;
            void (^block)(int , int) = ^(int a ,int b ){
                NSLog(@"%d",c);
            };
            block(10, 10);
    

    走到项目main.m的目录下通过反编译:

    xcrun  -sdk  iphoneos  clang  -arch  arm64  -rewrite-objc main.m -o mian.cpp
    

    得到main.cpp,拉入工程,找到这个block对象的底层结构:

    278065EC-F00C-44CE-A147-EE9CC1913656.png

    实际上block在底层对应的就是__main_block_impl_1,搜索__main_block_impl_1:

    struct __main_block_impl_1 {
      struct __block_impl impl;
      struct __main_block_desc_1* Desc;
      int c;
      __main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, int _c, int flags=0) : c(_c) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    我们发现,里面存储着__block_impl的结构体impl,以及__main_block_desc_1的结构体指针Desc
    搜索对象的内容我们可以找到:

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
    }
    

    这里可以看到__main_block_desc_1包含着isa指针,以及FuncPtrFuncPtr就是block的调用地址,是在声明block的时候初始化传递进来的,以及Block_size为block的内存大小,还有int c,也封装到了Block内部,我们知道OC对象的特征就是isa指针,所以,block就是封装了函数调用、以及函数调用环境的OC对象

    我们用一张图来总结这个结构:


    9AA000FA-1451-4F43-8675-3693D02CD85A.png

    Capture (捕获)

    对于局部变量:值传递,Block只是把局部变量的值捕获存储在了block的结构体内 存储

            int c = 10;
            void (^block)(void) = ^{
                NSLog(@"%d",c);
            };
            c = 20;
            block(); // 输出10
    

    对于Static :指针传递,Block把static 的变量的指针存储在block的结构体内,所以取值的话就是取对应最后的赋值

            static int c = 10;
            void (^block)(void) = ^{
                NSLog(@"%d",c);
            };
            c = 20;
            block();  // 输出20
    

    全局变量:直接访问

    static int a = 20;
    int b = 15;
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            void (^block)(void) = ^{
                NSLog(@"%d",a);
                NSLog(@"%d",b);
            };
            a = 25;
            b = 10;
            block();  // 输出 25 10
        }
        return 0;
    }
    

    block 的类型

    block分为3中类型,但是最终都是继承自NSObject
    __NSGlobalBlock__ : block内部没有访问auto变量:

            // Global - 没有访问auto变量
            void (^block1)(void) = ^{
                NSLog(@"-----");
            };
            NSLog(@"%@",[block1 class]);
    

    __NSStackBlock__ : block内部访问了auto变量(存放在栈):

            // stack - 访问了auto变量 
            int c = 2;
            void (^block2)(void) = ^{
                NSLog(@"-----%d",c);
            };
            NSLog(@"%@",[block2 class]);
    

    Tip:stack 需要在非arc下才可以看出结果

    67CFC3BE-2ED6-4DA5-AEEC-2A97234D7A61.png
    Building Setting 暂时关掉arc
    __NSMallocBlock__ : Stack Block调用了copy
            // malloc - stack Block 调用了copy
            int d = 2;
            void (^block3)(void) = [^{
                NSLog(@"-----%d",d);
            } copy];
            NSLog(@"%@",[block3 class]);
    

    Tip:stack block存放在栈内存,如果block存放在函数内,一旦函数作用域结束,则block内容则会被清除,如果存放在堆内存(调用copy),就会变成malloc block,则不会自动清除,这也是为什么block需要用copy修饰的原因

    用一幅图来总结block的类型 :


    F92EC499-0EA3-46C1-B421-096ADC7C1A05.png

    __block

    block内部默认是无法修改auto变量的,因为在block底部的话执行block、声明age(main函数)的地方分别是2个不同的函数,并没办法从一个函数去修改另一个函数的局部变量,而如果使用static或者使用全局变量是可以的,因为block在底层存储static变量是存储它的指针地址,全局变量就全部都可以访问
    如果要修改auto变量的话,则需要使用__block:

            __block int a = 10;  // __block 修饰auto变量
            void (^block)(void) = ^{
                a = 20;
                NSLog(@"%d",a);
            };
            block();
    

    __block实现原理:当使用__block修饰auto变量时,编译器会把a 变成一个__Block_byref_a_0对象:

    FF2A76D7-0ADC-4871-B79D-1779F2F04AC9.png AF23EDF8-0F33-403D-A00C-E49CE01D0977.png

    我们可以看到__Block_byref_a_0它包含了__isaa,以及__forwarding(指向自己的指针),以及其他信息,它就是一个对象,而我们再看main函数声明block的地方

    BEB698F6-4672-4E66-996E-AB9A8B5CA122.png
    这里__block声明就是声明了一个__Block_byref_a_0的对象,并把&a传递给了__forwarding10传递给了a,最后我们在看执行block的地方:
    DD0BCCD4-AC01-46BA-A5D4-CCDB1D05DABF.png
    这里,执行block时,取出了__Block_byref_a_0 所存储的&a(指向__Block_byref_a_0的指针地址 __forwarding),再取出a,最后进行修改/使用,用一张图来总结__block的底层结构:
    A249145A-816D-4314-AF79-ECA46794B49C.png

    循环引用

    首先,我们声明一个Person对象,在Person上声明一个属性block:

    B0A61092-AFD0-4E67-8F31-C7C2935D2B88.png
    接着调用:
    1EA42152-332A-4814-BCCB-1B85D1A94514.png
    此便为循环引用,首先这里我们创建一个Person对象,并用*p指针指向它,而我们在block内部又使用了p,则在底层block会捕获p对象,并用强指针指向它,而我们定义的Person对象,又定义了属性block,属性block则会自动生成成员变量_block,而我们执行p.block = xxx 时,又将这个{}block赋值给了block,这样就造成了双方互相强引用,导致内存不会释放:
    8BBE5395-CAE3-4676-ACB1-0D1C03F70CE6.png

    解决循环引用: 使用__weak或者__unsafe_unretained 修饰变量(block内的person指针指向Person对象时为弱引用)
    区别:
    __weak,在Person对象回收掉后,block内指向Person对象的指针person则会自动置为nil,即:person = nil
    __unsafe_unretained,在Person对象回收掉后,block内指向Person对象的指针person还会指向原来Person对象的内存地址,即野指针

    另外还有一种解决方案作为了解:
    使用__block配合person = nil 以及必须调用block

    54A46A58-63A1-4DD1-BA80-2908E55825EC.png

    问题总结

    2E1E161E-DE33-4327-9DEB-7C1714D2839E.png

    相关文章

      网友评论

          本文标题:iOS底层day6 - 探索block

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