笔记 - Block

作者: 强子ly | 来源:发表于2019-08-24 11:07 被阅读0次

目录

  • block实质
  • 源码分析
  • 自动变量值的截获
  • 循环引用问题
  • 资料推荐

关于block,我们在开发中经常用到,使用上就不具体阐述了,无外乎是回调传值,但是兄弟在某次面试的时候遇到过这样一道笔试,当时就懵逼了,这也是促使我想了解block底层的原因。

下面四个方法打印的内容分别是什么?

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

void test2() {
    __block int a = 10;
    void (^block)(void) = ^{
        NSLog(@"a is %d", a);
    };
    a = 20;
    block(); // 20
}

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

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

一、block实质:

1.1、实质

带有自动变量值的匿名函数

1.2、分类

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

  • NSGlobalBlock (NSConcreteGlobalBlock)
  • NSMallocBlock (NSConcreteStackBlock)
  • NSMallocBlock (NSConcreteMallocBlock)

二、源码分析

  • 2.1、源码转换命令
$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

error: cannot create __weak reference because the current deployment target does not support weak references

使用 __weak 解决循环引用的时转换会报错,此时需要加上 runtime 版本

$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 MyObject.m
  • 2.2、源码分析

创建一个最简单的block(无参无返回值)

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^block)(void) = ^{
            printf("this is a block");
        };
        block();
    }
    return 0;
}

通过上面使用clang生成源码

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};


struct __main_block_impl_0 {
  // 第一个成员变量impl结构体,结构体为上面的__block_impl
  struct __block_impl impl;
  // 第二个成员变量是 Desc 指针,对应的结构体为 __main_block_desc_0
  struct __main_block_desc_0* Desc;

  // 构造函数(类似于OC的init方法)
  __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;
  }
};


// block执行逻辑函数,也能看出来,这个函数对应代码中的打印
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    printf("this is a block")
}


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) = ((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);
    }
    return 0;
}
  • 2.3、函数命名规则

block语法所属的函数名 + 该语法在函数中出现的顺序值


在main函数中实现的第一个block和第二个block
  • 2.4、函数解答
    首先,我们来看 __main_block_func_0 函数,因为它的标记最明显,含有 block 的执行代码 printf("this is a block"),也就是说源代码中的 block 被转换成了以下函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    printf("this is a block")
}
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;  
        // 第一行
        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);

        /*
         * 上述代码删除强制转换后
           void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
           block->FuncPtr(block);
         */
    }
    return 0;
}

我们先来看一下删除强制转换后的第一行代码,该代码将__main_block_impl_0结构体类型的自动变量,赋值给block。
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_impl_0,
                                                     &__main_block_desc_0_DATA);
struct __main_block_impl_0 *block = &tmp;


第二行代码对应源代码中的 block(),去掉转换部分为
(*block -> impl.FuncPtr)(block)
由block转换的__main_block_func_0函数的指针被赋值成员变量FuncPtr中

__main_block_desc_0 结构体实例初始化

static struct __main_block_desc_0 __main_block_desc_0_DATE = {
    0,
    sizeof(struct __main_block_impl_0)
}
  • __main_block_impl_0** 构造函数初始化
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = 0;
    impl.FuncPtr = __main_block_func_0;
    Desc = &__main_block_desc_0_DATA;
}

三、自动变量值的截获

所谓 截获自动变量值 就是在执行block语法时,语法表达式所使用的自动变量值被保存到block的结构体实例(即Block自身)中;
目的是为了保证block内部能够正常访问外部变量,因为自动变量内存随时可能销毁。

  • 3.1、自动变量值截获只能保存执行Block语法的瞬间值,保存后就不能修改
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSInteger age = 10;
        void(^block)(void) = ^{
            age = 20;
        };
        block();
    }
    return 0;
}

error:Variable is not assignable (missing __block type specifier)
3.2、解决不能改写被截获自动变量值的两种方法
  • 使用全局变量、静态全局变量、静态变量截获自动变量值
