美文网首页iOS 底层原理
iOS 开发 Block 原理分析

iOS 开发 Block 原理分析

作者: 天空像天空一样蓝 | 来源:发表于2020-12-21 22:00 被阅读0次

前言

无论在面试还是在工作中,总会碰到 block 是什么?block 循环引用怎么办?block 修饰符使用什么?等等这种类似的问题。

一、 什么是 block

一、Demo1

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        /// 下面的代码,就是一个简单的 block
        ^{
            NSLog(@"Hello Block");
        };
        
    }
    return 0;
}

分析:上面的代码,就是一个最简单的 block 但是 NSLog 里面的代码不会被打印出来,因为这个 block 没人调用,所以永远不会执行。

二、Demo2

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

分析:如上,运行程序,控制台会打印出 age--18。

三、分析 block 内部实现

  • 通过 clang 编译可以将 OC 代码转化成 C++ 代码,来查看 block 底层的实现原理
  • 在终端上输入 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m 会在main.m 这级目录下生成一个 main.cpp 文件,就是我们想要的 转化后的代码
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

// 这个就是 block 内部的结构,
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  // c++ 的构造函数(类似于 OC 的 init 方法),返回一个结构体对象。
  // age(_age) 这句代码,就是把 _age 赋值给 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;
  }
};
// 封装了 block 执行逻辑的函数,传入到 fp,fp 在赋值到 impl.funcPtr
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_f__7ngz5gzx5sjgs4dlqrh7t58w0000gn_T_main_679898_mi_0, age);
        }

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)};
// 这句代码对应我们上面的 main 函数
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        int age = 18;
            // 定义 block 变量
        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
         // 执行 block 内部的代码
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

分析上面的代码,在 main 函数里我们对比,转化后的 C++ 代码和 原生的 OC 代码

图片.png

__main_block_impl_0 这个结构体就是 block 的本来面目

__main_block_impl_0

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  // c++ 的构造函数(类似于 OC 的 init 方法),返回一个结构体对象。
  // age(_age) 这句代码,就是把 _age 赋值给 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;
  }
};

上面这个结构体的构造函数,传入的参数:void *fp、struct __main_block_desc_0 *desc、int _age、int flags=0

由于 int flags=0 传入的是常亮数值 0 因此可以忽略,int _age 这个参数是因为外面定义的 局部变量,也可以忽略,所以 这个构造函数的必须参数有两个 void *fp、struct __main_block_desc_0 *desc

上面的 C++ 代码中在 可以看出 void *fp 对应的是 &__main_block_impl_0、struct __main_block_desc_0 *desc 对应的是 &__main_block_desc_0_DATA ,fp又赋值到了imp里面的 FuncPtr,desc 赋值到 Desc 就是 当前结构体的 Desc。

__main_block_func_0

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_f__7ngz5gzx5sjgs4dlqrh7t58w0000gn_T_main_679898_mi_0, age);
        }

上面是 block 执行逻辑的函数,传入到 fp,fp 在赋值到 impl.funcPtr

__block_impl

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

__main_block_desc_0

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)};

这个结构体存了 block 的大小。

四、Demo3 捕获 auto 变量的 block

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

如上,我们修改了局部变量的 age 修改为20,但是运行后,block 里面打印的结果仍为 18.

  • 这是因为 block 捕获了 age = 18,进行了值传递,相当于直接把 age= 18 赋值给了 age, 无论外面怎么修改,都不会改变 age 的值,
  • C 语言会在我们定义局部变量的时候,自动给我们的属性加上 auto 修饰

五、Demo4 捕获 static 变量的 block

int main(int argc, const char * argv[]) {
    @autoreleasepool {
      
        int age = 18;
        static int height = 170;
        
        void (^block)(void) = ^{
            NSLog(@"age--%d,height--%d", age, height);
        };
        age = 20;
        height = 180;
        block();
    }
    return 0;
}

修改 height 的值,运行代码,结果为 age--18,height--180

这是为什么呢,再次编译运行生成 C++ 代码

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        int age = 18;
        static int height = 170;

        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));
        age = 20;
        height = 180;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

从上面的 C++ 代码可以看出传递给block 的height 是地址传递,指针传递

  • block 内部捕获的是 *height 而不是 height。

六、Demo5 全局变量

int age = 22;
int height = 170;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
      
        void (^block)(void) = ^{
            NSLog(@"age--%d,height--%d", age, height);
        };
        age = 20;
        height = 180;
        block();
    }
    return 0;
}

