Objective-C基礎之四(深刻理解Block)

什麼是Block

Block其實就是一個代碼塊,一般被稱爲「閉包」,它封裝了函數調用以及函數調用環境,以便在合適的時機進行調用,在OC中,Block其實就是一個OC對象,它能夠當作參數傳遞。ios

Block的結構以下:面試

Block的本質

無外部變量訪問時Block的底層結構

  • 首先,建立一個Demo,在main.m中加入以下代碼:
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        void(^test)(void) = ^{
            NSLog(@"Block");
        };
        test();
    }
    return 0;
}
複製代碼
  • 而後經過xcrun指令將main.m文件轉換成C++代碼
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
複製代碼
  • 查看生成的main.cpp文件,首先看main函數,轉換成C++以後,結構以下,此處去除了多餘的強轉操做,方便閱讀
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        //Block的定義
        void(*test)(void) = &__main_block_impl_0(
                                                  __main_block_func_0,
                                                  &__main_block_desc_0_DATA
                                                  );
        //block的調用
        test->FuncPtr(test);
    }
    return 0;
}
複製代碼
  • Block在編譯完成以後,轉換成了__main_block_impl_0類型的結構體,它的內部結構以下
struct __main_block_impl_0 {
  //存放了block的一些基本信息,包括isa,函數地址等等
  struct __block_impl impl; 
  //存放block的一些描述信息
  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_impl_0結構體的內部,因此__main_block_impl_0結構體也能夠轉換成以下形式bash

struct __block_impl {
  void *isa;    //isa指針,能夠看出Block其實就是一個OC對象
  int Flags;    //標識,默認爲0
  int Reserved; //保留字段
  void *FuncPtr;//函數內存地址
};

struct __main_block_impl_0 {
  void *isa;    
  int Flags;    
  int Reserved; 
  void *FuncPtr;
  struct __main_block_desc_0* Desc;
};
複製代碼

block將咱們所要調用的代碼封裝成了函數__main_block_func_0,而且將函數__main_block_func_0的內存地址保存在到void *FuncPtr中,具體函數以下閉包

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    //此處就是調用的NSLog
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_f3_lg91hwts5rjdlzjph0sn82m80000gp_T_main_4f0065_mi_0);
}
複製代碼

結構體__main_block_desc_0中則保存了block所佔用內存大小等描述信息iphone

static struct __main_block_desc_0 {
  size_t reserved;      //保留字段
  size_t Block_size;    //__main_block_impl_0結構體所佔內存大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
複製代碼
  • 由此能夠看出block底層其實就是一個OC對象,由於它內部擁有isa指針。同時block將內部所要執行的代碼封裝成了函數,而且將函數內存地址保存到結構體當中,以便在合適的時機進行調用

訪問外部變量時Block的底層結構

咱們在使用Block的過程當中,能夠在Block內部訪問外部的變量,包含局部變量、靜態變量(至關於私有的全局變量)、全局變量等等。如今就經過一個Demo來看一下block底層是如何訪問外部變量的。async

  • 首先建立Demo,在main.m文件中添加以下代碼
//定義全局變量c
int c = 30;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //局部變量a
        int a = 10;
        //靜態變量b
        static int b = 20;
        void(^test)(void) = ^{
            NSLog(@"Block - %d, %d, %d", a, b, c);
        };
        test();
    }
    return 0;
}
複製代碼
  • 將main.m轉換成C++代碼後,再次查看main函數,結果以下
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        int a = 10;
        static int b = 20;
        void(*test)(void) = (&__main_block_impl_0(
                                                  __main_block_func_0,
                                                  &__main_block_desc_0_DATA,
                                                  a,
                                                  &b));
        test->FuncPtr(test);
    }
    return 0;
}
複製代碼

能夠看出,此時__main_block_impl_0結構體中多了兩個參數,分別是局部變量a的值,靜態變量b的指針,也就是它的內存地址。函數

  • 查看__main_block_impl_0結構體的內存結構
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  int *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 = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
複製代碼

