iOS原理篇(五):Block探究

  • Block原理
  • Block變量捕獲
  • Block類型
  • copy操作和Block內部訪問對象類型的變量
  • __block修改變量及其本質
  • __block內存管理
  • Block循環(huán)引用問題

Block是一種可以在CC++以及Objective-C代碼中使用,類似于“閉包(closure)”的代碼塊分扎,借助Block機制哥力,開發(fā)者可以將代碼像對象一樣在不同的上下文環(huán)境中進行傳遞。
(這里說的不同上下文環(huán)境,我舉個例子:比如在A函數(shù)中定義了一個變量,它是一個局部變量,那么我要在B函數(shù)中去訪問肋联,這里就屬于兩個不同的上下文環(huán)境)

一、Block原理

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 20;
        void (^block)(int,int) = ^(int a, int b){
            NSLog(@"a = %d, b = %d, age = %d",a,b,age);
        };
        block(3,5);
    }
    return 0;
}

將上面main.m編譯生成C++代碼:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

main()函數(shù)

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int age = 20;
        void (*block)(int,int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
        ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 3, 5);
    }
    return 0;
}

__main_block_impl_0結構體

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

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

struct __maib_block_desc_0 {
    size_t reserved;
    size_t Block_size;
};

我們定義block變量刁俭,其實下面這句代碼:

void (*block)(int,int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));

就是調用結構體__main_block_impl_0內部的構造函數(shù)初始化一個結構體出來橄仍,然后取結構體地址&__main_block_impl_0賦給block指針,所以block底層是下面結構體;調用構造函數(shù)傳了三個參數(shù):
(void *)__main_block_func_0侮繁、&__main_block_desc_0_DATA虑粥、age

其中(void *)__main_block_func_0是下面函數(shù)的地址,這個函數(shù)就是封裝了block執(zhí)行邏輯的函數(shù)宪哩,通過上面的構造函數(shù)傳給__block_impl結構體的FuncPtr

static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
  int age = __cself->age; // bound by copy

    NSLog((NSString *)&__NSConstantStringImpl__var_folders__h_yv1h9mrx1155q6tq7brn8b9m0000gp_T_main_b54551_mi_0,a,b,age);
}

同樣娩贷,第二個參數(shù)類型&__main_block_desc_0_DATA是下面結構體地址,最終通過構造函數(shù)賦給了Desc锁孟,其中Block_size表示block結構體的大小;

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

main()函數(shù)中調用block(3, 5)最終轉化為下面代碼彬祖,通過將block強制轉換為__block_impl(這里__block_impl類型是__main_block_impl_0結構體第一個成員,所以可以轉) 品抽,最終直接找到impl中的FuncPtr進行調用

((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 3, 5);

二储笑、Block變量捕獲

Block變量捕獲是指在Block內部訪問外部變量時,如果外部變量是局部變量圆恤,則Block內部會將其捕獲突倍,具體捕獲形式看外部的這個局部變量是auto類型還是static類型:
如果是auto類型,直接將變量的值傳遞給Block內部盆昙,Block結構體內部會生成一個變量來存儲傳進來的值羽历,所以在Block外邊改變age=20,調用block()時內部打印的結果依然是age=10淡喜,因為此時進行的是值傳遞秕磷;
如果是static類型,會將變量的地址傳遞給Block內部拆火,block結構體內部會生成一個指針變量來存儲傳進來的地址值跳夭,所以在block外邊改變height=20,調用block()時內部打印的結果是height=20,因為此時進行的是指針傳遞们镜;

下面進行驗證:

  1. 局部變量兩種情況:
// 局部變量兩種情況
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // case-1: auto變量,離開作用域就銷毀
        auto int age = 10; //等價于 int age = 10;
        // case-2: static變量
        static int height = 10;
        
        void (^block)(void) = ^{
            NSLog(@"age is %d, height is %d",age, height);
        };
        age = 20;
        height = 20;
        
        block();
    }
    return 0;
}

打印結果:

age is 10, height is 20

編譯成C++:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  int *height;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

