iOS之Block總結以及內存管理

block定義

struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};
struct Block_layout {
    void *isa;
    int flags;
    int reserved; 
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};

從上面代碼看出,Block_layout就是對block結構體的定義:html

isa指針:指向代表該block類型的類。ios

flags:按bit位表示一些block的附加信息,好比判斷block類型、判斷block引用計數、判斷block是否須要執行輔助函數等。閉包

reserved:保留變量,個人理解是表示block內部的變量數。ide

invoke:函數指針,指向具體的block實現的函數調用地址。函數

descriptor:block的附加描述信息,好比保留變量數、block的大小、進行copy或dispose的輔助函數指針。學習

variables:由於block有閉包性,因此能夠訪問block外部的局部變量。這些variables就是複製到結構體中的外部局部變量或變量的地址。動畫

舉例,定義一個最簡單block 打印hello world:atom

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

    void (^block)()=^{printf("hello world");};
    block();
    return 0;
}

使用clang指令spa

clang -rewrite-objc main.m 3d

獲得一個cpp文件,編譯後,你就會看到什麼是block了

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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

        printf("hello world");
}

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)};
int main(int argc, const char * argv[]) {

    void (*block)()=((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 struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
View Code

你定義完block以後,實際上是建立了一個函數,在建立結構體的時候把函數的指針一塊兒傳給了block,因此以後能夠拿出來調用。

再看看值捕獲的問題

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

    int a=10;
     //__block int a=10; //__block前綴
    void (^block)()=^{printf("打印a=%d",a);};
    block();
    
    return 0;
}

定義block的時候,變量a的值就傳遞到了block結構體中,僅僅是值傳遞,因此在block中修改a是不會影響到外面的a變量的。

而加了__block前綴,編譯後:

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref
printf("打印a=%d",(a->__forwarding->a));}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

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*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, const char * argv[]) {

    __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};

    void (*block)()=((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
View Code

並非直接傳遞a的值了,而是把a的地址(&a)傳過去了,因此在block內部即可以修改到外面的變量了。

isa:isa指針,在Objective-C中,任何對象都有isa指針。block 有三種類型:
_NSConcreteGlobalBlock 全局靜態,不會訪問任何外部變量,不會涉及到任何拷貝,好比一個空的block。例如:

#include int main()
{
    ^{ printf("Hello, World!\n"); } ();
    return 0;
}

_NSConcreteStackBlock 保存在棧中,出函數做用域就銷燬,例如:

#include int main()
{
    char a = 'A';
    ^{ printf("%c\n",a); } ();
    return 0;
}

_NSConcreteMallocBlock 保存在堆中,retainCount == 0銷燬

該類型的block都是由_NSConcreteStackBlock類型的block從棧中複製到堆中造成的。例以下面代碼中,在exampleB_addBlockToArray方法中的block仍是_NSConcreteStackBlock類型的,在exampleB方法中就被複制到了堆中,成爲_NSConcreteMallocBlock類型的block:

void exampleB_addBlockToArray(NSMutableArray *array) {
    char b = 'B';
    [array addObject:^{
            printf("%c\n", b);
    }];
}
void exampleB() {
    NSMutableArray *array = [NSMutableArray array];
    exampleB_addBlockToArray(array);
    void (^block)() = [array objectAtIndex:0];
    block();
}

總結一下:

_NSConcreteGlobalBlock類型的block要麼是空block,要麼是不訪問任何外部變量的block。它既不在棧中,也不在堆中,我理解爲它可能在內存的全局區。

_NSConcreteStackBlock類型的block有閉包行爲,也就是有訪問外部變量,而且該block只且只有有一次執行,由於棧中的空間是可重複使用的,因此當棧中的block執行一次以後就被清除出棧了,因此沒法屢次使用。

_NSConcreteMallocBlock類型的block有閉包行爲,而且該block須要被屢次執行。當須要屢次執行時,就會把該block從棧中複製到堆中,供以屢次執行。

而ARC和MRC中,還略有不一樣

題目:下面代碼在按鈕點擊後,在ARC下會發生什麼,MRC下呢?爲何?

@property(nonatomic, assign) void(^block)();

- (void)viewDidLoad {
    [superviewDidLoad];
    int value = 10;
    void(^blockC)() = ^{
        NSLog(@"just a block === %d", value);
    };

    NSLog(@"%@", blockC);
    _block = blockC;

}

- (IBAction)action:(id)sender {
    NSLog(@"%@", _block);
}

在ARC 打印:

mytest[25284:7473527] test:<__NSMallocBlock__: 0x60000005f3e0>
mytest[25284:7473527] NSShadow {0, -1} color = {(null)}

雖然不會crash,第二個是野指針

MRC 會打印:test:<__NSStackBlock__: 0x7fff54941a38> 而後crash

例如:

 NSArray *testArr = @[@"1", @"2"];
    NSLog(@"block is %@", ^{
        NSLog(@"test Arr :%@", testArr);
        
    });//結果:block is <__NSStackBlock__: 0x7fff54f3c808> void (^TestBlock)(void) = ^{
        NSLog(@"testArr :%@", testArr);
    };
    NSLog(@"block2 is %@", TestBlock);//block2 is <__NSMallocBlock__: 0x600000045e80>
//其實上面這句在非arc中打印是 NSStackBlock, 可是在arc中就是NSMallocBlock
//即在arc中默認會將block從棧複製到堆上,而在非arc中,則須要手動copy.

 

循環引用

  Block的循環引用是比較容易被忽視,本來也是相對比較難檢查出來的問題。固然如今蘋果在XCode編譯的層級就已經作了循環引用的檢查,因此這個問題的檢查就忽然變的沒有難度了。

  簡單說一下循環引用出現的原理:Block的擁有者在Block做用域內部又引用了本身,所以致使了Block的擁有者永遠沒法釋放內存,就出現了循環引用的內存泄漏。下面舉個例子說明一下:

@interface ObjTest () {
    NSInteger testValue;
}
@property (copy, nonatomic) void (^block)();
@end

@implement ObjTest
- (void)function {
    self.block = ^() {
        self.testValue = 100;
    };
}
@end

在這個例子中,ObjTest擁有了一個名字叫block的Block對象;而後在這個Block中,又對ObjTest的一個成員變量testValue進行了賦值。因而就產生了循環引用:ObjTest->block->ObjTest。

  要避免循環引用的關鍵就在於破壞這個閉合的環。在目前只考慮ARC環境的狀況下,筆者所知的只有一種方法能夠破壞這個環:在Block內部對擁有者使用弱引用。

@interface ObjTest () {
    NSInteger testValue;
}
@property (copy, nonatomic) void (^block)();
@end

@implement ObjTest
- (void)function {
    __weak ObjTest* weakSelf = self;
    self.block = ^() {
        weakSelf.testValue = 100;
    };
}
@end

在單例模式下 Block避免循環引用,以下:

@interface Singleton : NSObject
@property (nonatomic, copy) void(^block)();
+ (instancetype)share;
@end

@implementation Singleton
+ (instancetype)share {
    static Singleton *singleton;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        singleton = [[Singleton alloc] init];
    });
    return singleton;
}
@end