發現,在__main_block_impl_0結構體中多了兩個成員變量,一個是int a,一個是int *b學習

  • 當經過test->FuncPtr(test)執行block時,會經過結構體中的FuncPtr找到函數__main_block_func_0的地址進行調用,查看__main_block_func_0函數以下:
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_f3_lg91hwts5rjdlzjph0sn82m80000gp_T_main_064cd6_mi_0, a, (*b), c);
}
複製代碼

__main_block_func_0函數中,訪問局部變量a和靜態變量b時都是經過傳遞過來的__main_block_impl_0結構體拿到對應的成員變量進行訪問,可是全局變量c並無存放在結構體中,而是直接進行訪問。測試

  • 由此咱們就能夠得出結論,block中有變量捕獲的機制
    • 當訪問局部變量的時候,會將局部變量的值捕獲到block中,存放在一個同名的成員變量中。
    • 當訪問靜態變量時,會將靜態變量的地址捕獲到block中,存放在一個同名的成員變量中。
    • 當訪問全局變量時,由於全局變量是一直存在,不會銷燬,因此在block中直接訪問全局變量,不須要進行捕獲

此處須要注意的是,其實在OC中有個默認的關鍵字auto,在咱們建立局部變量的時候,會默認在局部變量前加上auto關鍵字進行修飾,例如上文中的int a,其實就至關於auto int a。auto關鍵字的含義就是它所修飾的變量會自動釋放,也表示着它所修飾的變量會存放到棧空間,系統會自動對其進行釋放。ui

block總結

block底層結構總結

block在編譯完成以後會轉換成結構體進行保存,結構體中的成員變量以下,其中在成員變量descriptor指向的結構體中,多了兩個函數指針分別爲copydispose,這兩個函數和block內部對象的內存管理有關,後面會具體說明。

block變量捕獲總結

block使用變量捕獲機制來保證在block內部可以正常的訪問外部變量。

  • 當block訪問的是auto類型的局部變量時,會將局部變量捕獲到block內部的結構體中,而且是直接捕獲變量的值。
  • 當block訪問的是static類型的靜態變量時,會將靜態變量捕獲到block內部的結構體中,而且捕獲的是靜態變量的地址。
  • 當block訪問的是全局變量時,不會進行捕獲,直接進行訪問。

block的類型

block的三種類型

在OC當中block其實擁有三種類型,能夠經過class或者isa指針來查看block具體的類型

  • 首先在main.m中添加如下示例代碼
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //第一種類型NSGlobalBlock
        NSLog(@"%@",[^{
            NSLog(@"NSGlobalBlock");
        } class]);
        
        //第二種類型NSStackBlock
        int a = 10;
        NSLog(@"%@",[^{
            NSLog(@"%d", a);
        } class]);
        
        //第三種類型NSMallocBlock - 1
        void(^test2)(void) = ^{
            NSLog(@"NSMallocBlock - %d", a);
        };
        NSLog(@"%@",[test2 class]);
        
        //第三種類型NSMallocBlock - 2
        NSLog(@"%@",[[^{
            NSLog(@"%d", a);
        } copy] class]);
    }
    return 0;
}
複製代碼

運行結果以下:

  • 運行後能夠發現,block能夠有三種類型,分別是NSGlobalBlockNSStackBlockNSMallocBlock,這三種類型分別存放在.data區、棧區堆區。對應的結構圖以下

圖中的block類型和上文中打印出來的block類型對應關係以下

class方法返回類型 isa指向類型
NSGlobalBlock _NSConcreteGlobalBlock
NSStackBlock _NSConcreteStackBlock
NSMallocBlock _NSConcreteMallocBlock

可是無論它是哪一種block類型,最終都是繼承自NSBlock類型,而NSBlock繼承自NSObject,因此這也說明了block自己就是一個對象。

如何區分block類型

