iOS底层原理总结 - 探寻block的本质(二)

上一篇文章iOS底层原理总结 - 探寻block的本质(一)中已经介绍过block的底层本质实现以及了解了变量的捕获,本文继续探寻block的本质。php

block对对象变量的捕获

block通常使用过程当中都是对对象变量的捕获,那么对象变量的捕获同基本数据类型变量相同吗?
查看一下代码思考:当在block中访问的为对象类型时,对象何时会销毁?css

typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block;
        {
            Person *person = [[Person alloc] init];
            person.age = 10;
            
            block = ^{
                NSLog(@"------block内部%d",person.age);
            };
        } // 执行完毕,person没有被释放
        NSLog(@"--------");
    } // person 释放
    return 0; 
}
复制代码

大括号执行完毕以后,person依然不会被释放。上一篇文章提到过,personaotu变量,传入的block的变量一样为person,即block有一个强引用引用person,因此block不被销毁的话,peroson也不会销毁。
查看源代码确实如此java

强指针引用

将上述代码转移到MRC环境下,在MRC环境下即便block还在,person却被释放掉了。由于MRC环境下block在栈空间,栈空间对外面的person不会进行强引用。ios

//MRC环境下代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block;
        {
            Person *person = [[Person alloc] init];
            person.age = 10;
            block = ^{
                NSLog(@"------block内部%d",person.age);
            };
            [person release];
        } // person被释放
        NSLog(@"--------");
    }
    return 0;
}
复制代码

block调用copy操做以后,person不会被释放。c++

block = [^{
   NSLog(@"------block内部%d",person.age);
} copy];
复制代码

上文中也提到过,只须要对栈空间的block进行一次copy操做,将栈空间的block拷贝到堆中,person就不会被释放,说明堆空间的block可能会对person进行一次retain操做,以保证person不会被销毁。堆空间的block本身销毁以后也会对持有的对象进行release操做。面试

也就是说栈空间上的block不会对对象强引用,堆空间的block有能力持有外部调用的对象,即对对象进行强引用或去除强引用的操做。安全

__weak

__weak添加以后,person在做用域执行完毕以后就被销毁了。微信

typedef void (^Block)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block;
        {
            Person *person = [[Person alloc] init];
            person.age = 10;
            
            __weak Person *waekPerson = person;
            block = ^{
                NSLog(@"------block内部%d",waekPerson.age);
            };
        }
        NSLog(@"--------");
    }
    return 0;
}
复制代码

将代码转化为c++来看一下上述代码之间的差异。
__weak修饰变量,须要告知编译器使用ARC环境及版本号不然会报错,添加说明-fobjc-arc -fobjc-runtime=ios-8.0.0app

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

__weak修饰变量

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

__main_block_copy_0 和 __main_block_dispose_0

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

__main_block_copy_0、__main_block_dispose_0函数

copydispose函数中传入的都是__main_block_impl_0结构体自己。

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

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

_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_object_dispose函数调用时机及做用

当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会出两个参数copydispose。由于访问的是个对象,block但愿拥有这个对象,就须要对对象进行引用,也就是进行内存管理的操做。好比说对对象进行retarn操做,所以一旦block捕获的变量是对象类型就会会自动生成copydispose来对内部引用的对象进行内存管理。

  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变量。

问题

1. 下列代码person在什么时候销毁 ?

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    Person *person = [[Person alloc] init];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"%@",person);
    });
    NSLog(@"touchBegin----------End");
}
复制代码

打印内容


打印内容

答:上文提到过ARC环境中,block做为GCD API的方法参数时会自动进行copy操做,所以block在堆空间,而且使用强引用访问person对象,所以block内部copy函数会对person进行强引用。当block执行完毕须要被销毁时,调用dispose函数释放对person对象的引用,person没有强指针指向时才会被销毁。

2. 下列代码person在什么时候销毁 ?

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    Person *person = [[Person alloc] init];
    
    __weak Person *waekP = person;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"%@",waekP);
    });
    NSLog(@"touchBegin----------End");
}
复制代码

打印内容

打印内容

答:block中对waekP__weak弱引用,所以block内部copy函数会对person一样进行弱引用,当大括号执行完毕时,person对象没有强指针引用就会被释放。所以block块执行的时候打印null

3. 经过示例代码进行总结。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    Person *person = [[Person alloc] init];
    
    __weak Person *waekP = person;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        NSLog(@"weakP ----- %@",waekP);
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"person ----- %@",person);
        });
    });
    NSLog(@"touchBegin----------End");
}
复制代码

