iOS Block

block的本質
block本質上也是一個oc對象,他內部也有一個isa指針。block是封裝了函數調用以及函數調用環境的OC對象。block其實也是NSObject的子類
block的類型
共有三種類型的block分別是:全局的,棧上的,堆上的

__NSGlobalBlock__ ( _NSConcreteGlobalBlock )//直到程序結束才會被回收
__NSStackBlock__ ( _NSConcreteStackBlock )//棧中的內存由系統自動分配和釋放,作用域執行完畢之后就會被立即釋放
__NSMallocBlock__ ( _NSConcreteMallocBlock )//平時編碼過程中最常使用到的。存放在堆中需要我們自己進行內存管理。

block是如何定義其類型

block是如何定義其類型

三種block的創建

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 1. 內部沒有調用外部變量的block
        void (^block1)(void) = ^{
            NSLog(@"Hello");
        };
        // 2. 內部調用外部變量的block
        int a = 10;
        void (^block2)(void) = ^{
            NSLog(@"Hello - %d",a);
        };
       // 3. 直接調用的block的class
        NSLog(@"%@ %@ %@", [block1 class], [block2 class], [^{
            NSLog(@"%d",a);
        } class]);
    }
    return 0;
}

打印結果:

2021-09-28 11:30:11.568128+0800 OC-Review[44456:6774536] __NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__

block的變量捕獲

局部變量

  • auto變量 - 值傳遞
  • static變量 - 指針傳遞
    全局變量
  • 直接訪問
  1. 一旦block中捕獲的變量為對象類型,block結構體中的__main_block_desc_0會出兩個參數copy和dispose。因為訪問的是個對象,block希望擁有這個對象,就需要對對象進行引用,也就是進行內存管理的操作。比如說對對象進行retarn操作,因此一旦block捕獲的變量是對象類型就會會自動生成copy和dispose來對內部引用的對象進行內存管理。
  2. 當block內部訪問了對象類型的auto變量時,如果block是在棧上,block內部不會對person產生強引用。不論block結構體內部的變量是__strong修飾還是__weak修飾,都不會對變量產生強引用。
  3. 如果block被拷貝到堆上。copy函數會調用_Block_object_assign函數,根據auto變量的修飾符(__strong,__weak,unsafe_unretained)做出相應的操作,形成強引用或者弱引用
  4. 如果block從堆中移除,dispose函數會調用_Block_object_dispose函數,自動釋放引用的auto變量。

block內修改變量的值

局部變量

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

        int a = 5;
        void(^blk)(void) = ^{
            a = 10;
        };

    }
    return 0;
}

block不能修改外部的局部變量,age是在main函數內部聲明的,說明age的內存存在于main函數的??臻g內部,但是block內部的代碼在__main_block_func_0函數內部。__main_block_func_0函數內部無法訪問age變量的內存空間,兩個函數的棧空間不一樣,__main_block_func_0內部拿到的age是block結構體內部的age,因此無法在__main_block_func_0函數內部去修改main函數內部的變量。
在最新的xcode中,已經對這種情況進行了錯誤代碼提示


錯誤代碼提示圖

修改方式
方式一:a使用static修飾。


int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        static int a = 5;
        void(^blk)(void) = ^{
            a = 10;
        };
        blk();
    }
    return 0;
}
-----
2021-09-29 14:40:10.580041+0800 OC-Review[71142:7289047] 修改前5
2021-09-29 14:40:10.581801+0800 OC-Review[71142:7289047] 修改后10

方式二:__block


int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        __block int a = 5;
        void(^blk)(void) = ^{
            a = 10;
        };
        NSLog(@"修改前%d",a);
        blk();
        NSLog(@"修改后%d",a);
    }
    return 0;
}
2021-09-29 14:42:12.904192+0800 OC-Review[71207:7291575] 修改前5
2021-09-29 14:42:12.905207+0800 OC-Review[71207:7291575] 修改后10

查看源碼:


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

            (a->__forwarding->a) = 10;
}

首先被__block修飾的a變量聲明變為名為age的__Block_byref_a_0結構體,也就是說加上__block修飾的話捕獲到的block內的變量為__Block_byref_a_0類型的結構體。

__isa指針 :__Block_byref_age_0中也有isa指針也就是說__Block_byref_age_0本質也一個對象。
__forwarding :__forwarding是__Block_byref_age_0結構體類型的,并且__forwarding存儲的值為(__Block_byref_age_0 *)&age,即結構體自己的內存地址。
__flags :0
__size :sizeof(__Block_byref_age_0)即__Block_byref_age_0所占用的內存空間。
a :真正存儲變量的地方,這里存儲局部變量10。