在上述示例中,提到了四種生成不一樣類型的block的方法,分別以下:

  1. 沒有訪問局部變量的block,而且沒有強指針指向block,則此block爲NSGlobalBlock
  2. 訪問了局部變量的block,可是沒有強指針指向block,則此block爲NSStackBlock
  3. 訪問了局部變量的block,而且有強指針指向block,則此block爲NSMallocBlock
  4. NSStackBlock類型的block,執行了copy操做以後,生成的block爲NSMallocBlock

其實第三點和第四點生成的都是NSMallocBlock,由此咱們就能夠獲得下面的結論

block的類型 block執行的操做
NSGlobalBlock 沒有訪問auto類型的變量
NSStackBlock 訪問了auto類型的變量
NSMallocBlock __NSStackBlock__類型的block執行了copy操做

block的copy操做

block執行copy操做後的內存變化

NSGlobalBlockNSStackBlockNSMallocBlock三種類型的block分別存放在了數據區、棧區和堆區。將三種類型的block分別進行copy操做以後,產生的結果以下:

  • NSGlobalBlock的block進行copy操做,什麼也不會發生,生成的仍是NSGlobalBlock類型的block
  • NSStackBlock類型的block進行操做,會將block從棧上覆制一份到堆中,生成NSMallocBlock類型的block
  • NSMallocBlock類型的block進行copy操做,此block的引用計數會加1

結構圖以下

ARC環境下哪些操做會自動進行copy操做?

在上述示例中,NSStackBlock類型的block,執行了copy操做以後,生成的block爲NSMallocBlock,其實不止這一種方式生成NSMallocBlock,如下是OC中在ARC環境下自動觸發copy操做的幾種狀況:

  1. block做爲返回值時,會自動進行copy
typedef void(^block)(void);
block test(){
    return ^{
        NSLog(@"NSMallocBlock");
    };
}
複製代碼
  1. 使用__strong類型的指針指向block時,會執行copy操做
void(^test2)(void) = ^{
            NSLog(@"NSMallocBlock - %d", a);
        };
複製代碼
  1. block做爲Cocoa API中含有usingBlock的方法的參數時,會執行copy操做
NSArray *arr = @[@"1",@"2"];
[arr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
     NSLog(@"NSMallocBlock");
}];
複製代碼
  1. block做爲GCD方法的參數時會執行copy操做
dispatch_async(dispatch_get_main_queue(), ^{
     NSLog(@"NSMallocBlock");
});
複製代碼

咱們日常在使用block做爲屬性的時候,都會使用copy修飾符來修飾,其實內部就是對block進行了一次copy操做,將block拷貝到堆上,以便咱們手動管理block的內存

block訪問對象類型

訪問對象類型的auto變量時,block的底層結構

上文中Block訪問的外部變量都是基本數據類型,因此不涉及到內存管理,若是在block中訪問外部對象時,block內部又是什麼樣的結構呢?

  • 首先在main.m中加入如下示例代碼
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //默認對象
        NSObject *obj1 = [[NSObject alloc] init];
        void(^test1)(void) = ^{
            NSLog(@"NSMallocBlock - %@", obj1);
        };
        test1();
        
        //使用__weak指針修飾對象
        NSObject *obj2 = [[NSObject alloc] init];
        __weak typeof(obj2) weakObj = obj2;
        void(^test2)(void) = ^{
            NSLog(@"NSMallocBlock - %@", weakObj);
        };
        test2();
    }
    return 0;
}
複製代碼
  • 使用以下指令將main.m文件轉換成C++代碼
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
複製代碼

此處因爲使用了__weak關鍵字來修飾對象,涉及到runtime,全部須要指定runtime的版本。

  • 轉換成main.cpp文件後,查看block的底層結構爲
//直接訪問外部對象的block內部結構
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  //生成strong類型的指針
  NSObject *__strong obj;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _obj, int flags=0) : obj(_obj) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
