<iOS知识体系>Block知识点及面试题

226 阅读23分钟

1.什么是Block?

Block是将函数及其执行上下文封装起来的对象。Block本质上也是一个OC对象,它内部也有一个isa指针。Block是封装了函数调用以及函数调用环境的OC对象。

1.1 Block的声明

    @property (nonatomic, copy) void(^block1)(void);
    // BlockType:类型别名
    typedef void(^BlockType)(void);
    @property (nonatomic, copy) BlockType block2;
    // 返回值类型(^block变量名)(参数1类型,参数2类型,...)
    void(^block)(void);

1.2 Block的定义

    // ^返回值类型(参数1,参数2,...){};
    // 1.无返回值,无参数
    void(^block1)(void) = ^{
        
    };
    // 2.无返回值,有参数
    void(^block2)(int) = ^(int a){
        
    };
    // 3.有返回值,无参数(不管有没有返回值,定义的返回值类型都可以省略)
    int(^block3)(void) = ^int{
        return 3;
    };
    // 以上Block的定义也可以这样写:
    int(^block4)(void) = ^{
        return 3;
    };
    // 4.有返回值,有参数
    int(^block5)(int) = ^int(int a){
        return 3 * a;
    };

2.Block的底层数据结构

  • Block本质上也是一个OC对象,它内部也有个isa指针;
  • Block是封装了函数调用以及调用环境的OC对象;
  • Block的底层数据结构如下图所示:

通过Clang将以下Block代码转换为C++代码,来分析Block的底层实现。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        void(^block)(int ,int) = ^(int a, int b){
            NSLog(@"this is block,a = %d,b = %d",a,b);
            NSLog(@"this is block,age = %d",age);
        };
        block(3,5);
    }
    return 0;
}

使用命令行将代码转化为c++查看其内部结构,与OC代码进行比较

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

上图中将c++中block的声明和定义分别与oc代码中相对应显示。将c++中block的声明和调用分别取出来查看其内部实现。

  • Block的底层数据结构就是一个__main_block_impl_0结构体对象,其中有__block_impl__main_block_desc_0两个结构体对象成员。

    • main:表示block所在的函数;
    • block:表示这是一个block;
    • impl:表示实现(implementation);
    • 0:表示这是该函数的第一个block
  • __main_block_func_0:结构体封装了block里的代码;

  • __block_impl:结构体才是真正定义block的结构,其中的FuncPtr指针指向__main_block_func_0

  • __main_block_desc_0:是block的描述对象,存储着block的内存大小等;

  • 定义Block的本质:调用__main_block_impl_0构造函数,并且给它传入两个参数__main_block_func_0&__main_block_desc_0_DATA。拿到函数的返回值,再取返回值的地址&__main_block_impl_0,把这个地址赋值给block变量;

  • 调用Block的本质:通过&__main_block_impl_0中的&__block_impl中的FuncPtr拿到函数地址,直接调用。

// main.cpp
struct __main_block_impl_0 {
    struct __block_impl impl;         // block的结构体
    struct __main_block_desc_0* Desc; // block的描述对象,描述block的大小等
    /*  构造函数
     ** 返回值:__main_block_impl_0 结构体
     ** 参数一:__main_block_func_0 结构体
     ** 参数二:__main_block_desc_0 结构体的地址
     ** 参数三:flags 标识位
     */
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock; //_NSConcreteStackBlock 表示block存在栈上
        impl.Flags = flags; 
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

// __main_block_func_0 封装了block里的代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_77_f_d18dtx6277bxbcd8s72my80000gn_T_main_58a448_mi_0);
}

struct __block_impl {
    void *isa;     // block的类型
    int Flags;     // 标识位
    int Reserved;  // 
    void *FuncPtr; // block的执行函数指针,指向__main_block_func_0
};

static struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size; // block本质结构体所占内存空间
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)}; 
 
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        /*
         ** void(^block)(void) = ^{
                NSLog(@"调用了block");
            };
         ** 定义block的本质:
         ** 调用__main_block_impl_0()构造函数
         ** 并且给它传了两个参数 __main_block_func_0 和 &__main_block_desc_0_DATA
         ** __main_block_func_0 封装了block里的代码
         ** 拿到函数的返回值,再取返回值的地址 &__main_block_impl_0,
         ** 把这个地址赋值给 block
         */
        void(*block)(void) = ((void (*)())&__main_block_impl_0(
                                                               (void *)__main_block_func_0,
                                                               &__main_block_desc_0_DATA
                                                              ));
        /*
         ** block();
         ** 调用block的本质:
         ** 通过 __main_block_impl_0 中的 __block_impl 中的 FuncPtr 拿到函数地址,直接调用
         */      
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

3.Block的变量捕获机制

为了保证Block内部能够正常访问外部的变量,Block有个变量捕获机制

  • 对于全局变量,不会捕获到Block内部,访问方式为直接访问;
  • 对于auto类型的局部变量,会捕获到Block内部,Block内部会自动生成一个成员变量,用来存储这个变量的值,访问方式为值传递;
  • 对于static类型的局部变量,会捕获到Block内部,Block内部会自动生成一个成员变量,用来存储这个变量的地址,访问方式为指针传递;
  • 对于对象类型的局部变量,Block会连同它的所有权修饰符一起捕获;

3.1 auto类型的局部变量

auto类型的局部变量会捕获到Block内部,访问方式为值传递。
通过Clang将以下代码转换为C++代码:

    int age = 10;
    void(^block)(void) = ^{
        NSLog(@"%d",age);
    };
    block();
  • __main_block_impl_0对象内部会生成一个相同的age变量;
  • __main_block_impl_0()构造函数多了个参数,用来捕获访问的外面的age变量的值,将它赋值给__main_block_impl_0对象内部的age变量。
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int age;
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int age = __cself->age; // bound by copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_77_f_d18dtx6277bxbcd8s72my80000gn_T_main_5ed490_mi_0,age);
}

......

    int age = 10;
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

由于是值传递,我们修改外部的age变量的值,不会影响到block内部的age变量。

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

3.2 static类型的局部变量

static类型的局部变量会捕获到block内部,访问方式为指针传递 通过Clang将以下代码转换为C++代码:

    static int age = 10;
    void(^block)(void) = ^{
        NSLog(@"%d",age);
    };
    block();
  • __main_block_impl_0对象内部会生成一个相同类型的age指针;
  • __main_block_impl_0()构造函数多了个参数,用来捕获访问的外面的age变量的地址,将它赋值给__main_block_impl_0对象内部的age指针。
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int *age;
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_age, int flags=0) : age(_age) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int *age = __cself->age; // bound by copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_77_f_d18dtx6277bxbcd8s72my80000gn_T_main_a4bc7d_mi_0,(*age));
}

......

    static int age = 10;
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &age));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

由于是指针传递,我们修改外部的age变量的值,会影响到block内部的age变量。

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

3.3 全局变量

全局变量不会捕获到Block内部,访问方式为直接访问。
通过Clang将以下代码转换成C++代码:

int _age = 10;
static int _height = 20;
......
 void(^block)(void) = ^{
     NSLog(@"%d,%d",_age,_height);
 };
block();
  • __main_block_impl_0对象并没有生成对应的变量,也就是说全局变量没有捕获到block内部,而是直接访问。
int _age = 10;
static int _height = 20;

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) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_77_f_d18dtx6277bxbcd8s72my80000gn_T_main_12efa5_mi_0,_age,_height);
}

......

    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
为什么局部变量需要捕获,全局变量不用捕获呢?
  • 作用域的原因,全局变量哪里都可以直接访问,所以不用捕获;
  • 局部变量,外部不能直接访问,所以需要捕获;
  • auto类型的局部变量可能会销毁,其内存会消失,block将来会执行代码的时候不可能再去访问那块内存,所以捕获其值;
  • static变量会一直保存在内存中,所以捕获其地址即可;

