block的本质

block的本质

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

1.png

block其实也是NSObject的子类

block的类型

一共有三种类型的block分别是:全局的,栈上的b,堆上的

__NSGlobalBlock__ ( _NSConcreteGlobalBlock )
__NSStackBlock__ ( _NSConcreteStackBlock )
__NSMallocBlock__ ( _NSConcreteMallocBlock )

通过代码查看一下block在什么情况下其类型会各不相同


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 1. 内部没有调用外部变量的block
        void (^block1)(void) = ^{
            NSLog(@"Hello");
        };
        // 2. 内部调用外部变量的block
        int a = 10;
        void (^block2)(void) = ^{
            NSLog(@"Hello - %d",a);
        };
       // 3. 直接调用的block的class
        NSLog(@"%@ %@ %@", [block1 class], [block2 class], [^{
            NSLog(@"%d",a);
        } class]);
    }
    return 0;
}


打印结果为:

2018-08-29 11:39:27.969734+0800 block的本质[40624:68783097] __NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__

__NSGlobalBlock__直到程序结束才会被回收
__NSStackBlock__类型的block存放在栈中,我们知道栈中的内存由系统自动分配和释放,作用域执行完毕之后就会被立即释放
__NSMallocBlock__是在平时编码过程中最常使用到的。存放在堆中需要我们自己进行内存管理。

block是如何定义其类型

2.png

block的实现

写一个简单的block

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

        int a = 5;
        void(^blk)(void) = ^{
            NSLog(@"%d",a);
        };

        blk();
    }
    return 0;
}

使用命令行将代码转化为c++查看其内部结构,与OC代码进行比较
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    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

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_sr_m_cfkwyx2h56vh4_kf65_vw40000gn_T_main_55e532_mi_0,a);
        }

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;

        int a = 5;
        void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));

        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    }
    return 0;
}

首先我们看一下__block_impl第一个成员就是__block_impl结构体

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

我们可以发现__block_impl结构体内部就有一个isa指针。因此可以证明block本质上就是一个oc对象

接着通过上面对__main_block_impl_0结构体构造函数三个参数的分析我们可以得出结论:

  1. __block_impl结构体中isa指针存储着&_NSConcreteStackBlock地址,可以暂时理解为其类对象地址,block就是_NSConcreteStackBlock类型的。
  2. block代码块中的代码被封装成__main_block_func_0函数,FuncPtr则存储着__main_block_func_0函数的地址。
  3. Desc指向__main_block_desc_0结构体对象,其中存储__main_block_impl_0结构体所占用的内存。

block的变量捕获

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

局部变量

  • auto变量 - 值传递
  • static变量 - 指针传递

全局变量

  • 直接访问

block对对象变量的捕获

void(^blk)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        {
        Person *person = [[Person alloc] init];
        person.age = 10;
        blk = ^{
             NSLog(@"------block内部%ld",person.age);
        };
        }

        NSLog(@"-----");

    }
    return 0;
}

大括号执行完毕之后,person依然不会被释放。person为aotu变量,即block有一个强引用引用person,所以block不被销毁的话,peroson也不会销毁。

查看源代码确实如此

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

__weak

__weak添加之后,person在作用域执行完毕之后就被销毁了

void(^blk)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        {
        Person *person = [[Person alloc] init];
        person.age = 10;

        __weak Person *weakPerson = person;
        blk = ^{
             NSLog(@"------block内部%ld",weakPerson.age);
        };
        }

        NSLog(@"-----");

    }
    return 0;
}

__weak修饰变量,需要告知编译器使用ARC环境及版本号否则会报错,添加说明:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

__weak修饰的变量,在生成的__main_block_impl_0中也是使用__weak修饰

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__weak weakPerson;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
__main_block_copy_0 和 __main_block_dispose_0