//訪問__weak修飾符修飾的外部對象的block內部結構
struct __main_block_impl_1 {
  struct __block_impl impl;
  struct __main_block_desc_1* Desc;
  //自動生成weak類型的指針
  NSObject *__weak weakObj;
  __main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, NSObject *__weak _weakObj, int flags=0) : weakObj(_weakObj) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

複製代碼

這時發現,若是直接在block中訪問外部的auto類型的對象,默認是在block結構體中生成一個strong類型的指針指向外部對象,如結構體__main_block_impl_0。若是在block中訪問了__weak修飾符修飾的外部對象,那麼在它的內部會生成一個weak類型的指針指向外部對象,如結構體__main_block_impl_1

__main_block_impl_0的構造函數中,obj(_obj)就表明着,之後構造函數傳過來的_obj參數會自動賦值給結構體中的成員變量obj。

  • 因爲__main_block_desc_0__main_block_desc_1結構相同,因此如下只以__main_block_desc_0爲例,查看__main_block_desc_0結構體,會發現它內部新增長了兩個函數指針,以下
static struct __main_block_desc_0 {
  size_t reserved;  //保留字段
  size_t Block_size; //整個block所佔內存空間
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);   //copy函數
  void (*dispose)(struct __main_block_impl_0*); //dispose函數
} __main_block_desc_0_DATA = { 0,
                               sizeof(struct __main_block_impl_0),
                               __main_block_copy_0,
                               __main_block_dispose_0};
複製代碼

新增長了copy和dispose兩個函數指針,對應着函數__main_block_copy_0__main_block_dispose_0,以下

//copy指針指向的函數
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
    
}
//dispose指針指向的函數
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

複製代碼

以前說過,block封裝了函數調用和函數調用環境,這也就意味這若是它引用了外部的對象,就須要對外部對象進行內存管理操做。__main_block_copy_0函數內部會調用_Block_object_assign函數,它的主要做用是根據外部引用的對象的修飾符來進行相應的操做,若是外部對象是使用__strong來修飾,那麼_Block_object_assign函數會對此對象進行一次相似retain的操做,使得外部對象的引用計數+1。

__main_block_dispose_0函數內部會調用_Block_object_dispose函數,它的做用就是在block內部函數執行完成以後對block內部引用的外部對象進行一次release操做。

總結

Block在棧上

若是block在棧上,那麼在block中訪問對象類型的auto變量時,是不會對auto變量產生強引用的。這個須要在MRC狀況下進行測試,將Xcode中Build Settings下的Automatic Reference Counting設置成NO,代表當前使用MRC環境。

  • 首先建立XLPerson類,重寫dealloc方法,方便測試
@implementation XLPerson

- (void)dealloc{
    [super dealloc];
    NSLog(@"%s", __func__);
}

@end
複製代碼
  • 在main.m中增長以下測試代碼
typedef void(^TestBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //建立block
        TestBlock block;
        {
            XLPerson *person = [[XLPerson alloc] init];
            block = ^{
                NSLog(@"block --- %p", &person);
            };
            NSLog(@"%@", [block class]);
            [person release];
        }
        NSLog(@"block執行前");
        block();
        [block release];
        NSLog(@"block執行後");
        
    }
    return 0;
}
複製代碼
  • 運行程序,獲得以下的打印信息

能夠發現,在MRC環境下,即便是有強指針指向block,系統也不會對block進行默認的copy操做,因此當前的block類型依舊爲NSStackBlock類型。並且,在block執行以前,XLPerson就已經釋放了,說明在棧上的block並無對person對象進行強引用。

block被copy堆上

  • 首先,若是block被copy到了堆上,在訪問auto修飾的對象變量時,內部會自動調用copy函數,它內部會調用_Block_object_assign函數,_Block_object_assign函數會根據auto變量的修飾符(__strong、__weak、__unsafe_unretained)作出相應的處理,若是是__strong修飾,則內部對外部的對象造成強引用,若是是__weak或者__unsafe_unretained,則會造成弱引用
  • 若是block執行完成,被系統從堆中移除時,會調用dispose函數,它內部調用_Block_object_dispose函數,_Block_object_dispose函數會自動釋放引用的auto變量,也就是對引用的auto變量進行一次release操做。
  • copy和dispose函數調用時機以下
