iOS - block原理解讀(一)

前言

block在網絡上的文章也比較多,
本文將開發中block使用細節和block實現原理結合起來,
加上個人的理解,
幫助大家更好地理解block和使用block。

問題

  • block的意義
  • block為什么不能修改外部變量?這里的外部變量又指的是什么?
  • 被block引用的對象,引用計數為何+=2?
  • 循環引用究竟是為何引起的?
  • __block 又是什么原理?
  • block與copy

等等

block產生的意義

程序始終都要遵循逐行執行的原則,
而block 可以理解為 邏輯觸發執行
定時器 可以理解為 時間觸發執行

舉個有點意思的例子:

背景:我現在身處異世界,我有很多酷炫的技能
我現在有這樣一個技能,發動這個技能,我可以在當前這個地方留一個分身并安排好任務,然后我可以繼續做我自己的事情了,等我再次使用這個技能時,我的分身將開始處理這項任務,處理完成后,我的分身將會消失。

隱含的問題:給分身安排具體任務的時候,這個任務在未來是否能夠完成是未知的,因為我們不知道未來會發生什么,
代碼亦是如此

進一步理解:程序是嚴密而又真實的,所以并不存在什么高科技呀,黑魔法呀~
block其實就是用程序實現了代碼緩存和對象緩存,相應的對象緩存在block對象中,
執行block,就是把緩存的代碼執行一遍,而相應的對象的狀態,可能會因為執行完緩存下來的代碼而發生變化。

到此,希望你對block會產生了那么一點點的興趣~

依舊還是要從源碼說起


強調:以下講的變量a 默認指的是 自動變量auto,不是對象類型


#import <UIKit/UIKit.h>

int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        int a = 0;
        void (^block)(void) = ^{
            NSLog(@"%d",a);
        };
        
       block();

        return 0;
    }
}

將其翻譯成底層c++文件, 一點一點看
main函數里的代碼

int a = 10;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

這是一大串什么?
不要著急,我們從上到下,從左往右進行說明。
首先,block初始化這一行

// 原代碼
void (^block)(void) = ^{
       NSLog(@"%d",a);
 };
// 翻譯成c++代碼
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
// 簡化后
block =  &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA,a);
  1. void (*block)(void) 函數指針,參數void,返回值void

  2. ((void (*)()) 上面函數指針的類型,作用是強轉

  3. &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a)) 分解來看

分解第一步:__main_block_impl_0 組成

__main_block_impl_0是一個結構體,包含一個構造函數和三個變量
一個普通的結構體類型,一個結構體指針類型,
還有一個和外部的變量a,名稱一樣,類型一樣。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
分解第二步:__main_block_impl_0 的 構造函數

第一個參數void *fp,傳入的是 &__main_block_func_0

__main_block_func_0是一個靜態函數,
它的參數又是__main_block_impl_0這個結構體指針

即 FuncPtr 存儲的是 __main_block_func_0 靜態函數地址

impl.FuncPtr = fp;
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_lb_tby1gwds2fnb89dzkf4cq3xh0000gn_T_main_9bc6d9_mi_0,a);
}

是不是快要繞暈了呢?

在靜態函數__main_block_func_0內,
先是獲取__main_block_impl_0結構體內的變量a,然后打印出來。

從這一點可以看出 靜態函數的作用就是寫在block內部的代碼的容器和入口。

而__main_block_impl_0結構體內的變量a的值來自外部,是在結構體的構造函數內進行了賦值操作。

得到如下結論:

獲取到的基礎變量的值在block初始化的時候已經確定了,
block外部的變量a在后續無論做什么操作,都不會影響block內部保存的變量a
這就是在block內部直接修改自動變量會報錯的原因,
如果這里直接允許修改了,在目前條件下,也僅僅能做到block內部的變量a進行重新賦值操作,和外部變量a沒有關系,產生歧義。

示例圖.png
當然,使用__block修飾的自動變量可以進行修改,那么又是什么原理呢?我們先繼續解讀當前的源碼

第二個參數 &__main_block_desc_0
__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)};

第三個參數為我們用到的外部的變量a,賦值給結構體內的變量a

第四個參數沒有傳,默認為0 (輔助變量,可以忽略,不會影響block的理解)

分解第三步:__main_block_impl_0 中的 __block_impl

存儲的block的信息,相應的參數在構造函數內進行了賦值操作

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

最后 block初始化代碼和執行代碼放在一起看

void (^block)(void) = ^{
       NSLog(@"%d",a);
 };
// c++代碼
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
// 簡化
block =  &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a)

block();
// c++代碼
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
// 簡化
block->FuncPtr(block);

block->FuncPtr 指的就是 __main_block_func_0這個函數的地址,參數為block本身

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_lb_tby1gwds2fnb89dzkf4cq3xh0000gn_T_main_19f603_mi_0,a);
 }

串起來再看block構造

block初始化:block 通過 __main_block_impl_0結構體構造函數進行初始化,同時生成__main_block_func_0靜態函數,并將其地址以及其他相關信息儲存在__block_impl這個結構體成員變量中。
其中,__block_impl這個結構體成員變量是__main_block_impl_0的首地址。

block調用:block指針指向的是__main_block_impl_0 的首地址,即__block_impl的地址,所以可以強轉為(__block_impl *)類型,并訪問其成員FuncPtr,指向的是靜態函數地址,并傳入參數__main_block_impl_0,也就是block自己。

名稱 類型 是否隨block內容改變 生成順序 說明
__block_impl 結構體 NO 1 底層結構體,屬于__main_block_impl_0成員
__main_block_impl_0 結構體 YES 2 緩存變量/對象,主結構體
__main_block_func_0 靜態函數 YES 2 緩存代碼,地址存放在__block_impl中

該緩存代碼指的是:

緩存代碼一詞,代碼的意思.png

總結:
將外部變量/對象的信息緩存在__main_block_impl_0中,
將代碼緩存在靜態函數中,
靜態函數在緩存代碼的時候需要用到外部變量/對象的信息
執行block就是執行了該靜態函數


如果到此有不理解的地方可能c++基礎較薄弱,百度一下輔助查看


最后

本文說明了block的用意,揭開了黑魔法的初級面紗,細講了block基礎源碼,解釋了基礎類型的變量為何不能在block內部直接修改

后續會借此基礎之上,繼續解讀目錄中的問題

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 前言 Blocks是C語言的擴充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了這...
    小人不才閱讀 3,785評論 0 23
  • 面試題 block的原理是怎樣的?本質是什么? __block的作用是什么?有什么使用注意點? block的屬性修...
    xx_cc閱讀 13,537評論 10 77
  • 《Objective-C高級編程》這本書就講了三個東西:自動引用計數、block、GCD,偏向于從原理上對這些內容...
    WeiHing閱讀 9,890評論 10 69
  • 年前賀歲檔電影預售時我只買了《瘋狂的外星人》(預售票房第一位),完全忽略了《流浪地球》。 看了《瘋狂的外星人》,略...
    珊言三語閱讀 466評論 1 5
  • 我穿越時空來到一個完全陌生的城市,那是一個死氣沉沉的完全沒有生氣的城市,城市里有很多人,但是很奇怪大家都是黑白色的...
    盈昃閱讀 373評論 0 0