天天看點

Block筆記(3)—— 基礎類型的變量捕獲

Block系列文章—————————————

Block筆記(1)—— 基本認識

Block筆記(2)—— 底層結構

Block筆記(3)—— 基礎類型的變量捕獲

Block筆記(4)—— Block的類型

Block筆記(5)—— 對象類型的auto變量捕獲

Block筆記(6)— __block的深入分析

————————————————————

上一章節裡面,我們分析了一下胚胎版的Block的底層結構。現在我們加點料進去

#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //Block的定義
        void (^block)(int, int) = ^(int a, int b){
            NSLog(@"I am a block! - %d - %d", a, b);
        };
        //Block的調用
        block(10, 20);
    }
    return 0;
}
****************************** 日志輸出 *******************************
2019-06-04 15:30:57.747093+0800 Interview03-block[3915:354992] I am a block! - 10 - 20
Program ended with exit code: 0
           

這裡我們給block所封裝的函數增加兩個參數a、b,還是慣例,通過指令

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp

然後打開編譯後的c++檔案

Block筆記(3)—— 基礎類型的變量捕獲

很明顯,參數a、b也被封裝進了block中,這種情況也比較簡單,過一下就好。

Block捕獲auto變量

接下來看看這種情況

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        //Block的定義
        void (^block)(void) = ^(){
            NSLog(@"Age is %d", age);
        };
        //先修改age的值
        age = 20;
        //Block的調用
        block();
    }
    return 0;
}
           

我在block之前定義了一個

int a = 10

,然後在block内部使用了這個

age

,而且我在調用block之前,先将

age

的值修改成了

20

,那麼此時程式運作會是什麼結果呢?相信以大部分人對block的了解,應該都能給出正确答案。

2019-06-04 15:46:01.244557+0800 Interview03-block[4064:375528] Age is 10
Program ended with exit code: 0
           

結果是block中列印出的

age

是10,我們在block外部對

age

的修改結果并沒有對block的内部列印産生影響,為什麼呢?我們同樣,借助編譯後的c++檔案來看一看。

(1)首先看一下此時block對應的結構體
Block筆記(3)—— 基礎類型的變量捕獲
我們發現有三處變化
  • 新增了一個

    int age

    成員變量
  • 構造函數裡面多了一個參數

    int _age

  • 構造函數裡面參數尾部多了一個

    : age(_age)

    ,這是c++的文法,作用是将參數

    _age

    自動指派給成員變量

    int age

(2)然後在看一下main函數中的block定義以及指派的代碼
Block筆記(3)—— 基礎類型的變量捕獲
在用

block

構造函數生成

block

的時候,使用了外部定義的

int a = 10

,因為c函數的參數都是值傳遞,是以這裡是将此時外部變量

a

的值

10

傳給了

block

的構造函數

__main_block_impl_0

,是以block内部的成員變量

age

會被指派成

10

(3)再看一下block内部封裝的函數__main_block_func_0
Block筆記(3)—— 基礎類型的變量捕獲
可以看到列印代碼裡面使用的

age

,實際上就是block内部的成員變量

age

,不是我們在外面定義的那個

age

,是以,當block被指派之後,其成員變量

age

被指派成了當時構造函數傳進來的參數

10

,是以最終列印出來值就是

10

,不論外部的

age

再如何的修改。外部的

age

跟block的成員變量

age

是兩個不同的變量,互不影響。
其實,上面我門讨論的這個block外部變量

age

是一個局部變量,也叫自動變量(auto變量),這是C語言的知識點,如果有不清楚的請自行補腦。我們知道除了

auto變量

,C語言裡面還有局部

static變量

(靜态變量)和全局變量,接下來我們就看看,Block對于這幾種變量的使用,做了如何的處理。

Block捕獲局部static變量

首先我們将上面的OC代碼改造如下

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        static int height = 10;
        //Block的定義
        void (^block)(void) = ^(){
            NSLog(@"Age is %d, height is %d", age, height);
        };
        //先修改age和height的值
        age = 20;
        height = 20;
        //Block的調用
        block();
    }
    return 0;
}
           

我們有增加了一個

static

變量

height

,并且在同樣的地方修改

height

的值,便于和之前的

age

進行對比。首先運作代碼看一下結果

2019-06-04 17:10:12.935220+0800 Interview03-block[4725:476530] Age is 10, height is 20
Program ended with exit code: 0
           

可以看到,block輸出的

height

值是我們在外部重新為其賦的

20

為什麼呢,我們進入編譯後的C++檔案一看究竟

(1)借用上面的分析流程一樣,先看一下block對應的結構體
Block筆記(3)—— 基礎類型的變量捕獲
你看,針對static變量height, block内部為其增加了一個

int *height;

成員變量,構造函數裡面對應的參數是

int *_height

