#iOS底層原理 - Block本質探究ios
block 本質是一個OC對象,也存在 isa 指針。或者說Block 是封裝了函數調用和函數調用環境的OC對象。c++
編寫一段最簡單的OC代碼頂一個block,代碼如:程序員
int main(int argc, const char * argv[]) {
@autoreleasepool {
int abc = 10086;
void(^block)(int number) = ^(int number) {
NSLog(@"%d",number);
};
}
return 0;
}
複製代碼
使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
命令將其OC代碼轉化爲底層的C++代碼,觀察block的底層結構。objective-c
咱們打開編譯生成的main.cpp代碼,會發現上述代碼被轉化爲以下:bash
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int abc = 10086;
void(*block)(int number) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
}
return 0;
}
複製代碼
block 代碼塊被定義爲 __main_block_impl_0
結構體。iphone
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;
}
};
複製代碼
結構體中包含兩個不一樣的結構體變量 __block_impl
和 __main_block_desc_0
函數
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
複製代碼
包含isa指針說明,block本質上也是一個OC對象,FuncPtr 指向block所封裝的代碼塊地址,等執行block時會經過FuncPtr尋找將要執行的代碼塊,而且調用。ui
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
}
複製代碼
其中: Block_size 爲當前block 佔用內存大小。atom
block 封裝的代碼塊被定義爲 __main_block_func_0
結構體spa
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int number) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gf_ct0sq2w17s16j4b1pz5_zx500000gn_T_main_68909d_mi_0,number);
}
複製代碼
若是咱們將main函數改成:
int main(int argc, const char * argv[]) {
@autoreleasepool {
int abc = 10086;
void(^block)() = ^() {
NSLog(@"%d",abc);
};
abc = 10010;
block();
}
return 0;
}
複製代碼
在block內部引用外部變量,咱們再看看內部組成結構。一樣執行 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
命令。 咱們能夠看到,較以前,__main_block_impl_0
結構體新增一個 int 類型變量abc,用於存儲所引用的外部變量的值。由於是值存儲,因此在block生成以後,不管外部變量作何更改,abc依然是以前所定義的值。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int abc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _abc, int flags=0) : abc(_abc) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
}
複製代碼
由於咱們所定義的外部變量 abc 以前沒有任何修飾符,也就是默認的auto變量,此時block是值捕獲。若是將外部變量聲明爲 static 類型再觀察底層實現。
int main(int argc, const char * argv[]) {
@autoreleasepool {
int abc = 10086;
static int def = 100;
void(^block)(void) = ^() {
NSLog(@"abc: %d - def: %d",abc,def);
};
abc = 10010;
def = 200;
block();
}
return 0;
}
複製代碼
轉化爲c++底層實現爲:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int abc;
int *def;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _abc, int *_def, int flags=0) : abc(_abc), def(_def) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
複製代碼
使用static 修飾的變量在block內部爲指針傳遞,block直接捕獲外部變量的內存地址,此時若外部變量在block聲明以後修改,block內部也會同步進行修改。
若是使用全局變量,block不會捕獲。由於聲明全局變量的類型會在程序的整個聲明週期都不會被釋放,因此在使用block時,直接會去訪問全局變量的值。因此捕獲就沒有意義了,感興趣的能夠自行查看底層實現。
當咱們聲明一個block 而且打印他的繼承鏈咱們能夠看到:
void(^block)(void) = ^() {
NSLog(@"abc");
};
NSLog(@"%@",[block class]);
NSLog(@"%@",[[block class] superclass]);
NSLog(@"%@",[[[block class] superclass] superclass]);
NSLog(@"%@",[[[[block class] superclass] superclass] superclass]);
複製代碼
輸出:
2018-06-28 10:37:23.901162+0800 BlockDemo[17574:719984] __NSGlobalBlock__
2018-06-28 10:37:23.901504+0800 BlockDemo[17574:719984] __NSGlobalBlock
2018-06-28 10:37:23.901522+0800 BlockDemo[17574:719984] NSBlock
2018-06-28 10:37:23.901535+0800 BlockDemo[17574:719984] NSObject
Program ended with exit code: 0
複製代碼
從而也進一步證實了block 本質上爲 OC對象。而且,在不引用外部變量的狀況下,block爲 NSGlobalBlock
類型。
咱們定義三個不一樣的block,分別打印他們的實際類型:
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^block1)(void) = ^() {
NSLog(@"abc");
};
NSLog(@"%@",[block1 class]);
int abc = 1;
void(^block2)(void) = ^() {
NSLog(@"abc: %d",abc);
};
NSLog(@"%@",[block2 class]);
NSLog( @"%@", [^(){
NSLog(@"hello %d",abc);
} class]);
}
return 0;
}
複製代碼
輸出:
2018-06-28 10:48:32.096859+0800 BlockDemo[17719:728991] __NSGlobalBlock__
2018-06-28 10:48:32.097224+0800 BlockDemo[17719:728991] __NSMallocBlock__
2018-06-28 10:48:32.097243+0800 BlockDemo[17719:728991] __NSStackBlock__
複製代碼
咱們能夠得出結論,
__NSGlobalBlock__
__NSMallocBlock__
__NSStackBlock__
他們在內存中位置分別:
那他們是如何區分的呢?可使用以下表格來講明:
Block 類型 | 條件 |
---|---|
NSGlobalBlock | block 內部沒有訪問auto變量 |
NSStackBlock | block 內部訪問了 auto變量 |
NSMallocBlock | NSStackBlock 調用了copy |
NSStackBlock 執行 copy 後會將棧區的block 複製到堆區,便於程序員管理,那其餘類型的block執行 copy 會有什麼變化呢?以下表所示:
Block 類型 | 存儲域 | 執行 copy 後效果 |
---|---|---|
NSGlobalBlock | 程序的數據區域 | 無任何改變 |
NSStackBlock | 棧 | 從棧複製到堆 |
NSMallocBlock | 堆 | 引用計數器加1 |
typedef void(^MyBlock)(void);
MyBlock testFunc() {
int a = 10;
MyBlock myBlock = ^ {
NSLog(@"test --- %d",a);
};
return myBlock;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyBlock myB = testFunc();
NSLog(@"%@",[myB class]);
}
return 0;
}
複製代碼
若是此代碼在MRC 環境下,會崩潰。Block訪問的變量已被釋放。 若是在ARC環境下,在參數的返回值爲block時,系統會對block自動執行一次 copy 操做,使其變爲 NSMallocBlock 類型。
int main(int argc, const char * argv[]) {
@autoreleasepool {
int abc = 10;
MyBlock myB = ^ {
NSLog(@"+++ %d",abc);
};
NSLog(@"%@",[myB class]);
}
return 0;
}
複製代碼
如上代碼,在MRC環境輸出:__NSStackBlock__
。在 ARC環境輸出:__NSMallocBlock__
例如:
NSArray *array = @[@1];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
複製代碼
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
複製代碼
因此在 MAC 環境下的block屬性必須使用 copy 修飾,而ARC環境下的block屬性便可使用 strong 修飾,也可使用 copy 修飾,二者都會對block自動執行copy操做,故無任何區別。
觀察以下代碼
int main(int argc, const char * argv[]) {
@autoreleasepool {
{
XWPerson *person = [[XWPerson alloc] init];
person.age = 10;
^{
NSLog(@"person -- %ld",(long)person.age);
}();
}
NSLog(@"*******");
}
return 0;
}
複製代碼
會發現當 函數體內 大括號執行完畢後 XWPerson 即被釋放,此時的block 是 棧類型的Block 即 __NSStackBlock__
. 存儲在棧區的block即使引用了對象,也會跟隨大括號一併釋放。
若是將以上代碼改成:
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyBlock myBlock;
{
XWPerson *person = [[XWPerson alloc] init];
person.age = 10;
myBlock = ^{
NSLog(@"person -- %ld",(long)person.age);
};
myBlock();
}
NSLog(@"*******");
}
return 0;
}
複製代碼
咱們會發如今執行到 **** 時,person 對象依然沒有被釋放,此時block 已經對 person 對象進行了強引用。由於 此時 的block 爲強指針引用,類型爲 堆block __NSMallocBlock__
. 爲何堆 block 會對外部對象強引用呢?
此時 使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
命令 觀察其底層 c++ 實現:
此時block 定義爲:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
XWPerson *__strong person;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, XWPerson *__strong _person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
複製代碼
其中 main_block_desc_0 定義爲:
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*);
}
複製代碼
較以前block引用基本成員類型時,其 main_block_desc_0 多了兩個參數分別爲 copy 和 dispose。而且傳入的都是 __main_block_impl_0
block 自己。
當 block 執行 copy 操做的時候,執行的是
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}
複製代碼
方法。最終調用的 _Block_object_assign
方法會對block引入的對象 person 進行引用計數操做,當所引入的對象使用 strong 修飾則使其引用計數加1,若使用weak修飾則引用計數不變。
當 block 執行完畢的時候會調用 dispose 方法,而dispose 在底層會調用
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}
複製代碼
方法,將block內部引用的對象成員引用計數減1,若是此時外部對象使用strong 修飾,引用計數在copy加1後 此時再減1.依然會強引用外部對象,不會釋放,若是使用weak修飾,此時由於自身以及被釋放,因此不會再持有所引用外部對象,然而此時所引用外部對象是否會被釋放取決於它的引用計數是否爲 0。
咱們知道,若是block 內部捕獲的外部變量爲 auto 類型,在block 內部生成的是該變量的值類型變量,沒法經過block內部的值修改外部變量。 若是想在block內部修改外部變量的值有幾種方法?
使用 static 修飾的變量block內部會直接獲取到變量的內存地址,能夠直接修改。
若使用 static 變量修飾,該變量的生命週期就會無限延長,這不符合咱們的設計思路,故咱們可使用 __block
來修飾外部變量,從而達到在block內部修改外部成員變量的目的。 那 __block
是如何實現此需求的呢?
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int a = 10;
MyBlock block = ^{
a = 20;
NSLog(@"a --- %d",a);
};
block();
}
return 0;
}
複製代碼
使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
命令將其轉化爲c++ 實現:
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;
}
};
複製代碼
咱們可知 經過 __block
修飾的外部成員變量被定義爲 __Block_byref_a_0
對象!它的聲明爲:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
複製代碼
此時在main函數內聲明 __block
類型的變量會以此方式初始化:
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
複製代碼
其中 __forwarding
保存的原變量 a 的內存地址,size
爲當前變量的內存大小,10 保存未原變量的值。
如此,咱們在block 內部修改 原變量時:
(a->__forwarding->a) = 20;
複製代碼
直接取原變量的地址進行更改,從而實如今block內部更改外部變量。
對於block內部捕獲的對象類型的auto變量和__block修飾的變量。若是block在棧區,不會對他們進行內存管理,即不會強引用外部變量
若是block被複制到堆區,則會調用內部 copy 函數對外部 __block 修飾的變量和對象類型的auto變量進行內存管理。
當block從內存中移除時,一樣也會調用dispose函數對所引用的外部變量進行釋放。
使用 block 很容易造成循環引用,若是一個類中定義的block內部引用了該類的外部屬性,包括 類自己的 self, 均會致使 self 強引用 block,block 也強引用 self。致使self不會被釋放。以下代碼就會形成循環引用:
.h
#import <Foundation/Foundation.h>
@interface XWPerson : NSObject
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, copy) void(^personBlock)(void);
@end
複製代碼
.m
#import "XWPerson.h"
@implementation XWPerson
- (void)test {
self.personBlock = ^{
NSLog(@"%d",self.age); //此處若即使使用 _age 也會產生循環引用。
};
}
- (void)dealloc {
NSLog(@" XWPerson -- dealloc -- age:%ld",(long)_age);
}
@end
複製代碼
產生循環引用的本質緣由是,在block內部實現裏,會將self 捕獲到block內部,而且strong 強引用。以下代碼所示:
struct __XWPerson__test_block_impl_0 {
struct __block_impl impl;
struct __XWPerson__test_block_desc_0* Desc;
XWPerson *const __strong self;
__XWPerson__test_block_impl_0(void *fp, struct __XWPerson__test_block_desc_0 *desc, XWPerson *const __strong _self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
複製代碼
int main(int argc, const char * argv[]) {
@autoreleasepool {
XWPerson *person1 = [[XWPerson alloc] init];
person1.age = 18;
__weak typeof(person1) weakPerson = person1;
person1.personBlock = ^{
NSLog(@"%ld",(long)weakPerson.age);
};
person1.personBlock();
}
return 0;
}
複製代碼
__unsafe_unretained XWPerson *person1 = [[XWPerson alloc] init];
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block XWPerson *person1 = [[XWPerson alloc] init];
person1.age = 18;
person1.personBlock = ^{
NSLog(@"%ld",(long)person1.age);
person1 = nil;
};
person1.personBlock();
}
return 0;
}
複製代碼