3.4 对象类型的auto变量

当block内部访问了对象类型的auto变量时:

  • 如果block在栈上,将不会对auto变量产生强引用;

  • 如果block被拷贝到堆上

    • block内部的desc结构体会新增两个函数:

      • copy(__main_block_copy_0,函数名命名规范同__main_block_impl_0)
      • dispose(__main_block_dispose_0)
    • 会调用block内部的copy函数

    • __Block_object_assgin函数会根据auto变量的修饰符(__strong__weak__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用

    • 如果block从堆上移除

      • 会调用block内部的dispose函数
      • dispose函数内部会调用__Block_object_dispose函数
      • __Block_object_dispose函数会自动释放引用的auto变量(release)
  • copy函数:栈上的block复制到堆时;

  • dispose函数:堆上的block被废弃时;

如下代码,block保存在堆上,当执行完作用域2的时候,Person对象并没有被释放,而是在执行完作用域1的时候释放,说明block内部对Person对象产生了强引用。

typedef void(^MyBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool { //作用域1
        MyBlock block;
        { //作用域2
            Person *p = [Person new];
            p.name = @"zhangsan";      
            block = ^{
                NSLog(@"%@",p.name);
            };
        }
        NSLog(@"-----");
    }
    return 0;
}

通过Clang将以上代码转换为C++代码:

__main_block_impl_0中生成了一个Person * __strong p指针,指向外面的person对象,而且是强引用。

typedef void(*MyBlock)(void);

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    Person *__strong p; // 强引用
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *_p, int flags=0) : p(_p) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    Person *__strong p = __cself->p; // bound by copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_77_f_d18dtx6277bxbcd8s72my80000gn_T_main_9e5699_mi_1,((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("name")));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);}

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(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        MyBlock block;
        {
            Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("new"));
            ((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)p, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_77_f_d18dtx6277bxbcd8s72my80000gn_T_main_9e5699_mi_0);

            block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, p, 570425344));
        }
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_77_f_d18dtx6277bxbcd8s72my80000gn_T_main_9e5699_mi_2);
    }
    return 0;
}

添加__weak修饰后,当执行完作用域2的时候,Person对象就被释放了。

typedef void(^MyBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool { //作用域1
        MyBlock block;
        { //作用域2
            __weak Person *p = [Person new];
            p.name = @"zhangsan";      
            block = ^{
                NSLog(@"%@",p.name);
            };
        }
        NSLog(@"-----");
    }
    return 0;
}

同样的,通过Clang将以上代码转换成C++代码。
__main_block_impl_0中生成了一个Person * __weak p指针,指向外面的person对象,且是弱引用。说明当block内部访问了对象类型的auto变量时,如果block被拷贝到堆上,会连同对象的所有权修饰符一起捕获。

......
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    Person *__weak p; //弱引用
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _p, int flags=0) : p(_p) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    Person *__weak p = __cself->p; // bound by copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_77_f_d18dtx6277bxbcd8s72my80000gn_T_main_c61841_mi_1,((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("name")));
}
......

3.5 __block修饰的变量

3.5.1 __block作用

默认情况下block是不能修改外面的auto变量的,解决办法?

  • 变量用static修饰(原因:捕获static类型的局部变量是指针传递,可以访问到该变量的内存地址)
  • 全局变量
  • __block
3.5.2 __block修饰符
  • __block可以用解决block内部无法修改auto变量值的问题;
  • __block不能修饰全局变量、静态变量;
  • 编译器会将__block变量包装成一个对象(struct __Block_byref_age_0(byref:按地址传递));
  • 加__block修饰不会修改变量的性质,它还是auto变量;
  • 一般情况下,对被捕获变量进行赋值(赋值!=使用)操作需要添加__block修饰符,比如给数组添加或者删除对象,就不用添加__block修饰;
  • 在MRC下使用__block修饰对象,在block内部不会对该对象进行retain操作,所以在MRC环境下可以通过__block解决循环引用的问题;