。看到這裡,應該就大緻能猜出來,我們這裡要存儲的是一個位址,該位址應該就是外部

static

變量

height

的位址值。
(2)那我們來看一下main函數裡的block指派過程
Block筆記(3)—— 基礎類型的變量捕獲
很清晰,确實block構造函數裡面傳入的,就是外部的這個height的位址值。
(3)最後看block内部的函數
Block筆記(3)—— 基礎類型的變量捕獲
那麼可以看到,block内部的函數也是通過block所存儲的位址值

*height

通路了外部的

static

變量

height

的值。

是以,當我們從外部修改

height

的值之後,調用block列印出的

height

的值也相應的改變了,因為block内部是通過 指針 引用了外部的這個

static

變量

height

❓思考❓對于

auto

static

變量,為什麼block選擇用不同方式處理它們呢?
我們知道,一個自動變量(

auto

)的存儲空間位于函數棧空間上,在函數開辟棧空間時被建立,在函數結束時銷毀,而block的調用時機有可能發生在函數結束之後的,是以就無法使用自動變量了,是以在block一開始定義指派的過程裡,就将自動變量的值拷貝到他自己的存儲空間上。

而對于局部靜态變量(

static

),C文法下

static

會改變所修飾的局部變量的生命周期,使其在 程式整個運作期間都存在 ,是以block選擇持有它的指針,在block被調用時,通過該指針通路這個變量的内容就行。

Block使用全局變量

上面我們讨論block對于局部變量的處理,我們在看一看對于全局變量,情況又是如何

Block筆記(3)—— 基礎類型的變量捕獲

輸出結果如下

2019-06-05 09:19:08.854599+0800 Interview03-block[13997:1263406] Age is 20, height is 20
Program ended with exit code: 0
           

在通過指令行生成一下編譯後的C++檔案,同樣還是在檔案底部去看

Block筆記(3)—— 基礎類型的變量捕獲

這次就非常痛快了,block沒有對全局變量進行捕獲行為,隻需要在要用的時候,直接通過變量名通路就行了,因為全局變量是跨函數的,是以可以直接通過變量的名字直接通路。

同樣,這也幫我我們了解了為什麼對于局部的變量,block需要對其采取“捕獲”行為,正是因為局部變量定在與函數内部,無法跨函數使用,是以根據局部變量不同的存儲屬性,要麼将其值直接進行拷貝(

auto

),要麼對其位址進行拷貝(

static

)。

總結
Block筆記(3)—— 基礎類型的變量捕獲

  1. 局部變量會被block捕獲
  • 自動變量(

    auto

    ),block通過值拷貝方式捕獲,在其内部建立一個同類型變量,并且将自動變量的值拷貝給block的内部變量,block代碼塊執行的時候,直接通路它的這個 内部變量。
  • 靜态變量(

    static

    ),block通過位址拷貝方式捕獲,在其内部建立一個指向同類型變量的指針, 将靜态變量的位址值拷貝給block内部的這個指針,block代碼塊執行的時候,通過内部存儲的指針 間接通路 靜态變量。
  1. 全局變量不會被block捕獲, block代碼塊執行的時候,通過全局變量名 直接通路。
Block對于self的處理
Block筆記(3)—— 基礎類型的變量捕獲

請問上圖block裡面的

self

會被該block捕獲嗎?

Block筆記(3)—— 基礎類型的變量捕獲

編譯結果顯示block對

self

進行了捕獲。But why?

我們知道,圖中的block位于

test

方法裡面,實際上任何的oc方法,轉換成底層的c函數,裡面都有兩個預設的參數,

self

_cmd

Block筆記(3)—— 基礎類型的變量捕獲

,是以作為函數預設參數的

self

的實際上也是該函數的局部變量,根據我們上面總結的原則,隻要是局部變量,block都會對其進行捕獲,這就解釋通了。

那麼有人會問(特别是面試官)下面的情況呢

Block筆記(3)—— 基礎類型的變量捕獲

先看編譯結果

Block筆記(3)—— 基礎類型的變量捕獲

看得出來,還是進行了捕獲,看我在圖中标明的黃色框框,就很好了解了,block最終通路

CLPerson

的成員變量

_age

的時候,是通過

self

+

_age偏移量

,獲得

_age

的位址後進而進行間接通路的,是以在oc代碼中,

_age

的寫法等同與

self->_age

,說白了,這裡還是需要用到

self

,是以block還是需要對

self

進行捕獲的。

至此,有關Block對于基礎類型環境變量的處理以及調用過程,就整理完畢了。
Block系列文章—————————————

Block筆記(1)—— 基本認識

Block筆記(2)—— 底層結構

Block筆記(3)—— 基礎類型的變量捕獲

Block筆記(4)—— Block的類型

Block筆記(5)—— 對象類型的auto變量捕獲

Block筆記(6)— __block的深入分析

————————————————————