函數 調用時機
copy 棧上的block被複制到堆上
dispose 堆上的block被釋放時

__block

__block的做用

使用block時,若是block中訪問到了外部被auto修飾的變量,咱們常用到__block來修飾外部變量,它的主要做用就是可以讓咱們在block內部來修改外部變量的值,固然,block只能用來修飾auto變量,不能用來修飾全局變量和靜態變量。

  • 首先來建立Demo,查看源碼
typedef void(^TestBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block XLPerson *person = [[XLPerson alloc] init];
        __block int a = 10;
        TestBlock block = ^{
            person = nil;
            a = 20;
            NSLog(@"block -- a:%d, person:%@",a,person);
        };
        block();
        NSLog(@"block調用後,a:%d, person:%@",a,person);
    }
    return 0;
}
複製代碼
  • 經過如下指令轉換成C++代碼
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
複製代碼
  • 首先查看block結構體的源碼,發現block內部多了兩個指針,__Block_byref_person_0類型的指針person和__Block_byref_a_1類型的指針a
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_person_0 *person; // by ref
  __Block_byref_a_1 *a; // by ref
};
複製代碼
  • 再查看main函數中,局部變量和block的建立方式
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        //封裝person對象
       __Block_byref_person_0 person = {
            0,      //
            &person,
            33554432,
            sizeof(__Block_byref_person_0),
            __Block_byref_id_object_copy_131,
            __Block_byref_id_object_dispose_131,
            objc_msgSend(objc_msgSend(objc_getClass("XLPerson"), sel_registerName("alloc")), sel_registerName("init"))};
            
        //封裝變量a
        __Block_byref_a_1 a = {
            0,
            (__Block_byref_a_1 *)&a,
            0,
            sizeof(__Block_byref_a_1),
            10
        };
        
        //建立block
        TestBlock block = (&__main_block_impl_0(
                                                __main_block_func_0,
                                                &__main_block_desc_0_DATA,
                                                &person,
                                                &a,
                                                570425344));
        block->FuncPtr(block);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_f3_lg91hwts5rjdlzjph0sn82m80000gp_T_main_115560_mi_1,(a.__forwarding->a),(person.__forwarding->person));
    }
    return 0;
}
複製代碼

__block修飾對象類型auto變量

經過__block修飾的person對象在編譯後被封裝成了__Block_byref_person_0類型的結構體,內部有多個成員變量,以下

#將person對象封裝成結構體__Block_byref_person_0
struct __Block_byref_person_0 {
  void *__isa;                                      //isa指針
__Block_byref_person_0 *__forwarding;               //forwarding指針
 int __flags;                                       //標識位
 int __size;                                        //結構體所佔內存大小
 void (*__Block_byref_id_object_copy)(void*, void*);//函數指針指向copy函數
 void (*__Block_byref_id_object_dispose)(void*);    //函數指針指向dispose函數
 XLPerson *__strong person;                         //強引用XLPerson的實例對象
};


//__Block_byref_person_0結構體的建立與賦值
__Block_byref_person_0 person = {
    0,                                  //對應isa指針,傳0
    &person,                            //對應forwarding指針,將結構體自身的地址傳給了forwarding指針
    33554432,                           //對應flags
    sizeof(__Block_byref_person_0),     //當前結構體所需內存大小
    __Block_byref_id_object_copy_131,   //copy函數
    __Block_byref_id_object_dispose_131,//dispose函數
    objc_msgSend(objc_msgSend(objc_getClass("XLPerson"),    
                              sel_registerName("alloc")), 
                              sel_registerName("init"))     //經過objc_msgSend建立XLPerson對象,而且將對象的指針傳入結構體中
};
複製代碼