使用实例:

    __block int age = 10;
    void(^block)(void) = ^{
        age = 20;
        NSLog(@"block-%d",age);
    };
    block();
    NSLog(@"%d",age);

通过Clang将以上代码转换为C++代码。

  • 编译器会将__block修饰的变量包装成一个__Block_byref_age_0对象
  • 以上age=20;的赋值过程为:通过block结构体里的(__Block_byref_age_0)类型的age指针,找到__Block_byref_age_0结构体的内存(即将__block包装成对象的内存),把__Block_byref_age_0结构体里的age变量的值改为20。
  • 由于编译器将__block变量包装成一个对象,所以它的内存管理几乎等同于访问对象类型的auto变量,但还是有差异。
struct __Block_byref_age_0 {
    void *__isa;
    __Block_byref_age_0 *__forwarding;
    int __flags;
    int __size;
    int age; 
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_age_0 *age; // by ref
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__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_age_0 *age = __cself->age; // bound by ref
    (age->__forwarding->age) = 20;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_77_f_d18dtx6277bxbcd8s72my80000gn_T_main_9578d0_mi_0,(age->__forwarding->age));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->age, 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(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_77_f_d18dtx6277bxbcd8s72my80000gn_T_main_9578d0_mi_1,(age.__forwarding->age));
    }
    return 0;
}
3.5.3 __block的内存管理
  • 当block在栈上时,并不会对__block变量产生弱引用;

  • 当block被copy到堆上时

    • block内部的desc结构体会新增两个函数:

      • copy(__main_block_copy_0,函数名命名规范同__main_block_impl)
      • dispose(__main_block_dispose_0)
    • 会调用block内部的copy函数

    • copy函数内部会调用__Block_object_assgin函数

    • __Block_object_assgin函数会对__block变量形成强引用(retain)

  • 当block从堆中移除时

    • 会调用block内部的dispose函数
    • dispose函数内部会调用__Block_object_dispose函数
    • __Block_object_dispose函数会自动释放引用的__block变量(release)
3.5.4 __block的__forwarding指针
__block的__forwarding指针存在的意义?
为什么要通过age结构体里的__forwarding指针拿到age变量的值,而不直接age结构体拿到age变量的值呢?

__block的__forwarding是指向自己本身的指针,为了不论任何内存位置,都可以顺利的访问同一个__block变量。

  • block对象copy到堆上时,内部的__block变量也会copy到堆上去。为了防止age的值赋值给栈上的__block变量,就使用了__forwarding;
  • 当__block变量在栈上的时候,__block变量的结构体中__forwarding指针指向自己,这样通过__forwarding取到结构体中的age给它赋值没有问题;
  • 当__block变量copy到堆上后,栈上的__forwarding指针会指向copy到堆上的__block变量结构体,而堆上的__forwarding指向自己;

这样不管我们访问您的是栈上还是堆上的__block变量结构体,只要通过__forwarding指针访问,都是访问到堆上的__block变量结构体;给age赋值,就肯定会在赋值给堆上的那个__block变量中的age。

3.5.5 对象类型的auto变量、__block变量内存管理区别

  • 当block在堆上时,对它们都不会产生强引用;
  • 当block拷贝到堆上时,都会通过copy函数来处理它们
  • 当block从堆上移除时,都会通过dispose函数来释放它们