打印内容

打印内容
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    Person *person = [[Person alloc] init];
    
    __weak Person *waekP = person;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        NSLog(@"person ----- %@",person);
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"weakP ----- %@",waekP);
        });
    });
    NSLog(@"touchBegin----------End");
}
复制代码

打印内容

打印内容

block内修改变量的值

本部分分析基于下面代码。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        Block block = ^ {
            // age = 20; // 没法修改
            NSLog(@"%d",age);
        };
        block();
    }
    return 0;
}

复制代码

默认状况下block不能修改外部的局部变量。经过以前对源码的分析能够知道。

age是在main函数内部声明的,说明age的内存存在于main函数的栈空间内部,可是block内部的代码在__main_block_func_0函数内部。__main_block_func_0函数内部没法访问age变量的内存空间,两个函数的栈空间不同,__main_block_func_0内部拿到的ageblock结构体内部的age,所以没法在__main_block_func_0函数内部去修改main函数内部的变量。

方式一:age使用static修饰。

前文提到过static修饰的age变量传递到block内部的是指针,在__main_block_func_0函数内部就能够拿到age变量的内存地址,所以就能够在block内部修改age的值。

方式二:__block

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

__block int age = 10;
复制代码

编译器会将__block修饰的变量包装成一个对象,查看其底层c++源码。

__block修饰的变量源码

上述源码中能够发现

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

经过下图查看__Block_byref_age_0结构体内存储哪些元素。

__Block_byref_age_0赋值

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

__forwarding__forwarding__Block_byref_age_0结构体类型的,而且__forwarding存储的值为(__Block_byref_age_0 *)&age,即结构体本身的内存地址。

__flags :0

__sizesizeof(__Block_byref_age_0)__Block_byref_age_0所占用的内存空间。

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

接着将__Block_byref_age_0结构体age存入__main_block_impl_0结构体中,并赋值给__Block_byref_age_0 *age;

__Block_byref_age_0 *age赋值

以后调用block,首先取出__main_block_impl_0中的age,经过age结构体拿到__forwarding指针,上面提到过__forwarding中保存的就是__Block_byref_age_0结构体自己,这里也就是age(__Block_byref_age_0),在经过__forwarding拿到结构体中的age(10)变量并修改其值。

后续NSLog中使用age时也经过一样的方式获取age的值。

修改结构体内的age值

为何要经过__forwarding获取age变量的值?

__forwarding是指向本身的指针。这样的作法是为了方便内存管理,以后内存管理章节会详细解释。

到此为止,__block为何能修改变量的值已经很清晰了。__block将变量包装成对象,而后在把age封装在结构体里面,block内部存储的变量为结构体指针,也就能够经过指针找到内存地址进而修改变量的值。

__block修饰对象类型

那么若是变量自己就是对象类型呢?经过如下代码生成c++源码查看

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block Person *person = [[Person alloc] init];
        NSLog(@"%@",person);
        Block block = ^{
            person = [[Person alloc] init];
            NSLog(@"%@",person);
        };
        block();
    }
    return 0;
}
复制代码

经过源码查看,将对象包装在一个新的结构体中。结构体内部会有一个person对象,不同的地方是结构体内部添加了内存管理的两个函数__Block_byref_id_object_copy__Block_byref_id_object_dispose

__block修饰对象类型源码

__Block_byref_id_object_copy__Block_byref_id_object_dispose函数的调用时机及做用在__block内存管理部分详细分析。

问题

1. 如下代码是否能够正确执行

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSMutableArray *array = [NSMutableArray array];
        Block block = ^{
            [array addObject: @"5"];
            [array addObject: @"5"];
            NSLog(@"%@",array);
        };
        block();
    }
    return 0;
}
复制代码

答:能够正确执行,由于在block块中仅仅是使用了array的内存地址,往内存地址中添加内容,并无修改arry的内存地址,所以array不须要使用__block修饰也能够正确编译。

所以当仅仅是使用局部变量的内存地址,而不是修改的时候,尽可能不要添加__block,经过上述分析咱们知道一旦添加了__block修饰符,系统会自动建立相应的结构体,占用没必要要的内存空间。

2. 上面提到过__block修饰的age变量在编译时会被封装为结构体,那么当在外部使用age变量的时候,使用的是__Block_byref_age_0结构体呢?仍是__Block_byref_age_0结构体内的age变量呢?

为了验证上述问题
一样使用自定义结构体的方式来查看其内部结构

typedef void (^Block)(void);

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

struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
    void (*copy)(void);
    void (*dispose)(void);
};

struct __Block_byref_age_0 {
    void *__isa;
    struct __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;
    struct __Block_byref_age_0 *age; // by ref
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int age = 10;
        Block block = ^{
            age = 20;
            NSLog(@"age is %d",age);
        };
        block();
        struct __main_block_impl_0 *blockImpl = (__bridge struct __main_block_impl_0 *)block;
        NSLog(@"%p",&age);
    }
    return 0;
}

复制代码

打印断点查看结构体内部结构

_Block_byref_age_0结构体

经过查看blockImpl结构体其中的内容,找到age结构体,其中重点观察两个元素:

  1. __forwarding其中存储的地址确实是age结构体变量本身的地址
  2. age中存储这修改后的变量20。

上面也提到过,在block中使用或修改age的时候都是经过结构体__Block_byref_age_0找到__forwarding在找到变量age的。

另外apple为了隐藏__Block_byref_age_0结构体的实现,打印age变量的地址发现实际上是__Block_byref_age_0结构体内age变量的地址。

age的内存地址推算

经过上图的计算能够发现打印age的地址同__Block_byref_age_0结构体内age值的地址相同。也就是说外面使用的age,表明的就是结构体内的age值。因此直接拿来用的age就是以前声明的int age

__block内存管理

上文提到当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复制到堆上时内存变化

__block copy内存管理

blockcopy到堆上时,block内部引用的__block变量也会被复制到堆上,而且持有变量,若是block复制到堆上的同时,__block变量已经存在堆上了,则不会复制。

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

__block 释放内存管理

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

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

一段代码更深刻的观察一下。

typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int number = 20;
        __block int age = 10;
        
        NSObject *object = [[NSObject alloc] init];
        __weak NSObject *weakObj = object;
        
        Person *p = [[Person alloc] init];
        __block Person *person = p;
        __block __weak Person *weakPerson = p;
        
        Block block = ^ {
            NSLog(@"%d",number); // 局部变量
            NSLog(@"%d",age); // __block修饰的局部变量
            NSLog(@"%p",object); // 对象类型的局部变量
            NSLog(@"%p",weakObj); // __weak修饰的对象类型的局部变量
            NSLog(@"%p",person); // __block修饰的对象类型的局部变量
            NSLog(@"%p",weakPerson); // __block,__weak修饰的对象类型的局部变量
        };
        block();
    }
    return 0;
}
复制代码

将上述代码转化为c++代码查看不一样变量之间的区别

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  
  int number;
  NSObject *__strong object;
  NSObject *__weak weakObj;
  __Block_byref_age_0 *age; // by ref
  __Block_byref_person_1 *person; // by ref
  __Block_byref_weakPerson_2 *weakPerson; // by ref
  
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _number, NSObject *__strong _object, NSObject *__weak _weakObj, __Block_byref_age_0 *_age, __Block_byref_person_1 *_person, __Block_byref_weakPerson_2 *_weakPerson, int flags=0) : number(_number), object(_object), weakObj(_weakObj), age(_age->__forwarding), person(_person->__forwarding), weakPerson(_weakPerson->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
复制代码

上述__main_block_impl_0结构体中看出,没有使用__block修饰的变量(object 和 weadObj)则根据他们自己被block捕获的指针类型对他们进行强引用或弱引用,而一旦使用__block修饰的变量,__main_block_impl_0结构体内一概使用强指针引用生成的结构体。

接着咱们来看__block修饰的变量生成的结构体有什么不一样

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};

struct __Block_byref_person_1 {
  void *__isa;
__Block_byref_person_1 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 Person *__strong person;
};

struct __Block_byref_weakPerson_2 {
  void *__isa;
__Block_byref_weakPerson_2 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 Person *__weak weakPerson;
};
复制代码

如上面分析的那样,__block修饰对象类型的变量生成的结构体内部多了__Block_byref_id_object_copy__Block_byref_id_object_dispose两个函数,用来对对象类型的变量进行内存管理的操做。而结构体对对象的引用类型,则取决于block捕获的对象类型的变量。weakPerson是弱指针,因此__Block_byref_weakPerson_2weakPerson就是弱引用,person是强指针,因此__Block_byref_person_1对person就是强引用。

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_assign((void*)&dst->object, (void*)src->object, 3/*BLOCK_FIELD_IS_OBJECT*/);
    _Block_object_assign((void*)&dst->weakObj, (void*)src->weakObj, 3/*BLOCK_FIELD_IS_OBJECT*/);
    _Block_object_assign((void*)&dst->person, (void*)src->person, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_assign((void*)&dst->weakPerson, (void*)src->weakPerson, 8/*BLOCK_FIELD_IS_BYREF*/);
}
复制代码

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

