iOS底層原理 - Block本質探究

極客學偉發表於2018-06-29

#iOS底層原理 - Block本質探究

本質

block 本質是一個OC物件,也存在 isa 指標。或者說Block 是封裝了函式呼叫和函式呼叫環境的OC物件。

1.底層實現

編寫一段最簡單的OC程式碼頂一個block,程式碼如:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int abc = 10086;
        void(^block)(int number) = ^(int number) {
            NSLog(@"%d",number);
        };
    }
    return 0;
}
複製程式碼

使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m 命令將其OC程式碼轉化為底層的C++程式碼,觀察block的底層結構。

我們開啟編譯生成的main.cpp程式碼,會發現上述程式碼被轉化為如下:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int abc = 10086;
        void(*block)(int number) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    }
    return 0;
}
複製程式碼

block 程式碼塊被定義為 __main_block_impl_0 結構體。

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_impl__main_block_desc_0

__block_impl

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

包含isa指標說明,block本質上也是一個OC物件,FuncPtr 指向block所封裝的程式碼塊地址,等執行block時會通過FuncPtr尋找將要執行的程式碼塊,並且呼叫。

__main_block_desc_0

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
}
複製程式碼

其中: Block_size 為當前block 佔用記憶體大小。

__main_block_func_0

block 封裝的程式碼塊被定義為 __main_block_func_0 結構體

static void __main_block_func_0(struct __main_block_impl_0 *__cself, int number) {

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_gf_ct0sq2w17s16j4b1pz5_zx500000gn_T_main_68909d_mi_0,number);
        }
複製程式碼

2. Block 變數捕獲

auto 變數

如果我們將main函式改為:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int abc = 10086;
        void(^block)() = ^() {
            NSLog(@"%d",abc);
        };
        abc = 10010;
        
        block();
    }
    return 0;
}
複製程式碼

在block內部引用外部變數,我們再看看內部組成結構。同樣執行 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m 命令。 我們可以看到,較之前,__main_block_impl_0 結構體新增一個 int 型別變數abc,用於儲存所引用的外部變數的值。因為是值儲存,所以在block生成之後,無論外部變數做何更改,abc依然是之前所定義的值。

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

static 變數

因為我們所定義的外部變數 abc 之前沒有任何修飾符,也就是預設的auto變數,此時block是值捕獲。如果將外部變數宣告為 static 型別再觀察底層實現。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int abc = 10086;
        static int def = 100;
        void(^block)(void) = ^() {
            NSLog(@"abc: %d - def: %d",abc,def);
        };
        abc = 10010;
        def = 200;
        
        block();
    }
    return 0;
}
複製程式碼

轉化為c++底層實現為:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int abc;
  int *def;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _abc, int *_def, int flags=0) : abc(_abc), def(_def) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
複製程式碼

使用static 修飾的變數在block內部為指標傳遞,block直接捕獲外部變數的記憶體地址,此時若外部變數在block宣告之後修改,block內部也會同步進行修改。

全域性 變數

如果使用全域性變數,block不會捕獲。因為宣告全域性變數的型別會在程式的整個宣告週期都不會被釋放,所以在使用block時,直接會去訪問全域性變數的值。所以捕獲就沒有意義了,感興趣的可以自行檢視底層實現。

3. Block 的型別

當我們宣告一個block 並且列印他的繼承鏈我們可以看到:

void(^block)(void) = ^() {
            NSLog(@"abc");
        };
        
        NSLog(@"%@",[block class]);
        NSLog(@"%@",[[block class] superclass]);
        NSLog(@"%@",[[[block class] superclass] superclass]);
        NSLog(@"%@",[[[[block class] superclass] superclass] superclass]);
複製程式碼

輸出:

2018-06-28 10:37:23.901162+0800 BlockDemo[17574:719984] __NSGlobalBlock__
2018-06-28 10:37:23.901504+0800 BlockDemo[17574:719984] __NSGlobalBlock
2018-06-28 10:37:23.901522+0800 BlockDemo[17574:719984] NSBlock
2018-06-28 10:37:23.901535+0800 BlockDemo[17574:719984] NSObject
Program ended with exit code: 0
複製程式碼
故我們可得出結論block的繼承關係為:NSGlobalBlock : __NSGlobalBlock : NSBlock : NSObject

從而也進一步證明了block 本質上為 OC物件。並且,在不引用外部變數的情況下,block為 NSGlobalBlock 型別。

我們定義三個不同的block,分別列印他們的實際型別:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        void(^block1)(void) = ^() {
            NSLog(@"abc");
        };
        NSLog(@"%@",[block1 class]);
        
        int abc = 1;
        void(^block2)(void) = ^() {
            NSLog(@"abc: %d",abc);
        };
        NSLog(@"%@",[block2 class]);
        
        
        NSLog( @"%@", [^(){
            NSLog(@"hello %d",abc);
        } class]);
        
        
    }
    return 0;
}
複製程式碼