從編譯生成的結構體看出润歉,age是值傳遞模狭,height是指針傳遞;定義完block就將10&height捕獲到block內部踩衩,后邊調用block時訪問的結構體內部age是捕獲到的值10嚼鹉,height是捕獲到的地址&height

  1. 全局變量:因為是在全局區(qū)驱富,所以任何函數(shù)內部可以直接訪問

總結一下:


Block的本質:Block本質上也是一個OC對象锚赤,它內部也有個isa指針,但它是一個封裝了函數(shù)調用以及函數(shù)調用環(huán)境的OC對象;

三褐鸥、Block類型

Block有三種類型线脚,可以通過調用class方法或者isa指針查看具體類型,最終都是繼承自NSBlock類型:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^block)(void) = ^{
            NSLog(@"Hello World!");
        };
        
        NSLog(@"%@",[block class]);
        NSLog(@"%@",[[block class] superclass]);
        NSLog(@"%@",[[[block class] superclass] superclass]);
        NSLog(@"%@",[[[[block class] superclass] superclass] superclass]);
    }
    return 0;
}

打印結果:
05-block類型[46881:69217078] __NSGlobalBlock__
05-block類型[46881:69217078] __NSGlobalBlock
05-block類型[46881:69217078] NSBlock
05-block類型[46881:69217078] NSObject

三種類型:

  • __NSGlobalBlock__ (_NSConcreteGlobalBlock)
  • __NSStackBlock___NSConcreteStackBlock
  • __NSMallocBlock___NSConcreteMallocBlock
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^block1)(void) = ^{
            NSLog(@"Hello");
        };
        int age = 10;
        void (^block2)(void) = ^{
            NSLog(@"Hello - %d", age);
        };
        NSLog(@"%@ %@ %@",[block1 class], [block2 class], [^{
            NSLog(@"%d",age);
        } class]);
    }
    return 0;
}

打印結果:
05-block類型[47475:69339707] __NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__

那不同類型Block分別對應什么情況呢?

static int height = 30;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 沒有訪問外部變量
        void (^block1)(void) = ^{
            NSLog(@"--------------");
        };
        
        // 訪問 auto 變量
        int age = 10;
        void (^block2)(void) = ^{
            NSLog(@"--------------%d", age);
        };
        
        // 訪問 static 變量
        void (^block3)(void) = ^{
            NSLog(@"=-------------%d", height);
        };
        
        NSLog(@"%@ %@ %@",[block1 class], [block2 class], [block3 class]);
    }
    return 0;
}

打印結果:
05-block類型[48630:69576321] __NSGlobalBlock__ __NSMallocBlock__ __NSGlobalBlock__

可以看出浑侥,在沒有訪問外部變量的情況下姊舵,block1是一個__NSGlobalBlock__類型,存放在數(shù)據區(qū)寓落,此時的block1就相當于我們定一個了一個函數(shù)括丁,函數(shù)中的代碼沒有訪問另外一個函數(shù)(此處為main())中的變量;同理伶选,block3雖然訪問外部變量史飞,但static變量是全局的,同樣相當于單獨拿出去定義一個和main()函數(shù)上下文無關的函數(shù)仰税;
由于block2訪問了auto變量构资,相當于在block2封裝的函數(shù)中訪問了另外一個函數(shù)內部的變量(main()函數(shù)中的局部變量age),此時block2變?yōu)?code>__NSStackBlock__肖卧,因為它需要保存這個局部變量蚯窥,由于是在ARC環(huán)境,會自動對__NSStackBlock__類型進行copy操作塞帐,所以 block2打印類型是一個 __NSMallocBlock__類型拦赠;

關閉ARCMRC環(huán)境下打印:

打印結果:
05-block類型[49786:69814242] __NSGlobalBlock__ __NSStackBlock__ __NSGlobalBlock__

可以看出block2確實是一個__NSStackBlock__類型葵姥;

四荷鼠、copy操作和Block內部訪問對象類型的變量


