1. 程式人生 > >iOS Block的本質(二)

iOS Block的本質(二)

iOS Block的本質(二)

1. 介紹引入block本質

  1. 通過上一篇文章Block的本質(一)已經基本對block的底層結構有了基本的認識,block的底層就是__main_block_impl_0
  2. 通過以下這張圖展示底層各個結構體之間的關係。

2. block的變數捕獲

  • 為了保證block內部能夠正常訪問外部的變數,block有一個變數捕獲機制。

區域性變數

  1. auto變數
    • Block的本質(一)我們已經瞭解過block對age變數的捕獲。
    • auto自動變數,離開作用域就銷燬,區域性變數前面自動新增auto關鍵字。自動變數會捕獲到block內部,也就是說block內部會專門新增加一個引數來儲存變數的值。
    • auto只存在於區域性變數中,訪問方式為值傳遞,通過上述對age引數的解釋我們也可以確定確實是值傳遞。
  2. static變數
    • static 修飾的變數為指標傳遞,同樣會被block捕獲。
  3. 分析aotu修飾的區域性變數和static修飾的區域性變數之間的差別

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            auto int a = 10;
            static int b = 10;
            void(^block)(void) = ^{
               NSLog(@"age is %d, height is %d", a, b);
            };
            a = 1;
            b = 2;
            block();
        }
        return 0;
    }
    // log : 資訊--> age = 10, height = 2
    // block中a的值沒有被改變而b的值隨外部變化而變化。
  4. 重新生成c++程式碼看一下內部結構中兩個引數的區別。

    struct __main_block_impl_0 {
        struct __block_impl impl;
        struct __main_block_desc_0* Desc;
        int a;  // a 為值
        int *b; // b 為指標
        __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int *_b, int flags=0) : a(_a), b(_b) {
            impl.isa = &_NSConcremainackBlock;
            impl.Flags = flags;
            impl.FuncPtr = fp;
            Desc = desc;
        }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        int a = __cself->a; // bound by copy
        int *b = __cself->b; // bound by copy
    
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_fd2a14_mi_0, a, (*b));
    }
    
    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)};
  5. 從上述原始碼中可以看出,a,b兩個變數都有捕獲到block內部。但是a傳入的是值,而b傳入的則是地址。
  6. 為什麼兩種變數會有這種差異呢,因為自動變數可能會銷燬,block在執行的時候有可能自動變數已經被銷燬了,那麼此時如果再去訪問被銷燬的地址肯定會發生壞記憶體訪問,因此對於自動變數一定是值傳遞而不可能是指標傳遞了。而靜態變數不會被銷燬,所以完全可以傳遞地址。而因為傳遞的是值得地址,所以在block呼叫之前修改地址中儲存的值,block中的地址是不會變得。所以值會隨之改變。
  7. 全域性變數
    • 我們同樣以程式碼的方式看一下block是否捕獲全域性變數
    int age_ = 10;
    static int height_ = 10;
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            void(^block)(void) = ^{
                NSLog(@"age is %d, height is %d", age_, height_);
            };
            age_ = 1;
            height_ = 2;
            block();
        }
        return 0;
    }
    // log 資訊--> age = 1, height = 2
  8. 同樣生成c++程式碼檢視全域性變數呼叫方式

    int age_ = 10;
    static int height_ = 10;
    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 = &_NSConcremainackBlock;
            impl.Flags = flags;
            impl.FuncPtr = fp;
            Desc = desc;
        }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_fd2a14_mi_0, age_, height_);
    }
    
    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)};
  9. 通過上述程式碼可以發現,__main_block_imp_0並沒有新增任何變數,因此block不需要捕獲全域性變數,因為全域性變數無論在哪裡都可以訪問。
    • 區域性變數因為跨函式訪問所以需要捕獲,全域性變數在哪裡都可以訪問 ,所以不用捕獲。
  10. block的變數總結
    • 總結:區域性變數都會被block捕獲,自動變數是值捕獲,靜態變數為地址捕獲。全域性變數則不會被block捕獲

