[iOS 理解] block

第一步,初步理解
網上博客千篇一律的內容,我找了一篇質量還不錯的
看完鏈接那篇,就可以應付大廠面試了,但還缺些東西。

// 必須完成第一步再看以下內容

第二步
上面鏈接的文章裏已經貼了大量源碼了,但是,閉包源碼的核心註釋,他沒貼
那50行註釋是真的精華,本文就是圍繞這點補充的。

官網鏈接,官網源碼不是 Xcode 項目
如果想看的舒服就要自己配 Xcode 關鍵字高亮、點擊跳轉
我這有配好的鏈接

block 核心文件只有一個,幾百行,幾個函數,讀者也可以自己分析(提示,原註釋必須真正理解)

下面是我的分析。。// 必須先完成第一步


閉包是一個結構體,當它捕獲外部變量時,一個變量要麼是 __block 聲明的,否則就剩:OC 對象,C++ 棧上對象,閉包對象,值類型。

關於值類型:比如 int 這類基礎數據類型,指針,結構體。指針也是?指針當然是,指針本質是 long 類型。OC 對象 id 不是 objc_object* 嗎?編譯器可以判斷出一個指針是不是 objc_object*,然後視爲 OC 對象。怎麼判斷?我想的一個辦法是取出 isa,比較 magic。。。扯遠了,總之,編譯器可以知道捕獲的一個變量是什麼類型的,就算手動欺騙編譯器,最壞的後果是內存泄露,沒必要。

C++ 棧上對象?堆上 new 出來的對象,因爲不像是 OC 是管理引用計數的,它只需要某個位置正確的 delete,所以傳給閉包時,只需要傳對象的指針,這個指針和 int* 和 int 沒區別,值類型。那 C++ 棧上對象不就和值類型結構體一樣嗎?但是,既然是對象這個概念,就要尊重對象可能的特殊行爲,C++對象恰好有拷貝構造函數,所以和結構體有一點點區別,需要特殊操作一下,具體操作後文會說。

__block 聲明的變量實際是一個結構體,閉包結構體裏保存了這個結構體的指針。所以轉移到堆的時候,必然要把變量結構體也複製到堆上。

說了這麼多,到底想表達什麼?
捕獲的變量保存在在閉包結構體內,閉包複製到堆上時,根據變量的類型、聲明方式需要不同的額外操作,或者不需要額外操作。原註釋:

A Block can reference four different kinds of things that require help when the Block is copied to the heap.

  1. C++ stack based objects
  2. References to Objective-C objects
  3. Other Blocks
  4. __block variables

上面四種之外,都不需要額外操作;否則編譯器會生成 copy 和 dispose。如果捕獲的是 C++棧上對象,函數內會調用 const 拷貝構造,閉包釋放時調用析構函數;剩下三種在 copy 和 dispose 裏調用 _Block_object_assign(_Block_object_assign 後面會說)。也就是第二段註釋:

In these cases helper functions are synthesized by the compiler for use in Block_copy and Block_release, called the copy and dispose helpers. The copy helper emits a call to the C++ const copy constructor for C++ stack based objects and for the rest calls into the runtime support function _Block_object_assign. The dispose helper has a call to the C++ destructor for case 1 and a call into _Block_object_dispose for the rest.

剩下這三種,都調用 _Block_object_assign,編譯器具體怎麼區分的?直接定一個 flag 作參數,傳給 _Block_object_assign,分情況討論唄。flag:
OBJECT(3,二進制11)
BLOCK(7,二進制111)
BYREF(8,二進制1000),BYREF 就是 __block variables

註釋在這,註釋下面先回顧一下閉包複製。對了,這些需要被幫的,閉包結構體的 flag 會打開標誌位 BLOCK_HAS_COPY_DISPOSE

The flags parameter of _Block_object_assign and _Block_object_dispose is set to
* BLOCK_FIELD_IS_OBJECT (3), for the case of an Objective-C Object,
* BLOCK_FIELD_IS_BLOCK (7), for the case of another Block, and
* BLOCK_FIELD_IS_BYREF (8), for the case of a __block variable.
If the __block variable is marked weak the compiler also or’s in BLOCK_FIELD_IS_WEAK (16).

(爲什麼 只有 __block 配 __weak,變成 8 + 16,二進制 11000?
爲什麼 __weak 配 id 這麼正常的情況,註釋沒寫???)

回顧 block copy

假設一個閉包 block,[block copy] 就是 objc_retainBlock(block)。

打開 runtime 源碼搜索:
objc_retainBlock
	_Block_copy(block)
		// 用我配好的鏈接可以直接點擊跳轉到實現
		// 跳轉到 block 源碼
		_Block_copy_internal
			如果是堆 block,block 的引用計數 +1
			// block 的引用計數不能通過 CFGetRetainCount 獲得
			// 是自己通過 flags 管理的
			如果是全局 block,直接返回 block
			否則就是棧 block:
				malloc,memmove
				設置 flag 標誌位:堆 block,引用計數置 0
				設置 isa 爲堆 block
				如果 BLOCK_HAS_COPY_DISPOSE:
					descriptor->copy(dst, src),
				返回堆 block  
			

執行 descriptor->copy(dst, src),注意這是閉包的(閉包的描述符的,給閉包結構體用的),一會變量結構體也可能有,不要混了。
閉包(的描述符中)的 copy,根據前面的分析,會分情況討論設置不同的 flag:OBJECT (3) BLOCK (7) BYREF (8或 weak24), 作爲參數傳給內部調用的:
void _Block_object_assign(void *destAddr, const void *object, const int flags);