copy操作分MRCARC兩種情況:
  • MRC環(huán)境:
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int age = 10;
        
        // 情況一:沒有訪問外部 auto 變量
        // 它是一個 NSGlobalBlock
        // 內存是放在數(shù)據段
        void (^block)(void) = ^{
            NSLog(@"---------");
        };
        

        // 情況二:訪問外部 auto 變量 age
        // 它是一個 NSStackBlock 隨時會被回收
        // 內存是在棧上
        // 通過 copy 操作轉變?yōu)?NSMallocBlock 把它放到堆上保活
        void (^block2)(void) = [^{
            NSLog(@"---------%d",age);
        } copy];
        
        // 因為在 MRC 環(huán)境 不用時要進行 release 操作
        [block2 release];
    
    }
    return 0;
}
  • ARC環(huán)境:
    ARC環(huán)境下榔幸,編譯器會根據情況自動將棧上的Block拷貝到堆上允乐,即自動進行一次copy操作,比如以下情況:
  1. 情況一:Block作為函數(shù)返回值
// 定義一個block類型
typedef void (^DJTBlock)(void);

// block作為函數(shù)返回值
DJTBlock myblock()
{
    // case1: 這里沒有訪問auto變量 是一個NSGlobalBlock
    return ^{
        NSLog(@"------------");
    };
    // 相當于下面這樣寫
    // DJTBlock block = ^{
    //   NSLog(@"------------");
    // };
    // return block;
    
    //-----------------------------------------------------------------
    
    // case2: 這里訪問了auto 是一個NSSackBlock 作為函數(shù)返回值ARC下自動copy成NSMallocBlock
    // int age = 10;
    // return ^{
    //   NSLog(@"------------%d",age);
    // };
}


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // ARC環(huán)境下 調用myblock()函數(shù)
        // DJTBlock作為myblock函數(shù)的返回值 編譯器自動進行一次 copy 操作
        // 所以 block變量指向的 DTJBlock 此時已經在堆上
        DJTBlock block = myblock();
        block();
        
        // 打印 block 類型
        NSLog(@"%@",[block class]);
    
    }
    return 0;
}
打印結果:
05-block--copy[64907:9167520] ------------
05-block--copy[64907:9167520] __NSGlobalBlock__

打印結果是一個NSGlobalBlock類型削咆,這是因為在函數(shù)my block()內部沒有訪問auto變量(上面block類型有闡述)牍疏,而對NSGlobalBlock類型的Block執(zhí)行copy操作生成的Block還是NSGlobalBlock,所以如果將返回改為myblock()函數(shù)內注釋部分拨齐,就會打印__NSMallocBlock__鳞陨。

  1. 情況二:將Block賦值給__strong強指針時
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int age = 10;
        // Block被強指針指著
        DJTBlock block = ^{
            NSLog(@"------------%d",age);
        };
        block();
       
        // 打印 block 類型
        NSLog(@"%@",[block class]);
    }
    return 0;
}
打印結果:
05-block--copy[69520:9293376] ------------10
05-block--copy[69520:9293376] __NSMallocBlock__
  1. 情況三:Block作為Cocoa API 中方法各含有usingBlock的方法參數(shù)時:
NSArray *array = @[];
[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

}];

這個參數(shù)Block也是一個堆上的block;

  1. 情況四:Block作為GCD API的方法參數(shù)時:
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
            
});
    
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0*NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            
});

Block內部訪問對象類型的變量

先看一個有趣的現(xiàn)象:

// DJTPerson.h
@interface DJTPerson : NSObject
@property(nonatomic, assign) int age;
@end

// DJTPerson.m
@implementation DJTPerson
- (void)dealloc
{
    NSLog(@"DJTPerson----dealloc");
}
@end
// main.m
#import "DJTPerson.h"

typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        {
            DJTPerson *person = [[DJTPerson alloc] init];
            person.age = 10;
        }
        NSLog(@"-----------------");// 打斷點
    }
    return 0;
}

在上面NSLog(@"-----------------");處打斷點,運行程序發(fā)現(xiàn)控制臺打印:

05-block訪問對象類型的auto變量[77563:9561984] DJTPerson----dealloc
(lldb) 

說明在斷點前的中括號結束瞻惋,person變量就已經釋放厦滤,接著我們定義一個block,在內部訪問personage屬性:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        DJTBlock block;
        
        {
            DJTPerson *person = [[DJTPerson alloc] init];
            person.age = 10;
            
            block = ^{
                NSLog(@"-----------%d",person.age);
            };
        }
        
        NSLog(@"-----------------");// 打斷點
        
    }
    return 0;
}

