iOSiOS Kit

ios底层原理-代码块(block)的本质(二)

2019-05-30  本文已影响17人  Yasuo_4a5f

问题

1.什么是block,block的本质是什么?
2.block的属性修饰词为什么是copy?使用block有哪些使用注意?
3.block为什么会发生循环引用?
4.block的变量捕获究竟是怎样进行的?
5.block在修改NSMutableArray,需不需要添加__block?
6.__block和__weak的作用是什么?有什么使用注意点?
7.block中访问的对象和变量什么时候会销毁?

上文ios底层原理-代码块(block)的本质(一)中已经解决了前三个问题,本文继续探寻block的本质,补充上面三个问题,以及解答下面的问题。

block对变量的捕获

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

首先我们先了解一下有些什么变量

  • auto 离开作用域就销毁,局部变量前面自动添加auto关键字。
  • static 修饰的变量为指针传递,同样会被block捕获。
  • 全局变量 跨函数访问所以需要捕获,全局变量在哪里都可以访问 ,所以不用捕获。
block的变量捕获

我们来验证上图是否正确
先新建一个Student类

@interface Student : NSObject
@property(nonatomic,assign)NSInteger age;
@end

@implementation Student
- (void)dealloc
{
    NSLog(@"%@ 对象销毁了",self);
}
@end

写下如下代码 :

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        auto int a = 1;
        static int b = 2;
        Student * studentA = [[Student alloc] init];
        void(^block)(void) = ^{
            NSLog(@"number, a = %d, b = %d", a,b);
            NSLog(@"student : %ld",(long)studentA.age);
        };
        a = 11;
        b = 22;
        block();
    }
    return 0;
}

先转化为c++代码

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  int *b;
  Student *__strong studentA;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int *_b, Student *__strong _studentA, int flags=0) : a(_a), b(_b), studentA(_studentA) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy
  int *b = __cself->b; // bound by copy
  Student *__strong studentA = __cself->studentA; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_1a94a7_mi_0, a,(*b));
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_1a94a7_mi_1,(long)((NSInteger (*)(id, SEL))(void *)objc_msgSend)((id)studentA, sel_registerName("age")));
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->studentA, (void*)src->studentA, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->studentA, 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; 
        auto int a = 1;
        static int b = 2;
        Student * studentA = ((Student *(*)(id, SEL))(void *)objc_msgSend)((id)((Student *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Student"), sel_registerName("alloc")), sel_registerName("init"));
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, &b, studentA, 570425344));
        a = 11;
        b = 22;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

直接找到__main_block_impl_0函数中的a,&b,studentA,而__main_block_impl_0结构体中直接定义了一个新的a把传入的a的值赋给了__main_block_impl_0结构体中a变量,把&b的地址传给了__main_block_impl_0结构体中b指针变量,把studentA指针变量赋值给了__main_block_impl_0结构体中的studentA指针变量。
其中studentA也是auto变量。此时此刻可以看到__main_block_impl_0中的Student *__strong studentA是__strong类型的,所以block强引用studentA对象。

那么为什么auto 和 static会有这样的差异呢,auto变量在栈中作用域结束就会被销毁,block在执行的时候有可能自动变量已经被销毁了,因此对于自动变量一定是值传递而不可能是指针传递了,而因为传递的是值得地址,所以在block调用之前修改地址中保存的值,block中的地址是不会变得。所以值会随之改变。归根结底还是为了防止野指针的出现和节约内存开销。

全局变量

typedef void (^Block)(void);
int a = 1;
static int b = 2;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^block)(void) = ^{
            NSLog(@"number, a = %d, b = %d", a,b);
        };
        a = 11;
        b = 22;
        block();
    }
    return 0;
}

转成c++

typedef void (*Block)(void);
int a = 1;
static int b = 2;

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_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_883251_mi_0, a,b);
        }

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) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        a = 11;
        b = 22;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

此时此刻,__main_block_imp_0并没有添加任何变量,直接调用全局定义的a,b。

