探究block的本質
在main.m的main函式中宣告一個block並執行block()
通過xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
命令將main.m轉為main.cpp。
在main.cpp中可以發現上圖中的程式碼。其中在main函式中的block和block(),被轉化為
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
複製程式碼
其中不乏強制轉換型別的程式碼,將強制型別轉換的程式碼去掉我們可以明確的看出:
void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
block->FuncPtr(block);
複製程式碼
block儲存了__main_block_impl_0
函式的地址,而此函式中傳遞的第一個值是一個函式指標,函式的作用就是block中儲存的程式碼的作用,即列印hello world
。第二個引數是一個結構體指標,其中包含有對block的描述。
而__main_block_impl_0
是一個結構體,__main_block_impl_0
函式是結構體__main_block_impl_0
的建構函式,在此結構中就可以看到這個與結構體名字相同的沒有返回值的函式。
該函式將型別地址賦值給isa,將存放block中程式碼的函式的指標賦值給funcptr,將block的描述賦值給desc。
__main_block_impl_0
結構體中第一個成員就是:
__main_block_impl_0
的第一個成員其實是isa.
而我們的block()
轉換成了block->FuncPtr(block);
就是通過block查詢到FuncPtr然後呼叫,執行相應的程式碼。至於block明明是__main_block_impl_0
型別的為什麼卻能直接查詢到FuncPtr成員,上面已經說明這裡存放的並不是結構體指標,也不是地址,而是結構體impl本身,所以block是可以經過型別強制轉換轉換成__block_impl
型別進而通過FuncPtr指標呼叫相應的函式的。
我們都知道,ObjC中的物件都有一個isa指標,而block中同樣存在isa指標。所以可以說block是一個ObjC指標。它封裝了block函式的呼叫。
Block的變數捕獲
像上述例子對我們來說通常沒有什麼實際作用,而我們通常要在block中處理一下外部變數的邏輯,那麼block是怎麼處理這些變數的呢?
auto區域性變數的捕獲
出現上述情況的原因是什麼呢?依然將main.m轉成main.cpp。
上面圖片中我們可以發現block的結構體中出現了一個age變數,並且其建構函式表明將_age賦值給age成員。而在FuncPtr儲存的函式__main_block_func_0
的地址中,我們發現呼叫此函式是從block的結構體中取出age成員的值進行列印的。所以當定義block的時候age的值已經被block用一個同樣名字的成員捕獲了。而且捕獲的僅僅是age的值。
static區域性變數的捕獲
通過上圖我們可以看出block中儲存的是a的地址,所以當FuncPtr指向的函式呼叫的時候會通過取a地址中儲存的值,所就出現下面這種情況了。
全域性變數的捕獲
我們可以看出全域性變數並沒有被block所捕獲,因為全域性變數存放在全域性區,隨時都可以訪問,所以當FuncPtr指向的函式呼叫的時候就會直接取a和b的值用。而區域性變數超過作用域就會自動回收所以block需要在自身存放一份,以保證其能準確訪問。
總結
區域性變數在block中使用的時候會被block捕獲,auto變數是值捕獲,而static變數是地址捕獲。全部變數不會被捕獲。
block型別
既然上面說block是一個oc物件,那麼他也應該是有型別的。那麼block的型別有什麼有趣的事呢?
由於在ARC環境下編譯器默默對block做了一些我們看不見的工作,所以我們將xcode的arc模式關掉,以便於窺探到本質。
通過上述結果我們可以看出當block訪問了auto變數的時候會變成__NSStackBlock__
型別。而其他情況下是__NSGlobalBlock__
型別。
__NSGlobalBlock__
型別存在於資料區,__NSStackBlock__
存在於棧區。
在ARC環境下列印的結果:
而在ARC環境下原本__NSGlobalBlock__
的block依然是__NSGlobalBlock__
型別,而原本是__NSStackBlock__
卻變成了__NSMallocBlock__
存放在堆區。這是因為當我們定義block的時候ARC預設為我們做了一次copy操作。
下面是block的型別以及在記憶體中存放的區域:
我們嘗試在MRC下對__NSGlobalBlock__
和__NSMallocBlock__
型別的block進行copy操作並列印結果:
block的型別(MRC環境)
訪問了auto變數的block是__NSStackBlock__
型別,沒有訪問auto變數的block是__NSGlobalBlock__
型別。而對__NSStackBlock__
型別進行copy操作就會變為__NSMallocBlock__
型別。
對三種型別的block分別進行copy操作結果如下:
[往深處看-Block(二)](