zoukankan      html  css  js  c++  java
  • iOS

    1. Block

    1.1 什么是Block

      之前都是对block的简单实用,这里重新了解下。

      代码块Block是苹果在iOS4开始引入的对C语言的扩展,实现匿名函数的特性,Block是一种特殊的数据类型,其可以正常定义变量、作为参数、作为返回值,特殊的,block还可以保存一段代码,在需要的时候调用,目前Block广泛的应用iOS开发中,常用于GCD、动画、排序及各类回调。

      注:Block的声明与赋值只是保存了一段代码段,必须调用才能执行内部的代码。  

    1.2 Block简单的使用

    Block的声明:

    Block变量的声明格式为: 返回值类型(^Block名字)(参数列表);
    
    // 声明一个无返回值,参数为两个字符串对象,叫做aBlock的Block
    void(^aBlock)(NSString *x, NSString *y);
    
    // 形参变量名称可以省略,只留有变量类型即可
    void(^aBlock)(NSString *, NSString *);
    

     Block的赋值:

    Block变量的赋值格式为: Block变量 = ^(参数列表){函数体};
    
    aBlock = ^(NSString *x, NSString *y){
        NSLog(@"%@ love %@", x, y);
    };
    

    Block声明并赋值:

    int(^myBlock)(int) = ^(int num){
        return num * 7;
    };
    
    // 如果没有参数列表,在赋值时参数列表可以省略
    void(^aVoidBlock)() = ^{
        NSLog(@"I am a aVoidBlock");
    };
    

    Block 变量的调用;

    // 调用后控制台输出"Li Lei love Han Meimei"
    aBlock(@"Li Lei",@"Han Meimei");
    
    // 调用后控制台输出"result = 63"
    NSLog(@"result = %d", myBlock(9));
    
    // 调用后控制台输出"I am a aVoidBlock"
    aVoidBlock();
    

    2. Block 数据结构

    2.1 Block 数据结构简单认识

    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实例实际上由6部分构成:

    1. isa 指针,所有对象都有该指针,用于实现对象相关的功能。
    2. flags,用于按bit位表示一些block的附加信息,本文后面介绍 block copy 的实现代码可以看到对该变量的使用
    3. reserved 保留变量
    4. invoke 函数指针,指向具体的block 实现的函数调用地址
    5. descriptor 表示该block的附加描述信息,主要是size大小,以及 copy 和 dispose 函数的指针。
    6. variables , capture 过来的变量,block能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中。

    在 OC 语言中,一共有 3 种类型的 block:

    1. _NSConcreteGlobalBlock 全局的静态 block,不会访问任何外部变量。
    2. _NSConcreteStackBlock 保存在栈中的 block,当函数返回时会被销毁
    3. _NSConcreteMallocBlock 保存在堆中的 block,当引用计数为 0 时会被销毁。

    遇到一个Block,我们怎么确定这个Block的存储位置呢?

    a。Block不访问外界变量(包括栈中和堆中的变量)

    Block既不在栈又不在堆中,在代码段中,ARC和MRC都是如此,此时为全局块。

    b。Block访问外界变量

    MRC 环境下:访问外界变量的Block默认存储在栈中。

    ARC 环境下:访问外界变量的Block默认存储在堆中(实际是放在栈区,然后ARC情况下自动又拷贝到堆区),自动释放。

    2.2 NSConcreteGlobalBlock 类型的 block 的实现

    我们可以新建一个block1.c文件:

    #include <stdio.h>
    int main()
    {
        ^{ printf("Hello, World!
    "); } ();
        return 0;
    }
    

     在终端输入 clang -rewrite-objc block1.c ,就可以在目录中看到 clang 输出了一个 block1.cpp 的文件,这个文件就是 block 在 C 语言的实现:

    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;
        }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        printf("Hello, World!
    ");
    }
    static struct __main_block_desc_0 {
        size_t reserved;
        size_t Block_size;
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0) };
    int main()
    {
        (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA) ();
        return 0;
    }
    
    1.   一个block实际就是一个对象,它主要由一个 isa 和一个 impl 和一个 descriptor 组成。
    2. 这里我们看到 isa 指向的还是 _NSConcreteStackBlock,但在 LLVM 的实现中,开启 ARC 时,block 应该是 _NSConcreteGlobalBlock 类型。感觉是当一个 block 被声明的时候,它都是一个 _NSConcreteStackBlock类的对象。
    3. impl 是实际的函数指针,本例中,它指向 _main_block_func_0。这里的 impl 相当于之前提到的 invoke 变量,只是 clang 编译器对变量的命名不一样。
    4. descriptor 是用于描述当前这个 block 的附加信息的,包括结构体的大小,需要 捕获 和 处理 的变量列表等。结构体大小需要保存是因为,每个 block 因为会 捕获 一些变量,这些变量会加到 __main_block_impl_0 这个结构体中,让其体积变大。后面会看到相关代码。

    2.3 NSConcreteStackBlock 类型的 block 的实现

    我们另外新建一个名为 block2.c 的文件,输入一下内容:

    #include <stdio.h>
    int main() {
        int a = 100;
        void (^block2)(void) = ^{
            printf("%d
    ", a);
        };
        block2();
        return 0;
    }
    

     再次使用 clang 工具,转换后的关键代码如下:

    struct __main_block_impl_0 {
        struct __block_impl impl;
        struct __main_block_desc_0* Desc;
        int a;
        __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
            impl.isa = &_NSConcreteStackBlock;
            impl.Flags = flags;
            impl.FuncPtr = fp;
            Desc = desc;
        }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        int a = __cself->a; // bound by copy
        printf("%d
    ", a);
    }
    static struct __main_block_desc_0 {
        size_t reserved;
        size_t Block_size;
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
    int main()
    {
        int a = 100;
        void (*block2)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a);
        ((void (*)(__block_impl *))((__block_impl *)block2)->FuncPtr)((__block_impl *)block2);
        return 0;
    }
    

      在本例中,我们可以看到:

    1. 本例中,isa 指向 _NSConcreteStackBlock,说明这是一个分配在栈上的实例。
    2. main_block_impl_0 中增加了一个变量a,在block中引用的变量a实际上是在申明block时,被复制到 main_block_impl_0 结构体中的那个变量a。y因为这样,我们就能理解,在block内部修改变量a的内容,不会影响外部的实际变量a。
    3. main_block_impl_0 中由于增加了一个变量a,所以结构体的大小变了,该结构体大小被写在了 main_block_desc_0 中。

    我们修改上面的代码,在变量前面增加 __block 关键字:

    #include <stdio.h>
    int main()
    {
        __block int i = 1024;
        void (^block1)(void) = ^{
            printf("%d
    ", i);
            i = 1023;
        };
        block1();
        return 0;
    }
    

      生成的关键代码如下,可以看到,差异很大:

    struct __Block_byref_i_0 {
        void *__isa;
        __Block_byref_i_0 *__forwarding;
        int __flags;
        int __size;
        int i;
    };
    struct __main_block_impl_0 {
        struct __block_impl impl;
        struct __main_block_desc_0* Desc;
        __Block_byref_i_0 *i; // by ref
        __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) {
            impl.isa = &_NSConcreteStackBlock;
            impl.Flags = flags;
            impl.FuncPtr = fp;
            Desc = desc;
        }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        __Block_byref_i_0 *i = __cself->i; // bound by ref
        printf("%d
    ", (i->__forwarding->i));
        (i->__forwarding->i) = 1023;
    }
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}
    static struct __main_block_desc_0 {
        size_t reserved;
        size_t Block_size;
        void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
        void (*dispose)(struct __main_block_impl_0*);
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
    int main()
    {
        __attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 1024};
        void (*block1)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344);
        ((void (*)(__block_impl *))((__block_impl *)block1)->FuncPtr)((__block_impl *)block1);
        return 0;
    }
    

      从代码中我们可以看到:

    1. 源码中增加一个名为 __block_byref_i_0 的结构体,用来保存我们要 捕获 并且修改的变量 i。
    2. main_block_impl_0 引用的是 Block_byref_i_0 的结构体指针,这样就可以达到修改外部变量的作用。
    3. __Block_byref_i_0 结构体中带有 isa,说明它也是一个对象。
    4. 我们需要负责 Block_byref_i_0 结构体相关的内存管理,所以 main_block_desc_0 中增加了 copy 和 dispose 函数指针,对于在调用前后修改响应变量的引用计数。

    为什么使用__block 修饰的外部变量的值就可以被block修改呢?

    我们发现一个局部变量加上 __block 修饰符后竟然跟block一样变成了一个__Block_byref_i_0结构体类型的自动变量实例。此时我们在block内部访问 i 变量则需要通过一个叫 __forwarding 的成员变量来间接访问 i 变量。

    __block 变量和 __forwarding

    在copy操作之后,既然__block变量也被copy到堆上去了,那么访问该变量是访问栈上还是堆上的呢?

    通过__forwarding, 无论是在block中还是 block外访问__block变量, 也不管该变量在栈上或堆上, 都能顺利地访问同一个__block变量。

    2.3 NSConcreteMallocBlock 类型的 block 的实现

    NSConcreteMallocBlock 类型的 block 通常不会在源码中直接出现,因为默认它是当一个 block 被 copy 的时候,才会将这个 block 赋值到堆中。以下是一个 block 被copy 时的示例代码,可以看到,在第8步,目标的 block 类型被修改为 _NSConcreteMallocBlock。

    static void *_Block_copy_internal(const void *arg, const int flags) {
        struct Block_layout *aBlock;
        const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;
        // 1
        if (!arg) return NULL;
        // 2
        aBlock = (struct Block_layout *)arg;
        // 3
        if (aBlock->flags & BLOCK_NEEDS_FREE) {
            // latches on high
            latching_incr_int(&aBlock->flags);
            return aBlock;
        }
        // 4
        else if (aBlock->flags & BLOCK_IS_GLOBAL) {
            return aBlock;
        }
        // 5
        struct Block_layout *result = malloc(aBlock->descriptor->size);
        if (!result) return (void *)0;
        // 6
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
        // 7
        result->flags &= ~(BLOCK_REFCOUNT_MASK);    // XXX not needed
        result->flags |= BLOCK_NEEDS_FREE | 1;
        // 8
        result->isa = _NSConcreteMallocBlock;
        // 9
        if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
            (*aBlock->descriptor->copy)(result, aBlock); // do fixup
        }
        return result;
    }
    

    3. 变量的复制

    对于 block 外的变量引用,block默认是将其复制到其数据结构中来实现访问的,也就是说block的自动变量只针对block内部使用的自动变量,不使用则不截获,因为截获的自动变量会存储于block的结构体内部,会导致block体积变大,默认情况下 block 只能访问不能修改局部变量的值,如下图所示:

    对于 __block 修饰的外部变量引用,block 是复制其引用地址来实现访问的,block可以修改__block 修饰的外部变量的值,如下图所示:

    4. ARC 对 block 类型的影响

    在 ARC 开启的情况下,将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。

    原本的 NSConcreteStackBlock 的 block 会被 NSConcreteMallocBlock 类型的 block替代。证明方式是以下代码再 XCode 中,会输出 <__NSMallocBlock__: 0x100109960>。

    在苹果的官方文档中也提到,当把栈中的block返回时,不需要调用 copy 方法了。

    #import <Foundation/Foundation.h>
    int main(int argc, const char * argv[])
    {
        @autoreleasepool {
            int i = 1024;
            void (^block1)(void) = ^{
                printf("%d
    ", i);
            };
            block1();
            NSLog(@"%@", block1);
        }
        return 0;
    }

     ARC下,访问外界变量的 Block 为什么要从栈区拷贝到堆区呢?

    栈上的Block,如果其所属的变量作用域结束,该Block就被废弃,如同一般的自动变量。当然,Block中的__block变量也同时被废弃:


    为了解决栈块在其变量作用域结束之后被废弃(释放)的问题,我们需要把Block复制到堆中,延长其生命周期。开启ARC时,大多数情况下编译器会恰当地进行判断是否有需要将Block从栈复制到堆,如果有,自动生成将Block从栈上复制到堆上的代码。Block的复制操作执行的是copy实例方法。Block只要调用了copy方法,栈块就会变成堆块。

    如下图:

    5. 链式语法的实现

      类似于第三方自动布局 Masonry 的代码:

    [view1 mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(superview.mas_top).with.offset(padding.top);
        make.left.equalTo(superview.mas_left).with.offset(padding.left);
        make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);
        make.right.equalTo(superview.mas_right).with.offset(-padding.right);
    }];
    

    5.1 如何实现

    我们举个例子,假如对于一个已有类的实例 classInstance,现在要用句点 . 和小括号 () 的方式连续调用它的"方法" method1,method2,method3,如下图所示:

    从图中我们可以知道,要实现链式语法,主要包含 点语法、小括号调用、连续访问 三部分:

    • 点语法:在OC中,对于点语法的使用,最常见于属性的访问,比如对在方法内部调用 self.xxx,在类的实例中用 classInstance.xxx;
    • 小括号调用:OC中一般用中括号 [] 来实现方法的调用,而对于 Block 的调用则还是保留使用小括号 ( ) 的方式,因此我们可以考虑用 Block来实现链式语法中的 ();
    • 如何实现连续访问?:Block可以理解为带有自动变量的匿名函数或函数指针,它也是有返回值的。我们可以把上述类实例每次方法的调用(实质为 Block 的调用)的返回值都设为当前类实例本身,即 classInstance.method1() 返回了当前 classInstance ,此时才能在其后面继续执行 .method2() 的调用,以此类推。

    总结一句话:我们可以定义类的一些只读 Block 类型的属性,并把这些 Block 的返回值类型设置为当前类本身,然后实现这些 Block 属性的 getter 方法。

    下面是一个Demo,链式计算器的例子,可以连续地调用计算器的加减乘除进行计算:

    @interface Cacluator : NSObject
    
    @property (assign, nonatomic) NSInteger result;
    
    // 下面分别定义加减乘除四个只读block类型的属性
    // 设置为只读是为了限制只需要实现 getter方法
    // 这里每个 Block 类型的属性携带一个 NSInteger 类型的参数,返回参数是当前类型
    @property (copy, nonatomic, readonly) Cacluator *(^add)(NSInteger number);
    @property (copy, nonatomic, readonly) Cacluator *(^minus)(NSInteger number);
    @property (copy, nonatomic, readonly) Cacluator *(^multiply)(NSInteger number);
    @property (copy, nonatomic, readonly) Cacluator *(^divide)(NSInteger number);
    
    @end
    
    
    @implementation Cacluator
    
    // 此处为 add 属性的 getter方法实现
    // 前面声明 add 属性的类型为 block 类型,所以此处 getter 返回一个 block
    // 对于返回的 block,返回值类型为 Calculator,所以返回self
    
    -(Cacluator *(^)(NSInteger))add{
        return ^id(NSInteger num){
            self.result += num;
            return self;
        };
    }
    
    -(Cacluator *(^)(NSInteger))minus{
        return ^id(NSInteger num){
            self.result -= num;
            return self;
        };
    }
    
    -(Cacluator *(^)(NSInteger))multiply{
        return ^id(NSInteger num){
            self.result *= num;
            return self;
        };
    }
    
    -(Cacluator *(^)(NSInteger))divide{
        return ^id(NSInteger num){
            NSAssert(num != 0, @"除数不能为0");
            self.result /= num;
            return self;
        };
    }
    
    @end
    

     测试代码:

    Calculator *calc = [[Calculator alloc] init]; // 初始化一个计算器类实例
    
    calc.add(8).minus(4).multiply(6).divide(3); // 链式调用
    
    NSLog(@"%d", (int)calc.result); // 输出 8
    

     分析:

    上面 calc.add 访问 calc 的 add 属性会调用 [calc add] 方法,此方法会返回一个Block如下:
    
    ^id(NSInteger num){
          self.result += num;
          return self;  
    };
    
    在这个Block中,前面已声明其返回值类型为:Caculator,所以在其里面返回了 self,这样当调用该 Block 时,会返回 self (实例本身),流程如下:
    
    1.calc.add 获得一个 Block
    2.calc.add(8) Block 的执行,并返回了 self (即实例 calc)
    3.于是在 calc.add(8) 后面可继续访问 calc 的其他属性,一路点下去
    

     5.2 更简洁的实现

    上面是通过先声明一系列的Block属性, 再去实现Block属性的getter 方法来实现链式调用,感觉还是有点麻烦,我们去看看是否有更简洁的实现方式:

    点语法的本质:

    • 在OC中,点语法实际上只是一种替换手段,对于属性的getter方法,class.xxx 的写法最终会被编译器替换成 [class xxx];对于setter 方法,即把 class.xxx 写在等号左边,class.xxx = value 会被转换成 [class setXxx:value],本质都是方法调用
    • 即使再class中并没有显式声明 xxx 属性,在编译代码时,代码中如果有 class.xxx 的写法也会被替换成 [class xxx],所以只要在class中有一个声明为 xxx 的方法,即可在代码中其它地方写 class.xxx

    所以,解决方案是:

      在定义类的头文件的@interface中,直接声明某一方法名为xxx,该方法的返回值是一个Block,而此block的返回值设为该类本身。

    @interface Calculator : NSObject
    
    @property (nonatomic, assign) NSInteger result; // 保存计算结果
    
    // 上面的属性声明其实是可以省略的,只要声明下面方法即可;
    // 在 Objective-C 中,点语法只是一种替换手段,class.xxx 的写法(写在等号左边除外)最终会被编译器替换成 [class xxx],本质上是方法调用;
    
    // add、minus、multiply、divide 四个方法都会返回一个 Block,
    // 这个 Block 有一个 NSInteger 类型的参数,并且其返回值类型为当前 Calculator 类型;
    // 下面四个方法的实现与上面 Calculator.m 中的一致。
    - (Calculator * (^)(NSInteger num)) add;
    - (Calculator * (^)(NSInteger num)) minus;
    - (Calculator * (^)(NSInteger num)) multiply;
    - (Calculator * (^)(NSInteger num)) divide;
    

     Masonry 也是这么做的,只声明了方法,并没有声明相应的属性。另外,对于Masonry链式语法中的 .and、.with 等写法只是为了让代码读起来更通顺,实现方式为:声明一个名为 and 和 with 的方法,在方法里返回self:

    - (MASConstraint *)with {
        return self;
    }
    
    - (MASConstraint *)and {
        return self;
    }
    

     存在的问题:

    当用点语法去访问类的某一个 Block 属性时,Block 后面的参数 Xcode

    XXXHTTPManager *http = [XXXHTTPManager manager];
    
    // 下面 .get(...) 里面的参数,Xcode 并不会提示自动补全,需要手动去填写,.success(...) .failure(...) 等也一样,
    // 这里不能像传统中括号 [] 方法调用那样,输入方法名就可以自动提示该方法所有的参数并按回车自动补全。
    http.get(@"https://kangzubin.cn", nil).success(^(NSURLSessionDataTask *task, id responseObject) {
        // Success TODO
    }).failure(^(NSURLSessionDataTask *task, NSError *error) {
        // Failure TODO
    }).resume();
    

     Xcode 中有个强大但未被充分利用的功能:Code Snippets(代码块)可以解决。

    http://www.imlifengfeng.com/blog/?utm_medium=email&utm_source=gank.io&p=457

  • 相关阅读:
    systemtap分析软raid io拆分问题
    Profiling Java Application with Systemtap
    中国南方ORACLE用户组
    Docker 核心技术与实现原理
    The Internals of PostgreSQL
    Alone_Monkey 逆向IOS
    淘宝内核月报 2017
    Linux kernel engineer--trace
    你的按钮到底在帮助用户还是在误导用户?
    2020年值得你去试试的10个React开发工具
  • 原文地址:https://www.cnblogs.com/chenjiangxiaoyu/p/7729273.html
Copyright © 2011-2022 走看看