• __weak与__block修饰符的区别


    API Reference对__block变量修饰符有如下几处解释:

    //A powerful feature of blocks is that they can modify variables in the same lexical scope. You signal that a block can modify variable using the __block storage type modifier. //At function level are __block variables. These are mutable within the block (and the enclosing scope) and are preserved if any referencing block is copied to the heap.

     

     

    大概意思归结出来就是: 

    __block对象在block中是可以被修改、重新赋值的。

    API Reference对__weak变量修饰符有如下几处解释:

    __weak specifies a reference that does not keep the referenced object alive. A weak reference is set to nil when there are no strong references to the object.

     

     

    使用了__weak修饰符的对象,作用等同于定义为weak的property。自然不会导致循环引用问题,因为苹果文档已经说的很清楚,当原对象没有任何强引用的时候,弱引用指针也会被设置为nil。

    因此,__block和__weak修饰符的区别其实是挺明显的: 
    1.__block不管是ARC还是MRC模式下都可以使用,可以修饰对象,还可以修饰基本数据类型。 
    2.__weak只能在ARC模式下使用,也只能修饰对象(NSString),不能修饰基本数据类型(int)。 
    3.__block对象可以在block中被重新赋值,__weak不可以。 
    4.__block对象在ARC下可能会导致循环引用,非ARC下会避免循环引用,__weak只在ARC下使用,可以避免循环引用。

     

    PS:__unsafe_unretained修饰符可以被视为iOS SDK 4.3以前版本的__weak的替代品,不过不会被自动置空为nil。所以尽可能不要使用这个修饰符。

    更多__block的解释

    在本文的开头,提出两个简单的问题,如果你不能从根本上弄懂这两个问题,那么希望你阅读完本文后能有所收获。

    1. 为什么block中不能修改普通变量的值?
    2. __block的作用就是让变量的值在block中可以修改么?

    如果有的读者认为,问题太简单了,而且你的答案是:

    1. 因为编译器会有警告,各种教程也都说了不能修改。
    2. 应该是的吧。

    那么我也建议你,抽出宝贵的几分钟时间阅读完本文吧。在开始揭开__block的神秘面纱之前,很不幸的是我们需要重新思考一下block的本质和它的实现。

    block是什么?

    很多教程、资料上都称Block是“带有自动变量值的匿名函数”。这样的解释显然是正确的,但也是不利于初学者理解的。我们首先通过一个例子看一看block到底是什么?

    typedef void (^Block)(void);
    
    Block block;
    {
        int val = 0;
        block = ^(){
            NSLog(@"val = %d",val);
        };
    }
    block();

    抛开block略有怪异的语法不谈,其实对于一个block来说:

    它更像是一个微型的程序。

    为什么这么说呢,我们知道程序就是数据加上算法,显然,block有着自己的数据和算法。可以看到,在这个简单的例子中,block的数据就是int类型变量val,它的算法就是一个简单的NSLog方法。对于一般的block来说,它的数据就是传入的参数和在定义这个block时截获的变量。而它的算法,就是我们往里面写的那些方法、函数调用等。

    我认为block像是一个微型程序的另一个主要原因是一个block对象可以由程序员选择在什么时候调用。比如,如果我喜欢,我可以设置一个定时器,在10s后执行这个block,或者在另一个类里执行这个block。

    当然,我们还注意到在上面的demo中,通过typedef,block非常类似于一个OC的对象。限于篇幅和主题,这里不加证明的给出一个结论:Block其实就是一个Objective-C的对象。有兴趣的读者可以结合runtime中类和对象的定义进一步思考。

    block是怎么实现的?

    刚刚我们已经意识到,block的定义和调用是分离的。通过clang编译器,可以看到block和其他Objective-C对象一样,都是被编译为C语言里的普通的struct结构体来实现的。我们来看一个最简单的block会被编译成什么样:

    //这个是源代码
    int main(){
        void (^blk)(void) = ^{printf("Block
    ");};
        block();
        return 0;
    }

    编译后的代码如下:

    struct __block_impl {
        void *isa;
        int Flags;
        int Reserved;
        void *FuncPtr;
    };
    
    struct __main_block_impl_0 {
        struct __block_impl impl;
        struct __main_block_desc_0 *Desc;
    
        __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,int flags=0){
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
    };
    
    struct void __main_block_func_0(struct __main_block_impl_0 *__cself){
        printf("Block
    ");
    }
    
    static struct __main_block_desc_0{
        unsigned long reserved;
        unsigned long Block_size;
    } __main_block_desc_0_DATA = {
        0,
        sizeof(struct __main_block_impl_0)
    };

    代码非常长,但是并不复杂,一共是四个结构体,显然一个block对象被编译为了一个__main_block_impl_0类型的结构体。这个结构体由两个成员结构体和一个构造函数组成。两个结构体分别是__block_impl和__main_block_desc_0类型的。其中__block_impl结构体中有一个函数指针,指针将指向__main_block_func_0类型的结构体。总结了一副关系图: 

    这里写图片描述

    block在定义的时候:

    //调用__main_block_impl_0结构体的构造函数
    struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
    struct __main_block_impl_0 *blk = &tmp;

    block在调用的时候:

    (*blk->impl.FuncPtr)(blk);

    之前我们说到,block有自己的数据和算法。显然算法(也就是代码)是放在__main_block_func_0结构体里的。那么数据在哪里呢,这个问题比较复杂,我们来看一看文章最初的demo会编译成什么样,为了简化代码,这里只贴出需要修改的部分。

    struct __main_block_impl_0 {
        struct __block_impl impl;
        struct __main_block_desc_0 *Desc;
        int val;
    
        __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,int _val, int flags=0) : val(_val){
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
    };
    
    struct void __main_block_func_0(struct __main_block_impl_0 *__cself){
        int val = __cself->val;
        printf("val = %d",val);
    }

    可以看到,当block需要截获自动变量的时候,首先会在__main_block_impl_0结构体中增加一个成员变量并且在结构体的构造函数中对变量赋值。以上这些对应着block对象的定义。

    在block被执行的时候,把__main_block_impl_0结构体,也就是block对象作为参数传入__main_block_func_0结构体中,取出其中的val的值,进行接下来的操作。

    为什么__block中不能修改变量值?

    如果你耐心地看完了上面非常啰嗦繁琐的block介绍,那么你很快就明白为什么block中不能修改普通的变量的值了。

    通过把block拆成这四个结构体,系统“完美”的实现了一个block,使得它可以截获自动变量,也可以像一个微型程序一样,在任意时刻都可以被调用。但是,block还存在这一个致命的不足:

    注意到之前的__main_block_func_0结构体,里面有printf方法,用到了变量val,但是这个block,和最初block截获的block,除了数值一样,再也没有一样的地方了。参见这句代码:

    int val = __cself->val;

    当然这并没有什么影响,甚至还有好处,因为int val变量定义在栈上,在block调用时其实已经被销毁,但是我们还可以正常访问这个变量。但是试想一下,如果我希望在block中修改变量的值,那么受到影响的是int val而非__cself->val,事实上即使是__cself->val,也只是截获的自动变量的副本,要想修改在block定义之外的自动变量,是不可能的事情。这就是为什么我把demo略作修改,增加一行代码,但是输出结果依然是”val = 0”。

    //修改后的demo
    typedef void (^Block)(void);
    
    Block block;
    {
        int val = 0;
        block = ^(){
            NSLog(@"val = %d",val);
        };
        val = 1;
    }
    block();

    既然无法实现修改截获的自动变量,那么编译器干脆就禁止程序员这么做了。

    __block修饰符是如何做到修改变量值的

    如果把val变量加上__block修饰符,编译器会怎么做呢?

        //int val = 0; 原代码
        __block int val = 0;//修改后的代码

    编译后的代码:

    struct __Block_byref_val_0 {
        void *__isa;
        __Block_byref_val_0 *forwarding;
        int __flags;
        int __size;
        int val;
    };
    
    struct __main_block_impl_0 {
        struct __block_impl impl;
        struct __main_block_desc_0 *Desc;
        __Block_byref_val_0 *val;
    
        __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,__Block_byref_val_0 *_val, int flags=0) : val(_val->__forwrding){
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
    };
    
    struct void __main_block_func_0(struct __main_block_impl_0 *__cself){
        __Block_byref_val_0 *val = __cself->val;
        printf("val = %d",val->__forwarding->val);
    }

    改动并不大,简单来说,只是把val封装在了一个结构体中而已。可以用下面这个图来表示五个结构体之间的关系。

    这里写图片描述

    但是关键在于__main_block_impl_0结构体中的这一行:

    __Block_byref_val_0 *val;

    由于__main_block_impl_0结构体中现在保存了一个指针变量,所以任何对这个指针的操作,是可以影响到原来的变量的。

    进一步,我们考虑截获的自动变量是Objective-C的对象的情况。在开启ARC的情况下,将会强引用这个对象一次。这也保证了原对象不被销毁,但与此同时,也会导致循环引用问题。

    需要注意的是,在未开启ARC的情况下,如果变量附有__block修饰符,将不会被retain,因此反而可以避免循环引用的问题。

    总结

    回到上面的两个问题,答案应该很明显了。

    1. 由于无法直接获得原变量,技术上无法实现修改,所以编译器直接禁止了。
    2. 都可以用来让变量在block中可以修改,但是在非ARC模式下,__block修饰符会避免循环引用。注意:block的循环引用并非__block修饰符引起,而是由其本身的特性引起的。
     
     
  • 相关阅读:
    nyoj 117 求逆序数 (归并(merge)排序)
    2018年四川理工学院软件工程考试大纲(软件需求分析)
    2018年四川理工学院软件工程考试大纲(软件计划与可行性研究)
    hdu 1166 敌兵布阵 (线段树、单点更新)
    hdu 1754 I Hate It (线段树、单点更新)(PS:ios::sync_with_stdio(false)可以加快cin、cout的读取写出速度)
    周末手撸管理系统(一)
    drf源码save以及response
    drf框架serializers中ModelSerializer类简化序列化和反序列化操作
    drf框架序列化和返序列化
    django模型中有外键关系的表删除相关设置
  • 原文地址:https://www.cnblogs.com/xu-antong/p/6503344.html
Copyright © 2020-2023  润新知