__block將變量包裝成對象,然后在把age封裝在結構體里面,block內部存儲的變量為結構體指針,也就可以通過指針找到內存地址進而修改變量的值。

循環引用

  1. 情景一
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        person.age = 10;
        person.block = ^{
            NSLog(@"%d",person.age);
        };
    }
    NSLog(@"大括號結束啦");
    return 0;
} 

可以發現大括號結束之后,person依然沒有被釋放,產生了循環引用
Person對象和block對象相互之間產生了強引用,導致雙方都不會被釋放,進而造成內存泄漏


循環引用示意
  1. 情景二
#import "Person.h"

@implementation Person


- (instancetype)init
{
    self = [super init];
    if (self) {
        self.block = ^{
            NSLog(@"%@",[self class]);
        };
    }
    return self;
}

這是開發中經常遇到的一個場景。之前我們說過block會捕獲局部,上面的OC函數調用轉化為runtime代碼為
objc_msgSend(self,@selector(init)) 在OC的方法中 有2個隱藏參數 self和_cmd 這2個參數作為函數的形參,在方法作用域中屬于局部變量 , 所以在block中使用self就滿足之前提到的 block會捕獲局部變量
查看源碼

struct __Person__init_block_impl_0 {
  struct __block_impl impl;
  struct __Person__init_block_desc_0* Desc;
  Person *self;
  __Person__init_block_impl_0(void *fp, struct __Person__init_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__init_block_func_0(struct __Person__init_block_impl_0 *__cself) {
  Person *self = __cself->self; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_sr_m_cfkwyx2h56vh4_kf65_vw40000gn_T_Person_3f840b_mi_0,((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")));
}

這里可以看到 __Person__init_block_impl_0結構體中 創建了一個Person *self的強指針 指向init方法中self
指針所指向的person對象,使person引用計數+1 而person對block也有一個強引用。這里就造成了循環引用。
解決方法
首先為了能隨時執行block,我們肯定希望person對block對強引用,而block內部對person的引用為弱引用最好。
使用__weak 和 __unsafe_unretained修飾符可以解決循環引用的問題。

  • __weak 和 __unsafe_unretained的區別
    a __weak不會產生強引用,指向的對象銷毀時,會自動將指針置為nil。因此一般通過__weak來解決問題。
    b __unsafe_unretained不會產生強引用,不安全,指向的對象銷毀時,指針存儲的地址值不變。會造成野指針問題
  1. 情景三
    在block中調用super也會造成循環引用 :
#import "Person.h"
@implementation Person
- (instancetype)init
{
    self = [super init];
    if (self) {
        self.block = ^{
            [super init];
        };
    }
    return self;
}

查看源碼

static void __Person__init_block_func_0(struct __Person__init_block_impl_0 *__cself) {
  ((Person *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Person"))}, sel_registerName("init"));
}

當使用[self class]時,會調用objc_msgSend函數,第一個參數receiver就是self,而第二個參數,要先找到self所在的這個class的方法列表

當使用[super class]時,會調用objc_msgSendSuper函數,此時會先構造一個__rw_objc_super的結構體作為objc_msgSendSuper的第一個參數。 該結構體第一個成員變量receiver仍然是self,而第二個成員變量super_class即是所在類的父類

struct __rw_objc_super {
    struct objc_object *object;
    struct objc_object *superClass;
    __rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {}
};

runtime對外暴露的類型為:

struct objc_super {
    __unsafe_unretained _Nonnull id receiver;

    __unsafe_unretained _Nonnull Class super_class;
};

結構體第一個成員receiver 就代表方法的接受者  第二個成員代表方法接受者的父類

所以

self.block = ^{
    [super init];
};
轉化后是:
self.block = ^{
        struct objc_super  superInfo = {
            .receiver = self,
            .super_class = class_getSuperclass(objc_getClass("Person")),
        };

        ((Class(*)(struct objc_super *, SEL))objc_msgSendSuper)(&superInfo,@selector(init));
    };

可以很明顯的看到問題,block強引用了self,而self也強持有了這個block
解決方法:

#import "Person.h"
@implementation Person
- (instancetype)init
{
    self = [super init];
    if (self) {
        __weak __typeof(self) weakSelf = self;
        self.block = ^{
            [super init];
        };
    }
    return self;
}



?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容