探索 Block 的本質

定義

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

本質

  • Block 是一個 Objc 對象

底層實現

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

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

使用clang -rewrite-objc main.m,咱們能夠將 Objc 的源碼轉成 Cpp 的相關源碼:bash

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 的真身:ui

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的主要數據:this

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

Desc 變量的數據結構

__main_block_desc_0中放着 Block 的描述信息spa

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正是下面的函數:3d

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 固然有不一樣的類型code

在 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 屬性用copystrong修飾的效果是同樣的,但在 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 copy
    int *static_val = __cself->static_val; // bound by copy

    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));
}
複製代碼
  • 全局變量、靜態全局變量: 做用域爲全局,所以在 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 copy
    SCPeople **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 valval
  • __forwarding在這裏只是單純指向了本身而已
    val->__forwarding->val

__forwarding 的存在乎義

上面的"棧Blcok"中__forwarding在這裏只是單純指向本身,可是在當"棧Blcok"複製變成"堆Block"後,__forwarding就有他的存在乎義了:

__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函數,釋放賦值在對象類型的結構體變量中的對象。
  • 經過copydispose並配合 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 環境下解決方案

  1. 弱引用持有:使用__weak__unsafe_unretained捕獲對象解決

    弱引用持有

    • weak修飾的指針變量,在指向的內存地址銷燬後,會在 Runtime 的機制下,自動置爲nil
    • _unsafe_unretained不會置爲nil,容易出現懸垂指針,發生崩潰。可是_unsafe_unretained__weak效率高。
  2. 使用__block變量:使用__block修飾對象,在 block 內部用完該對象後,將__block變量置爲nil便可。雖然能控制對象的持有期間,而且能將其餘對象賦值在__block變量中,可是必須執行該 block。(意味着這個對象的生命週期徹底歸咱們控制)

    使用`__block`變量

MRC 環境下解決方案

  1. 弱引用持有:使用__unsafe_unretained捕獲對象
  2. 直接使用__block修飾對象,無需手動將對象置爲nil,由於底層_Block_object_assign函數在 MRC 環境下對 block 內部的對象不會進行retain操做。

MRC 下的 Block

ARC 無效時,須要手動將 Block 從棧複製到堆,也須要手動釋放 Block

  • 對於棧上的 Block 調用retain實例方法是不起做用的
  • 對於棧上的 Block 須要調用一次copy實例方式(引用計數+1),將其配置在堆上,纔可繼續使用retain實例方法
  • 須要減小引用的時候,只需調用release實例方法便可。
  • 對於在 C 語言中使用 Block,須要使用Block_copyBlock_release代替copyrelease
相關文章
相關標籤/搜索