通用在NSLog(@"-----------------");處打斷點歼狼,運行程序發(fā)現(xiàn)控制臺無打印掏导,說明person沒被回收。

為什么被第二種情況下person沒有被回收呢羽峰?為了驗證我們將代碼簡化并編譯成C++來進行底層原理分析:

typedef void (^DJTBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        DJTPerson *person = [[DJTPerson alloc] init];
        person.age = 10;
        
        DJTBlock block = ^{
            NSLog(@"-----------%d",person.age);
        };
    }
    return 0;
}

日常操作命令:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

在編譯生成的C++文件中查看生成的block結構體:

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

由于personDJTPerson *類型趟咆,所以捕獲到block內部也是DJTPerson *類型添瓷,即struct __main_block_impl_0結構體內部可以看到有一個DJTPerson *類型變量person;下面先從一個角度理解為什么person沒有被釋放:

在上面代碼中忍啸,我們定義的Block是被一個DJTBlock類型的變量block強引用的仰坦,即這句代碼:

DJTBlock block = ^{
   NSLog(@"-----------%d",person.age);
};

ARC環(huán)境下,被強引用的這個Block(訪問了auto變量)會自動拷貝到堆上计雌,而這個Block內部(編譯成C++即為struct __main_block_impl_0結構體)又有一個DJTPerson*類型的指針指向外面這個person對象悄晃,所以只要這個Block在,那么這個強指針就在凿滤,所以外邊的person對象不會被釋放妈橄;

換成MRC環(huán)境:

// DJTPerson.h
@interface DJTPerson : NSObject
@property(nonatomic, assign) int age;
@end

// DJTPerson.m
@implementation DJTPerson
- (void)dealloc
{
   [super dealloc];
    NSLog(@"DJTPerson----dealloc");
}
@end
// main.m
#import "DJTPerson.h"

typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        DJTBlock block;
        {
            DJTPerson *person = [[DJTPerson alloc] init];
            person.age = 10;
            
            block = ^{
                NSLog(@"-----------%d",person.age);
            };
            
            [person release];
        }
        NSLog(@"---------------");// 打斷點
    }
    return 0;
}

依然在NSLog(@"---------------");打斷點,發(fā)現(xiàn)控制臺打印結果:

05-block訪問對象類型的auto變量[83896:9803518] DJTPerson----dealloc

發(fā)現(xiàn)person被釋放翁脆,這是因為即使block內部訪問了person對象眷蚓,MRC環(huán)境下,block內部訪問了auto變量反番,它是一個棧上block沙热,但并不會自動拷貝到堆上,由于它是一個NSStackBlock罢缸,內部并不會對外部person強引用(這里說強引用并不準確篙贸,在MRC環(huán)境沒有強引用說法,應該描述為沒有對外邊person進行retain操作枫疆,但為了好理解 so...)爵川,所以在執(zhí)行完[person release]以后,雖然Block還沒有離開其作用域(Block作用域到return 0;前到大括號)息楔,但person就被釋放寝贡;可以通過[block copy]將其復制到堆上,這樣內部就會對外邊的person強引用(其實是retain操作)從而敝狄溃活person圃泡,當然在Block銷毀的時候,內部對person還會進行一次release操作愿险,這樣一加一減洞焙,就保持了平衡;

要點:椪玻空間的BlockNSStackBlock)是不會對外邊auto對象進行保活(ARC環(huán)境表現(xiàn)為不會強引用熔任,MRC下表現(xiàn)為不會進行retain操作)褒链,只有拷貝到堆上(NSMallocBlock)才會對其自動保活疑苔。

回到ARC環(huán)境:
看一下__weak作用:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        DJTBlock block;
        {
            DJTPerson *person = [[DJTPerson alloc] init];
            person.age = 10;
            
            // 這里使用 __weak 修飾
            __weak DJTPerson *weakPerson = person;
            block = ^{
                NSLog(@"-----------%d",weakPerson.age);
            };
            
        }
        NSLog(@"---------------"); // 打斷點
    }
    return 0;
}

