1. 程式人生 > 其它 >iOS-block如何捕獲外部變數(基本資料型別變數)

iOS-block如何捕獲外部變數(基本資料型別變數)

Block如何捕獲外部變數一:基本資料型別

共有三種:auto變數 、static變數、全域性變數

這一篇,我們詳細講解Block捕獲外部變數的機制.我們把block捕獲外部基本資料型別變數的情況分為以下幾種,見下圖:

一:auto變數

auto變數:自動變數,離開作用域就會銷燬,一般我們建立的區域性變數都是auto變數,比如int age = 10,系統會在預設在前面加上auto int age = 10
首先我們要搞清楚,什麼是捕獲,所謂捕獲外部變數,意思就是在block內部,建立一個變數來存放外部變數,這就叫做捕獲.先做一個小小的Test:

    {
        
int age = 10; void (^block)(void) = ^{ NSLog(@"age is %d",age); }; age = 20; block(); }

輸出的age是10

定義一個age變數,在block內部訪問這個age,在呼叫block之前,修改這個age值,那麼最後輸出的age是多少呢?很簡單,輸出的age還是10,相信很多人都知道結果,我們從底層來看一下為什麼會這樣.

上面程式碼通過Clang編譯器轉換後如下:

{
        int age = 10;
       
//定義block void (*block)(void) = &__main_block_impl_0( __main_block_func_0, &__main_block_desc_0_DATA, age ); age
= 20; //呼叫block block->FuncPtr(block); return 0; }

我們看到在呼叫block的建構函式時,傳入了三個引數,分別是:__main_block_func_0,&__main_block_desc_0_DATA,age,
我們找到block的建構函式,看看內部如何處理這個age:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;  // 1: 定義了一個同名的age變數
//block建構函式
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
    age = _age; //2 :C++的特殊語法,在建構函式內部會預設把_age賦值給age
  }
};

通過檢視block的內部結構看我們發現,block內部建立了一個age變數,並且在block建構函式中,把傳遞進來的_age賦值給了這個age變數.我們看看呼叫block時,他的底部取的是哪個age:

//呼叫block的FuncPtr函式,把block當做引數傳遞進去
block->FuncPtr(block);
//FuncPtr函式內部
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
//通過傳遞的block,找到block內部的age
  int age = __cself->age; // bound by copy
//列印age
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_5t_pxd6sp5x6rl9gnk21q2q934h0000gn_T_main_3089d7_mi_0,age);
}

通過底層程式碼,我們看到,在呼叫block時,block會找到自己內部的age變數, 然後列印數出,所以我們修改age = 20, 並不會影響block內部的age

二:static變數

我們把上面的程式碼稍作修改:

        auto int age = 10;
        static int height = 20;
        void (^block)(void) = ^{
            NSLog(@"age is %d, height is %d",age,height);
        };
        age = 20;
        height = 30;
        block();

列印的結果是age is 10, height is 30


同樣轉換 C++ 程式碼,檢視底層:

{
        auto int age = 10;
        static int height = 20;
        void (*block)(void) = &__main_block_impl_0(
                                                   __main_block_func_0,
                                                   &__main_block_desc_0_DATA,
                                                   age,
                                                   &height //傳遞指標
                                                   );
        age = 20;
        height = 30;
        (block->FuncPtr(block);
    }

我們看到,在定義block時,呼叫block的建構函式,傳遞引數時,age傳遞的是值,而height傳遞的是指標,看看建構函式內部:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;//定義 age 變數
  int *height;//定義一個 指標變數,存放外部變數的指標
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

我們看到,在block內部,定義了兩個變數age,height,不同的是,height是一個指標指標變數,用於存放外部變數的指標.我們再來看看執行block程式碼塊的內部:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy
  int *height = __cself->height; // bound by copy
 // *height : 取出指標變數所指向的記憶體的值
   NSLog((NSString *)&__NSConstantStringImpl__var_folders_5t_pxd6sp5x6rl9gnk21q2q934h0000gn_T_main_bf6cae_mi_0,age,(*height));
 }

我們看到,對於age是捕獲到內部,把外部age的值存起來,而對於height,是把外部變數的指標儲存起來,所以, 我們在修改height時,會影響到block內部的值

思考:為什麼會出現這兩種情況 ?(block捕獲外部基本資料型別變數: auto變數是值傳遞;static變數是指標傳遞)?

原因很簡單,因為auto是自動變數,出了作用域後會自動銷燬的,如果我們保留他的指標,就會存在訪問野指標的情況

//定義block型別
void(^block)(void);

void test(){
    int age = 10;
    static int height = 20;
//在block內部訪問 age , height
    block = ^{
        NSLog(@"age is %d, height is %d",age,height);
    };
    age = 20;
    height = 30;
}

//在main函式中呼叫
int main(int argc, const char * argv[]) {
        test();
        //test呼叫後,age變數就會自動銷燬,如果block內部是保留age變數的指標,那麼我們在呼叫block()時,就出現訪問野指標
        block();
}

三:全域性變數

全域性變數哪裡都可以訪問,所以block內部是不會捕獲全域性變數的,直接訪問,這個很好理解,我們直接看程式碼:

全域性變數底層

為什麼全域性變數不需要捕獲?
因為全域性變數無論哪個函式都可以訪問,block內部當然也可以正常訪問,所以根本無需捕獲


為什麼區域性變數就需要捕獲呢?
因為作用域的問題,我們在一個函式中定義變數,在block內部訪問,本質上跨函式訪問,所以需要捕獲起來.

Test1:
我們在Person類中寫一個test()方法,在test()方法中定義一個block並訪問self, 請問block會不會捕獲self.

@implementation Person

- (void)test{
    void(^block)(void) = ^{
        NSLog(@"會不會捕獲self--%@",self);
    };
    block();
}

@end

答案是會捕獲self,我們看看底層程式碼:

struct __Person__test_block_impl_0 {
  struct __block_impl impl;
  struct __Person__test_block_desc_0* Desc;
  Person *self;
  __Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

很顯然block內部的確聲明瞭一個Person *self用於儲存self,既然block內部捕獲了self,那就說明self肯定是一個區域性變數.那問題就來了,
為什麼self會是一個區域性變數?它應該是一個全域性變數呀?我們看一下轉換後的test()方法:

static void _I_Person_test(Person * self, SEL _cmd) {
    void(*block)(void) = ((void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

我們 OC 中的test()方法時沒有引數的,但是轉換成 C++ 後就多了兩個引數self,_cmd,其實我們每個 OC 方法都會預設有這兩個引數,這也是為什麼我們在每個方法中都能訪問self_cmd,而引數就是區域性變數,所以block就自然而然的捕獲了self.

Test2:
我們對 Test1 稍加修改,增加一個name屬性,然後在block中訪問_name,這時候block會捕獲self嗎?
答案是:會.繼續看一下底層:

我們看到,block底層仍然捕獲了self,這是因為,我們去訪問_name屬性的時候,實際上相當於self -> name,要想獲取name,必須要先獲取self,因為name是從self中來的,所以block內部會對self進行捕獲.

總結:

一:只要是區域性變數,不管是auto 變數,還是static 變數,block都會捕獲. 不同的是,對於auto 變數,block是儲存值,而static 變數 是儲存的指標.
二:如果是全域性變數,根本不需要捕獲,直接訪問
本篇只是講解了block捕獲基本資料型別的auto變數,下一篇會講解block捕獲物件型別的auto變數