iOS開發——block底層實現與變量捕獲

前言

首發地址:block底層實現與變量捕獲

帶着問題閱讀

  1. block的本質是什麼?你能講出來它的底層結構嗎?
  2. 全局變量會被block捕獲嗎?block會捕獲哪些變量?

block的底層數據結構

block又叫代碼塊,是OC語法中非常重要的一個概念,我們先來看一下Block的簡單使用。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ^{
            NSLog(@"hello block");
        }();

        int d = 5;
        void (^block)(int, int) = ^(int a, int b) {
            int c = a + b + d;
            NSLog(@"a + b + d = %d", c);
        };
        block(3, 4);
    }
    return 0;
}
複製代碼

上面的代碼中,我們創建了兩個Block,一個直接執行,輸出Hello World。 一個通過block變量進行調用,並引用了一個外部變量d。輸出12

作爲一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這是一個我的iOS交流羣:413038000,不管你是大牛還是小白都歡迎入駐 ,分享BAT,阿里面試題、面試經驗,討論技術, 大家一起交流學習成長!

推薦閱讀

iOS開發——最新 BAT面試題合集(持續更新中)

我們將以上代碼編譯成C代碼:

# 在main.m所在目錄執行該命令。
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
複製代碼

從main-arm64.cpp文件中,我們可以看到Block的結構如下:

struct __main_block_impl_1 {
  struct __block_impl impl;
  struct __main_block_desc_1* Desc;
  int d;
  __main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, int _d, int flags=0) : d(_d) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
複製代碼

我們可以看出Block的底層是結構體,__main_block_impl_1 包含一個變量impl 其結構和 Class的結構類似,其包含一個isa指針,可見Block本質上也是一個類,其中FuncPtr表示要執行的代碼塊的函數地址。d表示它引用的外部變量。

下面,我們一起看一下Block的調用過程,首先我們將下面代碼,編譯成C代碼。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^block)(void) =  ^{
            NSLog(@"hello block");
        };
        block();
    }
    return 0;
}

// 下面是編譯後的C代碼
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        //可以看得出來,Block的調用集中在這兩行。
        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_bv_k_7y193n6tvf34wjvqvnn3q40000gn_T_main_0422f2_mi_0);
        }

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,並將地址賦值給我block。而__main_block_func_0 是對應block內部要執行的代碼,是一個靜態的方法,它會賦值給__block_impl中的FuncPtr

__main_block_desc_0_DATA是也是一個結構體變量,裏面的兩個參數reserved 爲 0, Block_size__main_block_impl_0 結構體的大小。

調用的時候,是從block裏面直接取出FuncPtr。 我們知道block__main_block_impl_0類型,由於結構體的特性,將block強轉爲__block_impl類型,是可以直接取到FuncPtr的。所以第二句的調用也是清晰的。

上面的兩句代碼去掉強制類型轉化,可以精簡爲:

void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
block->FuncPtr(block);
複製代碼

這樣,調用過程就清晰多了。

通過上面的分析,我們可以看出block的結構應該是如下圖所示:

[圖片上傳中...(image-107b00-1595249453719-0)]

變量捕獲

auto自動變量

auto自動變量是離開作用域,就會銷燬, 只存在局部變量裏面,不能修飾全局變量。

比如,下面例子中的ageweight 就是auto變量,他們離開自己所在的作用局就會銷燬。默認情況下auto關鍵字會自動添加。

int main(int argc, const char * argv[]) {
    @autoreleasepool { 
      {
        int age = 20;
        auto int weight = 60;
      }
      // 在這裏訪問age, weight就報錯了。
    }
    return 0;
}
複製代碼

如果block中使用了auto變量,那麼block就會捕獲該變量,下面代碼

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        {
          int age = 20;
          auto int weight = 60;

            void (^block)(void) =  ^{
                NSLog(@"age = %d, weight = %d", age, weight); //age的結果是20
            };
            age = 40;
            block();
        }
    }
    return 0;
}
複製代碼

