block的本質
block本質上也是一個oc對象,他內部也有一個isa指針。block是封裝了函數調用以及函數調用環境的OC對象。
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是如何定義其類型
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結構體構造函數三個參數的分析我們可以得出結論:
- __block_impl結構體中isa指針存儲著
&_NSConcreteStackBlock
地址,可以暫時理解為其類對象地址,block就是_NSConcreteStackBlock
類型的。 - block代碼塊中的代碼被封裝成
__main_block_func_0
函數,FuncPtr則存儲著__main_block_func_0
函數的地址。 - 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對象自己的引用計數。
總結
一旦block中捕獲的變量為對象類型,block結構體中的__main_block_desc_0會出兩個參數copy和dispose。因為訪問的是個對象,block希望擁有這個對象,就需要對對象進行引用,也就是進行內存管理的操作。比如說對對象進行retarn操作,因此一旦block捕獲的變量是對象類型就會會自動生成copy和dispose來對內部引用的對象進行內存管理。
當block內部訪問了對象類型的auto變量時,如果block是在棧上,block內部不會對person產生強引用。不論block結構體內部的變量是__strong修飾還是__weak修飾,都不會對變量產生強引用。
如果block被拷貝到堆上。copy函數會調用_Block_object_assign函數,根據auto變量的修飾符(__strong,__weak,unsafe_unretained)做出相應的操作,形成強引用或者弱引用
如果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依然沒有被釋放,產生了循環引用
通過一張圖看一下他們之間的內存結構
上圖中可以發現,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));
};