探尋Block的本質(5)—— 對象類型的變量捕獲

首先我們來看這么一段代碼案例

*********************CLPerson.h*********************
#import <Foundation/Foundation.h>

@interface CLPerson : NSObject
@property (nonatomic,assign) int age;
@end


*********************CLPerson.m*********************
#import "CLPerson.h"

@implementation CLPerson
-(void)dealloc {
    NSLog(@"%s",__func__);
}
@end

*********************main.m*********************
#import <Foundation/Foundation.h>
#import "CLPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        {//臨時作用域開始
            CLPerson *person = [[CLPerson alloc] init];
            person.age = 10;
        }//臨時作用域結束
        
        NSLog(@"-----------flag1");
    }
    return 0;
}

image

通過在打印標記flag1處斷點調試可看出,在臨時作用域里面的person對象只要出了作用域就會被釋放,這一點是很好理解的。

上面的代碼加入block,調整如下

*********************CLPerson.h*********************
#import <Foundation/Foundation.h>

@interface CLPerson : NSObject
@property (nonatomic,assign) int age;
@end


*********************CLPerson.m*********************
#import "CLPerson.h"

@implementation CLPerson
-(void)dealloc {
    NSLog(@"%s",__func__);
}
@end

*********************main.m*********************
#import <Foundation/Foundation.h>
#import "CLPerson.h"

typedef void(^CLBlock)(void);//???

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    
        CLBlock myBlock;//???
        {//臨時作用域開始
            CLPerson *person = [[CLPerson alloc] init];
            person.age = 10;

             myBlock = ^{//???
                NSLog(@"---------%d",person.age);
            };
        }//臨時作用域結束
        
        NSLog(@"-----------flag1");
    }
    return 0;
}

再次在打印標記flag1處斷點調試運行一下

在這里插入圖片描述

結果告訴我們,出了臨時作用域,person對象沒有被釋放。這里有兩個注意點:

  • 由于現在是ARC環境,myBlock屬于強指針,因此在將block對象賦值給myBlock指針的時候,編譯器會自動對block對象執行copy操作,因此賦值完成后,myBlock指向的是一個堆空間上的block對象副本。

順便,通過終端命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp,拿到編譯后的.cpp文件,我們先查看一下當前代碼編譯底層樣式,經整理簡化后如下


struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  CLPerson *person;

  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, CLPerson *_person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};


static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  CLPerson *person = __cself->person; // bound by copy

                NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__p19yp82j0xd2m_1k8fpr77z40000gn_T_main_2cca58_mi_0,((int (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("age")));
            }



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*/);
}




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




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[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        CLBlock myBlock;

        {
            CLPerson * person = objc_msgSend(objc_msgSend(objc_getClass("CLPerson"), 
                                                          sel_registerName("alloc")
                                                          ), 
                                             sel_registerName("init")
                                             );
            

            objc_msgSend(person, 
                         sel_registerName("setAge:"), 
                         30
                         );


            myBlock = objc_msgSend(&__main_block_impl_0(__main_block_func_0, 
                                                        &__main_block_desc_0_DATA, 
                                                        person, 
                                                        570425344), 
                                   sel_registerName("copy")
                                   );


        }


    }


    return 0;
}

以上是 【ARC環境-->堆上的block-->強指針CLPerson *person 】所對應的運行結果以及底層實現。我們發現于之前捕獲一個基本類型的auto變量所不同的是,當block捕捉對象類型的auto變量的時候,__main_block_desc_0結構體里面多了兩個彩蛋

  • 函數指針copy,也就是__main_block_copy_0(),內部調用了_Block_object_assign()
  • 函數指針dispose,也就是__main_block_dispose_0(),內部調用了_Block_object_dispose()
    捕捉對象類型的auto變量時__main_block_desc_0的變化

    這里還需要注意的是,ARCCLPerson *person被認為是強指針,等價于_strong CLPerson *person,而弱指針需要顯式地表示為__weak CLPerson *person。通過終端命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-9.0.0 main.m -o main.cpp,可以看到block的內捕獲到的person指針如下
    image

為了對比,我們再分別看一下下面三種 場景分別是什么情況的:

  • ARC環境-->堆上的block-->弱指針__weak CLPerson *person
  • ARC環境-->棧上的block-->強指針CLPerson *person
  • ARC環境-->棧上的block-->弱指針__weak CLPerson *person




ARC環境-->堆上的block-->弱指針__weak CLPerson *person】案例如下

***********************main.m*************************
#import <Foundation/Foundation.h>
#import "CLPerson.h"

typedef void(^CLBlock)(void);


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLBlock myBlock;
        
        {//臨時作用域開始
            __weak CLPerson * person = [[CLPerson alloc] init];
            person.age = 30;
            
            myBlock = ^{
                NSLog(@"---------%d",person.age);
            } ;
            
        }//臨時作用域結束

        NSLog(@"-------------");
        
    }
    
    NSLog(@"------main autoreleasepool end-------");
    
    return 0;
}

block的底層結構如下

image

ARC->堆block->弱指針運行結果

運行結果顯示堆上的block使用弱指針__weak CLPerson *person,沒有影響person所指向對象的生命周期,出了臨時作用域的之后就被釋放了。




ARC環境-->棧上的block-->強指針CLPerson *person

***********************main.m*************************
#import <Foundation/Foundation.h>
#import "CLPerson.h"