运行上面的代码,打印结果为 age--20,height--180,同样,我们分析 生成的 C++ 代码

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));
        age = 20;
        height = 180;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}
  • block 并没有捕获 任何全局变量

七、总结

一、block 的内存结构

图片.png 图片.png

二、block 本质

  • block 本质上也是一个 OC 对象,它内部也有一个 isa 指针
  • block 是封装了函数调用以及函数调用环境的 OC 对象

二、block 的变量捕获

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

  • 如果变量的类型是局部变量,无论是 auto 还是 static 修饰,都会被捕获到 block 内部,但是 auto 变量是值传递,static 是指针传递。
  • 如果变量类型是全局变量,不会被 block 捕获到内部,直接使用。
变量类型 是否能捕获到 block 内部 访问方式
局部变量 auto 值传递
局部变量 static 指针传递
全局变量 直接访问

为什么 block 要捕获局部变量的值呢,auto:自动变量,离开作用域就销毁

三、block 的类型

一、block 的对象特性

我们知道 block 的本质就是OC 对象,所以OC对象的一些方法同样适用于 block

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
      void (^block)(void) = ^{
          NSLog(@"Hello");
      };
      
      NSLog(@"%@", [block class]);
      NSLog(@"%@", [[block class] superclass]);
      NSLog(@"%@", [[[block class] superclass] superclass]);
      NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);

    }
    return 0;
}

2020-12-20 16:55:50.239327+0800 MyBlock[18424:5457366] __NSGlobalBlock__
2020-12-20 16:55:50.239799+0800 MyBlock[18424:5457366] __NSGlobalBlock
2020-12-20 16:55:50.239839+0800 MyBlock[18424:5457366] NSBlock
2020-12-20 16:55:50.239871+0800 MyBlock[18424:5457366] NSObject

得出继承关系为 NSGlobalBlock -> __NSGlobalBlock -> NSBlock ->NSObject

二、block 的类型查询

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
      int a = 10;
      
      // 堆:动态分配内存,需要程序员申请申请,也需要程序员自己管理内存
      void (^block1)(void) = ^{
          NSLog(@"Hello");
      };
      
      int age = 10;
      void (^block2)(void) = ^{
          NSLog(@"Hello - %d", age);
      };
      
      NSLog(@"%@ %@ %@", [block1 class], [block2 class], [^{
          NSLog(@"%d", age);
      } class]);

    }
    return 0;
}

在ARC 环境下:
2020-12-20 17:00:31.886757+0800 MyBlock[18515:5460672] __NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__
在 MRC 环境下:
2020-12-20 17:26:28.303551+0800 MyBlock[19030:5476820] __NSGlobalBlock__ __NSStackBlock__ __NSStackBlock__

​ 在 MRC 环境下使用 copy

int main(int argc, const char * argv[]) {
    @autoreleasepool {
      int age = 10;
      void (^block2)(void) = [^{
          NSLog(@"Hello - %d", age);
      } copy];
    }
    return 0;
}
同样在 MRC 环境下,进行 copy 操作后打印结果和 ARC 环境下一样
2020-12-20 17:27:42.096753+0800 MyBlock[19064:5478650]  __NSMallocBlock__ 

如上,我们看打印结果得知 block 有 3 种类型,可以通过调用 class 方法或者 isa 指针查看具体类型,最终都是继承自 NSBlock 类型

__NSGlobalBlock__(_NSConcreteGlobalBlock)没有访问 auto 变量
__NSMallocBlock__(_NSConcreteMallocBlock)__NSStackBlock__ 调用了 copy
__NSStackBlock__(_NSConcreteStackBlock)访问了 auto 变量,(在ARC环境下显示NSMallocBlock,在 MRC 环境下,显示 NSStackBlock

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

Block 的类 副本源的配置存储域 复制效果
_NSConcreteStackBlock 从栈复制到堆
_NSConcreteGlobalBlock 程序的数据区域 什么也不做
_NSConcreteMallocBlock 引用计数 + 1

内存分布

图片.png

四、block 的 copy

一、自动复制

在 ARC 环境下,编译器会根据情况自动将栈上的 block 复制到堆上

1、block 作为函数返回值

typedef void (^MyBlock)(void);

MyBlock myblock() {
    return ^{
        NSLog(@"我被调用了");
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        MyBlock block =  myblock();
   
        block();

    }
    return 0;
}

会打印出结果,如果没有进行 copy

2、将 block复制给 __strong 指针时

typedef void (^MyBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        int age = 10;
        MyBlock block = ^{
            NSLog(@", %d", age);
        };
   
        block();
        NSLog(@"%@", [block class]);

    }
    return 0;
}