3. 變數捕獲拓展

  1. 以下Persion類程式碼中block變數分析

    @interface Person : NSObject
    @property (copy, nonatomic) NSString *name;
    
    - (void)test;
    
    - (instancetype)initWithName:(NSString *)name;
    @end
    
    #import "Person.h"
    @implementation Person
    int age_ = 10;
    - (void)test
    {
        void (^block)(void) = ^{
            NSLog(@"-------%d", [self name]);
        };
        block();
    }
    
    - (instancetype)initWithName:(NSString *)name
    {
        if (self = [super init]) {
            self.name = name;
        }
        return self;
    }
    @end
  2. 同樣轉化為c++程式碼檢視其內部結構

    
    int age_ = 10;
    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;
      }
    };
    static void __Person__test_block_func_0(struct __Person__test_block_impl_0 *__cself) {
      Person *self = __cself->self; // bound by copy
    
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_Person_1027e6_mi_0, ((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("name")));
        }
    static void __Person__test_block_copy_0(struct __Person__test_block_impl_0*dst, struct __Person__test_block_impl_0*src) {_Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    static void __Person__test_block_dispose_0(struct __Person__test_block_impl_0*src) {_Block_object_dispose((void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    static struct __Person__test_block_desc_0 {
      size_t reserved;
      size_t Block_size;
      void (*copy)(struct __Person__test_block_impl_0*, struct __Person__test_block_impl_0*);
      void (*dispose)(struct __Person__test_block_impl_0*);
    } __Person__test_block_desc_0_DATA = { 0, sizeof(struct __Person__test_block_impl_0), __Person__test_block_copy_0, __Person__test_block_dispose_0};
    
    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);
    }
    
    
    static instancetype _I_Person_initWithName_(Person * self, SEL _cmd, NSString *name) {
        if (self = ((Person *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Person"))}, sel_registerName("init"))) {
            ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)self, sel_registerName("setName:"), (NSString *)name);
        }
        return self;
    }
    
    static NSString * _I_Person_name(Person * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Person$_name)); }
    extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
    
    static void _I_Person_setName_(Person * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _name), (id)name, 0, 1); }
    // @end
    
    struct _prop_t {
        const char *name;
            const char *attributes;
    };
  3. 可以發現,self同樣被block捕獲,接著我們找到test方法可以發現,test方法預設傳遞了兩個引數self和_cmd。
  4. 同理得,類方法也同樣預設傳遞了類物件self和方法選擇器_cmd。
  5. 不論物件方法還是類方法都會預設將self作為引數傳遞給方法內部,既然是作為引數傳入,那麼self肯定是區域性變數。上面講到區域性變數肯定會被block捕獲。

  6. 在block內部使用name成員變數或者呼叫例項的屬性

    - (void)test
    {
        void(^block)(void) = ^{
            NSLog(@"%@",self.name);
            NSLog(@"%@",_name);
        };
        block();
    }

  7. 得到結論:在block中使用的是例項物件的屬性,block中捕獲的仍然是例項物件,並通過例項物件通過不同的方式去獲取使用到的屬性。

4. block的型別

1.型別分析

  1. 通過原始碼分析得到,block中的isa指標指向的是_NSConcreteStackBlock類物件地址。那麼block是否就是_NSConcreteStackBlock型別的呢?

  2. 我們通過程式碼用class方法或者isa指標檢視具體型別。

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // __NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject
            void (^block)(void) = ^{
                NSLog(@"Hello");
            };
    
            NSLog(@"%@", [block class]);
            NSLog(@"%@", [[block class] superclass]);
            NSLog(@"%@", [[[block class] superclass] superclass]);
            NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
        }
        return 0;
    }
    // log 列印結果  __NSGlobalBlock__
    // log 列印結果  __NSGlobalBlock
    // log 列印結果  NSBlock
    // log 列印結果  NSObjcet
  3. 從上述列印內容可以看出block最終都是繼承自NSBlock型別,而NSBlock繼承於NSObjcet。那麼block其中的isa指標其實是來自NSObject中的。這也更加印證了block的本質其實就是OC物件。

