日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

探索 Block 的本质

發布時間:2025/3/20 编程问答 44 豆豆
生活随笔 收集整理的這篇文章主要介紹了 探索 Block 的本质 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

定義

  • Block 是 C 語言的擴充功能
  • Block 是帶有自動變量(局部變量)的匿名函數

本質

  • Block 是一個 Objc 對象

底層實現

下面我將通過一個簡單的例子,結合源代碼進行介紹

int main(int argc, const char * argv[]) {void (^blk)(void) = ^{ printf("Hello Block\n"); };blk();return 0; } 復制代碼

使用clang -rewrite-objc main.m,我們可以將 Objc 的源碼轉成 Cpp 的相關源碼:

int main(int argc, const char * argv[]) {// Block 的創建void (*blk)(void) =(void (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);// Block 的使用((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);return 0; } 復制代碼

由上面的源碼,我們能猜想到:

  • Block 的創建涉及__main_block_impl_0結構體
  • Block 的涉及到了FuncPtr函數指針的調用

從這里為切入點看看上面提到的都是啥

Block 的數據結構

Block 的真身:

struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;// 省略了構造函數 }; 復制代碼
  • Block 其實不是一個匿名函數,他是一個結構體
  • __main_block_impl_0名字的命名規則: __所在函數_block_impl_序號

impl 變量的數據結構

__main_block_impl_0的主要數據:

struct __block_impl {void *isa;int Flags;int Reserved;void *FuncPtr; }; 復制代碼
  • isa指針: 體現了 Block 是 Objc 對象的本質
  • FuncPtr指針: 其實就是一個函數指針,指向所謂的匿名函數。

Desc 變量的數據結構

__main_block_desc_0中放著 Block 的描述信息

static struct __main_block_desc_0 {size_t reserved;size_t Block_size; } __main_block_desc_0_DATA = {0,sizeof(struct __main_block_impl_0) }; 復制代碼

"匿名函數"

__main_block_impl_0即 Block 創建時候使用到了__main_block_func_0正是下面的函數:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {printf("Hello Block\n"); } 復制代碼
  • 這部分和^{ printf("Hello Block\n"); }十分相似,由此可看出: 通過 Blocks 使用的匿名函數實際上被作為簡單的 C 語言函數來處理
  • 函數名是根據 Block 語法所屬的函數名(此處main)和該 Block 語法在函數出現的順序值(此處為 0)來命名的。
  • 函數的參數__cself相當于 C++ 實例方法中指向實例自身的變量this,或是 Objective-C 實例方法中指向對象自身的變量self,即參數__cself為指向 Block 的變量。
  • 上面的(*blk->impl.FuncPtr)(blk);中的blk就是__cself

介紹了基本的數據結構,下面到回到一開始的main函數,看看 Block 具體的使用

Block 的創建

void (*blk)(void) =(void (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA); /** 去掉轉換的部分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; */ 復制代碼
  • void (^blk)(void)就是是一個struct __main_block_impl_0 *blk
  • Block 表達式的其實就是通過所謂的匿名函數__main_block_func_0的函數指針創建一個__main_block_impl_0結構體,我們用的時候是拿到了這個結構體的指針。

Block 的使用

((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk); /** 去掉轉換的部分(*blk->impl.FuncPtr)(blk); */ 復制代碼
  • Block 真正的使用方法就是使用__main_block_impl_0中的函數指針FuncPtr
  • (blk)這里是傳入自己,就是給_cself傳參

Block 的類型

從 Block 中的簡單實現中,我們從isa中發現 Block 的本質是 Objc 對象,是對象就有不同類型的類。因此,Block 當然有不同的類型

在 Apple 的libclosure-73中的data.c上可見,isa可指向:

void * _NSConcreteStackBlock[32] = { 0 }; // 棧上創建的block void * _NSConcreteMallocBlock[32] = { 0 }; // 堆上創建的block void * _NSConcreteAutoBlock[32] = { 0 }; void * _NSConcreteFinalizingBlock[32] = { 0 }; void * _NSConcreteGlobalBlock[32] = { 0 }; // 作為全局變量的block void * _NSConcreteWeakBlockVariable[32] = { 0 }; 復制代碼

其中我們最常見的是:

Block的類型名稱行為存儲位置
_NSConcreteStackBlock棧Block捕獲了局部變量
_NSConcreteMallocBlock堆Block對棧Block調用copy所得
_NSConcreteGlobalBlock全局Block定義在全局變量中常量區(數據段)

PS: 內存五大區:棧、堆、靜態區(BSS 段)、常量區(數據段)、代碼段

關于 copy 操作

對象有copy操作,Block 也有copy操作。不同類型的 Block 調用copy操作,也會產生不同的復制效果:

Block的類型副本源的配置存儲域復制效果
_NSConcreteStackBlock從棧復制到堆
_NSConcreteGlobalBlock常量區(數據段)什么也不做
_NSConcreteMallocBlock引用計數增加

棧上的 Block 復制到堆上的時機

  • 調用 Block 的copy實例方法

編譯器自動調用_Block_copy函數情況

  • Block 作為函數返回值返時
  • 將 Block 賦值給 __strong 指針(id或 Block 類型成員變量)
  • 在 Apple 的 Cocoa、GCD 等 api 中傳遞 Block 時

PS: 在 ARC 環境下,聲明的 Block 屬性用copy或strong修飾的效果是一樣的,但在 MRC 環境下用 copy 修飾。

捕獲變量

基礎類型變量

以全局變量、靜態全局變量、局部變量、靜態局部變量為例:

int global_val = 1; static int static_global_val = 2;int main(int argc, const char * argv[]) {int val = 3;static int static_val = 4;void (^blk)(void) = ^{printf("global_val is %d\n", global_val);printf("static_global_val is %d\n", static_global_val);printf("val is %d\n", val);printf("static_val is %d\n", static_val);};blk();return 0; } 復制代碼

轉換后“匿名函數”對應的代碼:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {int val = __cself->val; // bound by copyint *static_val = __cself->static_val; // bound by copyprintf("global_val is %d\n", global_val);printf("static_global_val is %d\n", static_global_val);printf("val is %d\n", val);printf("static_val is %d\n", (*static_val)); } 復制代碼
  • 全局變量、靜態全局變量: 作用域為全局,因此在 Block 中是直接訪問的。
  • 局部變量: 生成的__main_block_impl_0中存在val實例,因此對于局部變量,Block 只是單純的復制創建時候局部變量的瞬時值,我們可以使用值,但不能修改值。
struct __main_block_impl_0 {// ...int val; // 值傳遞// ... }; 復制代碼
  • 靜態局部變量: 生成的__main_block_impl_0中存在static_val指針,因此 Block 是在創建的時候獲取靜態局部變量的指針值
struct __main_block_impl_0 {// ...int *static_val; // 指針傳遞// ... }; 復制代碼

對象類型變量

模仿基礎類型變量,實例化四個不一樣的SCPeople變量:

int main(int argc, const char * argv[]) {// 省略初始化[globalPeople introduce];[staticGlobalPeople introduce];[people introduce];[staticPeople introduce];return 0; } 復制代碼

轉換后"匿名函數"對應的代碼:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {SCPeople *people = __cself->people; // bound by copySCPeople **staticPeople = __cself->staticPeople; // bound by copy// 省略 objc_msgSend 轉換[globalPeople introduce];[staticGlobalPeople introduce];[people introduce];[*staticPeople introduce]; } 復制代碼
  • 全局對象、靜態全局對象: 作用域依然是全局,因此在 Block 中是直接訪問的。
  • 局部對象: 生成的__main_block_impl_0中存在people指針實例,因此 Block 獲取的是指針瞬間值,我們可以在 Block 中通過指針可以操作對象,但是不能改變指針的值。
struct __main_block_impl_0 {// ...SCPeople *people;// ... }; 復制代碼
  • 靜態局部對象: 生成的__main_block_impl_0中存在staticPeople指針的指針,因此 Block 是在創建的時候獲取靜態局部對象的指針值(即指針的指針)。
struct __main_block_impl_0 {// ...SCPeople **staticPeople;// ... }; 復制代碼

小結

通過對基礎類型、對象類型與四種不同的變量進行排列組合的小 Demo,不難得出下面的規則:

變量類型是否捕獲到 Block 內部訪問方式
全局變量直接訪問
靜態全局變量直接訪問
局部變量值訪問
靜態局部變量指針訪問

PS:

  • 基礎類型和對象指針類型其實是一樣的,只不過指針的指針看起來比較繞而已。
  • 全局變量與靜態全局變量的存儲方式、生命周期是相同的。但是作用域不同,全局變量在所有文件中都可以訪問到,而靜態全局變量只能在其申明的文件中才能訪問到。

變量修改

上面的篇幅通過底層實現,向大家介紹了 Block 這個所謂"匿名函數"是如何捕獲變量的,但是一些時候我們需要修改 Block 中捕獲的變量:

修改全局變量或靜態全局變量

全局變量與靜態全局變量的作用域都是全局的,自然在 Block 內外的變量操作都是一樣的。

修改靜態局部變量

在上面變量捕獲的章節中,我們得知 Block 捕獲的是靜態局部變量的指針值,因此我們可以在 Block 內部改變靜態局部變量的值(底層是通過指針來進行操作的)。

修改局部變量

使用__block修飾符來指定我們想改變的局部變量,達到在 Block 中修改的需要。

我們用同樣的方式,通過底層實現認識一下__block,舉一個?:

__block int val = 0; void (^blk)(void) = ^{ val = 1; }; blk(); 復制代碼

經過轉換的代碼中出現了和單純捕獲局部變量不同的代碼:

__Block_byref_val_0結構體

struct __Block_byref_val_0 {void *__isa; // 一個 Objc 對象的體現__Block_byref_val_0 *__forwarding; // 指向該實例自身的指針int __flags;int __size;int val; // 原局部變量 }; 復制代碼
  • 編譯器會將__block修飾的變量包裝成一個 Objc 對象。

val轉換成__Block_byref_val_0

__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val,0,sizeof(__Block_byref_val_0),0 }; 復制代碼

__main_block_impl_0捕獲的變量

struct __main_block_impl_0 {// ...__Block_byref_val_0 *val; // by ref// ... }; 復制代碼
  • Block的__main_block_impl_0結構體實例持有指向__block變量的__Block_byref_val_0結構體實例的指針。
  • 這個捕獲方式和捕獲靜態局部變量相似,都是指針傳遞

"匿名函數"的操作

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {__Block_byref_val_0 *val = __cself->val; // bound by ref(val->__forwarding->val) = 1; } 復制代碼

(val->__forwarding->val) 解釋

  • 左邊的val是__main_block_impl_0中的val,這個val通過__block int val的地址初始化
  • 右邊的val是__Block_byref_val_0中的val,正是__block int val的val
  • __forwarding在這里只是單純指向了自己而已

__forwarding 的存在意義

上面的"棧Blcok"中__forwarding在這里只是單純指向自己,但是在當"棧Blcok"復制變成"堆Block"后,__forwarding就有他的存在意義了:

PS:__block修飾符不能用于修飾全局變量、靜態變量。

內存管理

Block 與對象類型

copy & dispose

眾所周知,對象其實也是使用一個指針指向對象的存儲空間,我們的對象值其實也是指針值。雖然是看似對象類型的捕獲與基礎類型的指針類型捕獲差不多,但是捕獲對象的轉換代碼比基礎指針類型的轉換代碼要多。(__block變量也會變成一個對象,因此下面的內容也適用于__block修飾局部變量的情況)。多出來的部分是與內存管理相關的copy函數與dispose函數:

底層實現

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->people, (void*)src->people, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_assign((void*)&dst->staticPeople, (void*)src->staticPeople, 3/*BLOCK_FIELD_IS_OBJECT*/); }static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->people, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_dispose((void*)src->staticPeople, 3/*BLOCK_FIELD_IS_OBJECT*/); } 復制代碼

這兩個函數在 Block 數據結構存在于Desc變量中:

static struct __main_block_desc_0 {// ...void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);void (*dispose)(struct __main_block_impl_0*); }; // 省略了初始化好的結構體 復制代碼

函數調用時機

函數調用時機
copy 函數棧上的 Block 復制到堆時
dispose 函數堆上的 Block 被廢棄時

函數意義

  • copy函數中的_Block_object_assign函數相當于內存管理中的retain函數,將對象賦值在對象類型的結構體成員變量中。
  • dispose函數中的_Block_object_dispose函數相當于內存管理中的release函數,釋放賦值在對象類型的結構體變量中的對象。
  • 通過copy和dispose并配合 Objc 運行時庫對其的調用可以實現內存管理

※ 例子

當 Block 內部訪問了對象類型的局部變量時:

  • 當 Block 存儲在棧上時: Block 不會對局部變量產生強引用。
  • 當 Block 被copy到堆上時: Block 會調用內部的copy函數,copy函數內部會調用_Block_object_assign函數,_Block_object_assign函數會根據局部變量的修飾符(__strong、__weak、__unsafe_unretained)作出相應的內存管理操作。(注意: 多個 Block 對同一個對象進行強引用的時,堆上只會存在一個該對象)
  • 當 Block 從堆上被移除時: Block 會調用內部的dispose函數,dispose函數內部會調用_Block_object_dispose函數,_Block_object_dispose函數會自動release引用的局部變量。(注意: 直到被引用的對象的引用計數為 0,這個堆上的該對象才會真正釋放)

PS: 對于__block變量,Block 永遠都是對__Block_byref_局部變量名_0進行強引用。如果__block修飾符背后還有其他修飾符,那么這些修飾符是用于修飾__Block_byref_局部變量名_0中的局部變量的。

現象: Block 中使用的賦值給附有__strong修飾符的局部變量的對象和復制到堆上的__block變量由于被堆的 Block 所持有,因而可超出其變量作用域而存在。

循環引用

由于 Block 內部能強引用捕獲的對象,因此當該 Block 被對象強引用的時候就是注意以下的引用循環問題了:

ARC 環境下解決方案

  • 弱引用持有:使用__weak或__unsafe_unretained捕獲對象解決

    • weak修飾的指針變量,在指向的內存地址銷毀后,會在 Runtime 的機制下,自動置為nil。
    • _unsafe_unretained不會置為nil,容易出現懸垂指針,發生崩潰。但是_unsafe_unretained比__weak效率高。
  • 使用__block變量:使用__block修飾對象,在 block 內部用完該對象后,將__block變量置為nil即可。雖然能控制對象的持有期間,并且能將其他對象賦值在__block變量中,但是必須執行該 block。(意味著這個對象的生命周期完全歸我們控制)

  • MRC 環境下解決方案

  • 弱引用持有:使用__unsafe_unretained捕獲對象
  • 直接使用__block修飾對象,無需手動將對象置為nil,因為底層_Block_object_assign函數在 MRC 環境下對 block 內部的對象不會進行retain操作。
  • MRC 下的 Block

    ARC 無效時,需要手動將 Block 從棧復制到堆,也需要手動釋放 Block

    • 對于棧上的 Block 調用retain實例方法是不起作用的
    • 對于棧上的 Block 需要調用一次copy實例方式(引用計數+1),將其配置在堆上,才可繼續使用retain實例方法
    • 需要減少引用的時候,只需調用release實例方法即可。
    • 對于在 C 語言中使用 Block,需要使用Block_copy和Block_release代替copy和release。

    轉載于:https://juejin.im/post/5c6dfd3d6fb9a049dd80e215

    總結

    以上是生活随笔為你收集整理的探索 Block 的本质的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。