依然在NSLog(@"---------------");處打斷點甫匹,打印結果為:

05-block訪問對象類型的auto變量[87323:9930285] DJTPerson----dealloc

這說明,即使在ARC環(huán)境,Block被拷貝到堆上兵迅,由于我們用__weak類型的__weakPerson訪問了外部auto變量抢韭,它也不會對外部person進行強引用。

同樣我們把上述代碼編譯成C++恍箭,由于弱引用需要運行時機制來支持刻恭,所以我們不能進行靜態(tài)編譯,還需要運行時調用扯夭,指定運行時系統(tǒng)版本鳍贾,所以編譯命令如下:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

在生成的C++代碼中找到__main_block_impl_0結構體,發(fā)現(xiàn)是一個弱引用:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  DJTPerson *__weak weakPerson;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, DJTPerson *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

總結一下:

  • Block在棧上交洗,無論 ARC骑科、MRC環(huán)境,block內部都不會對外部對象類型的auto變量產生強引用构拳,就算Block內部生成強指針咆爽,也不會對外部person產生強引用,因為Block自己就在棧上置森,隨時可能被銷毀斗埂;
  • Block在堆上:
    ARC環(huán)境下,訪問外部對象類型的auto變量暇藏,編譯后:
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  DJTPerson *__strong person;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, DJTPerson *__strong _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__main_block_desc_0結構體中蜜笤,多了兩個函數(shù):__main_block_copy_0__main_block_dispose_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*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

分別看它們實現(xiàn):

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

當對Block進行copy操作時,會調用這個__main_block_copy_0函數(shù)盐碱,在它內部調用_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/)把兔,會對外部person對象產生強引用或者弱引用,這取決于block內部使用__strong指針還是__weak指針訪問瓮顽。

Block從堆上移除县好,會調用__main_block_dispose_0函數(shù),它內部調用_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);暖混,會對外部person對象進行一次release操作缕贡。

MRC環(huán)境下,也是由這兩個函數(shù)決定是否進行retainrelease操作拣播。

五晾咪、__block修改變量及其本質

我們先看下面一段代碼:

typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int age = 10;
        
        DJTBlock block = ^{
            age = 20; // 報錯:Variable is not assignable (missing __block type specifier)
            NSLog(@"-----------%d",age);
        };   
    }
    return 0;
}

ARC下直接在Block內部修改age會報錯,這就相當于在block生成的結構體中FuncPtr指向的函數(shù)中去修改main函數(shù)中的局部變量(如果這里agestatic或者全局變量贮配,可以修改谍倦,因為這兩種變量一直在內存中),上下文環(huán)境發(fā)生了改變泪勒,所以不能直接訪問age昼蛀;我們使用__block修飾age變量宴猾,然后編譯成C++

typedef void (^DJTBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int age = 10;
        DJTBlock block = ^{
            age = 20;
            NSLog(@"-----------%d",age);
        };
    }
    return 0;
}

會生成下面結構體:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_age_0 *age; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看到在Block結構體中多了__Block_byref_age_0 *age;,看一下 __Block_byref_age_0發(fā)現(xiàn)它也是一個結構體:

struct __Block_byref_age_0 {
    void *__isa;
    __Block_byref_age_0 *__forwarding;
    int __flags;
    int __size;
    int age;
};

這里看出編譯器將 __block修飾的變量(這里是age)包裝成一個對象__Block_byref_age_0(因為它內部有isa指針叼旋,所以可以認為它是個對象)仇哆,Block內部(__main_block_impl_0結構體中)并不會直接擁有這個變量age,而是擁有__Block_byref_age_0這個結構體夫植,然后__Block_byref_age_0結構體中有一個int age變量讹剔,我們在Block內部改變age = 20,實際上就是賦值給__Block_byref_age_0結構體中的age變量偷崩。

我們對__block int age = 10轉化成的C++代碼進行簡化:

// __block int age = 10;對應下面c++代碼:
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};

// 進行簡化
__Block_byref_age_0 age = {
     0,
     &age,
     0,
     sizeof(__Block_byref_age_0),
     10    
 };

