探尋Block的本質(1)—— 基本認識markdown
探尋Block的本質(2)—— 底層結構iphone
你們應該都知道,若是想在block內部修改從外部捕獲的auto
變量的值,能夠在該auto
變量定義的時候,加上關鍵字__block
。代碼案例以下學習
#import <Foundation/Foundation.h>
typedef void(^CLBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int a = 10;
int b = 30;
CLBlock myblock = ^{
a = 20;
NSLog(@"%d",b);
};
myblock();
NSLog(@"myblock執行完以後,a = %d",a);
}
return 0;
}
*********************運行結果*********************
2019-09-04 19:41:51.709406+0800 Block學習[29867:3904669] 30
2019-09-04 19:41:51.709706+0800 Block學習[29867:3904669] myblock執行完以後,a = 20
複製代碼
__block
只能夠用來做用於auto
變量,它的目的就是爲了可以讓auto
變量可以在block內部內修改。而全局變量和static
變量原本就能夠從block內部進行修改,所以__block
對它們來講沒有意義,因此__block
被規定只能用於修飾auto
變量,這一點應該不難理解。spa
老套路,咱們先經過終端命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
來看一下__block
以及block在底層張什麼樣子。首先看看block的底層結構.net
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int b;
__Block_byref_a_0 *a; // by ref
__main_block_impl_0(void *fp,
struct __main_block_desc_0 *desc,
int _b,
__Block_byref_a_0 *_a,
int flags=0) : b(_b), a(_a->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
複製代碼
爲了比較,我特地加了一個int b
做爲對比,順便回顧一下,基本類型的auto
變量被block捕獲的時候,就是經過值拷貝的形式把值賦給block內部相對應的基本類型變量。而案例裏面的__block int a = 10
,咱們能夠看到在底層,系統是把int a
包裝到了一個叫__Block_byref_a_0
的對象裏面。這個對象的結構以下設計
struct __Block_byref_a_0 {
void *__isa;//有isa,是一個對象
__Block_byref_a_0 *__forwarding;//指向自身類型對象的指針
int __flags;//不用關心
int __size;//本身所佔大小
int a;//被封裝的 基本數據類型變量
};
複製代碼
看看在main
函數中__Block_byref_a_0
被賦了什麼值3d
//__block int a = 10;
__Block_byref_a_0 a = {(void*)0,
&a,
0,
sizeof(__Block_byref_a_0),
10
};
複製代碼
圖中能夠看出來,10被存儲到了block內部__Block_byref_a_0
對象的成員變量int a上。__Block_byref_a_0
對象裏面的成員變量__forwarding
實際上指向了__Block_byref_a_0
對象自身。 咱們來看block內的代碼對於a的賦值是如何操做的
爲何用a->__forwarding->a
,而不是a->a
直接拿到int a
,經過__forwarding
轉一圈有什麼用意?這個等會解答。
這樣__block
的底層實現就說完了。
上面,咱們知道了經過 __block int a = 10
定義以後,這個a
底層是一個__Block_byref_a_0
對象,數值10存放在這個對象內部的成員變量int a
上面。可是咱們在寫代碼的時候,能夠直接經過__Block_byref_a_0
對象a來賦值,那麼在block定義初始化結束,完成變量捕獲以後,oc代碼中再次經過a
訪問到的究竟是什麼呢?例以下面
咱們先來看一份代碼案例
**********************testVC.m**********************
#import "testVC.h"
@implementation testVC typedef void(^CLBlock)(void);
struct __Block_byref_a_0 {
void *__isa;
struct __Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
struct __Block_byref_a_0 *a; // by ref
};
struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void *copy;
void *dispose;
};
- (void)viewDidLoad {
[super viewDidLoad];
[self blockTest];
}
- (void)blockTest {
__block int a = 10;
CLBlock myblock = ^{
a = 20;
NSLog(@"此時在在myblock內部的oc代碼裏直接經過a訪問的內存空間是:%p",&a);
};
struct __main_block_impl_0 *tmpBlock = (__bridge struct __main_block_impl_0 *)myblock;
NSLog(@"myBlock通過初始化,完成變量捕獲以後,其內部的[__Block_byref_a_0 *] a = %p",tmpBlock->a);
NSLog(@"此時在在myblock外部的oc代碼裏直接經過a訪問的內存空間是:%p",&a);
myblock();
}
@end
*************************運行結果************************
2019-09-04 21:28:07.189104+0800 BT[30733:3968805] myBlock捕獲完變量以後,[__Block_byref_a_0 *] a = 0x7ffeede8f840
2019-09-04 21:28:07.189221+0800 BT[30733:3968805] 此時在在block外部的oc代碼裏直接經過a訪問的內存空間是:0x7ffeede8f858
2019-09-04 21:28:07.189296+0800 BT[30733:3968805] 此時在在block內部的oc代碼裏直接經過a訪問的內存空間是:0x7ffeede8f858
複製代碼
從打印咱們看到,myblock
內部的[__Block_byref_a_0 *] a
指向的地址是0x7ffeede8f840
,以後咱們在任意地方經過a
訪問的內存地址是0x7ffeede8f858
,十六進制下它們地址相差了0x18
,也就是十進制下的24個字節。
從示意圖能夠看出,經過[__Block_byref_a_0 *] a
的地址往高地址走24
個字節,正好是它內部封裝的那個int a
。也就是說咱們在oc代碼裏面完成了myblock
的初始化以及 __block變量的捕獲以後,只能經過a
訪問到被封裝在 __ Block_byref_a_0 *
內部的這個int a
的內存空間。
蘋果這麼作的意圖我猜想是想向開發者隱藏__ Block_byref_a_0 *
的存在,但願開發者把__block int a
就當成一個普通的int a
來看待。蘋果嗎,老是這麼小家子氣,能夠理解。(此處純屬自我發揮,還待大牛給出正解:)
咱們知道,若是block捕獲一個基礎類型的auto
變量,是不用考慮內存管理的。可是__block
的本質做用,是將所修飾的對象包裝成一個__ Block_byref_xx_x *
,而後進行捕獲,而__ Block_byref_xx_x *
本質上也是一個對象,所以確定須要處理它的內存管理問題。
咱們已經知道,若是一個block位於棧空間上,那麼是不須要考慮被它所捕獲的對象類型的
auto
變量的內存管理問題的。所謂的內存管理,是針對建立在堆空間上的oc對象而言的,由於咱們做爲開發者,只可以管理堆上的空間。棧空間的內存是由系統管理的,不用咱們操心。
關於內存管理問題這裏,咱們所討論的問題須要考慮三個關鍵因素:__block
、__weak
、對象變量
、基本類型變量
,他們合法的組合有以下幾種:
基本類型變量
對象變量
__weak
+ 對象變量
__block
+ 基本類型變量
__block
+ 對象變量
__block
+ __weak
+ 對象變量
我在【對象類型的auto變量捕獲】這一篇裏面詳細分析了一個對象類型的auto
變量被block捕獲時的內存管理過程,上面的一、二、3這三種場景已經獲得了說明。下面咱們來分析一下四、五、6這三種場景。
__block
+ 基本類型變量
首先代碼上一份
int main(int argc, const char * argv[]) {
@autoreleasepool {
CLBlock myblock;
{
__block int a = 10;
myblock = ^ {
a = 20;
};
}
myblock();
}
return 0;
}
複製代碼
編譯以後block相關的底層結構以下
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
};
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src)
{
_Block_object_assign(
(void*)&dst->a,
(void*)src->a,
8/*BLOCK_FIELD_IS_BYREF*/♥️♥️♥️♥️♥️♥️
);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src)
{
_Block_object_dispose(
(void*)src->a,
8/*BLOCK_FIELD_IS_BYREF*/♥️♥️♥️♥️♥️♥️
);
}
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 int a = 10;
這句代碼的做用,是將int a
包裝在struct __Block_byref_a_0
內部,這樣block實際上捕獲的是這個struct __Block_byref_a_0
,它能夠被看成一個對象來看待,因此內存管理上面,最終仍然是經過_Block_object_assign
和_Block_object_dispose
這兩個函數來處理,可是能夠看到這兩個函數的最後一個參數是8
(對於對象類型的捕獲,傳遞的參數是3
),這個參數代表了即將要處理的是一個struct __Block_byref_a_0
,由於它是沒有__weak
和__strong
標記的,因此處理方式很簡單,就是copy到堆上的時候,同時須要進行retain
,dispose的時候同時須要進行release
。
__block
+ 對象變量
上代碼
int main(int argc, const char * argv[]) {
@autoreleasepool {
CLBlock myblock;
{
CLPerson *person = [[CLPerson alloc] init];
__block CLPerson *blockPerson = person;
myblock = ^ {
blockPerson.age = 10;
};
}
myblock();
}
return 0;
}
複製代碼
編譯以後, __block CLPerson *blockPerson
的底層結構以下
struct __Block_byref_blockPerson_0 {
void *__isa;
__Block_byref_blockPerson_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
CLPerson *__strong blockPerson;
};
複製代碼
從這個結構能夠看出兩點變化:
__block
修飾後,底層所生成的__Block_byref_xxx_x
結構體裏面多了兩個函數指針,__Block_byref_id_object_copy
和__Block_byref_id_object_dispose
。__Block_byref_xxx_x
內部之後,默認是被__strong
修飾的。上面發現的兩個新函數指針__Block_byref_id_object_copy
和__Block_byref_id_object_dispose
就是當__Block_byref_xxx_x
被拷貝到堆空間的時候,以及將要被系統釋放的時候調用的。咱們能夠在main函數裏面找到它們的最終賦值,分別是__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_object_assign
和_Block_object_assign
這兩個函數,二從參數能夠看出,它們所要處理的對象就是__Block_byref_id_object_dispose
內部所封裝的對象類型變量,也就是咱們代碼中的CLPerson *blockPerson
,由於默認blockPerson
是被__strong
修飾的,因此接下來對於blockPerson
的內存管理方式就和咱們以前所分析過的是同樣的。
__block
+ __weak
+ 對象變量
上代碼
int main(int argc, const char * argv[]) {
@autoreleasepool {
CLBlock myblock;
{
CLPerson *person = [[CLPerson alloc] init];
__block __weak CLPerson *weakBlockPerson = person;
myblock = ^ {
weakBlockPerson.age = 10;
};
}
myblock();
}
return 0;
}
複製代碼
這裏就直接給出編譯以後的底層結構struct __Block_byref_xxx_x
來進行對比
struct __Block_byref_weakBlockPerson_0 {
void *__isa;
__Block_byref_weakBlockPerson_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
CLPerson *__weak weakBlockPerson;
};
複製代碼
由於咱們顯式地給對象變量加上了__weak
,所以struct __Block_byref_xxx_x
內部封裝的就是一個指向對象的弱指針CLPerson *__weak weakBlockPerson
。根據上面的分析,最後一樣進入到_Block_object_assign
和_Block_object_assign
這兩個函數進行處理,處理方式再也不贅述。
最後在經過圖例在梳理一下
最後再來解決那個咱們中篇遺留的問題:__forwarding
的做用 從上圖能夠很清晰的看出,當__Block_byref_xxx_x
(假設爲A)從棧空間被拷貝到堆空間(假設堆上的那一份爲B)的時候,棧上A的__forwarding
指針會被指向堆空間上的B,而B自己的__forwarding
仍然指向B本身,由於在底層訪問__Block_byref_xxx_x
所封裝的目標變量,是經過__Block_byref_xxx_x
->__forwarding
->目標變量,這樣,不管咱們訪問入口對象__Block_byref_xxx_x
是在棧上仍是在堆上,都能保證最終訪問到的目標變量是堆空間上的那一份。這樣的設計就正好契合了堆空間上的__Block_byref_xxx_x
對象存在的目的。
到此,關於__block
的底層實現就分析到這裏。