打印的結果中 age爲20 還是 40? 編譯後,__main_block_impl_0的結構如下,增加了兩個int 變量。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  int weight;
  //: age(_age), weight(_weight) 是C++語法,表示參數_age會賦值給變量age.
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int _weight, int flags=0) : age(_age), weight(_weight) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
複製代碼

我們可以看出,捕獲了auto變量,而且是值傳遞。

static變量

下面代碼輸入結果是什麼?

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        {
            static int height = 40;
            void (^block)(void) =  ^{
                NSLog(@"height = %d, ", height);
            };
            height = 80;
            block();
        }
    }
    return 0;
}
複製代碼

結果是80,爲什麼呢? 我們依然通過編譯後的結果查看。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *height;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_height, int flags=0) : height(_height) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        {
            static int height = 40;
            void (*block)(void) = ((void (*)())&__ma.in_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &height));
            height = 80;
            ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        }
    }
    return 0;
}

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *height = __cself->height; // bound by copy

                NSLog((NSString *)&__NSConstantStringImpl__var_folders_bv_k_7y193n6tvf34wjvqvnn3q40000gn_T_main_8d6bbf_mi_0, (*height));
            }
複製代碼

我們可以看出__main_block_impl_0中增加了一個變量height,但需要注意的是它是int * 類型的,在給它賦值的時候傳入的是&height 。 在__main_block_func_0中訪問的時候是通過*height取值的。

因此我們可以得出結論,靜態變量也是會被Block捕獲的,但它捕獲的是指針。

全局變量

下面代碼,輸出的結果是什麼?

int age = 10;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        {
            void (^block)(void) =  ^{
                NSLog(@"height = %d, ", age);
            };
            age = 20;
            block();
        }
    }
    return 0;
}
複製代碼

輸入結果是20,那block捕獲了age嗎?是通過指針訪問的嗎?我們看一下編譯結果:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
複製代碼

可以看出block並沒有捕獲全局變量。

結論

通過上面的分析,我們可以得出結論:

  • 對於全局變量,block不會捕獲,通過全局變量訪問。
  • 對於局部變量,auto自動變量將會捕獲,且是值傳遞。
  • 對於局部變量,static變量將會捕獲,且是指針傳遞。

捕獲self

下面代碼中,Person類中的test方法中block捕獲了變量了嗎?捕獲了那個變量?

// main.m
#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        p.name = @"樂戈";
        [p test];
    }
    return 0;
}

// Person.h
@interface Person : NSObject
@property (nonatomic, copy)NSString *name;
- (void)test;
@end

// Person.m
@implementation Person
- (void)test {
    void (^block)(void) = ^{
        NSLog(@"name == %@", self.name);
    };
    block();
}
@end
複製代碼

用上面的命令將Person.m編譯C++代碼,如下:

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;
  }
};
複製代碼

可以看出,這裏捕獲的並不是name,而是Person對象,這涉及了block的循環引用,我們將在下面的文章中講述。

思考題

下面各個代碼的輸出結果是什麼?

//問題1
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSMutableArray *array = [@[@"abc"] mutableCopy];
        void (^block)(void) =  ^{
          NSLog(@"hello block---%@", [array firstObject]);
        };
        array[0] = @"dgf";
        block();
    }
    return 0;
}

//問題2
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSMutableArray *array = [@[@"abc"] mutableCopy];
        void (^block)(void) =  ^{
          NSLog(@"hello block---%@", [array firstObject]);
        };
        array =  [@[@"dgf"] mutableCopy];
        block();
    }
    return 0;
}
複製代碼

交流互動

作爲一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這是一個我的iOS交流羣:413038000,不管你是大牛還是小白都歡迎入駐 ,分享BAT,阿里面試題、面試經驗,討論技術, 大家一起交流學習成長!

推薦閱讀

iOS開發——最新 BAT面試題合集(持續更新中)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章