typedef void(^CLBlock)(void);


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLBlock myBlock;
        
        {//臨時作用域開始
            CLPerson * person = [[CLPerson alloc] init];
            person.age = 30;
            
            ^{
                NSLog(@"---------%d",person.age);
            } ;
            
        }//臨時作用域結束

        NSLog(@"-------------");
        
    }
    
    NSLog(@"------main autoreleasepool end-------");
    
    return 0;
}

block底層結構如下


image
ARC->棧block->強指針運行結果

運行結果顯示棧上的block使用強指針CLPerson *person,沒有影響person所指向對象的生命周期,出了臨時作用域的之后就被釋放了。




ARC環境-->棧上的block-->弱指針__weak CLPerson *person

***********************main.m*************************
#import <Foundation/Foundation.h>
#import "CLPerson.h"

typedef void(^CLBlock)(void);


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLBlock myBlock;
        
        {//臨時作用域開始
            __weak CLPerson * person = [[CLPerson alloc] init];
            person.age = 30;
            
            ^{
                NSLog(@"---------%d",person.age);
            } ;
            
        }//臨時作用域結束

        NSLog(@"-------------");
        
    }
    
    NSLog(@"------main autoreleasepool end-------");
    
    return 0;
}

block底層結構為


image
ARC->棧block->弱指針運行結果

運行結果顯示棧上的block使用弱指針__weak CLPerson *person,沒有影響person所指向對象的生命周期,出了臨時作用域的之后就被釋放了。




從上面的四種情況的對比看出,只有堆上block使用強指針CLPerson *person的時候,才會影響該指針所指對象的生命周期,這是怎么一回事呢?

這個時候就要回到我們上面發現的那兩個彩蛋了】也就是__main_block_desc_0中的函數指針copydispose。實際上這兩個函數指針分別是在block從棧空間拷貝到堆空間的時候,以及堆空間的block被釋放的時候使用的。

block從棧拷貝到堆時,系統會通過block中的函數指針copy調用函數__xxx_block_copy_x

__xxx_block_copy_x(struct __main_block_impl_0*dst, 
                   struct __main_block_impl_0*src
                   );

注意這個函數的兩個參數,

  • 第一個dst代表拷貝之前棧空間的block
  • 第二個src代表拷貝之后堆空間上的block

這個函數里面調用的是_Block_object_assign函數

_Block_object_assign((void*)&dst->person, 
                     (void*)src->person, 
                     3/*BLOCK_FIELD_IS_OBJECT*/
                     );

這個函數的作用是把dst(棧block)內部所捕獲的那個person對象(指針)的值賦值(assign)給src(堆block)內的person指針。
如果是ARC環境,會根據person的強弱性(也就是之前的修飾詞 __weak__strong)來決定src(堆block)是否持有對象。如果person是被__strong修飾,就會通過 [person retain],使得person所指向的對象的引用計數+1,這樣,src(堆block)就持有了(也就是強引用person所指向的對象。

當堆上的block即將被釋放的時候,系統會通過block中的函數指針dispose調用函數__xxx_block_dispose_x

__main_block_dispose_0(struct __main_block_impl_0*src);

很明顯其中的參數src就是堆上的block,該函數內部調用了_Block_object_dispose函數

_Block_object_dispose((void*)src->person, 
                      3/*BLOCK_FIELD_IS_OBJECT*/
                      );

ARC下,該函數的作用是,根絕src(堆block)內部的person指針的強弱性(__weak/__strong),決定如何處理person所指向的對象,如果是__strong,說明src持有了person所指向的對象(也就是強引用),因為此時src即將被釋放,所以需要放開持有,也即是調用一下[person release],使得person所指對象的引用計數-1

為什么上面我都強調了ARC環境下 呢,因為MRC里面是沒有__weak__strong這兩個東西的。ARC就是根據我們代碼中的__weak/__strong標記,來自動進行一些內存管理相關的處理。相信上面的分析應該能讓你有所領悟。

如果回到MRC,其實內存管理本質原則沒有變化,無非就是retainrelease操作,我個人認為過程會相對ARC來說更簡單一些。
首先,內部使用了auto變量(基礎類型/對象類型)的block是在棧空間上的,它不會對對象類型的auto變量進行retain操作,也就是不會持有該對象。
當我們對一個棧block調用copy方法進行拷貝操作的時候,會跟ARC一樣以相同的方式最終調用_Block_object_assign函數,只不過此時會直接對堆block上 所捕獲的對象進行調用retain方法,進行持有,不會進行__weak/__strong的判斷,因為MRC里面根本沒有這兩個小兄弟。
當一個堆上的block即將釋放的時候,也會最終調用_Block_object_dispose函數,對該block 所捕獲的對象調用release方法,這樣堆上的block就放開了對該對象的持有(強引用)。

最后,在通過幾張圖示,來說明一下【block對于對象類型的auto變量的捕獲原理

MRC下棧block捕獲對象類型的auto變量

MRC下堆block捕獲對象類型的auto變量
ARC下棧block捕獲對象類型的auto變量,__strong
ARC下棧block捕獲對象類型的auto變量,__weak
ARC下堆block捕獲對象類型的auto變量,__strong
ARC下堆block捕獲對象類型的auto變量,__weak

以上就是block對于對象類型auto變量的捕獲過程。




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

推薦閱讀更多精彩內容