3.5.6 被__block修饰的对象类型

  • 当__block变量在栈上时,不会对指向的对象产生强引用;

  • 当__block变量被copy到堆上时

    • __Block_byref_object_0即__block变量内部会新增两个函数:

      • copy(__Block_byref_id_object_copy)
      • dispose(__Block_byref_id_object_dispose)
    • 会调用__block变量内部的copy函数

    • copy函数内部会调用__Block_object_assgin函数

    • __Block_object_assgin函数会根据所指向对象的修饰符(__strong__weak__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain)

  • 如果__block变量从堆上移除

    • 会调用__block变量内部的dispose函数
    • dispose函数内部会调用__Block_object_dispose函数
    • __Block_object_dispose函数会自动释放指向的对象(release)
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        
        __block NSObject *object = [[NSObject alloc] init];
        void(^block)(void) = ^{
            object = [[NSObject alloc] init];
        };
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
struct __Block_byref_object_0 {
    void *__isa;
    __Block_byref_object_0 *__forwarding;
    int __flags;
    int __size;
    void (*__Block_byref_id_object_copy)(void*, void*); // copy
    void (*__Block_byref_id_object_dispose)(void*);     // dispose
    NSObject *__strong object;
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_object_0 *object; // by ref
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_object_0 *_object, int flags=0) : object(_object->__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_object_0 *object = __cself->object; // bound by ref
    (object->__forwarding->object) = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->object, (void*)src->object, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->object, 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(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));

        __attribute__((__blocks__(byref))) __Block_byref_object_0 object = {
            (void*)0,
            (__Block_byref_object_0 *)&object, 
            33554432, 
            sizeof(__Block_byref_object_0), 
            __Block_byref_id_object_copy_131,
            __Block_byref_id_object_dispose_131, 
            ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"))
        };
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_object_0 *)&object, 570425344));
    }
    return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
    // __block 对象结构体的地址+40个字节,即为结构体中 object 对象的地址
    _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
    _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

4.Block的类型

block有3种类型,可以通过调用class方法或者isa指针,查看具体类型,最终都是继承自NSBlock类型。

  • NSGlobalBlock(_NSConcreteGlobalBlock):全局block,保存在数据段,没有访问auto变量;
  • NSStackBlock(_NSConcreteStackBlock):栈block,保存在栈区,访问了auto变量;
  • NSMallocBlock(_NSConcreteMallocBlock):堆block,保存在堆区,__NSStackBlock__调用了copy;

每一种类型的block调用copy后的结果如下所示:

  • _NSConcreteGlobalBlock:程序的数据段区,什么也不做
  • _NSConcreteStackBlock:栈block,从栈复制到堆
  • _NSConcreteMallocBlock:堆block,引用计数增加

__NSConcreteStackBlock__存在的问题:

在MRC环境下,block类型为NSStackBlock。当test()函数执行完毕,栈上的东西可能会被销毁,数据就会变成垃圾数据。尽管block还能正常调用,但是输出的age的值发生了错乱。

void (^block)(void);
void test()
{    
    // __NSStackBlock__
    int age = 10;
    block = ^{
        NSLog(@"%d", age);
    };
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        block();
    }
    return 0;
}

解决办法:调用copy方法,将栈block复制到堆上。

void (^block)(void);
void test()
{    
    // __NSMallocBlock__
    int age = 10;
    block = [^{
        NSLog(@"%d", age);
    } copy];
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        block();
        [block release];
    }
    return 0;
}

5.Block的copy

在ARC环境下,编译器会根据情况自动将栈上的block复制到栈上,比如以下几种情况:

  • 手动调用block的copy方法时;
  • block作为函数返回值时(Masonry框架中用很多);
  • 将block赋值给__strong指针时;
  • block作为Cocoa API中方法名含有usingBlock;
  • block作为GCD API的方法参数时

block作为属性的写法:

  • ARC下写strong或者copy都会对block进行强引用,都会自动将block从栈copy到堆上;
  • 建议都写成copy,这样MRC和ARC下一致;
// MRC
@property (nonatomic, copy) void(^block)(void);
// ARC
@property (nonatomic, copy) void(^block)(void);
@property (nonatomic, strong) void(^block)(void);

6.Block的循环引用问题