輸出:

2018-06-28 10:48:32.096859+0800 BlockDemo[17719:728991] __NSGlobalBlock__
2018-06-28 10:48:32.097224+0800 BlockDemo[17719:728991] __NSMallocBlock__
2018-06-28 10:48:32.097243+0800 BlockDemo[17719:728991] __NSStackBlock__
複製程式碼

我們可以得出結論,

block 型別分三種:分別為 __NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__

他們在記憶體中位置分別:

Snip20180628_6

那他們是如何區分的呢?可以使用如下表格來說明:

Block 型別 條件
NSGlobalBlock block 內部沒有訪問auto變數
NSStackBlock block 內部訪問了 auto變數
NSMallocBlock NSStackBlock 呼叫了copy

NSStackBlock 執行 copy 後會將棧區的block 複製到堆區,便於程式設計師管理,那其他型別的block執行 copy 會有什麼變化呢?如下表所示:

Block 型別 儲存域 執行 copy 後效果
NSGlobalBlock 程式的資料區域 無任何改變
NSStackBlock 從棧複製到堆
NSMallocBlock 引用計數器加1

4. ARC 下某些情況下系統會對 Block 自動執行一次 copy 操作,將 Block 從棧區轉移到堆區

1.當 block 作為函式返回值時

typedef void(^MyBlock)(void);

MyBlock testFunc() {
    int a = 10;
    MyBlock myBlock = ^ {
        NSLog(@"test --- %d",a);
    };
    return myBlock;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyBlock myB = testFunc();
        NSLog(@"%@",[myB class]);
    }
    return 0;
}
複製程式碼

如果此程式碼在MRC 環境下,會崩潰。Block訪問的變數已被釋放。 如果在ARC環境下,在引數的返回值為block時,系統會對block自動執行一次 copy 操作,使其變為 NSMallocBlock 型別。

2.當Block 被強指標引用時會自動執行copy操作

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int abc = 10;
        MyBlock myB = ^ {
            NSLog(@"+++ %d",abc);
        };
        NSLog(@"%@",[myB class]);
    }
    return 0;
}
複製程式碼

如上程式碼,在MRC環境輸出:__NSStackBlock__ 。在 ARC環境輸出:__NSMallocBlock__

3.當 Block 做為cocoa API 或 GCD API 的方法引數時也會自動執行 copy 操作

例如:

   NSArray *array = @[@1];
        [array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            
        }];
複製程式碼
  static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            
        });
複製程式碼

所以在 MAC 環境下的block屬性必須使用 copy 修飾,而ARC環境下的block屬性即可使用 strong 修飾,也可以使用 copy 修飾,兩者都會對block自動執行copy操作,故無任何區別。

5.Block 內部引用物件

觀察如下程式碼

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        {
            XWPerson *person = [[XWPerson alloc] init];
            person.age = 10;
            ^{
                NSLog(@"person -- %ld",(long)person.age);
            }();
        }
        
        NSLog(@"*******");
    }
    return 0;
}
複製程式碼

會發現當 函式體內 大括號執行完畢後 XWPerson 即被釋放,此時的block 是 棧型別的Block 即 __NSStackBlock__. 儲存在棧區的block即便引用了物件,也會跟隨大括號一併釋放。

如果將以上程式碼改為:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyBlock myBlock;
        {
            XWPerson *person = [[XWPerson alloc] init];
            person.age = 10;
            myBlock = ^{
                NSLog(@"person -- %ld",(long)person.age);
            };
            myBlock();
            
        }
        
        NSLog(@"*******");
    }
    return 0;
}
複製程式碼

我們會發現在執行到 **** 時,person 物件依然沒有被釋放,此時block 已經對 person 物件進行了強引用。因為 此時 的block 為強指標引用,型別為 堆block __NSMallocBlock__. 為什麼堆 block 會對外部物件強引用呢?

此時 使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m 命令 觀察其底層 c++ 實現:

此時block 定義為:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  XWPerson *__strong person;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, XWPerson *__strong _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
複製程式碼

其中 main_block_desc_0 定義為:

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} 
複製程式碼

較之前block引用基本成員型別時,其 main_block_desc_0 多了兩個引數分別為 copy 和 dispose。並且傳入的都是 __main_block_impl_0 block 本身。

當 block 執行 copy 操作的時候,執行的是

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}
複製程式碼

方法。最終呼叫的 _Block_object_assign 方法會對block引入的物件 person 進行引用計數操作,當所引入的物件使用 strong 修飾則使其引用計數加1,若使用weak修飾則引用計數不變。

當 block 執行完畢的時候會呼叫 dispose 方法,而dispose 在底層會呼叫

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}
複製程式碼

方法,將block內部引用的物件成員引用計數減1,如果此時外部物件使用strong 修飾,引用計數在copy加1後 此時再減1.依然會強引用外部物件,不會釋放,如果使用weak修飾,此時因為自身以及被釋放,所以不會再持有所引用外部物件,然而此時所引用外部物件是否會被釋放取決於它的引用計數是否為 0。