当block中捕获对象类型的变量时,我们发现block结构体__main_block_impl_0的描述结构体__main_block_desc_0中多了两个参数copy和dispose函数,查看源码:

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

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->weakPerson, 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本质就是__main_block_copy_0函数,__main_block_copy_0函数内部调用_Block_object_assign函数
    当block进行copy操作的时候就会自动调用__main_block_desc_0内部的__main_block_copy_0函数
    __main_block_copy_0函数内部会调用_Block_object_assign函数。

_Block_object_assign函数会自动根据__main_block_impl_0结构体内部的person是什么类型的指针,对person对象产生强引用或者弱引用。可以理解为_Block_object_assign函数内部会对person进行引用计数器的操作,如果__main_block_impl_0结构体内person指针是__strong类型,则为强引用,引用计数+1,如果__main_block_impl_0结构体内person指针是__weak类型,则为弱引用,引用计数不变。

  • 当block从堆中移除时就会自动调用__main_block_desc_0中的__main_block_dispose_0函数,__main_block_dispose_0函数内部会调用_Block_object_dispose函数。

_Block_object_dispose会对person对象做释放操作,类似于release,也就是断开对person对象的引用,而person究竟是否被释放还是取决于person对象自己的引用计数。

总结

  1. 一旦block中捕获的变量为对象类型,block结构体中的__main_block_desc_0会出两个参数copy和dispose。因为访问的是个对象,block希望拥有这个对象,就需要对对象进行引用,也就是进行内存管理的操作。比如说对对象进行retarn操作,因此一旦block捕获的变量是对象类型就会会自动生成copy和dispose来对内部引用的对象进行内存管理。

  2. 当block内部访问了对象类型的auto变量时,如果block是在栈上,block内部不会对person产生强引用。不论block结构体内部的变量是__strong修饰还是__weak修饰,都不会对变量产生强引用。

  3. 如果block被拷贝到堆上。copy函数会调用_Block_object_assign函数,根据auto变量的修饰符(__strong,__weak,unsafe_unretained)做出相应的操作,形成强引用或者弱引用

  4. 如果block从堆中移除,dispose函数会调用_Block_object_dispose函数,自动释放引用的auto变量。

block内修改变量的值

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

        int a = 5;
        void(^blk)(void) = ^{
            a = 10;
        };

    }
    return 0;
}

block不能修改外部的局部变量

age是在main函数内部声明的,说明age的内存存在于main函数的栈空间内部,但是block内部的代码在__main_block_func_0函数内部。__main_block_func_0函数内部无法访问age变量的内存空间,两个函数的栈空间不一样,__main_block_func_0内部拿到的age是block结构体内部的age,因此无法在__main_block_func_0函数内部去修改main函数内部的变量。

方式一:age使用static修饰。

static修饰的age变量传递到block内部的是指针,在__main_block_func_0函数内部就可以拿到age变量的内存地址,因此就可以在block内部修改age的值。

方式二:__block

__block用于解决block内部不能修改auto变量值的问题,__block不能修饰静态变量(static) 和全局变量

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

        __block int a = 5;
        void(^blk)(void) = ^{
            a = 10;
        };

    }
    return 0;
}

查看源码:


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

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__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_a_0 *a = __cself->a; // bound by ref

            (a->__forwarding->a) = 10;
}

首先被__block修饰的a变量声明变为名为age的__Block_byref_a_0结构体,也就是说加上__block修饰的话捕获到的block内的变量为__Block_byref_a_0类型的结构体。

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


__isa指针 :__Block_byref_age_0中也有isa指针也就是说__Block_byref_age_0本质也一个对象。

__forwarding :__forwarding是__Block_byref_age_0结构体类型的,并且__forwarding存储的值为(__Block_byref_age_0 *)&age,即结构体自己的内存地址。

__flags :0

__size :sizeof(__Block_byref_age_0)即__Block_byref_age_0所占用的内存空间。

a :真正存储变量的地方,这里存储局部变量10。


