iOS 底层原理

iOS 开发 Block 原理分析

2020-12-21  本文已影响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 内部实现

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.

五、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 是地址传递,指针传递

六、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 的内存结构

图片.png 图片.png

二、block 本质

二、block 的变量捕获

为了保证 block 内部能正常访问外部的变量,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 环境下

2、ARC 环境下

三、对象类型的 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};
函数 调用时机
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

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 或者 全局变量,但是,

一、使用__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 的内存管理

图片.png 图片.png

四、__block 的 forwarding 指针

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

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

六、被__block 修饰的对象类型

六、循环引用

一、什么是循环引用

图片.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

图片.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

图片.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

2、__block

上一篇 下一篇

猜你喜欢

热点阅读