美文网首页程序员
Objective-C 块(Block)的使用与理解

Objective-C 块(Block)的使用与理解

作者: yahtzee_ | 来源:发表于2016-03-17 15:28 被阅读927次

    简书上的所有内容都可以在我的个人博客上找到(打个广告😅)


    块(Block)是在 iOS 开发中很常用的一个工具,它可以使我们代码的业务逻辑更加的紧凑。本文会分两部分来讲块,第一部分是块的基础知识,第二部分是对块的本质的一些理解。

    块的基础知识


    块的声明

    块的声明类似于函数指针,只不过把 * 换成了 ^ 。声明时最前面是返回类型,中间是块名,最后是参数类型。

    // returnType (^blockName) (parameters)
    int (^someBlock) (int, int);
    

    块的实现

    // ^returnType(parameters){/*code*/};
    ^int(int a, int b) {
        return a+b;
    };
    

    其中返回值是可以省略的,它可以自动判断,如果参数也是空的话我们可以简化成这样:

    ^{
        return 100;
    };
    

    块的使用

    块的使用就像使用 C 函数一样

    someBlock(1,2);
    

    用 typedef 来简化块的声明

    typedef int (^CYAddBlock) (int, int);
    CYAddBlock addBlock = ^(int a, int b) {
        return a+b;
    };
    addBlock(1,2);
    

    块能捕获在它声明范围里的所有变量

    但是不能再块中修改捕获到的变量,如果我们尝试修改编译器会报错,无法通过编译。

    int count = 0;
    int (^someBlock) () = ^{
        return count;
    };
    int result = someBlock();   // result = 0
    

    用 __block 修饰变量,可以在块中修改变量的值

    __block int count = 0;
    int (^someBlock) () = ^{
        return ++count;
    };
    int result = someBlock();   // result = 1
    

    其实块在捕获变量时只是拷贝了一份变量的值,而用 __block 修饰后,拷贝的是变量的地址,所以我们就可以在块中修改变量了。这就和 C 语言中函数的参数是类似的道理。

    小心产生循环引用

    在一个类中,块能直接访问并修改实例变量的值,但是一定要注意不管是通过 self 来访问属性还是直接通过 _instanceVariable 来访问都会捕获 self。因为在直接访问 _instanceVariable 等效于这样:

    self->_instanceVariable;
    

    而一旦捕获了self, 我们就一定要注意循环引用导致的内存泄露了。我们在 CYClass 中声明了一个块和一个属性:

    typedef void (^CYBlock) ();
    
    @interface CYClass : NSObject
    @property (nonatomic, copy)NSString *aString;
    @property (nonatomic, copy)CYBlock aBlock;
    @end
    

    然后重写它的 init 和 dealloc 方法:

    @implementation CYClass
    - (instancetype)init
    {
        if (self = [super init]) {
            _aString = @"Hello Cyrus";
            _aBlock = ^{
                NSLog(@"%@", _aString);
            };
        }
        return self;
    }
    
    - (void)dealloc {
        NSLog(@"CYClass deinit");
    }
    @end
    

    然后我们实例化一个对象,紧接着就把它设为 nil 会发现 dealloc 方法并没有被调用,也就是说发生了内存泄露。

    CYClass *c = [CYClass new];
    c = nil;
    

    就像这样:

    我们可以通过 __weak 来打破这个循环,我们修改一下初始化方法:

    - (instancetype)init
    {
        if (self = [super init]) {
            _aString = @"Hello Cyrus";
            __weak typeof(self) weakSelf = self;
            _aBlock = ^{
                __strong typeof(self) strongSelf = weakSelf;
                NSLog(@"%@", strongSelf.aString);
            };
        }
        return self;
    }
    

    在执行之前的代码就会发现这个对象成功的销毁了。

    2016-03-12 16:30:24.995 BlockExample[4777:111670] CYClass deinit
    

    块的本质


    块的结构

    我们可以在这里找到 Block 的定义:

    struct Block_descriptor {
        unsigned long int reserved;
        unsigned long int size;
        void (*copy)(void *dst, void *src);
        void (*dispose)(void *);
    };
    
    struct Block_layout {
        void *isa;
        int flags;
        int reserved; 
        void (*invoke)(void *, ...);
        struct Block_descriptor *descriptor;
        /* Imported variables. */
    };
    

    看到 Block_layout 结构体里的第一个变量是一个 isa 指针, 我们应该就能猜到 Block 也是一个对象。然后再看第四个变量是一个叫 invoke 的函数指针,它指向的就是 Block 具体实现的函数了,这个函数至少要接受一个 void* 类型的参数,这个参数就是块本身。再下面的 descriptor 指向的就是 Block_descriptor 结构体,里面就是一些 Block 的信息。而在这些变量下面存放的就是 Block 捕获的那些变量

    块的类型

    在 OC 中一共有3种块,分别是:

    • 全局块,不会访问任何外部变量
    • 栈块,保存在栈中, 当函数退出后就会被销毁,通过copy可以复制到堆上
    • 堆块,保存在堆中

    我们在 MRC 的环境下运行下面的代码:

    void (^globalBlock)()  = ^{ NSLog(@"global block"); };
    NSLog(@"%@", globalBlock);
            
    NSString *str1 = @"stack block";
    void (^stackBlcok)() = ^{ NSLog(@"%@", str1); };
    NSLog(@"%@", stackBlcok);
        
    NSString *str2 = @"malloc block";
    void (^mallockBlock)() = [^{ NSLog(@"%@", str2); } copy];
    NSLog(@"%@", mallockBlock);
    

    打印结果:

    2016-03-13 16:23:50.676 BlockExample[1760:28902] <__NSGlobalBlock__: 0x1000042b0>
    2016-03-13 16:23:50.677 BlockExample[1760:28902] <__NSStackBlock__: 0x7fff5fbff790>
    2016-03-13 16:23:50.677 BlockExample[1760:28902] <__NSMallocBlock__: 0x100303fa0>
    

    结果和我们预期的结果是一样的。我们在换成 ARC 的环境下运行一次:

    2016-03-13 16:26:05.767 BlockExample[1805:29822] <__NSGlobalBlock__: 0x1000052f0>
    2016-03-13 16:26:05.768 BlockExample[1805:29822] <__NSMallocBlock__: 0x100400340>
    2016-03-13 16:26:05.769 BlockExample[1805:29822] <__NSMallocBlock__: 0x1004001b0>
    

    我们发现原来的栈块也变成了堆块。这是因为在把一个快赋值给一个strong对象时 ARC 会自动帮我们执行一次copy。如果我们直接这样,那么还是会打印出 __NSStackBlock :

    NSString *str1 = @"stack block";
    NSLog(@"%@", ^{ NSLog(@"%@", str1); });
    // 打印结果
    2016-03-13 16:30:06.180 BlockExample[1881:32260] <__NSStackBlock__: 0x7fff5fbff798>
    

    相关文章

      网友评论

        本文标题:Objective-C 块(Block)的使用与理解

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