能夠明顯看出,在結構體__Block_byref_person_0中,存在以下成員變量

  • isa指針,此處賦值爲0,同時也能說明此結構體也是一個OC對象
  • forwarding指針,指向結構體自身的內存地址
  • flags,標誌位,此處傳33554432
  • size,結構體大小,經過sizeof(__Block_byref_person_0)得到,此處能夠簡單計算出結構體所需內存大小爲48個字節
  • __Block_byref_id_object_copy,copy函數,由於在結構體中引用到了person對象,因此調用此方法來根據person指針的引用類型決定是否對person對象進行retain操做,此處person對象是使用__strong來修飾,因此copy函數的做用就是對person對象進行一次retain操做,引用計數+1。
  • __Block_byref_id_object_dispose_131,dispose函數,在結構體從內存中移除的時候,會調用dispose函數,對person對象進行一次release操做,引用計數-1
  • person指針,由於咱們外部建立的是XLPerson的實例對象,因此結構體內部直接保存了person指針來指向咱們建立的XLPerson對象。

前文提到過,由於block封裝了函數調用環境,因此一旦它內部引用了外部的auto對象,就須要對外部對象的內存進行管理,因此纔有了copy函數和dispose函數。此處也同樣,由於使用__block修飾的XLPerson對象的指針存放在告終構體內部,因此須要使用copy函數和dispose函數來管理對象的內存。

__block修飾基本數據類型auto變量

若是使用__block來修飾基本數據類型的auto變量,就會將變量封裝成__Block_byref_a_1類型的結構體,內部結構以下

#將變量a封裝成結構體__Block_byref_a_1
struct __Block_byref_a_1 {
  void *__isa;                  //isa指針
__Block_byref_a_1 *__forwarding;//forwarding指針
 int __flags;                   //標識位
 int __size;                    //結構體大小
 int a;                         //變量a
};

//封裝變量a
__Block_byref_a_1 a = {
    0,                          //isa,傳0
    (__Block_byref_a_1 *)&a,    //傳入當前結構體a的地址
    0,                          //flags
    sizeof(__Block_byref_a_1),  //結構體的大小
    10                          //外部變量a的值
};
複製代碼

相對於__block修飾auto對象,若是修飾基本數據類型,則結構體中少了copy函數和dispose函數,由於基本數據類型不須要進行內存管理,因此不須要調用這兩個函數。

  • isa指針,此處傳0
  • forwarding指針,指向結構體自身的內存地址
  • flags,此處傳0
  • size,結構體的大小,使用sizeof(__Block_byref_a_1)來獲取,此處結構體大小爲28個字節
  • a,保存了外部變量a的值,此處爲10

總結

  • __block能夠用來解決在block內部沒法修改auto變量值的問題。
  • __block只能用來修飾auto類型變量,沒法用來修飾全局變量、靜態變量等等
  • 使用__block修飾的auto變量,編譯器會將此變量封裝成一個結構體(其實也是一個對象),結構體內部有如下幾個成員變量
    • isa指針
    • forwarding指針,指向自身內存地址
    • flags
    • size,結構體的大小
    • val(使用的外部變量,若是是基本數據類型,就是變量的值,若是是對象類型,就是指向對象的指針)
  • __block修飾基本數據類型的auto變量,例如__block int a,那麼封裝的結構體內部成員變量如上,若是是修飾對象類型的auto變量,如__block XLPerson *person,那麼生成的結構體中會多出copy和dispose兩個函數,用來管理person對象的內存。

__block的內存管理

當block訪問外部__block修飾的auto變量時,會將變量封裝成結構體,而且將結構體的地址值存放在block內部

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_person_0 *person; // by ref
  __Block_byref_a_1 *a; // by ref
};
複製代碼