//============分割線=================
//控制器中代碼的實現

@implementation NextViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf=self;
    
    void (^blockTest)()=^(){
//        NSLog(@"print %@", self);//會內存泄漏
        NSLog(@"print %@", weakSelf);
    };
    
    Singleton *singleton = [Singleton share];
    singleton.block = blockTest;
}
- (IBAction)btnClick:(UIButton *)sender {
    
    [Singleton share].block();
}

- (void)dealloc {
    NSLog(@"%s", __FUNCTION__);
}
@end

 爲何iOS中系統的block方法可使用self

由於:首先循環引用發生的條件就是持有這個block的對象,被block裏邊加入的對象持有。固然是強引用。
因此UIView的動畫block不會形成循環引用的緣由就是,這是個類方法,當前控制器不可能強引用一個類,因此循環沒法造成。

ARC狀況下:
一、若是用copy修飾Block,該Block就會存儲在堆空間。則會對Block的內部對象進行強引用,致使循環引用。內存沒法釋放。

解決方法:新建一個指針(__weak typeof(Target) weakTarget = Target )指向Block代碼塊裏的對象,而後用weakTarget進行操做。就能夠解決循環引用問題。
二、若是用weak修飾Block,該Block就會存放在棧空間。不會出現循環引用問題。MRC狀況下用copy修飾後,若是要在Block內部使用對象,則須要進行(__block typeof(Target) blockTarget = Target )處理。在Block裏面用blockTarget進行操做。
返回值類型(^block變量名)(形參列表) = ^(形參列表) {};調用Block保存的代碼block變量名(實參);默認狀況下,,Block內部不能修改外面的局部變量Block內部能夠修改使用__block修飾的局部變量

 

參考 收藏:https://www.zhihu.com/question/30779258/answer/49492783

Objective-C中的Block原理

雲端之巔 Objective-C中block的底層原理

iOS學習之block總結及block內存管理(必看)

Block總結以及內存管理

相關文章
相關標籤/搜索