對應到下面結構體初始化:

struct __Block_byref_age_0 {
   void *__isa;
   __Block_byref_age_0 *__forwarding;
   int __flags;
   int __size;
   int age;
};

__forwarding指針傳入&age指向__Block_byref_age_0 age結構體自己(這里&age是結構體地址辟拷,不要混淆),10賦值給了__Block_byref_age_0結構體內部的age變量阐斜;我們再看下修改age為20的代碼:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_age_0 *age = __cself->age; // bound by ref
 // 這里是 age = 20;
  (age->__forwarding->age) = 20;
   NSLog((NSString *)&__NSConstantStringImpl__var_folders__h_yv1h9mrx1155q6tq7brn8b9m0000gp_T_main_05a705_mi_0,(age->__forwarding->age));
}

可以發(fā)現(xiàn)先通過 __cself->age找到__Block_byref_age_0結構體衫冻,然后(age->__forwarding->age) = 20;通過__forwarding指針修改結構體內部的age變量,__forwarding指向結構體自己谒出,那為什么要多此一舉通過__forwarding指針去修改內部age隅俘,而不通過結構體指針直接去修改呢?這是為了保證Blockcopy到堆上時笤喳,不管訪問棧上還是堆上Block为居,通過forwarding指針都是找到堆上。

這里如果__block修飾的是一個對象類型杀狡,比如下面代碼:

typedef void (^DJTBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int age = 10;
        __block NSObject *obj = [[NSObject alloc] init];
        DJTBlock block = ^{
            obj = nil;
            age = 20;
        };
    }
    return 0;
}

轉換為C++同樣會多生成一個對應的結構體蒙畴,只不過內部會多出兩個方法copy``和dispose方法來負責相應的內存管理:

// __block age 對應的結構體
struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};

// __block NSObject 對應的結構體
struct __Block_byref_obj_1 {
  void *__isa;
__Block_byref_obj_1 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *__strong obj;
};

下面看一個例子:

typedef void (^DJTBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSMutableArray *mutarray = [NSMutableArray array];
        DJTBlock block = ^{
            [mutarray addObject:@(12)];
            [mutarray addObject:@(13)];
        };
    }
    return 0;
}

這里不會報錯,是因為我們并沒有修改mutarray指針呜象,而是在使用mutarray指針膳凝,除非我們修改mutarray指針的值,比如 mutarray = nil;才需要__block來修飾恭陡;

六蹬音、__block的內存管理

我們知道,當Block內部訪問外部對象類型的變量時休玩,如下面簡單代碼:

typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *object = [[NSObject alloc] init];
        DJTBlock block = ^{
            NSLog(@"%p", object);
        };
        block();
    }
    return 0;
}

block編譯成C++后的結構:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSObject *__strong object; //內部強引用外部變量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _object, int flags=0) : object(_object) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看到在block結構體內部會生成一個強指針指向外邊的object對象著淆,并且在block被拷貝到堆上時,調用__main_block_desc_0中的copy函數(shù)拴疤,對這個指針指向的對象進行一次retain操作永部,即引用計數(shù)+1,當然如果用__weak修飾object會生NSObject *__weak object;此時不會強引用;
那當我們用__block修飾變量時呐矾,比如分別修飾基礎數(shù)據類型age扬舒,和對象類型obj1,如下代碼:

typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
       
        __block int age = 10;
        __block NSObject *obj1 = [[NSObject alloc] init];
        NSObject *object = [[NSObject alloc] init];
        DJTBlock block = ^{
            NSLog(@"%d %p %p", age,obj1, object);
        };
        block();
    }
    return 0;
}