打印 [block class] 显示为NSMallocBlock ,证明是NSStackBlock copy 之后的类型。

3、block 作为 Cocoa API 中方法名含有 usingBlock的方法参数时

// 如 NSArray 里面的函数, 此时的 block 都是在堆上的,都是进行了 copy 的
NSArray *arr = @[];
[arr sortedArrayUsingComparator:^NSComparisonResult(id  _Nonnull obj1, id  _Nonnull obj2) {
            
}];

4、block 作为GCD API 的方法参数时

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
            
});

二、block属性的建议写法

1、MRC 环境下

  • @property (copy, nonatomic) void (^block)(void);

2、ARC 环境下

  • @property (strong, nonatomic) void (^block)(void);
  • @property (copy, nonatomic) void (^block)(void);

三、对象类型的 auto 变量

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

#import "MyPerson.h"
typedef void (^MyBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        MyBlock block;
        {
            MyPerson *person = [[MyPerson alloc] init];
            person.age = 18;
            
            block = ^{
                NSLog(@"-----person.age===%d", person.age);
            };
        }
        
        NSLog(@"-----");
    }
    return 0;
}

转化C++ 代码

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  MyPerson *person;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, MyPerson *_person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 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};
  • 如果 block 是在栈上
    • 将不会对 auto 变量产生强引用。
  • 如果 block 被拷贝到堆上
    • 会调用 block 内部的 copy 函数
    • copy 函数内部会调用 _Block_object_assign 函数
    • _Block_object_assign 函数会根据 auto 变量的修饰符(__strong、__weak、__unsafe_unretained)作出相对应的操作,形成强引用或者弱引用
  • 如果 block 从堆中移除
    • 会调用 block 内部的dispose 函数
    • dispose 函数内部会调用 _Block_object_dispose 函数
    • _Block_object_dispose 函数会自动释放引用的 auto 变量
函数 调用时机
copy 函数 栈上的 block 复制到堆时
dispose 函数 堆上的 block 被废弃时

把上面的 person 使用 __weak 修饰

__weak MyPerson *weakPerson = person;
block = ^{
    NSLog(@"-----person.age===%d", weakPerson.age);
};

使用 __weak 修饰时,在使用clang转换OC为C++代码时,可能会遇到以下问题

/var/folders/f_/7ngz5gzx5sjgs4dlqrh7t58w0000gn/T/main-643d6e.mi:28880:28: error:
      cannot create __weak reference because the current deployment target does
      not support weak references
            __attribute__((objc_ownership(weak))) MyPerson *weakPerson = person;
                           ^
1 error generated.

针对 cannot create __weak reference in file using manual reference 这个问题,解决方案:支持ARC、指定运行时系统版本,比如增加 -fobjc-arc -fobjc-runtime=ios-8.0

  • xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  MyPerson *__weak weakPerson;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, MyPerson *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

五、__block 修饰符

执行 下面的代码,显然会报错,从上面的代码中得知,block 中的 age 是block内部的,想要main 函数中的 age 是不可能的。

typedef void (^MyBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        int age = 14;
        
        MyBlock block = ^{
            age = 18;//报错 Variable is not assignable (missing __block type specifier)
            NSLog(@"%d", age);
        };
    }
    return 0;
}

如果 block 想要修改内部的变量,可以使用 static 或者 全局变量,但是,

  • 这种做法会一直占用内存的空间
  • 使用 static 修饰,会修改 block的类型,使用 static 修饰后,block 的类型变成了 ”NSGlobalBlock“,之前为”NSMallocBlock

一、使用__block 修饰,修改变量

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        __block int age = 14;
        MyBlock block = ^{
            age = 18;
            NSLog(@"%d", age);
        };
        block();
                NSLog(@"%@", [block class]);
    }
    return 0;
}

使用 __block 修饰后,block 的类型 依旧为 ”NSMallocBlock“,使用后发现 block 内部 有一个 __Block_byref_age_0 引用这 age这个指针,__Block_byref_age_0 里面有一个 age 变量,我们修改 age 其实就是修改 __Block_byref_age_0 里面的 age这个值。__forwarding 这个指针时指向自己的一个 指针。

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; // 真正修改 age 的地方
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_f__7ngz5gzx5sjgs4dlqrh7t58w0000gn_T_main_f41d15_mi_0, (age->__forwarding->age));
        }