2.型別分類

  1. block有3中型別
    • __NSGlobalBlock__ ( _NSConcreteGlobalBlock )
    • __NSStackBlock__ ( _NSConcreteStackBlock )
    • __NSMallocBlock__ ( _NSConcreteMallocBlock )
  2. 通過程式碼檢視一下block在什麼情況下其型別會各不相同

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // 1. 內部沒有呼叫外部變數的block
            void (^block1)(void) = ^{
            };
            // 2. 內部呼叫外部變數的block
            int a = 10;
            void (^block2)(void) = ^{
                NSLog(@"log :%d",a);
            };
           // 3. 直接呼叫的block的class
            NSLog(@"%@ %@ %@", [block1 class], [block2 class], [^{
                NSLog(@"%d",a);
            } class]);
        }
        return 0;
    }
    // 最後一行 Log :列印結果 __NSGlobalBlock__, __NSStackBlock__ ,__NSMallocBlock__
  3. 上述程式碼轉化為c++程式碼檢視原始碼時卻發現block的型別與打印出來的型別不一樣,c++原始碼中三個block的isa指標全部都指向_NSConcreteStackBlock型別地址。
  4. 我們可以推測runtime執行時過程中也許對型別進行了轉變。最終型別當然以runtime執行時型別也就是我們打印出的型別為準。

5. block在記憶體中的儲存

  1. 通過下面一張圖看一下不同block的存放區域

  2. 上圖中可以發現,根據block的型別不同,block存放在不同的區域中。
    資料段中的__NSGlobalBlock__直到程式結束才會被回收,不過我們很少使用到__NSGlobalBlock__型別的block,因為這樣使用block並沒有什麼意義。
  3. __NSStackBlock__型別的block存放在棧中,我們知道棧中的記憶體由系統自動分配和釋放,作用域執行完畢之後就會被立即釋放,而在相同的作用域中定義block並且呼叫block似乎也多此一舉。
  4. __NSMallocBlock__是在平時編碼過程中最常使用到的。存放在堆中需要我們自己進行記憶體管理。
  5. block是如何定義其型別

  6. 接著我們使用程式碼驗證上述問題,首先關閉ARC回到MRC環境下,因為ARC會幫助我們做很多事情,可能會影響我們的觀察。

    // MRC環境!!!
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // Global:沒有訪問auto變數:__NSGlobalBlock__
            void (^block1)(void) = ^{
                NSLog(@"block1---------");
            };
            // Stack:訪問了auto變數: __NSStackBlock__
            int a = 10;
            void (^block2)(void) = ^{
                NSLog(@"block2---------%d", a);
            };
            NSLog(@"%@ %@", [block1 class], [block2 class]);
            // __NSStackBlock__呼叫copy : __NSMallocBlock__
            NSLog(@"%@", [[block2 copy] class]);
        }
        return 0;
    }
    // Log 列印資訊 --> __NSGlobalBlock__ ,__NSStackBlock__ ,__NSMallocBlock__
  7. 通過列印的內容可以驗證上圖中所示的正確性。
    • 沒有訪問auto變數的block是__NSGlobalBlock__型別的,存放在資料段中。
    • 訪問了auto變數的block是__NSStackBlock__型別的,存放在棧中。
    • __NSStackBlock__型別的block呼叫copy成為__NSMallocBlock__型別並被複制存放在堆中。
  8. 上面提到過__NSGlobalBlock__型別的我們很少使用到,因為如果不需要訪問外界的變數,直接通過函式實現就可以了,不需要使用block。
  9. 但是__NSStackBlock__訪問了aotu變數,並且是存放在棧中的,上面提到過,棧中的程式碼在作用域結束之後記憶體就會被銷燬,那麼我們很有可能block記憶體銷燬之後才去呼叫他,那樣就會發生問題,通過下面程式碼可以證實這個問題。

    void (^block)(void);
    void test()
    {
        // __NSStackBlock__
        int a = 10;
        block = ^{
            NSLog(@"block---------%d", a);
        };
    }
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            test();
            block();
        }
        return 0;
    }
    // Log 列印資訊 : block---------27374635
  10. 可以發現a的值變為了不可控的一個數字。為什麼會發生這種情況呢?因為上述程式碼中建立的block是__NSStackBlock__型別的,因此block是儲存在棧中的,那麼當test函式執行完畢之後,棧記憶體中block所佔用的記憶體已經被系統回收,因此就有可能出現亂得資料。檢視其c++程式碼可以更清楚的理解。

  11. 為了避免這種情況發生,可以通過copy將__NSStackBlock__型別的block轉化為__NSMallocBlock__型別的block,將block儲存在堆中,以下是修改後的程式碼。

    void (^block)(void);
    void test()
    {
        // __NSStackBlock__ 呼叫copy 轉化為__NSMallocBlock__
        int age = 10;
        block = [^{
            NSLog(@"block---------%d", age);
        } copy];
        [block release];
    }
    // Log 列印資訊 : block---------10
  12. 那麼其他型別的block呼叫copy會改變block型別嗎?下面表格已經展示的很清晰了。

  13. 所以在平時開發過程中MRC環境下經常需要使用copy來儲存block,將棧上的block拷貝到堆中,即使棧上的block被銷燬,堆上的block也不會被銷燬,需要我們自己呼叫release操作來銷燬。而在ARC環境下回系統會自動copy,是block不會被銷燬。