編譯成C++:

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};
struct __Block_byref_obj1_1 {
  void *__isa;
__Block_byref_obj1_1 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *__strong obj1;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSObject *__strong object;
  __Block_byref_age_0 *age; // by ref
  __Block_byref_obj1_1 *obj1; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _object, __Block_byref_age_0 *_age, __Block_byref_obj1_1 *_obj1, int flags=0) : object(_object), age(_age->__forwarding), obj1(_obj1->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__block修飾的變量ageobj1分別生成構體__Block_byref_age_0__Block_byref_obj1_1凫佛,它們的本質就是OC對象讲坎,所以在block對應的結構體內部生成兩個結構體指針指向這兩個對象,即

__Block_byref_age_0 *age; // by ref
__Block_byref_obj1_1 *obj1; // by ref

它們其實和object一樣愧薛,因為__block修飾的變量也是轉換成結構體晨炕,而且內部有isa指針,其實就是OC對象毫炉,所以也會在__main_block_desc_0中生成兩個函數(shù):copydispose瓮栗,來管理對象的內存,可以看下結構:

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) 
{
  _Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
  _Block_object_assign((void*)&dst->obj1, (void*)src->obj1, 8/*BLOCK_FIELD_IS_BYREF*/);
  _Block_object_assign((void*)&dst->object, (void*)src->object, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) 
{
  _Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
  _Block_object_dispose((void*)src->obj1, 8/*BLOCK_FIELD_IS_BYREF*/);
  _Block_object_dispose((void*)src->object, 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};

所以__block內存管理可以總結一下:

  • Block在棧上時瞄勾,內部并不會對__block修飾的外部變量產生強引用

  • Blockcopy到堆上時,會調用Block內部的copy函數(shù),而copy函數(shù)內部會調用_Block_object_assign函數(shù)孩饼,它內部會對__block變量形成強引用(retain)萝衩。


  • Block從堆上移除時,會調用Block內部的dispose函數(shù)趾疚,dispose函數(shù)內部會調用_Block_object_dispose函數(shù)缨历,在_Block_object_dispose函數(shù)中會對引用的__block變量進行引用計數(shù)-1release


下面我們對比下Block內部訪問外部變量幾種情況:

typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
      
         //直接將20存放在Block生成的結構體中
        int num = 20; 
        //Block結構體內部生成一個強指針 強引用object對象
        NSObject *object = [[NSObject alloc] init]; 
        // Block內部生成一個弱指針 弱引用object對象
        __weak NSObject *weakObject = object; 
        // Block內部生成一個結構體指針,指針指向的結構體內部存儲著變量age
        __block int age = 10; 
        //Block內部生成一個結構體指針糙麦,指針指向的結構體內部存儲著變量obj1
        __block NSObject *obj1 = [[NSObject alloc] init];
        
        DJTBlock block = ^{
            NSLog(@"%d %d %p %p %p",num, age, obj1, object, weakObject);
        };
        block();
    }
    return 0;
}