二、__block 修饰符 原理

  • __block 可以用于解决 block 内部无法修改 auto 变量值得问题
  • __block 不能修饰全局变量、静态变量
  • 使用 ____block 修饰时,编译器会将 ____block 变量包装成一个对象。

三、___block 的内存管理

  • 当 block 在栈上时,并不对 __block 变量产生强引用
  • 当 block 在堆上时
    • 会调用 block 内部的 copy 函数
    • copy 函数内部会调用 _Block_object_assign 函数
    • _Block_object_assign 函数会对 __block 变量形成强引用
图片.png
  • 当block 从堆中移除时
    • 会调用 block 内部的dispose 函数
    • dispose 函数内部会调用 _Block_object_dispose 函数
    • _Block_object_dispose 函数会自动释放引用的 __block 变量
图片.png

四、__block 的 forwarding 指针

[图片上传失败...(image-a5af69-1608559220453)]

五、对象类型的 auto 变量、__block 变量

  • 当 block 在栈上时,对它们都不会产生强引用

  • 当 block 拷贝到堆上时,都会通过 copy 函数来处理它们

    • __block变量(假设变量名叫做a)
    • _Block_object_assign((void)&dst->a, (void)src->a, 8/BLOCK_FIELD_IS_BYREF/);
  • 对象类型的 auto 变量(假设变量名叫做p)

    • _Block_object_assign((void)&dst->p, (void)src->p, 3/BLOCK_FIELD_IS_OBJECT/);
  • 当 block 从堆上移除时,都会通过 dispose 函数来释放它们

    • __block变量(假设变量名叫做a)
    • _Block_object_dispose((void)src->a, 8/BLOCK_FIELD_IS_BYREF*/);
  • 对象类型的auto变量(假设变量名叫做p)

    • _Block_object_dispose((void)src->p, 3/BLOCK_FIELD_IS_OBJECT*/);
  • 对象 BLOCK_FIELD_IS_OBJECT
    __block 变量 BLOCK_FIELD_IS_BYREF

六、被__block 修饰的对象类型

  • 当__block变量在栈上时,不会对指向的对象产生强引用
  • 当__block变量被copy到堆时
    • 会调用__block变量内部的copy函数
    • copy函数内部会调用_Block_object_assign函数
    • _Block_object_assign函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain)
  • 如果__block变量从堆上移除
    • 会调用__block变量内部的dispose函数
    • dispose函数内部会调用_Block_object_dispose函数
    • _Block_object_dispose函数会自动释放指向的对象(release)

六、循环引用

一、什么是循环引用

图片.png
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        MyPerson *p = [[MyPerson alloc] init];
        p.age = 18;
        p.block = ^{
            NSLog(@"age is %d", p.age);
        };
        
        NSLog(@"-------");
        
    }
    return 0;
}

// MyPerson

typedef void (^MyBlock) (void);

@interface MyPerson : NSObject

@property (copy, nonatomic) MyBlock block;
@property (assign, nonatomic) int age;

@end
  

上面的代码产生了循环引用,block 里面的代码 无法执行

二、解决循环引用

一、ARC 环境

1、使用 __weak

  • 不会产生强引用,指向的对象销毁时,会自动让指针置为nil
图片.png
MyPerson *p = [[MyPerson alloc] init];
__weak typeof(p) weakP = p;
p.age = 18;
p.block = ^{
    NSLog(@"age is %d", weakP.age);
};

2、使用 __unsafe_unretained

  • 不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变
 MyPerson *p = [[MyPerson alloc] init];
__unsafe_unretained typeof(p) weakP = p;
p.age = 18;
p.block = ^{
    NSLog(@"age is %d", weakP.age);
};

3、使用 __block

  • 使用 __block 必须 调用 block,并把对象置为 nil
图片.png
__block MyPerson *p = [[MyPerson alloc] init];
        
p.age = 18;
p.block = ^{
    NSLog(@"age is %d", p.age);
        p = nil;
    };
p.block();

二、MRC 环境

由于 MRC 环境不支持 __weak,所以只有两种情况

1、__unsafe_unretained

  • 和 ARC 一样

2、__block

  • 不需要置为 nil 和 手动调用 block

相关文章

网友评论

    本文标题:iOS 开发 Block 原理分析

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