6. ARC環境下的block

  • 在ARC環境下,編譯器會根據情況自動將棧上的block進行一次copy操作,將block複製到堆上。

  • 會自動將block進行一次copy操作的情況。

  1. block作為函式返回值時

    typedef void (^Block)(void);
    Block myblock()
    {
        int a = 10;
        // 上文提到過,block中訪問了auto變數,此時block型別應為__NSStackBlock__
        Block block = ^{
            NSLog(@"---------%d", a);
        };
        return block;
    }
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Block block = myblock();
            block();
           // 列印block型別為 __NSMallocBlock__
            NSLog(@"%@",[block class]);
        }
        return 0;
    }
    Log  列印資訊 :---------10
    Log  列印資訊 :__NSMallocBlock__
    • 上文提到過,如果在block中訪問了auto變數時,block的型別為__NSStackBlock__,上面列印內容發現blcok為__NSMallocBlock__型別的,並且可以正常打印出a的值,說明block記憶體並沒有被銷燬。
    • 上面提到過,block進行copy操作會轉化為__NSMallocBlock__型別,來講block複製到堆中,那麼說明RAC在 block作為函式返回值時會自動幫助我們對block進行copy操作,以儲存block,並在適當的地方進行release操作。
  2. 將block賦值給__strong指標時
    • block被強指標引用時,ARC也會自動對block進行一次copy操作。
     int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // block內沒有訪問auto變數
            Block block = ^{
                NSLog(@"block---------");
            };
            NSLog(@"%@",[block class]);
            int a = 10;
            // block內訪問了auto變數,但沒有賦值給__strong指標
            NSLog(@"%@",[^{
                NSLog(@"block1---------%d", a);
            } class]);
            // block賦值給__strong指標
            Block block2 = ^{
              NSLog(@"block2---------%d", a);
            };
            NSLog(@"%@",[block1 class]);
        }
        return 0;
    }
    Log  列印資訊 :__NSGlobalBlock__
    Log  列印資訊 :__NSStackBlock__
    Log  列印資訊 :__NSMallocBlock__
  3. block作為Cocoa API中方法名含有usingBlock的方法引數時
    • 例如:遍歷陣列的block方法,將block作為引數的時候。
    NSArray *array = @[];
    [array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    
    }];
  4. block作為GCD API的方法引數時
    • 例如:GDC的一次性函式或延遲執行的函式,執行完block操作之後系統才會對block進行release操作。
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    
    });        
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    
    });

7. block宣告寫法

  • 通過上面對MRC及ARC環境下block的不同型別的分析,總結出不同環境下block屬性建議寫法。
  1. MRC下block屬性的建議寫法
    @property (copy, nonatomic) void (^block)(void);

  2. ARC下block屬性的建議寫法
    @property (strong, nonatomic) void (^block)(void);
    @property (copy, nonatomic) void (^block)(void);