__block将变量包装成对象,然后在把age封装在结构体里面,block内部存储的变量为结构体指针,也就可以通过指针找到内存地址进而修改变量的值。

循环引用

情景一:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        person.age = 10;
        person.block = ^{
            NSLog(@"%d",person.age);
        };
    }
    NSLog(@"大括号结束啦");
    return 0;
}

可以发现大括号结束之后,person依然没有被释放,产生了循环引用

通过一张图看一下他们之间的内存结构

3.png

上图中可以发现,Person对象和block对象相互之间产生了强引用,导致双方都不会被释放,进而造成内存泄漏

情景二:

#import "Person.h"

@implementation Person


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

这是开发中经常遇到的一个场景。之前我们说过block会捕获局部,上面的OC函数调用转化为runtime代码为
objc_msgSend(self,@selector(init)) 在OC的方法中 有2个隐藏参数 self和_cmd 这2个参数作为函数的形参
在方法作用域中属于局部变量 , 所以在block中使用self就满足之前提到的 block会捕获局部变量,查看源码为:

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

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_sr_m_cfkwyx2h56vh4_kf65_vw40000gn_T_Person_3f840b_mi_0,((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")));
}

这里可以看到 __Person__init_block_impl_0结构体中 创建了一个Person *self的强指针 指向init方法中self
指针所指向的person对象,使person引用计数+1 而person对block也有一个强引用。这里就造成了循环引用。

  • 解决循环引用问题

    首先为了能随时执行block,我们肯定希望person对block对强引用,而block内部对person的引用为弱引用最好。
    使用__weak__unsafe_unretained修饰符可以解决循环引用的问题

    • __weak__unsafe_unretained的区别。

      __weak不会产生强引用,指向的对象销毁时,会自动将指针置为nil。因此一般通过__weak来解决问题。

      __unsafe_unretained不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变。会造成野指针问题

情景三

在block中调用super也会造成循环引用 :

#import "Person.h"

@implementation Person


- (instancetype)init
{
    self = [super init];
    if (self) {
        self.block = ^{
            [super init];
        };
    }
    return self;
}

查看源码为:

static void __Person__init_block_func_0(struct __Person__init_block_impl_0 *__cself) {

            ((Person *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Person"))}, sel_registerName("init"));
        }

当使用[self class]时,会调用objc_msgSend函数,第一个参数receiver就是self,而第二个参数,要先找到self所在的这个class的方法列表

当使用[super class]时,会调用objc_msgSendSuper函数,此时会先构造一个__rw_objc_super的结构体作为objc_msgSendSuper的第一个参数。 该结构体第一个成员变量receiver仍然是self,而第二个成员变量super_class即是所在类的父类

struct __rw_objc_super {
    struct objc_object *object;
    struct objc_object *superClass;
    __rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {}
};

runtime对外暴露的类型为:

struct objc_super {
    __unsafe_unretained _Nonnull id receiver;

    __unsafe_unretained _Nonnull Class super_class;
};

结构体第一个成员receiver 就代表方法的接受者  第二个成员代表方法接受者的父类

所以

self.block = ^{
    [super init];
};

转换为源码是:


self.block = ^{
        struct objc_super  superInfo = {
            .receiver = self,
            .super_class = class_getSuperclass(objc_getClass("Person")),
        };

        ((Class(*)(struct objc_super *, SEL))objc_msgSendSuper)(&superInfo,@selector(init));
    };

可以很明显的看到问题,block强引用了self,而self也强持有了这个block

  • 解决方法

    正确的调用姿势跟平常我们切断block的循环引用的姿势一模一样:

__weak __typeof(self) weakSelf = self;
self.block = ^{
    struct objc_super superInfo = {
            .receiver = weakSelf,
            .super_class = class_getSuperclass(NSClassFromString(@"ViewController")),
        };
        ((Class(*)(struct objc_super *, SEL))objc_msgSendSuper)(&superInfo,@selector(class));
};
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容