当block从堆中移除时经过dispose函数来释放他们。

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_dispose((void*)src->object, 3/*BLOCK_FIELD_IS_OBJECT*/);
    _Block_object_dispose((void*)src->weakObj, 3/*BLOCK_FIELD_IS_OBJECT*/);
    _Block_object_dispose((void*)src->person, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_dispose((void*)src->weakPerson, 8/*BLOCK_FIELD_IS_BYREF*/);
    
}
复制代码

__forwarding指针

上面提到过__forwarding指针指向的是结构体本身。当使用变量的时候,经过结构体找到__forwarding指针,在经过__forwarding指针找到相应的变量。这样设计的目的是为了方便内存管理。经过上面对__block变量的内存管理分析咱们知道,block被复制到堆上时,会将block中引用的变量也复制到堆中。

咱们重回到源码中。当在block中修改__block修饰的变量时。

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;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_jm_dztwxsdn7bvbz__xj2vlp8980000gn_T_main_b05610_mi_0,(age->__forwarding->age));
        }
复制代码

经过源码能够知道,当修改__block修饰的变量时,是根据变量生成的结构体这里是__Block_byref_age_0找到其中__forwarding指针,__forwarding指针指向的是结构体本身所以能够找到age变量进行修改。

当block在栈中时,__Block_byref_age_0结构体内的__forwarding指针指向结构体本身。

而当block被复制到堆中时,栈中的__Block_byref_age_0结构体也会被复制到堆中一份,而此时栈中的__Block_byref_age_0结构体中的__forwarding指针指向的就是堆中的__Block_byref_age_0结构体,堆中__Block_byref_age_0结构体内的__forwarding指针依然指向本身。

此时当对age进行修改时

// 栈中的age
__Block_byref_age_0 *age = __cself->age; // bound by ref
// age->__forwarding获取堆中的age结构体
// age->__forwarding->age 修改堆中age结构体的age变量
(age->__forwarding->age) = 20;
复制代码

经过__forwarding指针巧妙的将修改的变量赋值在堆中的__Block_byref_age_0中。

咱们经过一张图展现__forwarding指针的做用

__forwarding指针

所以block内部拿到的变量实际就是在堆上的。当block进行copy被复制到堆上时,_Block_object_assign函数内作的这一系列操做。

被__block修饰的对象类型的内存管理

使用如下代码,生成c++代码查看内部实现

typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block Person *person = [[Person alloc] init];
        Block block = ^ {
            NSLog(@"%p", person);
        };
        block();
    }
    return 0;
}
复制代码

来到源码查看__Block_byref_person_0结构体及其声明

__Block_byref_person_0结构体

typedef void (*Block)(void);
struct __Block_byref_person_0 {
  void *__isa;  // 8 内存空间
__Block_byref_person_0 *__forwarding; // 8
 int __flags; // 4
 int __size;  // 4
 void (*__Block_byref_id_object_copy)(void*, void*); // 8
 void (*__Block_byref_id_object_dispose)(void*); // 8
 Person *__strong person; // 8
};
// 8 + 8 + 4 + 4 + 8 + 8 + 8 = 48 
复制代码
// __Block_byref_person_0结构体声明

__attribute__((__blocks__(byref))) __Block_byref_person_0 person = {
    (void*)0,
    (__Block_byref_person_0 *)&person,
    33554432,
    sizeof(__Block_byref_person_0),
    __Block_byref_id_object_copy_131,
    __Block_byref_id_object_dispose_131,
    
    ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"))
};
复制代码

以前提到过__block修饰的对象类型生成的结构体中新增长了两个函数void (*__Block_byref_id_object_copy)(void*, void*);void (*__Block_byref_id_object_dispose)(void*);。这两个函数为__block修饰的对象提供了内存管理的操做。

能够看出为void (*__Block_byref_id_object_copy)(void*, void*);void (*__Block_byref_id_object_dispose)(void*);赋值的分别为__Block_byref_id_object_copy_131__Block_byref_id_object_dispose_131。找到这两个函数

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
 _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
复制代码

上述源码中能够发现__Block_byref_id_object_copy_131函数中一样调用了_Block_object_assign函数,而_Block_object_assign函数内部拿到dst指针即block对象本身的地址值加上40个字节。而且_Block_object_assign最后传入的参数是131,同block直接对对象进行内存管理传入的参数3,8都不一样。能够猜测_Block_object_assign内部根据传入的参数不一样进行不一样的操做的。