为什么block会产生循环引用?

  • 相互循环引用:如果当前block对当前对象的某一成员变量进行捕获的话,可能会对它产生强引用。而当前block又由于当前对象对其有一个强引用,就产生了相互循环引用的问题;
  • 大环引用:如果使用__block的话,在ARC下可能会产生循环引用(MRC则不会),在ARC下可以通过断环的方式去解除循环引用。但是有一个弊端,如果该block一直得不到调用,循环引用就一直存在。

6.1 ARC

  • __weak或用__unsafe_unretained解决:
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        NSLog(@"%p",weakSelf);
    };
    __unsafe_unretained id weakSelf = self;
    self.block = ^{
        NSLog(@"%p",weakSelf);
    };
  • __block解决(必须要调用block):

    缺点:必须要调用block,而且block里要将指针置为nil。如果一直不调用block,对象就会一直保存在内存中,造成内存泄漏。

    __block id weakSelf = self;
    self.block = ^{
        NSLog(@"%p",weakSelf);
        weakSelf = nil;
    };
    self.block();

6.2 MRC

  • __unsafe_unretained解决:同ARC
  • __block解决(在MRC下使用__block修饰对象类型,在block内部不会对该对象进行retain,所以MRC环境下可以通过__block解决循环引用的问题)
    __block id weakSelf = self;
    self.block = ^{
        NSLog(@"%p",weakSelf);
    };

7.面试题

1.Block的本质是什么?

封装了函数调用及调用环境的OC对象

2.Block的属性修饰词为什么是copy?使用Block有哪些使用注意?

Block一旦没有进行copy操作,就不会被复制到堆上,无法对生命周期进行控制。
使用注意:循环引用的问题

3.Block在给NSMuatbleArray添加或者移除对象,需不需要添加__block?

不需要。修改内容也是对数组的使用,只有对对象赋值的时候才需要__block。如果修改的是NSMuatbleArray的存储内容的话,是不需要添加__block修饰的,如果修改的是NSMuatbleArray对象的本身,那必须添加__block修饰。

4.Block的变量捕获机制是什么?

Block的变量捕获机制,是为了保证Block内部能正常访问外部的变量。

  • 对于全局变量,不会捕获到block内部,访问方式是直接访问;
  • 对于auto类型的局部变量,会捕获到block内部,block内部会自动生成一个成员变量,用来存储这个变量的值,访问方式为值传递;
  • 对于static类型的局部变量,会捕获到block内部,block内部会自动生成一个成员变量,用来存储这个变量的地址,访问方式为值传递;
  • 对于对象类型的局部变量,block会连同它的所有权修饰符一起捕获

5.为什么局部变量需要捕获,全局变量不用捕获呢?

  • 作用域的原因,全局变量哪里都可以直接访问,所以不用捕获;
  • 局部变量,外部不能直接访问,所以需要捕获;
  • auto类型的局部变量可能会销毁,其内存会小时,block将来执行代码的时候不可能再去访问那块内存,所以捕获其值;
  • static变量会一直保存在内存中,所以捕获其地址即可;

6.self会不会捕获到block内部?

会捕获。OC方法都有两个隐式参数,方法调用者self和方法名_cmd。参数也是一种局部变量。

7._name会不会捕获到block内部?

会捕获。不是将_name变量进行捕获,而是直接将self捕获到block内部,因为_name是Person类的成员变量,_name来自当前的对象/方法调用者self(self->_name)

如果使用self.name即调用selfgetter方法,即给self对象发送一条消息,那还是要访问到selfself是局部变量,不是全局变量,所以self会捕获到block内部。

8.__NSStackBlock__存在的问题:

如果没有将block从栈上copy到堆上,那我们访问栈上的block的话,可能会由于变量作用域结束导致栈上的block以及__block变量被销毁,而造成内存崩溃。或者数据可能会变成垃圾数据,尽管将来block还能正常调用,但是它捕获的变量的值已经错乱了。