6. block 內部修改外部變數的值。

我們知道,如果block 內部捕獲的外部變數為 auto 型別,在block 內部生成的是該變數的值型別變數,無法通過block內部的值修改外部變數。 如果想在block內部修改外部變數的值有幾種方法?

1.外部變數使用 static 修飾

使用 static 修飾的變數block內部會直接獲取到變數的記憶體地址,可以直接修改。

2.使用 __block

若使用 static 變數修飾,該變數的生命週期就會無限延長,這不符合我們的設計思路,故我們可以使用 __block 來修飾外部變數,從而達到在block內部修改外部成員變數的目的。 那 __block 是如何實現此需求的呢?

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int a = 10;
        MyBlock block = ^{
            a = 20;
            NSLog(@"a --- %d",a);
        };
        block();
    }
    return 0;
}
複製程式碼

使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m 命令將其轉化為c++ 實現:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
複製程式碼

我們可知 通過 __block 修飾的外部成員變數被定義為 __Block_byref_a_0 物件!它的宣告為:

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};
複製程式碼

此時在main函式內宣告 __block 型別的變數會以此方式初始化:

     __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
複製程式碼

其中 __forwarding 儲存的原變數 a 的記憶體地址,size 為當前變數的記憶體大小,10 儲存未原變數的值。

如此,我們在block 內部修改 原變數時:

(a->__forwarding->a) = 20;
複製程式碼

直接取原變數的地址進行更改,從而實現在block內部更改外部變數。

7. __block 和 物件型別的auto變數 的記憶體管理

對於block內部捕獲的物件型別的auto變數和__block修飾的變數。如果block在棧區,不會對他們進行記憶體管理,即不會強引用外部變數

如果block被複制到堆區,則會呼叫內部 copy 函式對外部 __block 修飾的變數和物件型別的auto變數進行記憶體管理。

當block從記憶體中移除時,同樣也會呼叫dispose函式對所引用的外部變數進行釋放。

8. 迴圈引用

使用 block 很容易形成迴圈引用,如果一個類中定義的block內部引用了該類的外部屬性,包括 類本身的 self, 均會導致 self 強引用 block,block 也強引用 self。導致self不會被釋放。如下程式碼就會造成迴圈引用:

.h
#import <Foundation/Foundation.h>

@interface XWPerson : NSObject

@property (nonatomic, assign) NSInteger age;

@property (nonatomic, copy)  void(^personBlock)(void);

@end
複製程式碼
.m
#import "XWPerson.h"

@implementation XWPerson

- (void)test {
    self.personBlock = ^{
        NSLog(@"%d",self.age); //此處若即便使用 _age  也會產生迴圈引用。
    };
}

- (void)dealloc {
    NSLog(@" XWPerson -- dealloc  --  age:%ld",(long)_age);
}
@end
複製程式碼

產生迴圈引用的本質原因是,在block內部實現裡,會將self 捕獲到block內部,並且strong 強引用。如下程式碼所示:

struct __XWPerson__test_block_impl_0 {
  struct __block_impl impl;
  struct __XWPerson__test_block_desc_0* Desc;
  XWPerson *const __strong self;
  __XWPerson__test_block_impl_0(void *fp, struct __XWPerson__test_block_desc_0 *desc, XWPerson *const __strong _self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
複製程式碼

9. 避免產生迴圈引用

1. (ARC 環境下) __weak : 弱引用物件,指向的物件銷燬時,會自動將指標置為nil。因此一般通過__weak來解決問題。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        XWPerson *person1 = [[XWPerson alloc] init];
        person1.age = 18;
        __weak typeof(person1) weakPerson = person1;
        person1.personBlock = ^{
            NSLog(@"%ld",(long)weakPerson.age);
        };
        person1.personBlock();
    }
    return 0;
}
複製程式碼

2. (ARC / MRC 環境下) __unsafe_unretained : 弱引用物件,指向的物件銷燬時,不會自動將指標置為nil。再次引用該物件時可能會產生訪問殭屍物件的錯誤,產生崩潰,故不建議使用!

__unsafe_unretained XWPerson *person1 = [[XWPerson alloc] init];

3. (ARC / MRC 環境下) __block : 使用__block 修飾物件. 在ARC環境下-前提是一定要呼叫此block,並且要在block內部將所引用的外部變數手動置nil。因為 MRC 環境下,引用__block 修飾的物件不會使其引用計數加1,所以不需要手動置nil,也不是必需要使用block。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        __block XWPerson *person1 = [[XWPerson alloc] init];
        person1.age = 18;
        person1.personBlock = ^{
            NSLog(@"%ld",(long)person1.age);
            person1 = nil;
        };
        person1.personBlock();
    }
    return 0;
}
複製程式碼

相關文章