ObjC block簡析(一)

SoC發表於2019-01-24

探究block的本質

Block

在main.m的main函式中宣告一個block並執行block()通過xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp命令將main.m轉為main.cpp。

cpp檔案中的main函式

在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結構體中第一個成員就是:

__block_impl結構體
我們可以看到這裡並不是結構體指標,而是結構體本身,所以__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區域性變數的捕獲

訪問auto變數

出現上述情況的原因是什麼呢?依然將main.m轉成main.cpp。

main.cpp

上面圖片中我們可以發現block的結構體中出現了一個age變數,並且其建構函式表明將_age賦值給age成員。而在FuncPtr儲存的函式__main_block_func_0的地址中,我們發現呼叫此函式是從block的結構體中取出age成員的值進行列印的。所以當定義block的時候age的值已經被block用一個同樣名字的成員捕獲了。而且捕獲的僅僅是age的值。

static區域性變數的捕獲

main.cpp

通過上圖我們可以看出block中儲存的是a的地址,所以當FuncPtr指向的函式呼叫的時候會通過取a地址中儲存的值,所就出現下面這種情況了。

static型別的變數捕獲

全域性變數的捕獲

全域性變數

我們可以看出全域性變數並沒有被block所捕獲,因為全域性變數存放在全域性區,隨時都可以訪問,所以當FuncPtr指向的函式呼叫的時候就會直接取a和b的值用。而區域性變數超過作用域就會自動回收所以block需要在自身存放一份,以保證其能準確訪問。

總結

區域性變數在block中使用的時候會被block捕獲,auto變數是值捕獲,而static變數是地址捕獲。全部變數不會被捕獲。

block型別

既然上面說block是一個oc物件,那麼他也應該是有型別的。那麼block的型別有什麼有趣的事呢?

由於在ARC環境下編譯器默默對block做了一些我們看不見的工作,所以我們將xcode的arc模式關掉,以便於窺探到本質。

關閉arc

型別

結果

通過上述結果我們可以看出當block訪問了auto變數的時候會變成__NSStackBlock__型別。而其他情況下是__NSGlobalBlock__型別。

__NSGlobalBlock__型別存在於資料區,__NSStackBlock__存在於棧區。

在ARC環境下列印的結果:

ARC下的block型別

而在ARC環境下原本__NSGlobalBlock__的block依然是__NSGlobalBlock__型別,而原本是__NSStackBlock__卻變成了__NSMallocBlock__存放在堆區。這是因為當我們定義block的時候ARC預設為我們做了一次copy操作。

下面是block的型別以及在記憶體中存放的區域:

block的型別

我們嘗試在MRC下對__NSGlobalBlock____NSMallocBlock__型別的block進行copy操作並列印結果:

copy操作

block的型別(MRC環境)

訪問了auto變數的block是__NSStackBlock__型別,沒有訪問auto變數的block是__NSGlobalBlock__型別。而對__NSStackBlock__型別進行copy操作就會變為__NSMallocBlock__型別。

對三種型別的block分別進行copy操作結果如下:

對三種型別的block進行copy操作

[往深處看-Block(二)](

相關文章