其中persona就是指向兩個__block結構體的指針,正由於在block中有引用到__Block_byref_person_0__Block_byref_a_1,那麼block就必須對這兩個結構體的內存進行管理,因此相應的在__main_block_desc_0中就生成了兩個函數copy和dispose,專門用來管理persona所指向的結構體(也是對象)的內存。以下

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*);  //copy函數
  void (*dispose)(struct __main_block_impl_0*);                            //dispose函數
} __main_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0),
    __main_block_copy_0,            //copy函數
    __main_block_dispose_0          //dispose函數
};
複製代碼

相應的copy函數和dispose函數以下

//copy函數
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign(&dst->person,src->person, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_assign(&dst->a, src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}
//dispose函數
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->person, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}
複製代碼

這裏和上文中說到的block中訪問外部對象的內存管理相同

  • 當block在棧上的時候,block內部並不會對__block變量產生強引用,在上文中已經使用demo驗證過
  • 當block被copy到堆上時,首先會調用copy函數,在copy函數內部會調用_Block_object_assign函數來對__block變量造成強引用。這裏和以前說到的block訪問外部auto對象有點不一樣,若是block訪問外部對象,_Block_object_assign會根據外部對象的修飾符是不是__strong仍是__weak來決定是否對對象造成強引用,可是若是是訪問__block變量,block就必定會對__block變量造成強引用。

當圖中的Block0被賦值到堆上時,會將他所引用的__block變量一塊兒賦值到堆上,而且對堆上的__block變量產生強引用

當圖中的Block1被複制到堆上時,由於以前__block變量已經被複制到了堆上,因此Block1只是對堆上的__block變量產生強引用。

  • 當block從堆中移除時,會調用block內部的dispose函數,dispose函數內部又會調用_Block_object_dispose函數來自動釋放引用的__block變量,至關於對__block變量執行一次release操做。

當Block0和Block1都被廢棄時,Block0和Block1對__block變量的引用會被釋放,因此__block變量最終由於沒有持有者而被廢棄

__block中的__forwarding指針

__block修飾的auto變量所對應的結構體以下

在結構體中有一個__forwarding指針指向本身,在後續訪問__block變量的時候也是經過__forwarding指針來進行訪問

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_1 *a = __cself->a; // bound by ref
  __Block_byref_person_0 *person = __cself->person; // bound by ref

  (a->__forwarding->a) = 20;        //經過__forwarding指針來拿到a進行修改
  (person->__forwarding->person) = __null; //經過__forwarding指針來拿到person進行修改
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_f3_lg91hwts5rjdlzjph0sn82m80000gp_T_main_fbc4b7_mi_0,(a->__forwarding->a),(person->__forwarding->person));
}
複製代碼

當block在棧上時,__block變量也存放在棧上,它內部的__forwarding指針指向它自己

當block被複制到堆上以後,block所引用的__block變量也會被複制到堆上,這樣在棧上和堆上各存在一份__block變量,此時將棧上__block變量中的__forwarding指針指向堆上__block變量的地址,同時,堆上的__block變量中的__forwarding指針指向它自己,那麼此時,無論咱們是訪問棧上__block變量中的屬性值仍是堆上__block變量中的屬性值,都是經過__forwarding指針訪問到堆上的__block變量。

__block修飾的對象類型內存管理總結

  • 當__block變量存放在棧上時,他內部不會對指向的對象產生強引用
  • 當block被copy到堆上時,它訪問的__block變量也會被copy到堆上
    • 會首先調用__block變量內部的copy函數,copy函數內部會調用_Block_object_assign函數
    • _Block_object_assign函數會更加所指向對象的修飾符(__strong、__weak、__unsafe_unretained)來作出相應的操做,若是是__strong修飾的對象,則會對它進行強引用
  • 若是block從堆上移除
    • 會調用__block變量內部的dispose函數,dispose函數內部會調用_Block_object_dispose函數
    • _Block_object_dispose函數會對__block變量內部引用的對象進行釋放操做,至關於執行一次release。

block訪問對象類型的auto變量和__block變量對比

相同點

當block存放在棧上是,對對象類型的auto變量和__block變量都不會產生強引用