如果 flag 是 7,就說明捕獲了一個閉包對象,操作就是,對閉包執行 copy 然後 assign 到自己閉包結構體裏,因爲執行了 copy,所以如果閉包內調用自己會循環引用,即遞歸調用,解決辦法就是在遞歸終止時讓閉包指針 = 0。
(這個 assign 單詞很有意思,“指定”,就是當作值類型指定,很霸道的感覺。。)

如果 flag 是 3,說明捕獲了一個 OC 對象,操作是執行 retain 然後 assign。
(前面提出了一個關於 __weak id 的問題,按照上面的邏輯,flag 爲 3,執行 retain???我們知道 ARC 下,閉包內使用 __weak id 是不會增加引用計數的,但是這裏卻???說明編譯器肯定在走到閉包源碼之前就做了一些工作了,有興趣的去研究吧,本文就不提 __weak id 了)

如果 flag 是 8,好玩的來了。如果 __block id 咋辦?還有 __block __weak id 呢?先看原註釋(後面也有中文的):

When a __block variable is either a C++ object, an Objective-C object, or another Block then the compiler also generates copy/dispose helper functions. Similarly to the Block copy helper, the “__block” copy helper (formerly and still a.k.a. “byref” copy helper) will do a C++ copy constructor (not a const one though!) and the dispose helper will do the destructor. And similarly the helpers will call into the same two support functions with the same values for objects and Blocks with the additional BLOCK_BYREF_CALLER (128) bit of information supplied.

So the __block copy/dispose helpers will generate flag values of 3 or 7 for objects and Blocks respectively, with BLOCK_FIELD_IS_WEAK (16) or’ed as appropriate and always 128 or’d in, for the following set of possibilities:
__block id 128+3
__weak __block id 128+3+16
__block (^Block) 128+7
__weak __block (^Block) 128+7+16

flag 爲 8或24,說明引用的變量是 by_ref 結構體(變量結構體),操作是調用
_Block_byref_assign_copy(destAddr, object, flags);
目的是把變量結構體複製到堆。代碼在下面,接下來這段話結合代碼看。複製此結構體講道理應該很簡單,開闢內存,複製內存,這對於 __block int 的確是這麼簡單。但是最後要判斷是不是複雜的 __block id,__block C++ 對象,__block 閉包對象。

如果是這種複雜變量結構體,變量結構體也會有自己的 flag,也有標誌位 BLOCK_HAS_COPY_DISPOSE,作用是,在變量結構體複製最後,如果有 BLOCK_HAS_COPY_DISPOSE,那就調用 變量結構體的 copy,copy 內調用
void _Block_object_assign(void *destAddr, const void *object, const int flags);
(如果是C++,調用非const拷貝構造)(如果flag是24,變量結構體標誌爲 weak)

前面見過這個函數啊,怎麼又來了?再捋一捋,前面閉包複製的時候,根據 3 7 8(24) 執行不同操作,現在是 8(24) 的子操作,執行 8(24) 的時候,內部要複製變量結構體,複製最後按需執行 變量結構體的 copy

這次調用 _Block_object_assign 時,編譯器給變量結構體的 flags 是都加了 128 的
比如 __block id 是 128+3 = 131,(是不是第一步裏見過 131?)
__weak __block id 128+3+16
__block (^Block) 128+7
__weak __block (^Block) 128+7+16

_Block_object_assign 判斷 flag 是 128 系列的,不管有沒有 weak,都直接 assign,不會再 retain,因爲是在聲明 __block 變量,產生變量結構體時,已經 retain 過的。

_Block_byref_assign_copy

_Block_byref_assign_copy(dst*, src, flag)
	如果該變量結構體已經在堆上(通過引用計數判斷)
		引用計數 +1 (有可能多個閉包捕獲一個 __block 變量)
	否則複製到堆:
		malloc 
		設置 flag
		設置 forwarding
		設置 size 等值,更新 src 的 forwarding
		如果有 BLOCK_HAS_COPY_DISPOSE
			執行變量結構體的 copy(改名了,byref_keep)
		*dst = src->forwarding 

block release 逆過程

閉包引用計數 -1
如果到 0 了 且是堆上閉包:
如果有 BLOCK_HAS_COPY_DISPOSE,調用 block 的 dispose
free

dispose 內 _Block_object_dispose 或調用 C++ 析構

_Block_object_dispose:

如果是 單7,對捕獲的閉包調用一次 block release
如果是 單3,release
如果是 8,__block 聲明的,變量結構體,執行 _Block_byref_release(object); 總體工作是讓變量結構體計數 -1。到 0 了 free、執行 by_ref 的 dispose,注意兩個 dispose 區分,和上面分析 block copy 的過程逆過程。

_Block_byref_release

前面說過有可能多個閉包捕獲一個 變量結構體
變量結構體有自己的引用計數

變量結構體引用計數 -1
如果計數到 0 了
	如果有 BLOCK_HAS_COPY_DISPOSE	
		執行變量結構體的 dispose (改名 byref_destroy)
	free

變量結構體的 dispose,調用 _Block_object_dispose,flag 是加了 128 的,對應什麼操作呢?無操作。爲什麼?因爲之前 _Block_object_assign 判斷 flag 是 128 系列是直接 assign 的,所以釋放的時候要對稱,不用任何操作。

再次提醒 assign 這個單詞,指定,直白的指定,像對待值類型一樣。
就像是老師佈置的作業叫 assignment,指給你的任務就必須做。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章