你真的理解 block修飾符的原理麼?

2021-07-04 03:16:35 字數 3961 閱讀 1379

為什麼block中不能修改普通變數的值?

__block的作用就是讓變數的值在block中可以修改麼?

如果有的讀者認為,問題太簡單了,而且你的答案是:

因為編譯器會有警告,各種教程也都說了不能修改。

應該是的吧。

很多教程、資料上都稱block是「帶有自動變數值的匿名函式」。這樣的解釋顯然是正確的,但也是不利於初學者理解的。我們首先通過乙個例子看一看block到底是什麼?

typedef

void (^block)(void);

block block;;}

block();

拋開block略有怪異的語法不談,其實對於乙個block來說:

它更像是乙個微型的程式。

為什麼這麼說呢,我們知道程式就是資料加上演算法,顯然,block有著自己的資料和演算法。可以看到,在這個簡單的例子中,block的資料就是int型別變數val,它的演算法就是乙個簡單的nslog方法。對於一般的block來說,它的資料就是傳入的引數和在定義這個block時截獲的變數。而它的演算法,就是我們往裡面寫的那些方法、函式呼叫等。

當然,我們還注意到在上面的demo中,通過typedef,block非常類似於乙個oc的物件。限於篇幅和主題,這裡不加證明的給出乙個結論:block其實就是乙個objective-c的物件。有興趣的讀者可以結合runtime中類和物件的定義進一步思考。

剛剛我們已經意識到,block的定義和呼叫是分離的。通過clang編譯器,可以看到block和其他objective-c物件一樣,都是被編譯為c語言裡的普通的struct結構體來實現的。我們來看乙個最簡單的block會被編譯成什麼樣:

//這個是源**

int main();

block();

return

0;}

編譯後的**如下:

struct __block_impl ;

struct __main_block_impl_0

};struct

void __main_block_func_0(struct __main_block_impl_0 *__cself)

static

struct __main_block_desc_0 __main_block_desc_0_data = ;

**非常長,但是並不複雜,一共是四個結構體,顯然乙個block物件被編譯為了乙個__main_block_impl_0型別的結構體。這個結構體由兩個成員結構體和乙個建構函式組成。兩個結構體分別是__block_impl和__main_block_desc_0型別的。其中__block_impl結構體中有乙個函式指標,指標將指向__main_block_func_0型別的結構體。總結了一副關係圖:

block在定義的時候:

//呼叫__main_block_impl_0結構體的建構函式

struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_data);

struct __main_block_impl_0 *blk = &tmp;

block在呼叫的時候:

(*blk->impl.funcptr)(blk);
之前我們說到,block有自己的資料和演算法。顯然演算法(也就是**)是放在__main_block_func_0結構體裡的。那麼資料在**呢,這個問題比較複雜,我們來看一看文章最初的demo會編譯成什麼樣,為了簡化**,這裡只貼出需要修改的部分。

struct __main_block_impl_0 

};struct void __main_block_func_0(struct __main_block_impl_0 *__cself)

可以看到,當block需要截獲自動變數的時候,首先會在__main_block_impl_0結構體中增加乙個成員變數並且在結構體的建構函式中對變數賦值。以上這些對應著block物件的定義。

在block被執行的時候,把__main_block_impl_0結構體,也就是block物件作為引數傳入__main_block_func_0結構體中,取出其中的val的值,進行接下來的操作。

如果你耐心地看完了上面非常囉嗦繁瑣的block介紹,那麼你很快就明白為什麼block中不能修改普通的變數的值了。

通過把block拆成這四個結構體,系統「完美」的實現了乙個block,使得它可以截獲自動變數,也可以像乙個微型程式一樣,在任意時刻都可以被呼叫。但是,block還存在這乙個致命的不足:

注意到之前的__main_block_func_0結構體,裡面有printf方法,用到了變數val,但是這個block,和最初block截獲的block,除了數值一樣,再也沒有一樣的地方了。參見這句**:

int

val = __cself->val;

當然這並沒有什麼影響,甚至還有好處,因為int val變數定義在棧上,在block呼叫時其實已經被銷毀,但是我們還可以正常訪問這個變數。但是試想一下,如果我希望在block中修改變數的值,那麼受到影響的是int val而非__cself->val,事實上即使是__cself->val,也只是截獲的自動變數的副本,要想修改在block定義之外的自動變數,是不可能的事情。這就是為什麼我把demo略作修改,增加一行**,但是輸出結果依然是」val = 0」。

//修改後的demo

typedef void (^block)(void);

block block;

; val = 1;

}block();

既然無法實現修改截獲的自動變數,那麼編譯器乾脆就禁止程式設計師這麼做了。

如果把val變數加上__block修飾符,編譯器會怎麼做呢?

//int val = 0; 原**

__block int val = 0;//修改後的**

編譯後的**:

struct __block_byref_val_0 ;

struct __main_block_impl_0

};struct void __main_block_func_0(struct __main_block_impl_0 *__cself)

改動並不大,簡單來說,只是把val封裝在了乙個結構體中而已。可以用下面這個圖來表示五個結構體之間的關係。

但是關鍵在於__main_block_impl_0結構體中的這一行:

__block_byref_val_0 *val;
由於__main_block_impl_0結構體中現在儲存了乙個指標變數,所以任何對這個指標的操作,是可以影響到原來的變數的。

進一步,我們考慮截獲的自動變數是objective-c的物件的情況。在開啟arc的情況下,將會強引用這個物件一次。這也保證了原物件不被銷毀,但與此同時,也會導致迴圈引用問題。

需要注意的是,在未開啟arc的情況下,如果變數附有__block修飾符,將不會被retain,因此反而可以避免迴圈引用的問題。

回到開篇的兩個問題,答案應該很明顯了。

由於無法直接獲得原變數,技術上無法實現修改,所以編譯器直接禁止了。

都可以用來讓變數在block中可以修改,但是在非arc模式下,__block修飾符會避免迴圈引用。注意:block的迴圈引用並非__block修飾符引起,而是由其本身的特性引起的。

vue sync 修飾符 理解

sync 修飾符 廢話不多說直接上 子元件 son.vue son v if show 傳過來的指是 所以是顯示的 p sonclick son 關閉 button div template export default script 父元件 father details childval styl...

CONST修飾符的理解

目前在進行c語言補習時,發現很多的同學對於const這個關鍵字的理解存在很大的誤解。現在總結下對這個關鍵字理解上的誤區,希望在以後的程式設計中,能夠靈活使用const這個關鍵字。1 const修飾的變數是常量還是變數 對於這個問題,很多同學認為const修飾的變數是不能改變,結果就誤認為該變數變成了...

CONST修飾符的理解

目前在進行c語言補習時,發現很多的同學對於const這個關鍵字的理解存在很大的誤解。現在總結下對這個關鍵字理解上的誤區,希望在以後的程式設計中,能夠靈活使用const這個關鍵字。1 const修飾的變數是常量還是變數 對於這個問題,很多同學認為const修飾的變數是不能改變,結果就誤認為該變數變成了...