編譯成C++看看block結構體辛孵,和上邊注釋的一致:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int num;
  NSObject *__strong object;
  NSObject *__weak weakObject;
  __Block_byref_age_0 *age; // by ref
  __Block_byref_obj1_1 *obj1; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, NSObject *__strong _object, NSObject *__weak _weakObject, __Block_byref_age_0 *_age, __Block_byref_obj1_1 *_obj1, int flags=0) : num(_num), object(_object), weakObject(_weakObject), age(_age->__forwarding), obj1(_obj1->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

這里主要對比一下 對象類型的auto變量和__block修飾的變量內存管理的區(qū)別:

  • 相同點:
    • Block在棧上時,對它們都不會產生強引用
    • Block拷貝到堆上時赡磅,都會通過copy函數(shù)來處理它們:
      (1)__block變量(假設變量名叫做a
      _Block_object_assign((void*)&dst->a, (void*)src->a,   8/*BLOCK_FIELD_IS_BYREF*/);
      
      (2)對象類型的auto變量(假設變量名叫做p
      _Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
      
    • Block從堆上移除時魄缚,都會通過dispose函數(shù)來釋放它們
      (1)__block變量(假設變量名叫做a
      _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*
      
      (2)對象類型的auto變量(假設變量名叫做p
      _Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
      
  • 不同點(主要是在引用的問題上)
    (1)對象類型的auto變量,根據傳進來時是__strong還是__weak類型決定調用copy函數(shù)時Block內部對傳進來的變量進行強還是弱引用焚廊。
    (2)如果時__block類型的變量冶匹,比如__block int age = 20;,它被封裝成一個OC對象节值,調用copy函數(shù)時Block內部直接對它產生強引用徙硅,對它的內存進行管理,不存在__weak修飾int age這種操作搞疗,所以沒有弱引用這一說嗓蘑。(這里強引用的是age轉換成的結構體對象,真正的age變量的值存儲在結構體里邊)匿乃;

但是如果是下面代碼

typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
       
        NSObject *object = [[NSObject alloc] init];
        __block __weak NSObject *weakObject = object;
        DJTBlock block = ^{
            NSLog(@"%p %p", object, weakObject);
        };
        block();
    }
    return 0;
}

編譯成C++:

struct __Block_byref_weakObject_0 {
  void *__isa;
__Block_byref_weakObject_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *__weak weakObject;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSObject *__strong object;
  __Block_byref_weakObject_0 *weakObject; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _object, __Block_byref_weakObject_0 *_weakObject, int flags=0) : object(_object), weakObject(_weakObject->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看到在__block修飾變量生成的結構體 __Block_byref_weakObject_0內部桩皿,通過__weak弱引用變量weakObject,即Block結構體內部是一個強指針指向__block生成的結構體幢炸,即這句代碼

__Block_byref_weakObject_0 *weakObject;
(注意雖然名字中有`weak`但這是一個強指針)

而在結構體__Block_byref_weakObject_0內部:

NSObject *__weak weakObject;

這才是一個弱指針泄隔,指向外部傳入的弱引用對象weakObject,它表達了外部傳入變量的類型是__weak還是__strong

注意:這里在MRC下有個特殊情況宛徊,在__block生成的結構體內部佛嬉,始終都是弱引用逻澳,不會對外邊對象進行強引用。


MRC環(huán)境下驗證暖呕, 下面代碼在block();調用前person就已經掛了斜做,說明確實內部沒有強引用:

#import "DJTPerson.h"

typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
       
        __block DJTPerson *person = [[DJTPerson alloc] init];
        DJTBlock block = [^{
            NSLog(@" %p", person);
        } copy];
        [person release];
        block();
    }
    return 0;
}

七、Block相關問題

  • Block的原理是怎樣的湾揽?本質是什么瓤逼?
  • __block的作用是什么?有什么使用注意點库物?
  • Block的屬性修飾詞為什么是copy霸旗?使用Block有哪些使用注意?
  • Block在修改NSMutableArray戚揭,需不需要添加__block诱告?

理解上邊原理再回答這些問題應該不難吧。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末毫目,一起剝皮案震驚了整個濱河市蔬啡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌镀虐,老刑警劉巖箱蟆,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異刮便,居然都是意外死亡空猜,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門恨旱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來辈毯,“玉大人,你說我怎么就攤上這事搜贤∽晃郑” “怎么了?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵仪芒,是天一觀的道長唁影。 經常有香客問我,道長掂名,這世上最難降的妖魔是什么据沈? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮饺蔑,結果婚禮上锌介,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好孔祸,可當我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布隆敢。 她就那樣靜靜地躺著,像睡著了一般融击。 火紅的嫁衣襯著肌膚如雪筑公。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天尊浪,我揣著相機與錄音,去河邊找鬼封救。 笑死拇涤,一個胖子當著我的面吹牛,可吹牛的內容都是我干的誉结。 我是一名探鬼主播鹅士,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼惩坑!你這毒婦竟也來了掉盅?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤以舒,失蹤者是張志新(化名)和其女友劉穎趾痘,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蔓钟,經...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡永票,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了滥沫。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片侣集。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖兰绣,靈堂內的尸體忽然破棺而出世分,到底是詐尸還是另有隱情,我是刑警寧澤缀辩,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布臭埋,位于F島的核電站,受9級特大地震影響雌澄,放射性物質發(fā)生泄漏斋泄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一镐牺、第九天 我趴在偏房一處隱蔽的房頂上張望炫掐。 院中可真熱鬧,春花似錦睬涧、人聲如沸募胃。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽痹束。三九已至检疫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間祷嘶,已是汗流浹背屎媳。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留论巍,地道東北人烛谊。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像嘉汰,于是被迫代替她去往敵國和親丹禀。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內容