int global_val = 1; // 全局变量
static int static_global_val = 2; // 静态全局变量

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        static int static_val = 3; // 静态变量
        void(^blk)(void) = ^{
            global_val *= 2;
            static_global_val *= 2;
            static_val *= 2;
        };
        blk();
    }
    return 0;
}

global_val = 1; 
static_global_val = 2; 
static_val = 3; 
-------------------
global_val = 2; 
static_global_val = 4; 
static_val = 6; 

源码分析

源码分析 总结分析
  • 使用__block说明符截获自动变量值 (编译器会将__block变量包装成一个对象)
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int var = 10;
        void(^blk)(void) = ^{
            var = 1;
        };
        blk();
    }
    return 0;
}

__block源码分析

// 编译器会将来__block变量包装成一个对象
struct __Block_byref_var_0 {
    void *__isa;
    // 自己类型的指针,指向自己,用于查找自己的结构体地址,找到变量(修改)
    __Block_byref_var_0 *__forwarding;
    int __flags;
    int __size;
    int var; // 10 => 20 该结构体持有相当于原自动变量的成员变量
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_var_0 *var; // by ref
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_var_0 *_var, int flags=0) : var(_var->__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_var_0 *var = __cself->var; // bound by ref
    (var->__forwarding->var) = 1; // 通过__forwarding拿到变量 
}

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->var, (void*)src->var, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->var, 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_var_0 var = {(void*)0,(__Block_byref_var_0 *)&var, 0, sizeof(__Block_byref_var_0), 10};
        /*
         __Block_byref_var_0 var = {
             0,
             &var, // 赋值给__forwarding
             0,
             sizeof(__Block_byref_var_0),
             10
         };
         */

        void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_var_0 *)&var, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
        /*
         void(*blk)(void) = &__main_block_impl_0(
                                                 __main_block_func_0,
                                                 &__main_block_desc_0_DATA,
                                                 (__Block_byref_var_0 *)&var,
                                                 570425344)
         );
         */
    }
    return 0;
}
3.4、问题解析
  • 截获Object-C对象,调用变更该对象的方法也会产生编译错误么?
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSMutableArray *arrayM = [[NSMutableArray alloc] init];
        void(^block)(void) = ^{
            [arrayM addObject@"123"];
        };
        block();
    }
    return 0;
}

Program ended with exit code: 0

为什么这样就不报错了呢?如果用C语言描述,即是截获了NSMutableArray类对象用的结构体实例指针,
虽然赋值给截获的自动变量array的操作会产生编译错误,但使用截获的值却不会有任何问题。
同理,如果像截获的变量array赋值则会产生编译错误

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSMutableArray *arrayM = [[NSMutableArray alloc] init];
        void(^block)(void) = ^{
            arrayM = [[NSMutableArray alloc] init];
        };
        block();
    }
    return 0;
}

Variable is not assignable (missing __block type specifier)
  • 使用C语言数组时,必须小心使用其指针
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        const char text1[] = "hello";
        void(^block)(void) = ^{
            printf("%c", text1[1]);
        };
        block();
    }
    return 0;
}
error:Cannot refer to declaration with an array type inside block

只是使用C语言的字符串字面量数组,而并没有向截获的自动变量赋值,因此看似没有任何问题,但实际会产生编译错误。
这是因为在现在的Blocks中,截获自动变量的方法并没有实现对C语言数组的截获。此时,使用指针可以解决该问题

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        const char *text1 = "hello";
        void(^block)(void) = ^{
            printf("%c", text1[1]);
        };
        block();
    }
    return 0;
}

四、循环引用问题

在block中,如果使用附有__strong修饰的对象,当block从栈复制到堆时,该对象为block所持有,也就是我们说的循环引用


typedef void(^blk_t)(void);

@interface MyObject : NSObject

@property (nonatomic, copy) blk_t blk;

@end


@implementation MyObject

- (instancetype)init {
    if (self = [super init]) {
        _blk = ^{
            NSLog(@"self = %@", self);
        };
        _blk()
    }
    return self;
}

- (void)dealloc {
    NSLog(@"dealloc");
}

@end


int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        MyObject *object = [[MyObject alloc] init];
    }
    return 0;
}

