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修飾的局部變量之間的差異html

    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++代碼看一下內部結構中兩個參數的區別。ios

    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++代碼查看全局變量調用方式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中捕獲的仍然是實例對象,並經過實例對象經過不一樣的方式去獲取使用到的屬性。atom

4. block的類型

1.類型分析

  1. 經過源碼分析獲得,block中的isa指針指向的是_NSConcreteStackBlock類對象地址。那麼block是否就是_NSConcreteStackBlock類型的呢?spa

  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內存銷燬以後纔去調用他,那樣就會發生問題,經過下面代碼能夠證明這個問題。MRC 環境下的。

    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 打印信息 :MRC 環境下 : block---------272632424
    // Log 打印信息 :ARC 環境下 :  block---------10
    • 若是執行copy操做打印結果爲10
    void (^block)(void);
    void test()
    {
        // __NSStackBlock__ 調用copy 轉化爲__NSMallocBlock__
        int age = 10;
        block = [^{
            NSLog(@"block---------%d", age);
        } copy];
        [block release];
    }
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            test();
    
            block();
    //     Log 打印信息 : block---------10
        }
        return 0;
    }
  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);

相關文章
相關標籤/搜索