不一樣點

當block被copy到堆上時

  • 訪問對象類型的auto變量時,block內部會調用copy函數,根據對象的修飾符(__strong、__weak、__unsafe_unretained)來決定是否對對象進行強引用,若是是__strong修飾的對象,則進行強引用。copy函數以下
//copy函數
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
    
}
複製代碼
  • 訪問__block變量時,block內部會直接調用copy函數,對__block變量進行強引用,copy函數以下
//copy函數
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign(&dst->a, src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}
複製代碼

當block從堆中移除

當block從堆中移除時,都會調用dispose函數來對引用的對象進行釋放

  • 引用對象類型的auto變量時調用的dispose函數
//dispose函數
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

複製代碼
  • 引用__block變量時調用的dispose函數
//dispose函數
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}
複製代碼

雖然調用的都是copy函數,可是傳遞的參數類型不一樣,訪問對象類型的auto變量時,傳遞的參數爲3(BLOCK_FIELD_IS_OBJECT),訪問__block變量時,傳遞的參數爲8(BLOCK_FIELD_IS_BYREF)

補充

block循環引用的問題

在使用block時,若是block做爲一個對象的屬性,而且在block中也使用到了這個對象,則會產生循環引用,致使block和對象相互引用,沒法釋放。Demo以下

typedef void(^TestBlock)(void);
@interface XLPerson : NSObject

@property(nonatomic, copy)NSString *name;
@property(nonatomic, copy)TestBlock block;

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        XLPerson *person = [[XLPerson alloc] init];
        person.name = @"張三";
        person.block = ^{
            NSLog(@"%@",person.name);
        };
        person.block();
    }
    return 0;
}
複製代碼

解決方式有兩種(此處主要講解ARC的狀況下):

  • 使用__weak來修飾對象
__weak typeof(person) weakPerson = person;
person.block = ^{
    NSLog(@"%@",weakPerson.name);
};
複製代碼
  • 使用__unsafe_unretained來修飾對象
__unsafe_unretained XLPerson *weakPerson = person;
person.block = ^{
    NSLog(@"%@",weakPerson.name);
};
複製代碼

__weak和__unsafe_unretained的區別

__weak和__unsafe_unretained最終的效果都是能shi使block不對外部訪問的對象造成強引用,而是造成弱引用。也就是說外部對象的引用計數不會增長。可是__weak和__unsafe_unretained也有區別,__weak在對象被銷燬後會自動將weak指針置爲nil,而__weak和__unsafe_unretained修飾的對象在被銷燬後,指針是不會被清空的,若是後續訪問到了這個指針,會報野指針的錯誤,所以在遇到循環引用的時候,優先使用__weak來解決。更多的關於__weak的內容會在後續文章中進行學習。

面試題

一、block的本質是什麼?

block其實就是封裝了函數調用與調用環境的OC對象,它的底層實際上是一個結構體。

二、__block的做用是什麼?

在block中若是想要修改外部訪問的auto變量,就須要使用__block來修飾auto變量,它會將修飾的變量封裝成一個結構體,結構體內部存放着變量的值。若是__block修飾的是對象類型,那麼在結構體中會保存着存儲對象內存地址的指針,同時在結構體中還多出兩個函數指針copy和dispose,用來管理對象的內存。

三、block做爲屬性時爲何要用copy來修飾?

在ARC中,block若是使用copy來修飾,會將block從棧上覆制到堆上,方便咱們手動管理block的內存,若是不用copy來修飾的話,那麼block就會存在棧上,由系統自動釋放內存。

四、使用block會遇到什麼問題?怎麼解決?

在使用block過程當中,會遇到循環引用的問題,解決方式就是使用__weak或者__unsafa_unretain來修飾外部引用的對象。優先使用__weak。

結束語

以上內容純屬我的理解,若是有什麼不對的地方歡迎留言指正。

一塊兒學習,一塊兒進步~~~

相關文章
相關標籤/搜索