2019-08-19 21:12:59.677199+0800 test[741:11435] self = <MyObject: 0x100631cc0>
Program ended with exit code: 0

这里可以看到,dealloc 方法没有调用,也就是说,当方法超出作用域之后 MyObject 没有被销毁,也就是产生了内存泄露。

循环引用代码内存图

对应代码为


ATmWgJxT19_07_19__08_22_2019.jpg
AUNuEU8N19_07_54__08_22_2019.jpg 当执行完作用域之后,object对象被销毁 相互持有 循环引用原理
打破循环引用
  • 4.1、使用__weak打破循环
- (instancetype)init {
    if (self = [super init]) {
        id __weak weakSekf = self;
        _blk = ^{
            NSLog(@"self = %@", weakSekf);
        };
    }
    return self;
}

__weak 指向的对象销毁时,会自动让指针置为nil,也是最推荐的解决 block 循环引用一种。

dhdVdjJ21_46_58__08_19_2019.jpg
Am0gxDuw19_24_35__08_22_2019.jpg
  • 4.2、使用__block解决

__block可以用于解决block内部无法修改auto变量值的问题;
__block不能修饰全局变量、静态变量;

- (instancetype)init {
    if (self = [super init]) {
        __block id blockSelf = self;
        _blk = ^{
            NSLog(@"self = %@", blockSelf);
            blockSelf = nil;
        };
        _blk();  // 使用__block解决循环引用,必须调用block
    }
    return self;
}

使用 __block 解决循环引用,必须调用 block,并且将 block 置为nil

blockSelf = nil;
Aipqitv20_12_16__08_11_2019.jpg
  • 4.3、使用__unsafe_unretained解决
- (instancetype)init {
    if (self = [super init]) {
        __unsafe_unretained MyObject *mySelf = self;
        _blk = ^{
            NSLog(@"self = %@", mySelf);
        };
        _blk();
    }
    return self;
}

使用__unsafe_unretained是不安全的,因为它指向的对象销毁时,指针存储的地址值不变,这样的话可能造成野指针。


五、资料推荐

如果觉得我总结的很一般,推荐三个资料,用心看完之后对 block 底层的认识能提升很多, 《Objective-C高级编程 iOS与OS X多线程和内存管理》啃起来费劲的话,建议先看视频,感觉李明杰讲 block 的时候是以这本书为教材的,不过底层就这些东西,无所谓谁先谁后的,学到知识是自己的

- 《Effective Objective-C 2.0》
   第6章 块于大中枢派发

- 《底层原理下 - 李明杰视频》 
   day8 - day10

- 《Objective-C高级编程 iOS与OS X多线程和内存管理》
   第2章 Blocks

相关文章

  • Block常见使用总结(传值/作为参数/作为返回值等)

    来做个block的笔记。 block定义 block类型 block传值 block作为参数的方法定义与调用 bl...

  • runtime 之给 button 添加 block 事件支持

    这只是简单的笔记UIButton+block.h UIButton+block.m 使用

  • Day7 读书笔记&心得体会

    一、读书笔记回顾昨天的收获:什么是block、proc? block和proc是两种不同的东西, block有形无...

  • Objective-C的Block

    声明:本文是读了 做的笔记,以及结合本人写的例子总结的Block知识。 目录 Block入门什么是Block如何定...

  • __block和非__block

    自己的笔记: NSString *text = @"test"; testBlock block = ^{ dis...

  • Block 笔记

    Block表达式语法:^ 返回值类型 (参数列表) {表达式}声明Block类型变量语法:返回值类型 (^变量名)...

  • block笔记

    1.我在一个类中声明了一个block属性:a类 定义如下: 这里用的是weak修饰的,表示我不会将block从本身...

  • block(笔记)

    oc 里面的block:block确实是个很好用的东西,简单的来说就是做完一个事情后要干些什么,用了block就么...

  • block 笔记

    block本质是OC对象(封装了函数调用以及调用环境的OC对象) 本质

  • block 笔记

    block简介 block 其实就是一个值,并且有类型。可以当做 int float 或者 Objective-C...

网友评论

    本文标题:笔记 - Block

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