经过对上面__Block_byref_person_0结构体占用空间计算发现__Block_byref_person_0结构体占用的空间为48个字节。而加40刚好指向的就为person指针。

也就是说copy函数会将person地址传入_Block_object_assign函数,_Block_object_assign中对Person对象进行强引用或者弱引用。

强引用示意图

若是使用__weak修饰变量查看一下其中的源码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        __block __weak Person *weakPerson = person;
        Block block = ^ {
            NSLog(@"%p", weakPerson);
        };
        block();
    }
    return 0;
}
复制代码
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_weakPerson_0 *weakPerson; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_weakPerson_0 *_weakPerson, int flags=0) : weakPerson(_weakPerson->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
复制代码

__main_block_impl_0中没有任何变化,__main_block_impl_0weakPerson依然是强引用,可是__Block_byref_weakPerson_0中对weakPerson变为了__weak指针。

struct __Block_byref_weakPerson_0 {
  void *__isa;
__Block_byref_weakPerson_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 Person *__weak weakPerson;
};
复制代码

也就是说不管如何block内部中对__block修饰变量生成的结构体都是强引用,结构体内部对外部变量的引用取决于传入block内部的变量是强引用仍是弱引用。

弱引用示意图

mrc环境下,尽管调用了copy操做,__block结构体不会对person产生强引用,依然是弱引用。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block Person *person = [[Person alloc] init];
        Block block = [^ {
            NSLog(@"%p", person);
        } copy];
        [person release];
        block();
        [block release];
    }
    return 0;
}
复制代码

上述代码person会先释放

blockcopy[50480:8737001] -[Person dealloc]
blockcopy[50480:8737001] 0x100669a50
复制代码

当block从堆中移除的时候。会调用dispose函数,block块中去除对__Block_byref_person_0 *person;的引用,__Block_byref_person_0结构体中也会调用dispose操做去除对Person *person;的引用。以保证结构体和结构体内部的对象能够正常释放。

循环引用

循环引用致使内存泄漏。

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;
}
复制代码

运行代码打印内容

block的copy[55423:9158212] 大括号结束啦
复制代码

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

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

产生循环引用示意图

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

解决循环引用问题 - ARC

首先为了能随时执行block,咱们确定但愿person对block对强引用,而block内部对person的引用为弱引用最好。

使用__weak__unsafe_unretained修饰符能够解决循环引用的问题

咱们上面也提到过__weak会使block内部将指针变为弱指针。blockperson对象为弱指针的话,也就不会出现相互引用而致使不会被释放了。

使用`__weak`和`__unsafe_unretained`修饰

__weak__unsafe_unretained的区别。

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

__unsafe_unretained不会产生前引用,不安全,指向的对象销毁时,指针存储的地址值不变。

使用__block也能够解决循环引用的问题。

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

上述代码之间的相互引用可使用下图表示

使用__block也能够解决循环引用

上面咱们提到过,在block内部使用变量使用的实际上是__block修饰的变量生成的结构体__Block_byref_person_0内部的person对象,那么当person对象置为nil也就断开告终构体对person的强引用,那么三角的循环引用就自动断开。该释放的时候就会释放了。可是有弊端,必须执行block,而且在block内部将person对象置为nil。也就是说在block执行以前代码是由于循环引用致使内存泄漏的。

解决循环引用问题 - MRC

使用__unsafe_unretained解决。在MRC环境下不支持使用__weak,使用原理同ARC环境下相同,这里不在赘述。

使用__block也能解决循环引用的问题。由于上文__block内存管理中提到过,MRC环境下,尽管调用了copy操做,__block结构体不会对person产生强引用,依然是弱引用。所以一样能够解决循环引用的问题。

__strong 和 __weak

__weak typeof(self) weakSelf = self;
person.block = ^{
    __strong typeof(weakSelf) myself = weakSelf;
    NSLog(@"age is %d", myself->_age);
};
复制代码

block内部从新使用__strong修饰self变量是为了在block内部有一个强指针指向weakSelf避免在block调用的时候weakSelf已经被销毁。

面试题

上文中提到的面试题,仔细研读两篇文章中均可以找到答案。这里不在赘述。

底层原理相关文章:

iOS底层原理总结 - 文集

欢迎添加个人微信一块儿交流学习

做者:xx_cc 连接:https://www.jianshu.com/p/8865ff43f30e 来源:简书
相关文章
相关标签/搜索