block里访问self是否会捕获?

写如下代码

@interface Student : NSObject
@property(nonatomic,assign)NSInteger age;
@end

@interface  Student()
{
    NSInteger sId;
}
@end
@implementation Student
- (void)catchSelf
{
    void(^block)(void) = ^{
        NSLog(@"%@",self);
        NSLog(@"%ld",(long)self.age);
        NSLog(@"%ld",(long)self->_age);
        NSLog(@"%ld",self->sId);
    };
    block();
}
- (void)dealloc
{
    NSLog(@"%@ 对象销毁了",self);
}
+ (void)staticFunc
{
    NSLog(@"类方法");
}
@end

转化为c++

struct __Student__catchSelf_block_impl_0 {
  struct __block_impl impl;
  struct __Student__catchSelf_block_desc_0* Desc;
  Student *self;
  __Student__catchSelf_block_impl_0(void *fp, struct __Student__catchSelf_block_desc_0 *desc, Student *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __Student__catchSelf_block_func_0(struct __Student__catchSelf_block_impl_0 *__cself) {
  Student *self = __cself->self; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_Student_bb73ff_mi_0,self);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_Student_bb73ff_mi_1,(long)((NSInteger (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("age")));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_Student_bb73ff_mi_2,(long)(*(NSInteger *)((char *)self + OBJC_IVAR_$_Student$_age)));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_Student_bb73ff_mi_3,(*(NSInteger *)((char *)self + OBJC_IVAR_$_Student$sId)));
    }
static void _I_Student_catchSelf(Student * self, SEL _cmd) {
    void(*block)(void) = ((void (*)())&__Student__catchSelf_block_impl_0((void *)__Student__catchSelf_block_func_0, &__Student__catchSelf_block_desc_0_DATA, self, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

找到_I_Student_catchSelf函数也就是我们定义的catchSelf函数其中默认传了两个参数Student * self, SEL _cmd

static void _C_Student_staticFunc(Class self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_Student_bb73ff_mi_5);
}

可以发现我们定义的类方法也是默认传了这两个参数
此时此刻看到我们的catchSelf方法

- (void)catchSelf
{
    void(^block)(void) = ^{
        NSLog(@"%@",self);
        NSLog(@"%ld",(long)self.age);
        NSLog(@"%ld",(long)self->_age);
        NSLog(@"%ld",self->sId);
    };
    block();
}

在block中我使用了sId成员变量self->_age实例变量以及age的get方法,但在__Student__catchSelf_block_impl_0中只是捕获了self(指向调用者)

注意

block中访问对象和变量的销毁

直接导入我们上边写过的Student类 写如下代码

typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block;
        {
            NSLog(@"开始");
            auto int a = 5;
            Student *student = [[Student alloc] init];
            student.age = 100;

            block = ^{
                NSLog(@"------block内%ld",(long)student.age);
            };
        } // 执行完毕,student没有被释放,a释放
        NSLog(@"结束");
    } // student 释放
    return 0;
}
打印内容

大括号执行完毕之后,student对象依然不会被释放,变量a处于作用域之外已经被释放了,上述block为堆block,block里面有一个student指针student指针指向student对象。只要block还在,student就还在。block强引用了student对象
@autoreleasepool之后block被销毁student 对象被销毁。

查看源代码,也发现block确实如上面说的一样强引用student

__weak作用

对student对象进行__weak操作

typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block;
        {
            NSLog(@"开始");
            auto int a = 5;
            Student *student = [[Student alloc] init];
            student.age = 100;
            __weak Student *weakStudent = student;
            block = ^{
                NSLog(@"------block内%ld",(long)weakStudent.age);
            };
        } // 执行完毕,student被释放,a被释放
        NSLog(@"结束");
    }
    return 0;
}
打印内容

