iOS底层(四)Block原理

Block的本质

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

#import 

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^block)(void) = ^{
            NSLog(@"Hello World");
        };
        block();
    }
    return 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)};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
    //定义block变量
        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    //执行block内部的代码
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 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)};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        void (*block)(void) = (
                               &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA)
                               );
        block->FuncPtr(block);
    }
    return 0;
}

分析:
定义block就是调用一个__main_block_impl_0方法,
并且传入了两个参数__main_block_func_0和__main_block_desc_0_DATA

#__main_block_func_0 封装了block内部的实现#
#__main_block_desc_0_DATA 方法保存的是block占用内存大小信息#

#__main_block_impl_0函数方法的定义#
struct __main_block_impl_0 {
  struct __block_impl impl;
  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;
  }
};


struct __block_impl {
  // block内存地址
  void *isa;
  // 固定值 0
  int Flags;
  // block 占用的内存大小
  int Reserved;
  // block内部的实现
  void *FuncPtr;
};

Block变量捕获机制

iOS底层(四)Block原理_第1张图片
image.png
#举个栗子#

#import 

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 平时定义的局部变量,前面都是有auto修饰词,只是不展示
        // auto 自动变量,离开作用域就销毁
        int age = 10;
        // static 静态变量
        static int weight = 20;
        
        void (^testBlock)(void) = ^(){
            // age 自动变量 捕获其值
            // weight static静态变量 捕获其指针
            NSLog(@"========= age: %d   weight : %d",age, weight);
        };
        
        age = 20;
        weight = 20;
        
        testBlock();
    }
    return 0;
}


输出结果:
========= age: 10   weight : 20

  • 再举个栗子:
    在对象方法中调用block,并且block中有使用当前对象,比如
@interface Person : NSObject
@property (nonatomic, copy, readwrite) NSString *name;
- (instancetype)initWithName:(NSString *)name;
- (void)test;
@end


#import "Person.h"
@implementation Person
- (instancetype)initWithName:(NSString *)name {
    if (self = [super init]) {
        self.name = name;
    }
    return self;
}

- (void)test {
    void (^block)(void) = ^{
        NSLog(@"----- %p",self);
    };
    block();
}
@end

分析:对于block是否捕捉变量,第一步是确认这个变量是局部变量还是全局变量
(1)如果全局变量,则直接访问,不捕获
(2)局部变量,自动变量则捕捉其值,静态变量则捕获其指针
那么这个例子中,“self”到底是局部变量还是全局变量了?
clang文件编译,了解test方法编译后是什么样的


image.png

在OC中,对象方法编译之后,第一个参数和第二个参数都是固定(第一个是实例对象自己self,第二是当前的方法指针_cmd),比如上图中的self和_cmd,而方法的参数是局部变量,所以block中调用的"self"是一个局部变量,下图是block编译后的结构体,从中可以看到block捕获到的一个对象指针

iOS底层(四)Block原理_第2张图片
image.png

Block类型

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

  • NSGlobalBlock:没有访问auto变量
  • NSStackBlock:访问了auto变量
  • NSMallocBlockNSStackBlock调用了copy

iOS底层(四)Block原理_第3张图片
block内存分配图

(备注:
程序区域(.text): 存放的是代码段
数据区域(.data): 存放的是全局变量
: 动态分配内存,需要程序员申请内存,也需要程序员自己管理内存(比如通过alloc分配的内存)
: 存放局部变量,系统会自动分配和管理内存

iOS底层(四)Block原理_第4张图片
image.png

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

  • block作为函数返回值时
  • 将block赋值给__strong指针时(所以ARC环境下,使用copy或者strong修饰都是一样的效果)
  • block作为Cocoa API中方法名含有UsingBlock的方法参数时
  • block作为GCD API的方法参数时

对象类型的auto变量

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

* 如果block是在栈上,将不会对auto变量产生强应用
* 如果block被拷贝到堆上

a:会调用block内部的copy函数
b:copy函数内部会调用_Block_object_assign函数
c:_Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,类似于retain(形成强引用、弱引用)(注意:这里仅限于ARC时会retain,MRC时不会retain)

* 如果block从堆上移除

a:会调用block内部的dispose函数
b:dispose函数内部会调用_Block_object_dispose函数
c:_Block_object_dispose函数会自动释放引用的auto变量,类似于release

iOS底层(四)Block原理_第5张图片
copy、dispose

__block修饰符

  • __block 可以用于解决block内部无法修改auto变量值的问题
  • __block 不能修饰全局变量、静态变量(static)
  • 编译器会将__block变量包装成一个对象
    iOS底层(四)Block原理_第6张图片
    image.png

block循环引用

解决循环引用问题

(1)ARC环境下
  • 用__weak、__unsafe_unretained解决
    __weak(首选):不会产生强引用(指向的对象销毁时,会自动让指针置为nil)
    __unsafe_unretained:不会产生强引用,不安全(指向的对象销毁时,指针存储的地址不变)
    iOS底层(四)Block原理_第7张图片
    image.png
  • 用__block解决(必须调用block)


    iOS底层(四)Block原理_第8张图片
    image.png
(2)MRC环境下
  • 用__unsafe_unretained 解决


    iOS底层(四)Block原理_第9张图片
  • 用__block解决


    iOS底层(四)Block原理_第10张图片
    image.png

面试题

(1)block的原理是怎么样的?本质是什么?

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

(2)__block的作用是什么?有什么使用注意点?

      将__block变量包装成对象
      注意点:当block在栈上,将不会对__block变量产生强引用
             当block被copy到堆时,会对__block变量形成强引用(只有在ARC环境下会强引用,MRC不会)

(3)block的属性修饰词为什么是copy?使用block有哪些使用注意?

      block一旦没有进行copy操作,内存就是在栈上,而不是在堆上(在堆上,生命周期由程序员控制)
      使用注意:循环引用的问题

(4)block在修改NSMutableArray,需不需要添加__block?

      不需要。修改内容只是对数组的使用,只有对对象赋值的时候才需要__block

你可能感兴趣的:(iOS底层(四)Block原理)