解决办法:将block的内存放在堆里,意味着它就不会自动销毁,而是由我们程序员来决定什么时候销毁它。

9.默认情况下Block是不能修改外面的auto变量的,解决办法?

  • 变量static修饰,原因是捕获static类型的局部变量是指针传递,可以访问到该变量的内存地址;
  • 全局变量;
  • __block,只是临时用一下这个变量临时改一下而已,而改为static变量和全局变量会一直存在在内存中

10.Block可以用strong修饰吗?

在MRC环境中,是不可以的,strong修饰符会对修饰符进行retain操作,这样并不会将栈中的block拷贝到堆内存中,而执行的block是在堆内存中,所以用strong修饰的block会导致在执行的时候因为错误的内存地址,导致闪退。

在ARC环境中,是可以的,因为在ARC环境中的block只能在堆内存或者全局内存中,因此不涉及到栈拷贝到堆中的操作。

11.解决循环引用时为什要用__strong、__weak修饰?

__weak修饰的变量,不会出现引用计数+1,也就不会造成block强持有外部变量,这样子也就不会出现循环引用的问题了。

但是,block内部执行的代码中,有可能是一个异步操作,或者延迟操作,此时引用的外部变量可能变成nil,导致意向不到的问题,而block内部通过__strong修饰这个变量时,block会在执行过程中强持有这个变量,此时这个变量也就不会出现nil的情况,当block执行完成后,这个变量也就会随之释放了。

12.Block发生copy时机?

一般情况下在ARC环境中,编译器将创建在栈中的block会自动拷贝到堆内存中,而block作为方法或函数的参数传递时,编译器不会做copy操作。

  • block作为方法或者函数的返回值时,编译器会自动完成copy操作;
  • 当block赋值给通过strong或copy修饰的id或block类型的成员变量时;
  • 当block作为参数被传入方法名带有unsingBlock的Cocoa Framework方法或GCD的API时;

13.Block访问对象类型的auto变量时,在ARC和MRC下有什么区别?

在ARC下,栈区创建的block会自动copy到堆区,而MRC下,就不会自动拷贝了,需要手动调用copy函数;

block的copy操作,当block从栈区copy到堆区的过程中,也会对block内部访问的外部变量进行处理,它会调用Block_object_assgin函数对变量进行处理,根据外部变量是strong、weak对block内部捕获的变量进行引用计数+1或-1,从而达到强引用或弱引用的作用。

在ARC下,由于block被自动copy到了堆区,从而对外部的对象进行强引用,如果这个对象同样强引用这个block,就会形成循环引用;

在MRC下,由于访问的外部变量是auto修饰的,所以这个block属于栈区的,如果不对block手动进行copy操作,在运行完block的定义代码段后,block就会被释放,而由于没有进行copy操作,所以这个变量也不会经过Block_object_assign处理,也就不会对变量强引用。

14.Block怎么进行内存管理的?

Block按照内存分布,分为三种类型:全局Block、栈Block、堆Block

在MRC和ARC下Block的分布情况不一样:

  • MRC下:

    • 当Block内部引用全局变量或者不引用任何外部变量时,该Block是在全局内存中;
    • 当Block内部引用了外部的非全局变量的时候,该Block是在栈内存中的;
    • 当栈中的block进行copy操作时,会将block拷贝到堆内存中;
    • 通过__block修饰的变量,不会对其引用计数+1,不会造成循环引用;
  • ARC下:

    • 当block内部引用全局变量或者不引用任何外部变量,该block是在全局内存中;
    • 当block内部引用了外部的非全局变量的时候,该block是在堆内存中的;
    • ARC下只存在全局block和堆block;
    • 通过__block修饰的变量,在block内部依然会对其引用计数+1,可能会造成循环引用;
    • 通过__weak修饰的变量,在block内部不会对其引用计数+1,不会造成循环引用;