可以发现__weak添加之后,student在作用域执行完毕之后就被销毁了。这是为什么呢?让我们把代码转化为c++,看看究竟做了些什么操作。
__weak修饰变量,需要告知编译器使用ARC环境及版本号否则会报错。
crun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o main-arm64-weak.cpp

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Student *__weak weakStudent;//用__weak修饰
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Student *__weak _weakStudent, int flags=0) : weakStudent(_weakStudent) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看到在__main_block_impl_0中依然是用的__weak修饰weakStudent

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结构体__main_block_impl_0的描述结构体__main_block_desc_0中多了两个参数copydispose函数

__main_block_copy_0 和 __main_block_dispose_0

首先我们已经说过__main_block_desc_0是block的描述,储存着block的字节大小。那两个参数又有什么用呢?
我们就点进__main_block_copy_0这个函数。

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

__main_block_copy_0函数中传入的都是__main_block_impl_0结构体本身。

copy本质就是__main_block_copy_0函数,__main_block_copy_0函数内部调用_Block_object_assign函数,_Block_object_assign中传入的是student对象的地址,student对象,以及3。

static void __main_block_dispose_0(struct __main_block_impl_0*src)
{
    _Block_object_dispose((void*)src->weakStudent, 3/*BLOCK_FIELD_IS_OBJECT*/);
    
}

__main_block_dispose_0函数中传入的都是__main_block_impl_0结构体本身。

dispose本质就是__main_block_dispose_0函数,__main_block_dispose_0函数内部调用_Block_object_dispose函数,_Block_object_dispose函数传入的参数是student对象,以及3。

__main_block_copy_0__main_block_dispose_0函数中会根据变量是强弱指针及有没有被__block修饰做出不同的处理,强指针在block内部产生强引用,弱指针在block内部产生弱引用。被__block修饰的变量最后的参数传入的是8,没有被__block修饰的变量最后的参数传入的是3。

_Block_object_assign函数调用时机及作用

上文已经说过ARC会在赋值给__strong指针时自动进行copy,而此时我们就需要对student对象的引用计数做修改,以确保这个对象在block存在的时候不会被销毁。
当block进行copy操作的时候就会自动调用__main_block_desc_0内部的__main_block_copy_0函数,__main_block_copy_0函数内部会调用_Block_object_assign函数。
_Block_object_assign函数会自动根据__main_block_impl_0结构体内部的student是什么类型的指针,对student对象产生强引用或者弱引用。可以理解为_Block_object_assign函数内部会对student进行引用计数器的操作,如果__main_block_impl_0结构体内student指针是__strong类型,则为强引用,引用计数+1,如果__main_block_impl_0结构体内student指针是__weak类型,则为弱引用,引用计数不变。而__main_block_impl_0结构体内部的student是什么类型的指针是由外面我们定义的对象的具体类型决定。

_Block_object_dispose函数调用时机及作用

当block从堆中移除时就会自动调用__main_block_desc_0中的__main_block_dispose_0函数,__main_block_dispose_0函数内部会调用_Block_object_dispose函数。
_Block_object_dispose会对student对象做释放操作,类似于release,也就是断开对student对象的引用,而student究竟是否被释放还是取决于student对象自己的引用计数。

总结
  1. 如果block是在栈上,将不会对auto变量产生强引用
  2. 栈上的block随时会被销毁,也没必要去强引用其他对象

如果block被拷贝到堆上:

  1. 会调用block内部的copy函数
  2. copy函数内部会调用_Block_object_assign函数
  3. _Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用

如果block从堆上移除

  1. 会调用block内部的dispose函数
  2. dispose函数内部会调用_Block_object_dispose函数
  3. _Block_object_dispose函数会自动释放引用的auto变量(release)
如果block在栈空间,不管外部变量是强引用还是弱引用,block都会弱引用访问对象
如果block在堆空间,如果外部强引用,block内部也是强引用;如果外部弱引用,block内部也是弱引用

__block作用

__forwarding指针指向

先来__block修饰auto变量

typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block;
        {
            __block auto int a = 5;
            block = ^{
                NSLog(@"------block内%ld",(long)a);
            };
        }
    }
    return 0;
}

转成c++代码

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        Block block;
        {
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_b48be2_mi_0);
        __attribute__((__blocks__(byref))) auto __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 5};
        block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
        }
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_b48be2_mi_2);
    }
    return 0;
}

可以发现__main_block_impl_0函数中传的不再是变量a而是__Block_byref_a_0类型的变量a的地址&a

查看__Block_byref_a_0结构体

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

此时此刻 已经很清晰了变量a包装成了__Block_byref_a_0 结构体,__forwarding指向的就是这个就是这个结构体自己的地址,__Block_byref_a_0 结构体中a变量才是存储值的地方

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_b48be2_mi_1,(long)(a->__forwarding->a));
        }

调用block时,先取出__main_block_impl_0中的a,通过a结构体拿到__forwarding指针,上面提到过__forwarding中保存的就是__Block_byref_a_0结构体本身,这里也就是a(__Block_byref_a_0),在通过__forwarding拿到结构体中的a(5)变量并修改其值。

__forwarding指针的作用

__forwarding指针指向的是结构体自己。当使用变量的时候,通过结构体找到__forwarding指针,在通过__forwarding指针找到相应的变量。这样设计的目的是为了方便内存管理。block被复制到堆上时,会将block中引用的变量也复制到堆中。

__forwarding指针
此时此刻__forwarding函数的操作为了方便堆栈的切换,内存的管理。

__block修饰对象变量
先写如下代码

typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block;
        {
            __block Student *student = [[Student alloc] init];
            student.age = 100;
            block = ^{
                NSLog(@"------block内%ld",(long)student.age);
            };
        }
    }
    return 0;
}

查看源码

typedef void (*Block)(void);
struct __Block_byref_student_0 {
  void *__isa;
__Block_byref_student_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 Student *__strong student;
};

我们对比上面的__Block_byref_a_0结构体 ,__Block_byref_student_0中添加了内存管理的两个函数__Block_byref_id_object_copy__Block_byref_id_object_dispose

上文提到当block中捕获对象类型的变量时,block中的__main_block_desc_0结构体内部会自动添加copydispose函数对捕获的变量进行内存管理。
那么同样的当block内部捕获__block修饰的对象类型的变量时,__Block_byref_person_0结构体内部也会自动添加__Block_byref_id_object_copy__Block_byref_id_object_dispose对被__block包装成结构体的对象进行内存管理。

block内存在栈上时,并不会对__block变量产生内存管理。当blcokcopy到堆上时
会调用block内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会对__block变量形成强引用(相当于retain)

__block copy内存管理

当block从堆中移除的话,就会调用dispose函数,也就是__main_block_dispose_0函数,__main_block_dispose_0函数内部会调用_Block_object_dispose函数,会自动释放引用的__block变量。

__block 释放内存管理

block内部决定什么时候将变量复制到堆中,什么时候对变量做引用计数的操作。

__block修饰的变量在block结构体中一直都是强引用,而其他类型的是由传入的对象指针类型决定。

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

typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSMutableArray *array = [NSMutableArray array];
        Block block = ^{
            [array addObject: @"c++"];
            [array addObject: @"c#"];
            NSLog(@"%@",array);
        };
        block();
    }
    return 0;
}

command + r

打印内容

在block块中仅仅是使用了array的内存地址,往内存地址中添加内容,并没有修改arry的内存地址,因此array不需要使用__block修饰也可以正确编译。同理add 和remove方法都不需要__block,同理 NSMutableDictionary,NSMutableSet,NSMutableStringNSMutable.....都是同一个道理。

归根结底,了解这么多并不只是为了知道怎么防止循环引用,学到的应该是学习进阶的方式,还有设计模式和逻辑结构。

到此block底层篇已经结束了,如有补充或交流请加微信


微信二维码
上一篇下一篇

猜你喜欢

热点阅读