精华内容
下载资源
问答
  • Block 原理详解

    千次阅读 2014-06-20 15:31:14
    Block 对象是C语言层面的语法,也是一个运行时特性. 它们很类似与标准的C函数,但是除了可执行的代码,它们还包含了与自动(栈)或托管(堆)的内存所绑定的变量。因此一个block维护了一系列的状态(即数据),在...

    介绍


    Block 对象是C语言层面的语法,也是一个运行时特性. 它们很类似与标准的C函数,但是除了可执行的代码,它们还包含了与自动(栈)或托管(堆)的内存所绑定的变量。因此一个block维护了一系列的状态(即数据),在执行时会改变代码的行为。

    你可以使用blocks编写函数表达式当参数传入API,也可以将其保存下来用于多线程。Blocks在回调中非常有用,因为block不仅包含着回调时需要执行的代码,还包含了执行代码时需要的数据。

    你可以在Mac OS X 10.6和iOS 4.0之后的版本上的GCC附带的Clang上使用。blocks运行时库是开源的,见此 LLVM’s compiler-rt subproject repository.Blocks也已经呈现给了C标准工作组,见 N1370: Apple’s Extensions to C (其中包含了垃圾回收). 正如Objective-C和C++都是C的衍生语言,blocks也被设计成可以在这三种语言中使用 (如同Objective-C++). (语法也可以表现出其目标).


    你应该阅读本文档来学习block的相关知识,以将block用于C,C++,Objective-C中,并提高你的程序的效率可可维护性。

     

    文档结构

    本文档包括以下章节:

    • "Blocks入门" 提供了快速,使用的blocks简介
    • “整体概念” 提供了blocks在概念上的介绍
    • "声明和创建Blocks" 为您展示如何声明block变量及实现blocks
    • "Blocks和变量" 描述blocks和变量之间的相互作用, __block 修饰符的作用
    • "使用Blocks" 详解各种用法范式 

    Blocks入门

    下面章节使用实际的例子帮助您入门blocks

    声明和使用Block

    你要使用^操作符去声明一个block变量,^也是标示着一段block文字的开始。block的实体包含在{}中,如下所示(形同C,;表示语句的终结):

     

    Objective-c代码  

    1. int multiplier = 7;  
    2. int (^myBlock)(int) = ^(int num) {  
    3.     return num * multiplier;  
    4. };  

     示例的详解如下(图片不好翻译凑合着看): 





     

    注意block可以使用其定义范围内的变量.

    如果你把block声明为一个变量,你可以把它当一个函数(function,本文中特指C语言形式的函数)一样调用:

     

    Objective-c代码  

    1. int multiplier = 7;  
    2. int (^myBlock)(int) = ^(int num) {  
    3.     return num * multiplier;  
    4. };  
    5.    
    6. printf("%d", myBlock(3));  
    7. // prints "21"  

     

    直接使用Block

    在很多场景下,你不需要定义一个block变量,作为替代,仅仅只需要在需要block参数的地方写block文字即可。下例使用了qsort_b 函数. qsort_b 很类似标准的 qsort_r 函数,不过它使用block作为最后一个参数.

     

    Objective-c代码  

    1. char *myCharacters[3] = { "TomJohn""George""Charles Condomine" };  
    2.    
    3. qsort_b(myCharacters, 3, sizeof(char *), ^(const void *l, const void *r) {  
    4.     char *left = *(char **)l;  
    5.     char *right = *(char **)r;  
    6.     return strncmp(left, right, 1);  
    7. });  
    8.    
    9. // myCharacters is now { "Charles Condomine""George""TomJohn" }  

     

    Cocos中的Blocks

    许多 Cocoa frameworks 中的方法(method,特指Objecitve-C的方法即[])使用block作为参数, 常见于对集合中对象的操作或一个操作完成之后的回调. 下例展示和如何在NSArray 的方法 sortedArrayUsingComparator: 中使用block。该方法使用一个block作为参数. 例子中的block被定义为一个 NSComparator 类型的局部变量:

     

    Objective-c代码  

    1. NSArray *stringsArray = [NSArray arrayWithObjects:  
    2.                                  @"string 1",  
    3.                                  @"String 21",  
    4.                                  @"string 12",  
    5.                                  @"String 11",  
    6.                                  @"String 02", nil];  
    7.    
    8. static NSStringCompareOptions comparisonOptions = NSCaseInsensitiveSearch | NSNumericSearch |  
    9.         NSWidthInsensitiveSearch | NSForcedOrderingSearch;  
    10. NSLocale *currentLocale = [NSLocale currentLocale];  
    11.    
    12. NSComparator finderSortBlock = ^(id string1, id string2) {  
    13.    
    14.     NSRange string1Range = NSMakeRange(0, [string1 length]);  
    15.     return [string1 compare:string2 options:comparisonOptions range:string1Range locale:currentLocale];  
    16. };  
    17.    
    18. NSArray *finderSortArray = [stringsArray sortedArrayUsingComparator:finderSortBlock];  
    19. NSLog(@"finderSortArray: %@", finderSortArray);  
    20.    
    21. /*  
    22. Output:  
    23. finderSortArray: (  
    24.     "string 1",  
    25.     "String 02",  
    26.     "String 11",  
    27.     "string 12",  
    28.     "String 21"  
    29. )  
    30. */  

     

    __block变量

    blocks有一个强大的特性,即它可以修改其当前词法范围内的变量. 只要对变量加上 __block 存储修饰符. 稍微修改上面的例子, 使用一个block变量去统计下例中有多少字符串是相同的。例子中的block还是直接使用的,并用到了叫 currentLocale 的只读变量:

     

    Objective-c代码  

    1. NSArray *stringsArray = [NSArray arrayWithObjects:  
    2.                          @"string 1",  
    3.                          @"String 21", // <-  
    4.                          @"string 12",  
    5.                          @"String 11",  
    6.                          @"Strîng 21", // <-  
    7.                          @"Striñg 21", // <-  
    8.                          @"String 02", nil];  
    9.    
    10. NSLocale *currentLocale = [NSLocale currentLocale];  
    11. __block NSUInteger orderedSameCount = 0;  
    12.    
    13. NSArray *diacriticInsensitiveSortArray = [stringsArray sortedArrayUsingComparator:^(id string1, id string2) {  
    14.    
    15.     NSRange string1Range = NSMakeRange(0, [string1 length]);  
    16.     NSComparisonResult comparisonResult = [string1 compare:string2 options:NSDiacriticInsensitiveSearch range:string1Range locale:currentLocale];  
    17.    
    18.     if (comparisonResult == NSOrderedSame) {  
    19.         orderedSameCount++;  
    20.     }  
    21.     return comparisonResult;  
    22. }];  
    23.    
    24. NSLog(@"diacriticInsensitiveSortArray: %@", diacriticInsensitiveSortArray);  
    25. NSLog(@"orderedSameCount: %d", orderedSameCount);  
    26.    
    27. /*  
    28. Output:  
    29.    
    30. diacriticInsensitiveSortArray: (  
    31.     "String 02",  
    32.     "string 1",  
    33.     "String 11",  
    34.     "string 12",  
    35.     "String 21",  
    36.     "Str\U00eeng 21",  
    37.     "Stri\U00f1g 21"  
    38. )  
    39. orderedSameCount: 2  
    40. */  

    整体概念

     

    Block对象提供了创建特殊函数的方法,函数体可用C,Objective-C,C++等C类的语言做表达式. 在其他的语言环境中,block变量可能会被叫做"闭包(closure)", 而在这里,除非和标准C术语的一段(block)代码混淆的情况之外,一般称为"blocks"。

    Block 功能性

    一个block就是一块匿名的代码块:

    • 和函数一样有含类型的参数列表
    • 有直接声明或可推断出的返回值
    • 可以获得当前词法范围的状态
    • 有能力修改当前词法范围的状态
    • 可以和当前词法范围的其他block共同修改状态
    • 可以持续共享和修改当前词法范围的状态,甚至在当前词法范围销毁之后

    你可以copy一个block还可以将其传到别的线程以延后执行 (或本线程的执行循环里). 编译器和运行时会把block里用到的所有变量保存到该block的所有拷贝的生存期后。尽管blocks可以由纯C或C++写成,但block本身始终是一个Objective-C变量.

    用法

    Blocks一般都是小段的,自成体系的代码块. 因此,特别适合用在封转并行操作所需的数据,或用于集合中,以及操作完成后的回调.

    Blocks基于两大理由,是传统回调函数的优秀替代品:

    1. 允许你把具体实现代码写在调用该方法的地方. Blocks也经常是framework的方法参数.    
    2. 可以访存局部变量. 不需要像以前的回调一样,把在操作后所有需要用到的数据封装成特定的数据结构, 你完全可以直接访问局部变量.

     

     

    声明和创建Blocks


    声明Block引用

     

    Block变量保持blocks的引用. 声明block的语法类似于声明函数指针,区别之处在于使用^而不是*.block的类型取决于C的类型系统. 下面都是有效的block变量声明:

    Objective-c代码  

    1. void (^blockReturningVoidWithVoidArgument)(void);  
    2. int (^blockReturningIntWithIntAndCharArguments)(int, char);  
    3. void (^arrayOfTenBlocksReturningVoidWithIntArgument[10])(int);  

     

     

     

     

     

    Blokcs也支持可变参数即(...). 如果不带参数则必须在参数列表中指定 void .

    Blocks被设计为完全的类型安全,编译器可由一整套的元数据去判断blocks,blocks的参数,及返回值的有效性(这句没太看懂,原文是Blocks are designed to be fully type safe by giving the compiler a full set of metadata to use to validate use of blocks, parameters passed to blocks, and assignment of the return value)。 你可以把block强制转换为任意类型的指针,反之亦然。不过,你不能像对指针一样用*运算符对block进行解引用(dereference)操作,因为block的大小无法在编译时算出.

    你也可以创建blocks的类型,在几个地方用到同样函数签名的block时,这是一种很好的做法:

     

     

    Objective-c代码  

    1. typedef float (^MyBlockType)(float, float);  
    2.    
    3. MyBlockType myFirstBlock = // ... ;  
    4. MyBlockType mySecondBlock = // ... ;  

     

     

    创建Block

    使用 ^ 操作符标示出block表达式的字面开始部分. 后面跟随着包含参数列表的().block主体则是包含在{}之中。 下例展示了如何定义一个简单的block,并将其赋值给之前定义的变量 (oneFrom).最后由常规的 ; 也是c语言的语句结束符结束

     

    Objective-c代码  

    1. int (^oneFrom)(int);  
    2.    
    3. oneFrom = ^(int anInt) {  
    4.     return anInt - 1;  
    5. };  

      如果你不想显式的声明block的返回值,也可以由block内容自动推断出来。如果返回值是可推断的,并且参数列表为 void, 你也可以省略参数列表. 当有多个返回语句时,它们必须是类型一直的(必要时会进行强行转换).

     

     

    全局Blocks

    在文件层面上,你可以使用block当全局的字面文字:

    Objective-c代码  

    1. #import <stdio.h>  
    2.    
    3. int GlobalInt = 0;  
    4. <p>int (^getGlobalInt)(void) = ^{ return GlobalInt; };</p>  


     


    Blocks和变量

    本部分阐述blocks和变量之间的交互作用,包括内存管理.

    变量的类型

    在block的主体代码块中,变量可以分为五种.

    你可以引用三种标准类型的变量,就如同函数传参:

    • 全局变量,包括静态局部变量
    • 全局函数 (理论上来说不是变量)
    • 作用域(enclosing scope)内的局部变量和参数

    Blocks还支持另外两种类型的变量:

    1. 在函数级别的 __block 变量. 它们在block中是可变的 (同时也在作用域中可变),如果有block被拷贝到了堆(heap)上,则它们也会被保存.
    2. const imports.

    最后,在一个方法的具体实现中,blocks可以引用Objective-C的实体变量,见 “对象和Block变量.”

    block使用变量适用如下规则:

    1. 全局变量是可访存的,包括作用域内的静态变量.
    2. block的参数是可访存的 (和函数的参数一样).
    3. 作用域内的局部栈(非静态)变量被当做 const 变量. 它们的值以block表达式在程序的点为准. 在嵌套blocks中,则是里该block最近的作用域中的值为准.
    4. 词法作用域中的局部变量有 __block 存储修饰符的,是按引用传递并可以修改. 所有的变动都反应到作用域中,包括其他在本作用域内定义的block中做的修改.
      详见 “__block 存储类型.”
    5. 在block内部声明的局部变量,就和函数内声明的变量一样. 每次调用block都产生变量的新的拷贝,这些变量轮流被当做 const 或按引用传递的变量用在block内部.

    下例使用局部非静态变量:


    Objective-c代码  

    1. int x = 123;  
    2.    
    3. void (^printXAndY)(int) = ^(int y) {  
    4.    
    5.     printf("%d %d\n", x, y);  
    6. };  
    7.    
    8. printXAndY(456); // prints: 123 456  

     

    必须要注意,如果想在block内改变x的值,会导致错误:

     

    Objective-c代码  

    1. int x = 123;  
    2.    
    3. void (^printXAndY)(int) = ^(int y) {  
    4.    
    5.     x = x + y; // error  
    6.     printf("%d %d\n", x, y);  
    7. };  

     

    如果想要在block内改变变量,你要使用 __block 类型的存储修饰符,详见“ __block 存储类型.”

     

    The __block Storage Type

    你可以指明一个导入的变量为可变的即可读写的,只需要使用 __block 类型存储修饰符. __block 存储类型类不同于 registerauto,  但和static 存储类型一样,对于局部变量提供了可变的能力.

     

    __block 变量生存于存储区内,并供当前词法范围的所有blocks共享使用. 因此,该存储区将存活到block栈frame销毁之后,甚至在其他拷贝或声明了这些block的block销毁后 (比如压到队列中供后续执行). 在给定的词法范围里的多个blocks可以同时使用一个共享的变量.(这段我整个没看懂,原文是__block variables live in storage that is shared between the lexical scope of the variable and all blocks and block copies declared or created within the variable’s lexical scope. Thus, the storage will survive the destruction of the stack frame if any copies of the blocks declared within the frame survive beyond the end of the frame (for example, by being enqueued somewhere for later execution). Multiple blocks in a given lexical scope can simultaneously use a shared variable.)

     

    作为优化, block变量和block本身一样开始是存储在栈上. 但如果用Block_copy (如果是Objecitve-C环境下, 可以直接向block发送 copy)对block进行拷贝, 变量就会拷贝到堆上. 因此 __block 变量的地址是可以改变的.

    对于 __block 变量还有两个更严格的限制: 不能是变长数组,也不能是含有C99的变长数组的结构体.

    下例示范如何使用 __block 变量:

     

     

    Objective-c代码  

    1. __block int x = 123; //  x lives in block storage  
    2.    
    3. void (^printXAndY)(int) = ^(int y) {  
    4.    
    5.     x = x + y;  
    6.     printf("%d %d\n", x, y);  
    7. };  
    8. printXAndY(456); // prints: 579 456  
    9. // x is now 579  

     

    下例展示几种变量和blocks之间的交互:

     

    Objective-c代码  

    1. extern NSInteger CounterGlobal;  
    2. static NSInteger CounterStatic;  
    3.    
    4. {  
    5.     NSInteger localCounter = 42;  
    6.     __block char localCharacter;  
    7.    
    8.     void (^aBlock)(void) = ^(void) {  
    9.         ++CounterGlobal;  
    10.         ++CounterStatic;  
    11.         CounterGlobal = localCounter; // localCounter fixed at block creation  
    12.         localCharacter = 'a'; // sets localCharacter in enclosing scope  
    13.     };  
    14.    
    15.     ++localCounter; // unseen by the block  
    16.     localCharacter = 'b';  
    17.    
    18.     aBlock(); // execute the block  
    19.     // localCharacter now 'a'  
    20. }  

     

    对象和Block变量

    Blocks支持Objecitve-C和C++对象,还包括其他的可以看成变量的blocks.

    Objective-C对象

    在引用计数的环境下,默认情况如果你引用了一个Objective-C对象,它将会被retain,即使你只是使用了这个对象的实体变量. 如果对象使用了 __block 存储修饰符,则不会被retain.

    Note: 在垃圾回收的环境下,如果你对变量同时使用了 __weak 和 __block 修饰符, block将不会保证其生存期.

     

    如果你在方法中使用了block,并且使用到了对象的实体变量,那么内存管理的规则将会更微妙:

    • 如果你使用了改实体变量的引用,则 self 将被retain;
    • 如果你是按指访问该实体变量,则存储那个实体变量将被retain.

    下例展示这两种情况的差别:

     

    Objecitve-c代码  

    1. dispatch_async(queue, ^{  
    2.     // instanceVariable is used by reference, self is retained  
    3.     doSomethingWithObject(instanceVariable);  
    4. });  
    5.    
    6.    
    7. id localVariable = instanceVariable;  
    8. dispatch_async(queue, ^{  
    9.     // localVariable is used by value, localVariable is retained (not self)  
    10.     doSomethingWithObject(localVariable);  
    11. });  

     

    C++对象

    你可以在block内使用C++变量. 在成员函数中,对成员变量和函数的引用实际上都是隐式使用了 this 指针,故而都是可变的. 当block被拷贝的时候,有两种情况需要留心:

    • 如果你使用的是栈基(stack-based)的C++变量,并且有 __block 存储修饰符, 通常使用拷贝构造函数来构造对象.
    • 除此以外的栈基C++对象, 则必须要有const拷贝构造函数(形如 Object(const Object&o)),拷贝这些对象时将使用该方法.

    Blocks

    当拷贝一个block,该block内所有引用到的其他block都将被拷贝,如果改block使用到的block变量里引用到了别的block,则别的block也将被拷贝.

    当拷贝一个栈基block,你将得到一个新block. 如果拷贝一个堆基的block,则仅仅是增加其引用计数,在拷贝函数和方法返回后还会降回去.

     

     

    使用Blocks

    调用Block

    把一个block声明为一个变量,你就可以把它当做函数一样用,如下所示:

     

    Objective-c代码  

    1. int (^oneFrom)(int) = ^(int anInt) {  
    2.     return anInt - 1;  
    3. };  
    4.    
    5. printf("1 from 10 is %d", oneFrom(10));  
    6. // Prints "1 from 10 is 9"  
    7.    
    8. float (^distanceTraveled) (float, float, float) =  
    9.                           ^(float startingSpeed, float acceleration, float time) {  
    10.    
    11.     float distance = (startingSpeed * time) + (0.5 * acceleration * time * time);  
    12.     return distance;  
    13. };  
    14.    
    15. float howFar = distanceTraveled(0.09.81.0);  
    16. // howFar = 4.9  

    你也可以把block当参数传给函数或者方法.某些场合,你也可以"内联(inline)"的创建block.

    使用Block作为函数参数

    你可以传一个block当参数给函数,就和其他类型的参数一样. 很多情况,你都没必要声明blocks, 而是简单的在哪里需要就哪里直接创建. 下例展示使用qsort_b 函数. qsort_b 类似于标准的 qsort_r 函数,不过只是把最后一个参数改为block.

     

    Objective-c代码  

    1. char *myCharacters[3] = { "TomJohn""George""Charles Condomine" };  
    2.    
    3. qsort_b(myCharacters, 3, sizeof(char *), ^(const void *l, const void *r) {  
    4.     char *left = *(char **)l;  
    5.     char *right = *(char **)r;  
    6.     return strncmp(left, right, 1);  
    7. });  
    8. // Block implementation ends at "}"  
    9.    
    10. // myCharacters is now { "Charles Condomine""George""TomJohn" }  

     

    注意block被整个包含在函数的参数列表中.

    下例演示如何在 dispatch_apply 函数中使用block. dispatch_apply 声明如下:

     

    Objective-c代码  

    1. void dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t));  

     

    函数将block投递到调度队列中供多次调用. 一共三个参数,其中第一个是调用总次数,第二个是投递到的队列,最后是block本身,而这个block带有一个参数,即当前被调用的次数.

     

    可以仅仅使用 dispatch_apply 去打印打钱的调度下标,如下:

     

    Objective-c代码  

    1. #include <dispatch/dispatch.h>  
    2. size_t count = 10;  
    3. dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);  
    4.    
    5. dispatch_apply(count, queue, ^(size_t i) {  
    6.     printf("%u\n", i);  
    7. });  

     

    使用Block作为方法参数

     

    Cocoa 提供了很多使用blocks的方法, 你可以传递block作为参数,就像调用其他的方法一样.

    下例展示如何使用给定的过滤条件为数组排出前五个元素.

     

    Objectvie-c代码  

    1. NSArray *array = [NSArray arrayWithObjects: @"A", @"B", @"C", @"A", @"B", @"Z",@"G", @"are", @"Q", nil];  
    2. NSSet *filterSet = [NSSet setWithObjects: @"A", @"Z", @"Q", nil];  
    3.    
    4. BOOL (^test)(id obj, NSUInteger idx, BOOL *stop);  
    5.    
    6. test = ^ (id obj, NSUInteger idx, BOOL *stop) {  
    7.    
    8.     if (idx < 5) {  
    9.         if ([filterSet containsObject: obj]) {  
    10.             return YES;  
    11.         }  
    12.     }  
    13.     return NO;  
    14. };  
    15.    
    16. NSIndexSet *indexes = [array indexesOfObjectsPassingTest:test];  
    17.    
    18. NSLog(@"indexes: %@", indexes);  
    19.    
    20. /*  
    21. Output:  
    22. indexes: <NSIndexSet: 0x10236f0>[number of indexes: 2 (in 2 ranges), indexes: (0 3)]  
    23. */  

     

    下例是判断一个 NSSet 对象是否包含了一个局部变量,并设置另一个局部变量(found),在查找到的情况下 为 YES (并停止查找) . 注意 found 被声明为了 __block 变量, 且block是用内联方式定义的:

     

    Objective-c代码  

    1. __block BOOL found = NO;  
    2. NSSet *aSet = [NSSet setWithObjects: @"Alpha", @"Beta", @"Gamma", @"X", nil];  
    3. NSString *string = @"gamma";  
    4.    
    5. [aSet enumerateObjectsUsingBlock:^(id obj, BOOL *stop) {  
    6.     if ([obj localizedCaseInsensitiveCompare:string] == NSOrderedSame) {  
    7.         *stop = YES;  
    8.         found = YES;  
    9.     }  
    10. }];  
    11.    
    12. // At this point, found == YES  

    拷贝Blocks

    一般而言,你没有表去copy(或retain)一个block. 除非你希望使用该block在当前声明的范围销毁之后. 拷贝会将block移到堆(heap)中.

    你可以使用C函数去拷贝或释放blocks:

     

    C代码  

    1. Block_copy();  
    2. Block_release();  

     

     

    如果你正在使用Objective-C, 你可以向block对象发送 copyretain, 和 release (也包括 autorelease) 消息.

    为了避免内存泄露, Block_copy() 和Block_release()必须要对应使用. 同样的也要对应 copy 或 retain 和 release (或 autorelease)的使用.除非是在垃圾回收的环境.

    应避免的做法

    block文本(即, ^{ ... }) 是表示block的局部栈数据结构(stack-local data structure)的地址. 所以这些栈数据仅在当前声明的范围内有效,必须避免如下的使用:

     

    Objective-c代码  

    1. void dontDoThis() {  
    2.     void (^blockArray[3])(void);  // an array of 3 block references  
    3.    
    4.     for (int i = 0; i < 3; ++i) {  
    5.         blockArray[i] = ^{ printf("hello, %d\n", i); };  
    6.         // WRONG: The block literal scope is the "for" loop  
    7.     }  
    8. }  
    9.    
    10. void dontDoThisEither() {  
    11.     void (^block)(void);  
    12.    
    13.     int i = random():  
    14.     if (i > 1000) {  
    15.         block = ^{ printf("got i at: %d\n", i); };  
    16.         // WRONG: The block literal scope is the "then" clause  
    17.     }  
    18.     // ...  
    19. }  

     

    调试

     

    你可以在blocks中设断点并单步跟踪. 你也可以在GDB里直接用 invoke-block命令调用blocks,如下所示:

     

    Shell代码  

    1. $ invoke-block myBlock 10 20  

     

     

    如果要传递C的字符串,你必须用引用括起来, 比如把 this string 传给 doSomethingWithString block, 得这么写:

    Shell代码  

    1. $ invoke-block doSomethingWithString "\"this string\"" 
    展开全文
  • 以上介绍是Block的简要实现,接下来我们来仔细研究一下Block的捕获外部变量的特性以及__block的实现原理。 研究工具:clang 为了研究编译器的实现原理,我们需要使用 clang 命令。clang 命令可以将 Objetive-C 的...

    主要参考了这些文章 , 有删改 : 

    http://www.jianshu.com/p/ee9756f3d5f6

    https://www.jianshu.com/p/c99f4974ddb5  

    https://www.jianshu.com/p/8865ff43f30e                      

    Blocks是C语言的扩充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了这个新功能“Blocks”。从那开始,Block就出现在iOS和Mac系统各个API中,并被大家广泛使用。block本质上也是一个oc对象(父类往上找回找到NSObject)或者说是一个结构体,内部也有一个isa指针。block是封装了函数调用(函数指针)以及函数调用环境(捕获到的参数)的OC对象。

     

    Block在OC中的实现如下:

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

    从结构图中很容易看到isa,所以OC处理Block是按照对象来处理的。

    block的类型

    在iOS中,isa常见的就是_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock这3种. 

    我们通过代码用class方法或者isa指针查看具体类型。

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // __NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject
            void (^block)(void) = ^{
                NSLog(@"Hello");
            };
            
            NSLog(@"%@", [block class]);
            NSLog(@"%@", [[block class] superclass]);
            NSLog(@"%@", [[[block class] superclass] superclass]);
            NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
        }
        return 0;
    }
    

    从上述打印内容可以看出block最终都是继承自NSBlock类型,而NSBlock继承于NSObjcet。那么block其中的isa指针其实是来自NSObject中的。这也更加印证了block的本质其实就是OC对象。

    以上介绍是Block的简要实现,接下来我们来仔细研究一下Block的捕获外部变量的特性以及__block的实现原理。

     

    研究工具:clang
    为了研究编译器的实现原理,我们需要使用 clang 命令。clang 命令可以将 Objetive-C 的源码改写成 C / C++ 语言的,借此可以研究 block 中各个特性的源码实现方式。该命令是

    clang -rewrite-objc main.m

    目录

    • 1.Block捕获外部变量实质
    • 2.Block的copy和release


    一.Block捕获外部变量实质

    拿起我们的Block一起来捕捉外部变量吧。

    说到外部变量,我们要先说一下C语言中变量有哪几种。一般可以分为一下5种:

    • 自动变量
    • 函数参数
    • 静态变量
    • 静态全局变量
    • 全局变量

    研究Block的捕获外部变量就要除去函数参数这一项,下面一一根据这4种变量类型的捕获情况进行分析。

    我们先根据这4种类型

    • 自动变量
    • 静态变量
    • 静态全局变量
    • 全局变量

    写出Block测试代码。

    这里很快就出现了一个错误,提示说自动变量没有加__block,由于__block有点复杂,我们先实验静态变量,静态全局变量,全局变量这3类。测试代码如下:

    #import <Foundation/Foundation.h>
    
    int global_i = 1;
    
    static int static_global_j = 2;
    
    int main(int argc, const char * argv[]) {
    
        static int static_k = 3;
        int val = 4;
    
        void (^myBlock)(void) = ^{
            global_i ++;
            static_global_j ++;
            static_k ++;
            NSLog(@"Block中 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);
        };
    
        global_i ++;
        static_global_j ++;
        static_k ++;
        val ++;
        NSLog(@"Block外 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);
    
        myBlock();
    
        return 0;
    }

    运行结果

    Block 外  global_i = 2,static_global_j = 3,static_k = 4,val = 5
    Block 中  global_i = 3,static_global_j = 4,static_k = 5,val = 4

    这里就有2点需要弄清楚了
    1.为什么在Block里面不加__bolck不允许更改变量?
    2.为什么自动变量的值没有增加,而其他几个变量的值是增加的?自动变量是什么状态下被block捕获进去的?

    为了弄清楚这2点,我们用clang转换一下源码出来分析分析。

    (main.m代码行37行,文件大小832bype, 经过clang转换成main.cpp以后,代码行数飙升至104810行,文件大小也变成了3.1MB)

    源码如下

    int global_i = 1;
    
    static int static_global_j = 2;
    
    // 这个结构体最后就赋值给了myBlock,所以说block的本质是结构体
    struct __main_block_impl_0 {
      struct __block_impl impl; // 封装了函数实现的结构体
      struct __main_block_desc_0* Desc; // 里面有内存管理函数,Block_size表示block的大小
      int *static_k; // 捕获到的局部静态变量
      int val; // 捕获到的普通局部变量
      // 如果还有其他捕获的变量,会继续在下面列出来
    
        // 这个结构体的初始化函数 , 入参 : fp,函数实现的函数指针, __main_block_desc_0,占用大小的描述
        // 返回一个__main_block_impl_0类型的结构体,赋值给了block
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_k, int _val, int flags=0) : static_k(_static_k), val(_val) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    struct __block_impl {
        void *isa;
        int Flags;
        int Reserved;
        void *FuncPtr; // 函数指针,指向block的实现__main_block_func_0 
    };
    // block中的代码被封装成一个函数,最后给到了__main_block_impl_0的impl.FuncPtr
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int *static_k = __cself->static_k; // bound by copy
      int val = __cself->val; // bound by copy
    
            global_i ++;
            static_global_j ++;
            (*static_k) ++;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_0,global_i,static_global_j,(*static_k),val);
        }
    
    static struct __main_block_desc_0 {
      size_t reserved; // 预留参数
      size_t Block_size; // block的大小
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
    
    
    int main(int argc, const char * argv[]) {
    
        static int static_k = 3;
        int val = 4;
        // 调用了block的初始化函数,返回了一个结构体给myBlock
        void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_k, val));
    
        global_i ++;
        static_global_j ++;
        static_k ++;
        val ++;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_1,global_i,static_global_j,static_k,val);
    
        // 找到了这个block的函数指针,调用
        ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    
        return 0;
    }

    首先全局变量global_i和静态全局变量static_global_j的值增加,它们没有被Block捕获进去,这一点很好理解,因为是全局的,作用域很广,在Block里面进行++操作,Block结束之后,它们的值依旧可以得以保存下来。

    接下来仔细看看自动变量和静态变量的问题。
    在__main_block_impl_0中,可以看到静态变量static_k和自动变量val,被Block从外面捕获进来,成为__main_block_impl_0这个结构体的成员变量了。

    接着看block的构造函数,

    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_k, int _val, int flags=0) : static_k(_static_k), val(_val)

    这个构造函数中,自动变量和静态变量被捕获为成员变量追加到了构造函数中。

    main里面的myBlock闭包中的__main_block_impl_0结构体,初始化如下

    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_k, val));
    
    
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = 0;
    impl.FuncPtr = __main_block_impl_0; 
    Desc = &__main_block_desc_0_DATA;
    *_static_k = 4;
    val = 4;

    到此,__main_block_impl_0结构体就是这样把自动变量捕获进来的。也就是说,在执行Block语法的时候,Block语法表达式所使用的自动变量的值是被保存进了Block的结构体实例中,也就是Block自身中。

    这里值得说明的一点是,如果Block外面还有很多自动变量,静态变量,等等,这些变量在Block里面并不会被使用到。那么这些变量并不会被Block捕获进来,也就是说并不会在构造函数里面传入它们的值。

    Block捕获外部变量仅仅只捕获Block闭包里面会用到的值,其他用不到的值,它并不会去捕获。

    再研究一下源码,我们注意到__main_block_func_0这个函数的实现

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int *static_k = __cself->static_k; // bound by copy
      int val = __cself->val; // bound by copy
    
            global_i ++;
            static_global_j ++;
            (*static_k) ++;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_0,global_i,static_global_j,(*static_k),val);
        }

    我们可以发现,系统自动给我们加上的注释,bound by copy,自动变量val虽然被捕获进来了,但是是用 __cself->val来访问的。Block仅仅捕获了val的值,并没有捕获val的内存地址。所以在__main_block_func_0这个函数中即使我们重写这个自动变量val的值,依旧没法去改变Block外面自动变量val的值。

    OC可能是基于这一点,在编译的层面就防止开发者可能犯的错误,因为自动变量没法在Block中改变外部变量的值,所以编译过程中就报编译错误。

     

    调用block执行内部代码

    // 执行block内部的代码
    ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 3, 5);
    // 去掉强制类型转换后
    block->FuncPtr(block, 3, 5);

    通过上述代码可以发现调用block是通过block找到FunPtr直接调用,通过上面分析我们知道block指向的是__main_block_impl_0类型结构体,但是我们发现__main_block_impl_0结构体中并不直接就可以找到FunPtr,而FunPtr是存储在__block_impl中的,为什么block可以直接调用__block_impl中的FunPtr呢?

    重新查看上述源代码可以发现,(__block_impl *)block将block强制转化为__block_impl类型的,因为__block_impl是__main_block_impl_0结构体的第一个成员,相当于将__block_impl结构体的成员直接拿出来放在__main_block_impl_0中,那么也就说明__block_impl的内存地址就是__main_block_impl_0结构体的内存地址开头。所以可以转化成功。并找到FunPtr成员。

    上面我们知道,FunPtr中存储着通过代码块封装的函数地址,那么调用此函数,也就是会执行代码块中的代码。并且回头查看__main_block_func_0函数,可以发现第一个参数就是__main_block_impl_0类型的指针。也就是说将block传入__main_block_func_0函数中,便于从中取出block捕获的值。
     

    最后看下转换的结果:

     

    小结一下:
    到此为止,上面提出的第二个问题就解开答案了。自动变量是以值传递方式传递到Block的构造函数里面去的。Block只捕获Block中会用到的变量。由于只捕获了自动变量的值,并非内存地址,所以Block内部不能改变自动变量的值。Block捕获的外部变量可以改变值的是静态变量,静态全局变量,全局变量。上面例子也都证明过了。

    剩下问题一我们还没有解决。

    回到上面的例子上面来,4种变量里面只有静态变量,静态全局变量,全局变量这3种是可以在Block里面被改变值的。仔细观看源码,我们能看出这3个变量可以改变值的原因。

    静态全局变量,全局变量由于作用域的原因,于是可以直接在Block里面被改变。他们也都存储在全局区。

    静态变量传递给Block是内存地址值,所以能在Block里面直接改变值。

    根据官方文档我们可以了解到,苹果要求我们在自动变量前加入 __block关键字(__block storage-class-specifier存储域类说明符),就可以在Block里面改变外部自动变量的值了。

    总结一下在Block中改变变量值有2种方式,一是传递内存地址指针到Block中,二是改变存储区方式(__block)。

    先来实验一下第一种方式,传递内存地址到Block中,改变变量的值。

    #import <Foundation/Foundation.h>
    
    int main(int argc, const char * argv[]) {
    
      NSMutableString * str = [[NSMutableString alloc]initWithString:@"Hello,"];
    
            void (^myBlock)(void) = ^{
                [str appendString:@"World!"];
                NSLog(@"Block中 str = %@",str);
            };
    
        NSLog(@"Block外 str = %@",str);
    
        myBlock();
    
        return 0;
    }
    
    控制台输出:
    Block 外  str = Hello,
    Block 中  str = Hello,World!
    

    看结果是成功改变了变量的值了,转换一下源码。

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      NSMutableString *str;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableString *_str, int flags=0) : str(_str) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      NSMutableString *str = __cself->str; // bound by copy
    
                ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)str, sel_registerName("appendString:"), 
    (NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_33ff12_mi_1);
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_33ff12_mi_2,str);
            }
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
          _Block_object_assign((void*)&dst->str, (void*)src->str, 3/*BLOCK_FIELD_IS_OBJECT*/);
    }
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {
         _Block_object_dispose((void*)src->str, 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[]) {
        NSMutableString * str = ((NSMutableString *(*)(id, SEL, NSString *))(void *)
    objc_msgSend)((id)((NSMutableString *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableString"),
     sel_registerName("alloc")), 
    sel_registerName("initWithString:"), 
    (NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_33ff12_mi_0);
    
            void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, str, 570425344));
    
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_33ff12_mi_3,str);
    
        ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    
        return 0;
    }

    在__main_block_func_0里面可以看到传递的是指针。所以成功改变了变量的值。

    改变外部变量值的第二种方式是加 __block这个放在实战篇里面讨论,接下来我们先讨论一下Block的copy的问题,因为这个问题会关系到 __block存储域的问题。

    二.Block的copy和dispose

    OC中,一般Block就分为以下3种,_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock。

    先来说明一下3者的区别。

    1.从捕获外部变量的角度上来看

    • _NSConcreteStackBlock:
      1 . 用到了外部局部变量 。
      2.  MRC下就会生成一个_NSConcreteStackBlock

    • _NSConcreteMallocBlock:

          1. 用到了外部局部变量。
          2. 调用了copy方法 , block会被复制一份到堆中成为MallocBlock ;

    在ARC下 , block 作为返回值时或者赋值给一个strong/copy修饰的对象会自动调用copy ,   所以ARC下大多数是_NSConcreteMallocBlock ; 
    MRC下手动调用copy , 也会到达堆区 . MRC下赋值不会触发copy操作 , 所以MRC下不手动调copy就是_NSConcreteStackBlock.

    ARC下一般对Block都有赋值操作,block 类型通过=进行传递时,会导致调用objc_retainBlock->_Block_copy->_Block_copy_internal方法链。并导致 __NSStackBlock__ 类型的 block 转换为 __NSMallocBlock__ 类型。

      ARC环境下不赋值就是__NSStackBlock__ , 比如:

    #import <Foundation/Foundation.h>
     
    int main(int argc, const char * argv[]) {
     
        __block int temp = 10;
     
        NSLog(@"%@",^{
                        NSLog(@"*******%d %p",temp ++,&temp);
                      });
     
        return 0;
    }

    输出

    <__NSStackBlock__: 0x7fff5fbff768>
    这种情况就是ARC环境下Block是__NSStackBlock的类型。

    block声明写法

    通过上面对MRC及ARC环境下block的不同类型的分析,总结出不同环境下block属性建议写法。
    MRC下block属性的建议写法
    @property (copy, nonatomic) void (^block)(void);

    ARC下block属性的建议写法
    @property (strong, nonatomic) void (^block)(void);
    @property (copy, nonatomic) void (^block)(void);

    • _NSConcreteGlobalBlock:

           不论是ARC还是MRC,没有用到外界变量或只用到全局变量、静态变量(结构体中没有捕获变量)的block为_NSConcreteGlobalBlock,生命周期从创建到应用程序结束。

    没有用到外部变量肯定是_NSConcreteGlobalBlock,这点很好理解。不过只用到全局变量、静态变量的block也是_NSConcreteGlobalBlock , 所有block的类型是根据是否捕获变量来判定的。举例如下:

    #import <Foundation/Foundation.h>
    
    int global_i = 1;
    static int static_global_j = 2;
    
    int main(int argc, const char * argv[]) {
    
        static int static_k = 3;
    
        void (^myBlock)(void) = ^{
                NSLog(@"Block中 变量 = %d %d %d",static_global_j ,static_k, global_i);
            };
    
        NSLog(@"%@",myBlock);
    
        myBlock();
    
        return 0;
    }

    输出:

    <__NSGlobalBlock__: 0x100001050>
    Block中 变量 = 2 3 1

    可见,只用到全局变量、静态变量的block也可以是_NSConcreteGlobalBlock。

    2.从持有对象的角度上来看:

    • _NSConcreteStackBlock是不持有对象的。
    //以下是在MRC下执行的
        NSObject * obj = [[NSObject alloc]init];
        NSLog(@"1.Block外 obj = %lu",(unsigned long)obj.retainCount);
    
        void (^myBlock)(void) = ^{
            NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount);
        };
    
        NSLog(@"2.Block外 obj = %lu",(unsigned long)obj.retainCount);
    
        myBlock();

    输出:

    1.Block外 obj = 1
    2.Block外 obj = 1
    Block中 obj = 1
    • _NSConcreteMallocBlock是持有对象的。
    //以下是在MRC下执行的
        NSObject * obj = [[NSObject alloc]init];
        NSLog(@"1.Block外 obj = %lu",(unsigned long)obj.retainCount);
    
        void (^myBlock)(void) = [^{
            NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount);
        }copy];
    
        NSLog(@"2.Block外 obj = %lu",(unsigned long)obj.retainCount);
    
        myBlock();
    
        [myBlock release];
    
        NSLog(@"3.Block外 obj = %lu",(unsigned long)obj.retainCount);

    输出:

    1.Block外 obj = 1
    2.Block外 obj = 2
    Block中 obj = 2
    3.Block外 obj = 1
    • _NSConcreteGlobalBlock也不持有对象
    //以下是在MRC下执行的
        void (^myBlock)(void) = ^{
    
            NSObject * obj = [[NSObject alloc]init];
            NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount);
        };
    
        myBlock();

    输出:

    Block 中 obj = 1

    由于_NSConcreteStackBlock所属的变量域一旦结束,那么该Block就会被销毁。在ARC环境下,编译器会自动的判断,把Block自动的从栈copy到堆。比如当Block作为函数返回值的时候,肯定会copy到堆上。

    1.手动调用copy
    2.Block是函数的返回值
    3.Block被强引用,Block被赋值给__strong或者id类型
    4.调用系统API入参中含有usingBlock的方法

    以上4种情况,系统都会默认调用copy方法把Block赋复制堆上

    但是当Block为函数参数的时候,就需要我们手动的copy一份到堆上了。这里除去系统的API我们不需要管,比如GCD等方法中本身带usingBlock的方法,其他我们自定义的方法传递Block为参数的时候都需要手动copy一份到堆上。

    copy函数把Block从栈上拷贝到堆上,dispose函数是把堆上的函数在废弃的时候销毁掉。

    #define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))
    #define Block_release(...) _Block_release((const void *)(__VA_ARGS__))
    
    // Create a heap based copy of a Block or simply add a reference to an existing one.
    // This must be paired with Block_release to recover memory, even when running
    // under Objective-C Garbage Collection.
    BLOCK_EXPORT void *_Block_copy(const void *aBlock);
    
    // Lose the reference, and if heap based and last reference, recover the memory
    BLOCK_EXPORT void _Block_release(const void *aBlock);
    
    // Used by the compiler. Do not call this function yourself.
    BLOCK_EXPORT void _Block_object_assign(void *, const void *, const int);
    
    // Used by the compiler. Do not call this function yourself.
    BLOCK_EXPORT void _Block_object_dispose(const void *, const int);

    上面是源码中2个常用的宏定义和4个常用的方法,一会我们就会看到这4个方法。

    static void *_Block_copy_internal(const void *arg, const int flags) {
        struct Block_layout *aBlock;
        const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;
    
        // 1
        if (!arg) return NULL;
    
        // 2
        aBlock = (struct Block_layout *)arg;
    
        // 3
        if (aBlock->flags & BLOCK_NEEDS_FREE) {
            // latches on high
            latching_incr_int(&aBlock->flags);
            return aBlock;
        }
    
        // 4
        else if (aBlock->flags & BLOCK_IS_GLOBAL) {
            return aBlock;
        }
    
        // 5
        struct Block_layout *result = malloc(aBlock->descriptor->size);
        if (!result) return (void *)0;
    
        // 6
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
    
        // 7
        result->flags &= ~(BLOCK_REFCOUNT_MASK);    // XXX not needed
        result->flags |= BLOCK_NEEDS_FREE | 1;
    
        // 8
        result->isa = _NSConcreteMallocBlock;
    
        // 9
        if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
            (*aBlock->descriptor->copy)(result, aBlock); // do fixup
        }
    
        return result;
    }

    上面这一段是Block_copy的一个实现,实现了从_NSConcreteStackBlock复制到_NSConcreteMallocBlock的过程。对应有9个步骤。

    void _Block_release(void *arg) {
        // 1
        struct Block_layout *aBlock = (struct Block_layout *)arg;
        if (!aBlock) return;
    
        // 2
        int32_t newCount;
        newCount = latching_decr_int(&aBlock->flags) & BLOCK_REFCOUNT_MASK;
    
        // 3
        if (newCount > 0) return;
    
        // 4
        if (aBlock->flags & BLOCK_NEEDS_FREE) {
            if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)(*aBlock->descriptor->dispose)(aBlock);
            _Block_deallocator(aBlock);
        }
    
        // 5
        else if (aBlock->flags & BLOCK_IS_GLOBAL) {
            ;
        }
    
        // 6
        else {
            printf("Block_release called upon a stack Block: %p, ignored\\\\n", (void *)aBlock);
        }
    }

    上面这一段是Block_release的一个实现,实现了怎么释放一个Block。对应有6个步骤。

    上述2个方法的详细解析可以看这篇文章

     

    为什么要有copy和dispose

    因为在C语言的结构体中,编译器没法很好的进行初始化和销毁操作。这样对内存管理来说是很不方便的。所以就在 __main_block_desc_0结构体中间增加成员变量 void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*)和void (*dispose)(struct __main_block_impl_0*),利用OC的Runtime进行内存管理。

    相应的增加了2个方法。

    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,  // 赋值给了copy的函数指针
         __main_block_dispose_0 // 赋值给了dispose的函数指针
       };
    
    static void __main_block_copy_0(struct __main_block_impl_0*dst,
     struct __main_block_impl_0*src) {
         _Block_object_assign((void*)&dst->str, (void*)src->str, 3/*BLOCK_FIELD_IS_OBJECT*/);
    }
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {
        _Block_object_dispose((void*)src->str, 3/*BLOCK_FIELD_IS_OBJECT*/);
    }

    这里的_Block_object_assign和_Block_object_dispose就对应着retain和release方法。堆空间的block自己销毁之后也会对持有的对象进行release操作。

     

    copydispose函数中传入的都是__main_block_impl_0结构体本身。

    copy本质就是__main_block_copy_0函数,__main_block_copy_0函数内部调用_Block_object_assign函数,_Block_object_assign中传入的是person对象的地址,person对象,以及8。

    dispose本质就是__main_block_dispose_0函数,__main_block_dispose_0函数内部调用_Block_object_dispose函数,_Block_object_dispose函数传入的参数是person对象,以及8。

    _Block_object_assign函数调用时机及作用

    当block进行copy操作的时候就会自动调用__main_block_desc_0内部的__main_block_copy_0函数,__main_block_copy_0函数内部会调用_Block_object_assign函数。

    _Block_object_assign函数会自动根据__main_block_impl_0结构体的捕获对象是什么类型的指针,对捕获对象产生强引用或者弱引用。可以理解为_Block_object_assign函数内部会对捕获的对象进行引用计数器的操作,如果__main_block_impl_0结构体内捕获的对象指针是__strong类型,则为强引用,引用计数+1,如果__main_block_impl_0结构体内对象指针是__weak类型,则为弱引用,引用计数不变。

    _Block_object_dispose函数调用时机及作用

    当block从堆中移除时就会自动调用__main_block_desc_0中的__main_block_dispose_0函数,__main_block_dispose_0函数内部会调用_Block_object_dispose函数。

    _Block_object_dispose会对捕获对象做释放操作,类似于release,也就是断开对person对象的引用,而person究竟是否被释放还是取决于person对象自己的引用计数。

    总结

    1. 一旦block中捕获的变量为对象类型,block结构体中的__main_block_desc_0会出两个参数copydispose。因为访问的是个对象,block希望拥有这个对象,就需要对对象进行引用,也就是进行内存管理的操作。比如说对对象进行retain操作,因此一旦block捕获的变量是对象类型就会会自动生成copydispose来对内部引用的对象进行内存管理。

    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变量 , 也会调用对象类型的dispose , 对象的引用计数减1。

     

    原理篇到此结束了 , 下面请看看实战篇 , __block , 编译器会做怎样的处理.

    https://blog.csdn.net/u014600626/article/details/86570932

     

    展开全文
  • 书接上文 : block原理篇 :...__block实现原理 我们继续研究一下__block实现原理。 __block修饰非对象的变量 先来看看普通变量的情况。 #import &lt;Foundation/Foundation.h&gt; int main(int ...

    书接上文 : block的原理篇 : https://blog.csdn.net/u014600626/article/details/78697535

    __block实现原理

    我们继续研究一下__block实现原理。

    __block修饰非对象的变量

    先来看看普通变量的情况。

    #import <Foundation/Foundation.h>
     
    int main(int argc, const char * argv[]) {
     
        __block int i = 0;
     
        void (^myBlock)(void) = ^{
            i ++;
            NSLog(@"%d",i);
        };
     
        myBlock();
     
        return 0;
    }


    把上述代码用clang转换成源码。

    // 加了__block后 i被封装成一个对象(结构体)
    struct __Block_byref_i_0 {
      void *__isa;
    __Block_byref_i_0 *__forwarding; //
     int __flags;
     int __size;
     int i; // 真实的i的值
    };
     
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_i_0 *i; // block持有这个结构体
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__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_i_0 *i = __cself->i; // bound by ref
     
            (i->__forwarding->i) ++;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_3b0837_mi_0,(i->__forwarding->i));
    }


    从源码我们能发现,带有 __block的变量也被转化成了一个结构体__Block_byref_i_0,这个结构体有5个成员变量。第一个是isa指针,第二个是指向自身类型的__forwarding指针,第三个是一个标记flag,第四个是它的大小,第五个是变量值,名字和变量名同名。


    至此,__block的实现原理也已经明了。__block将变量包装成对象,然后在把捕获到的变量封装在block的结构体里面,block内部存储的变量为结构体指针,也就可以通过指针找到内存地址进而修改变量的值。

     

    由此在引出来一个问题 :  __block修饰的变量在编译时会被封装为结构体,那么当在外部使用同一个变量的时候,使用的是int age呢?还是__Block_byref_age_0结构体内的age->__forwarding->age变量呢?

    为了验证上述问题
    同样使用自定义结构体的方式来查看其内部结构

    typedef void (^Block)(void);
    
    struct __block_impl {
        void *isa;
        int Flags;
        int Reserved;
        void *FuncPtr;
    };
    
    struct __main_block_desc_0 {
        size_t reserved;
        size_t Block_size;
        void (*copy)(void);
        void (*dispose)(void);
    };
    
    struct __Block_byref_age_0 {
        void *__isa;
        struct __Block_byref_age_0 *__forwarding;
        int __flags;
        int __size;
        int age;
    };
    struct __main_block_impl_0 {
        struct __block_impl impl;
        struct __main_block_desc_0* Desc;
        struct __Block_byref_age_0 *age; // by ref
    };
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            __block int age = 10;
            Block block = ^{
                age = 20;
                NSLog(@"age is %d",age);
            };
            block();
            struct __main_block_impl_0 *blockImpl = (__bridge struct __main_block_impl_0 *)block;
            NSLog(@"%p",&age);
        }
        return 0;
    }
    
    

    打印断点查看结构体内部结构

    _Block_byref_age_0结构体

    通过查看blockImpl结构体其中的内容,找到age结构体,其中重点观察两个元素:

    1. __forwarding其中存储的地址是age结构体变量自己的地址
    2. age中存储这修改后的变量20。

    上面也提到过,在block中使用或修改age的时候都是通过结构体__Block_byref_age_0找到__forwarding在找到变量age的。

    另外apple为了隐藏__Block_byref_age_0结构体的实现,打印age变量的地址发现其实是__Block_byref_age_0结构体内age变量的地址。

    通过上图的计算可以发现打印age的地址同__Block_byref_age_0结构体内age值的地址相同。也就是说外面使用的age,就是结构体内的age。

     

    __block修饰对象类型

    那么如果变量本身就是对象类型呢?通过以下代码生成c++源码查看

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            __block Person *person = [[Person alloc] init];
            NSLog(@"%@",person);
            Block block = ^{
                person = [[Person alloc] init];
                NSLog(@"%@",person);
            };
            block();
        }
        return 0;
    }
    

    通过源码查看,将对象包装在一个新的结构体中。结构体内部会有一个person对象,不一样的地方是结构体内部添加了内存管理的两个函数__Block_byref_id_object_copy__Block_byref_id_object_dispose . 下面就说说__block的内存管理

    __block内存管理

    上文提到当block中捕获对象类型的变量时,block中的__main_block_desc_0结构体内部会自动添加copydispose函数对捕获的变量进行内存管理。

    那么同样的当block内部捕获__block修饰的对象类型的变量时,__Block_byref_person_0结构体内部也会自动添加__Block_byref_id_object_copy__Block_byref_id_object_dispose对被__block包装成结构体的对象进行内存管理。

    block内存在栈上时,并不会对__block变量产生内存管理。当blcokcopy到堆上时
    会调用block内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会对__block变量形成强引用(相当于retain)

    首先通过一张图看一下block复制到堆上时内存变化

    __block copy内存管理

    blockcopy到堆上时,block内部引用的__block变量也会被复制到堆上,并且持有变量,如果block复制到堆上的同时,__block变量已经存在堆上了,则不会复制。

    当block从堆中移除的话,就会调用dispose函数,也就是__main_block_dispose_0函数,__main_block_dispose_0函数内部会调用_Block_object_dispose函数,会自动释放引用的__block变量。

    __block 释放内存管理

    block内部决定什么时候将变量复制到堆中,什么时候对变量做引用计数的操作。

    __block修饰的变量在block结构体中一直都是强引用,而其他类型的是由传入的对象指针类型决定。

    一段代码更深入的观察一下。

    首先需要说明的一点是对象在OC中,默认声明自带__strong所有权修饰符的 . 
    在转换出来的源码中,我们也可以看到,Block捕获了__block,并且强引用了,因为在__Block_byref_person_1结构体中,有一个变量是 Person *__strong person; ,这个默认也是带__strong所有权修饰符的。

    typedef void (^Block)(void);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            int number = 20;
            __block int age = 10;
            
            NSObject *object = [[NSObject alloc] init];
            __weak NSObject *weakObj = object;
            
            Person *p = [[Person alloc] init];
            __block Person *person = p;
            __block __weak Person *weakPerson = p;
            
            Block block = ^ {
                NSLog(@"%d",number); // 局部变量
                NSLog(@"%d",age); // __block修饰的局部变量
                NSLog(@"%p",object); // 对象类型的局部变量
                NSLog(@"%p",weakObj); // __weak修饰的对象类型的局部变量
                NSLog(@"%p",person); // __block修饰的对象类型的局部变量
                NSLog(@"%p",weakPerson); // __block,__weak修饰的对象类型的局部变量
            };
            block();
        }
        return 0;
    }
    

    将上述代码转化为c++代码查看不同变量之间的区别

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      
      int number;
      NSObject *__strong object;
      NSObject *__weak weakObj;
      __Block_byref_age_0 *age; // by ref
      __Block_byref_person_1 *person; // by ref
      __Block_byref_weakPerson_2 *weakPerson; // by ref
      
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _number, NSObject *__strong _object, NSObject *__weak _weakObj, __Block_byref_age_0 *_age, __Block_byref_person_1 *_person, __Block_byref_weakPerson_2 *_weakPerson, int flags=0) : number(_number), object(_object), weakObj(_weakObj), age(_age->__forwarding), person(_person->__forwarding), weakPerson(_weakPerson->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    上述__main_block_impl_0结构体中看出,没有使用__block修饰的变量(object 和 weadObj)则根据他们本身被block捕获的指针类型对他们进行强引用或弱引用,而一旦使用__block修饰的变量,__main_block_impl_0结构体内一律使用强指针引用生成的结构体。

    接着我们来看__block修饰的变量生成的结构体有什么不同

    struct __Block_byref_age_0 {
      void *__isa;
    __Block_byref_age_0 *__forwarding;
     int __flags;
     int __size;
     int age;
    };
    
    struct __Block_byref_person_1 {
      void *__isa;
    __Block_byref_person_1 *__forwarding;
     int __flags;
     int __size;
     void (*__Block_byref_id_object_copy)(void*, void*);
     void (*__Block_byref_id_object_dispose)(void*);
     Person *__strong person;
    };
    
    struct __Block_byref_weakPerson_2 {
      void *__isa;
    __Block_byref_weakPerson_2 *__forwarding;
     int __flags;
     int __size;
     void (*__Block_byref_id_object_copy)(void*, void*);
     void (*__Block_byref_id_object_dispose)(void*);
     Person *__weak weakPerson;
    };
    

    如上面分析的那样,__block修饰对象类型的变量生成的结构体内部多了__Block_byref_id_object_copy__Block_byref_id_object_dispose两个函数,用来对对象类型的变量进行内存管理的操作。而结构体对对象的引用类型,则取决于block捕获的对象类型的变量。weakPerson是弱指针,所以__Block_byref_weakPerson_2weakPerson就是弱引用,person是强指针,所以__Block_byref_person_1对person就是强引用。

    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_object_assign((void*)&dst->object, (void*)src->object, 3);
        _Block_object_assign((void*)&dst->weakObj, (void*)src->weakObj, 3);
        _Block_object_assign((void*)&dst->person, (void*)src->person, 8);
        _Block_object_assign((void*)&dst->weakPerson, (void*)src->weakPerson, 8);
    }
    

    __main_block_copy_0函数中会根据变量是强弱指针及有没有被__block修饰做出不同的处理,强指针在block内部产生强引用,弱指针在block内部产生弱引用。被__block修饰的变量最后的参数传入的是8,没有被__block修饰的变量最后的参数传入的是3。

    当block从堆中移除时通过dispose函数来释放他们。

    static void __main_block_dispose_0(struct __main_block_impl_0*src) {
        _Block_object_dispose((void*)src->age, 8);
        _Block_object_dispose((void*)src->object, 3);
        _Block_object_dispose((void*)src->weakObj, 3);
        _Block_object_dispose((void*)src->person, 8);
        _Block_object_dispose((void*)src->weakPerson, 8);
        
    }
    

    __forwarding指针

    上面提到过__forwarding指针指向的是结构体自己。当使用变量的时候,通过结构体找到__forwarding指针,在通过__forwarding指针找到相应的变量。这样设计的目的是为了方便内存管理。通过上面对__block变量的内存管理分析我们知道,block被复制到堆上时,会将block中引用的变量也复制到堆中。

    我们重回到源码中。当在block中修改__block修饰的变量时。

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

    通过源码可以知道,当修改__block修饰的变量时,是根据变量生成的结构体这里是__Block_byref_age_0找到其中__forwarding指针,__forwarding指针指向的是结构体自己因此可以找到age变量进行修改。

    当block在栈中时,__Block_byref_age_0结构体内的__forwarding指针指向结构体自己。

    而当block被复制到堆中时,栈中的__Block_byref_age_0结构体也会被复制到堆中一份,而此时栈中的__Block_byref_age_0结构体中的__forwarding指针指向的就是堆中的__Block_byref_age_0结构体,堆中__Block_byref_age_0结构体内的__forwarding指针依然指向自己。

    此时当对age进行修改时

    // 栈中的age
    __Block_byref_age_0 *age = __cself->age; // bound by ref
    // age->__forwarding获取堆中的age结构体
    // age->__forwarding->age 修改堆中age结构体的age变量
    (age->__forwarding->age) = 20;
    

    通过__forwarding指针巧妙的将修改的变量赋值在堆中的__Block_byref_age_0中。

    我们通过一张图展示__forwarding指针的作用

    __forwarding指针

    因此block内部拿到的变量实际就是在堆上的。当block进行copy被复制到堆上时,_Block_object_assign函数内做的这一系列操作。

    被__block修饰的对象类型的内存管理

    使用以下代码,生成c++代码查看内部实现

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

    来到源码查看__Block_byref_person_0结构体及其声明

    __Block_byref_person_0结构体
    
    typedef void (*Block)(void);
    struct __Block_byref_person_0 {
      void *__isa;  // 8 内存空间
    __Block_byref_person_0 *__forwarding; // 8
     int __flags; // 4
     int __size;  // 4
     void (*__Block_byref_id_object_copy)(void*, void*); // 8
     void (*__Block_byref_id_object_dispose)(void*); // 8
     Person *__strong person; // 8
    };
    // 8 + 8 + 4 + 4 + 8 + 8 + 8 = 48 
    
    // __Block_byref_person_0结构体声明
    
    __attribute__((__blocks__(byref))) __Block_byref_person_0 person = {
        (void*)0,
        (__Block_byref_person_0 *)&person,
        33554432,
        sizeof(__Block_byref_person_0),
        __Block_byref_id_object_copy_131,
        __Block_byref_id_object_dispose_131,
        
        ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"))
    };
    

    之前提到过__block修饰的对象类型生成的结构体中新增加了两个函数void (*__Block_byref_id_object_copy)(void*, void*);void (*__Block_byref_id_object_dispose)(void*);。这两个函数为__block修饰的对象提供了内存管理的操作。

    可以看出为void (*__Block_byref_id_object_copy)(void*, void*);void (*__Block_byref_id_object_dispose)(void*);赋值的分别为__Block_byref_id_object_copy_131__Block_byref_id_object_dispose_131。找到这两个函数

    static void __Block_byref_id_object_copy_131(void *dst, void *src) {
     _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
    }
    static void __Block_byref_id_object_dispose_131(void *src) {
     _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
    }
    

    上述源码中可以发现__Block_byref_id_object_copy_131函数中同样调用了_Block_object_assign函数,而_Block_object_assign函数内部拿到dst指针即block对象自己的地址值加上40个字节。并且_Block_object_assign最后传入的参数是131,同block直接对对象进行内存管理传入的参数3,8都不同。可以猜想_Block_object_assign内部根据传入的参数不同进行不同的操作的。

    通过对上面__Block_byref_person_0结构体占用空间计算发现__Block_byref_person_0结构体占用的空间为48个字节。而加40恰好指向的就为person指针。

    也就是说copy函数会将person地址传入_Block_object_assign函数,_Block_object_assign中对Person对象进行强引用或者弱引用。

    强引用示意图

    如果使用__weak修饰变量查看一下其中的源码

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Person *person = [[Person alloc] init];
            __block __weak Person *weakPerson = person;
            Block block = ^ {
                NSLog(@"%p", weakPerson);
            };
            block();
        }
        return 0;
    }
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_weakPerson_0 *weakPerson; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_weakPerson_0 *_weakPerson, int flags=0) : weakPerson(_weakPerson->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    __main_block_impl_0中没有任何变化,__main_block_impl_0weakPerson依然是强引用,但是__Block_byref_weakPerson_0中对weakPerson变为了__weak指针。

    struct __Block_byref_weakPerson_0 {
      void *__isa;
    __Block_byref_weakPerson_0 *__forwarding;
     int __flags;
     int __size;
     void (*__Block_byref_id_object_copy)(void*, void*);
     void (*__Block_byref_id_object_dispose)(void*);
     Person *__weak weakPerson;
    };
    

    也就是说无论如何block内部中对__block修饰变量生成的结构体都是强引用,结构体内部对外部变量的引用取决于传入block内部的变量是强引用还是弱引用。

    弱引用示意图

    mrc环境下,尽管调用了copy操作,__block结构体不会对person产生强引用,依然是弱引用。

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

    上述代码person会先释放

    block的copy[50480:8737001] -[Person dealloc]
    block的copy[50480:8737001] 0x100669a50
    

    当block从堆中移除的时候。会调用dispose函数,block块中去除对__Block_byref_person_0 *person;的引用,__Block_byref_person_0结构体中也会调用dispose操作去除对Person *person;的引用。以保证结构体和结构体内部的对象可以正常释放。

    循环引用

    循环引用导致内存泄漏。

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Person *person = [[Person alloc] init];
            person.age = 10;
            person.block = ^{
                NSLog(@"%d",person.age);
            };
        }
        NSLog(@"大括号结束啦");
        return 0;
    }
    

    运行代码打印内容

    block的copy[55423:9158212] 大括号结束啦
    

    可以发现大括号结束之后,person依然没有被释放,产生了循环引用。

    通过一张图看一下他们之间的内存结构

    产生循环引用示意图

    上图中可以发现,Person对象和block对象相互之间产生了强引用,导致双方都不会被释放,进而造成内存泄漏。

    解决循环引用问题 - ARC

    首先为了能随时执行block,我们肯定希望person对block对强引用,而block内部对person的引用为弱引用最好。

    使用__weak__unsafe_unretained修饰符可以解决循环引用的问题

    我们上面也提到过__weak会使block内部将指针变为弱指针。blockperson对象为弱指针的话,也就不会出现相互引用而导致不会被释放了。

    使用`__weak`和`__unsafe_unretained`修饰

    __weak__unsafe_unretained的区别。

    __weak不会产生强引用,指向的对象销毁时,会自动将指针置为nil。因此一般通过__weak来解决问题。

    __unsafe_unretained不会产生前引用,不安全,指向的对象销毁时,指针存储的地址值不变。

    使用__block也可以解决循环引用的问题。

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            __block Person *person = [[Person alloc] init];
            person.age = 10;
            person.block = ^{
                NSLog(@"%d",person.age);
                person = nil;
            };
            person.block();
        }
        NSLog(@"大括号结束啦");
        return 0;
    }
    

    上述代码之间的相互引用可以使用下图表示

    使用__block也可以解决循环引用

    上面我们提到过,在block内部使用变量使用的其实是__block修饰的变量生成的结构体__Block_byref_person_0内部的person对象,那么当person对象置为nil也就断开了结构体对person的强引用,那么三角的循环引用就自动断开。该释放的时候就会释放了。但是有弊端,必须执行block,并且在block内部将person对象置为nil。也就是说在block执行之前代码是因为循环引用导致内存泄漏的。

    __strong 和 __weak

    __weak typeof(self) weakSelf = self;
    person.block = ^{
        __strong typeof(weakSelf) myself = weakSelf;
        NSLog(@"age is %d", myself->_age);
    };
    

    block内部重新使用__strong修饰self变量是为了在block内部有一个强指针指向weakSelf避免在block调用的时候weakSelf已经被销毁。主要实在多线程中使用 , 实际开发环境中 , 线程比较简单 , __strong ,一般可省略. 还是用一个例子来说明吧.

    如果block里面不加__strong __typeof(weakSelf)strongSelf = weakSelf会如何呢?

    
    #import "ViewController.h"
    #import "Student.h"
    
    @interface ViewController ()
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        Student *student = [[Student alloc]init];
        student.name = @"Hello World";
        __weak typeof(student) weakStu = student;
        
        student.study = ^{
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                NSLog(@"my name is = %@",weakStu.name);
            });
        };
    
        student.study();
    }
    
    输出:
    my name is = (null)
    

    为什么输出是这样的呢?

    重点就在dispatch_after这个函数里面。在study()的block结束之后,student被自动释放了。又由于dispatch_after里面捕获的__weak的student,根据__weak的实现原理,在原对象释放之后,__weak对象就会变成null,防止野指针。所以就输出了null了。

    那么我们怎么才能在weakSelf之后,block里面还能继续使用weakSelf之后的对象呢?

    究其根本原因就是weakSelf之后,无法控制什么时候会被释放,为了保证在block内不会被释放,需要添加__strong。

    在block里面使用的__strong修饰的weakSelf是为了在函数生命周期中防止self提前释放。strongSelf是一个自动变量当block执行完毕就会释放自动变量strongSelf不会对self进行一直进行强引用。

    #import "ViewController.h"
    #import "Student.h"
    
    @interface ViewController ()
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        Student *student = [[Student alloc]init];
        
        student.name = @"Hello World";
        __weak typeof(student) weakSelf = student;
        
        student.study = ^{
            __strong typeof(student) strongSelf = weakSelf;
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                NSLog(@"my name is = %@",strongSelf.name);
            });
            
        };
    
        student.study();
    }
    输出:
    my name is = Hello World
    

    至此,我们就明白了weakSelf、strongSelf的用途了。

    weakSelf 是为了block不持有self,避免Retain Circle循环引用。在 Block 内如果需要访问 self 的方法、变量,建议使用 weakSelf。

    strongSelf的目的是因为一旦进入block执行,假设不允许self在这个执行过程中释放,就需要加入strongSelf。block执行完后这个strongSelf 会自动释放,没有不会存在循环引用问题。如果在 Block 内有多线程的或者block的嵌套访问 self,则需要使用 strongSelf。

     

    为什么Masonry中使用self,不会引起循环应用??

     [self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
            make.left.equalTo(self.view.mas_left).mas_offset(0);
            make.right.equalTo(self.view.mas_right).mas_offset(0);
            make.bottom.equalTo(self.save_btn.mas_top).mas_offset(0);
            make.top.equalTo(self.view.mas_top).mas_offset(0);
      }];

    看到这里,读者应该就应该能回答这个问题了。

    - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
        self.translatesAutoresizingMaskIntoConstraints = NO;
        MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
        block(constraintMaker);
        return [constraintMaker install];
    }
    

    block内部会引用变量 self,闭包虽然引用了self,但是self并没有引用这个闭包,self.tableview也没有引用 , 没有任何变量引用了block ,   block只是一个局部参数 , 函数执行完毕后 , block的内存就被系统回收了.   而且还有更骚的地方 , 这个block 是一个__NSStackBlock__ .  虽然是在arc环境下 , 但是由于没有赋值 ,  没有调copy  所以这个是__NSStackBlock__,自然也没有引起循环依赖。

     

    展开全文
  • Posted by ...block原理和内存中的位置 是主角。 如何理解blcok block:可以理解为匿名的函数,就是预先准备好的一段代码,在需要的时候调用。 底层实现 block是一个指针结构体,在终端

    简介

    今天回顾一下blcok,基本用法在我的这篇文章中有较为详细的描述,这次不再回顾,本次 block的原理和内存中的位置 是主角。


    如何理解blcok

    block:可以理解为匿名的函数,就是预先准备好的一段代码,在需要的时候调用。

    底层实现

    block是一个指针结构体,在终端下通过clang -rewrite-objc 指令看看C++代码。

    创建一个OS X 工程,写一个最简单的block

    #import <Foundation/Foundation.h>
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
    
            void (^myblock)() = ^() {
                NSLog(@"hello block");
            };
    
            myblock();
        }
        return 0;
    }

    利用终端编译生成C++代码:
    clang -rewrite-objc main.m

    几个重要的结构体和函数简介:

    • __block_impl:这是一个结构体,也是C面向对象的体现,可以理解为block的基类;
    • __main_block_impl_0: 可以理解为block变量;
    • __main_block_func_0: 可以理解为匿名函数;
    • __main_block_desc_0:block的描述, Block_size;
    1、__block_impl
    
    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    
    2、__main_block_impl_0
    
    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;
      }
    };
    
    3、__main_block_func_0
    
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_gc_5fkhcz0n6px48vzc744hmp6c0000gn_T_main_eef954_mi_0);
            }
    
    4、 __main_block_desc_0
    
    staticstruct __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[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
            void (*myblock)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
            ((void (*)(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);
        }
        return 0;
    }
    • 注意事项:block容易造成循环引用,在block里面如果使用了self,然后形成强引用时,需要打断循环引用;在MRC下用_block,在ARC下使用__weak;

    关于block在内存中的位置

    深入理解

    block快的存储位置(block入口的地址)可能存放在3个地方:代码区(全局区)、堆区、栈区(ARC情况下回自动拷贝到堆区、因此ARC下只有两个地方:代码区和堆区)。

    • 代码区:不访问栈区的变量(如局部变量),且不访问堆区的变量(如用alloc创建的对象)时,此时block存放在代码区;
    • 堆区:如果访问了堆区的变量(如局部变量),或堆区的变量(如用alloc创建的对象),此时block存方在堆区;--需要注意
      • 实际是放在栈区,在ARC情况下自动拷贝到堆区,如果不是ARC则存放在栈区,所在函数执行完毕就回释放,想再外面调用需要用copy指向它,这样就拷贝到了堆区,strong属性不会拷贝、会造成野指针错区。(需要理解ARC是一种编译器特性,即编译器在编译时在核实的地方插入retain、release、autorelease,而不是iOS的运行时特性)。
      • 此外代码存在堆区时,需要注意,因为堆区不像代码区不变化,堆区是动态的(不断的创建销毁),当没有强指针指向的时候就会被销毁,如果再去访问这段代码时,程序就会崩溃!所以此种情况在定义block属性时需要指定为strong or copy。block是一段代码,即不可变,所以使用copy也不会深拷贝。

    如果看的不太明白,需要先补充点iOS内存的知识

    简单记忆:

    • Block如果没有引用外部变量
      保存在全局区(MRC/ARC一样)
    • Block如果引用外部变量
      ARC保存在 堆区; MRC保存在 栈区必须用copy修饰block;


    作者:iOS音视频
    链接:http://www.jianshu.com/p/7b1c2951b508
    來源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    展开全文
  • Object-c block原理

    千次阅读 2013-08-30 17:03:55
    1,block的声明格式: 返回值类型(^block的名字)(参数类型1,参数类型2,.......) 2,block的定义格式: ^返回值类型(参数类型1 参数名,参数类型2 参数名.......){return 返回值类型;} 注:返回值类型是可以...
  • Block原理浅析,循环引用的产生方式

    千次阅读 2018-10-28 22:44:31
    经常在一些面试题里面或者其他规范里面看到,block里面不能用self,否则会产生循环引用,但是为什么不能用self,循环引用是什么,如何产生的循环引用,这些总是一知半解。带着问题,查询了一些资料了解了一部分产生...
  • block原理详细讲解(二)

    千次阅读 2015-10-10 14:06:50
    本系列博文总结自《Pro Multithreading and Memory Management for iOS and OS X with ARC》 如果您觉得我的博客对您有帮助,请通过关注我的新浪微博 ...本文将介绍可写变量、block的内存段、__block变量的内存段等
  • block原理详细讲解(三)

    千次阅读 2015-10-10 14:10:03
    本系列博文总结自《Pro Multithreading and Memory Management for iOS and OS X with ...在上一篇文章中,我们讲了很多关于 block 和基础变量的内存管理,接着我们聊聊 block 和对象的内存管理,如 block 经常会碰到的
  • block原理详细讲解(一)

    千次阅读 2015-10-10 14:04:16
    本系列博文总结自《Pro Multithreading and Memory Management for iOS...block 顾名思义就是代码块,将同一逻辑的代码放在一个块,使代码更简洁紧凑,易于阅读,而且它比函数使用更方便,代码更美观,因而广受开发者欢
  • block实现原理

    2019-01-14 18:09:43
    iOS开发,block实现原理。深入讲解block实现原理以及讲解与内存管理之间的联系。
  • iOS底层原理进阶,从底层原理和本质详细解析block的使用
  • Block实现原理

    2015-09-21 01:06:19
    iOS中block实现的探究 分类: iOS 职业 | 工作 2012-07-17 20:23 61205人阅读 评论(25) 收藏 举报 ioslambda编译器apple语言python 目录(?)[+] [0. Brief introduction of block] Block...
  • iOS之深入解析Block的底层原理

    万次阅读 2021-03-16 20:01:34
    ④ __block原理二、block 底层源码分析① block 源码位置② block 类型分析③ 内存变化④ 调用三、签名四、block 三次 copy① _Block_copy 源码分析② _Block_object_assign 分析③ 调试验证④ 总结五、_Block_...
  • Block 底层实现原理

    2019-02-16 20:50:46
    Block 底层实现原理 在理解 Block 的底层实现原理的过程中,可以通过 clang 命令将一个 block 例子转换为 C 代码,从而解读其具体的代码实现逻辑。 clang main.m -rewrite-objc -o dest.cpp 上面的命令,是将 main.m...
  • Spark BlockManager原理与源码分析

    千次阅读 2018-05-24 17:07:42
    1、BlockManager原理示意图①Driver上的BlockManagerMaster管理各个节点上BlockManager的元数据信息和维护block的状态信息。②每个节点上BlockManager的每个组件: DiskStore:负责磁盘上的数据读写 MemoryStore: ...
  • Block的实现原理

    千次阅读 2015-04-11 15:08:10
    Block的实现原理这篇是记录根据网上的文章进行的实际操作。C函数的生成中间编译代码的编译方法是clang -rewrite-objc xxxx.c。由于编译文件中内容较多,这里我们只截取有关block的部分。首先,看看没有block的C函数...
  • 2010年WWDC发布iOS4时Apple对Objective-C进行了一次重要的升级:支持Block。说到底这东西就是闭包,其他高级语音例如Java和C++已有支持,第一次使用Block感觉满简单好用的,但是慢慢也遇到很多坑。本文聊聊ARC和non-...
  • Block实现原理与内存特性 博客原文: https://blog.csdn.net/wangyanchang21/article/details/79525394。
  • 浅谈block实现原理及内存特性系列文章之二: 持有变量
  • Block Ack 基本原理

    千次阅读 2014-11-07 17:49:08
    Block Ack 基本原理  802.11n最主要的添加了Block Ack这个技术,但是在实际应用中对于BA还是知之甚少,转贴一个讲述BA基本原理的文章,但是我通过wireshark进行抓报并没有在BAR(Block Ack Requst)里面找到...
  • 在使用 CSS 实现表现的时候,会经常接触到 display:inline-block 这一属性,无论是初接触 Web 标准还是接触标准已久的朋友,大都会对这一属性感觉很迷惑和模糊。 复制代码代码如下: display:inline-block 将对象...
  • 主要给大家介绍了关于iOS中block变量捕获原理的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。
  • BlockManager原理:1.Driver上有BlockManagerMaster,负责对各个节点上的BlockManager内部管理的元数据进行维护。2.每个节点的BlockManager有几个关键组件,DiskStore负责对磁盘上的数据进行读写,MemoryStore负责对...
  • block使用copy原理

    千次阅读 2016-10-31 14:01:40
    简单来说,block就像一个函数指针,指向我们要使用的函数。 就和函数调用一样的,不管你在哪里写了这个block,只要你把它放在了内存中(通过调用存在这个block的方法或者是函数),不管放在栈中还是在堆中,还是...
  • block底层实现原理

    千次阅读 2015-03-05 00:51:08
    #include"stdio.h" int main() ... void (^blk)(void) = ^{printf("Block.\n");}; blk(); return 0; } clang -rewrite-objc main.m #ifndef __OBJC2__ #define __OBJC2__ #endif struct objc_sele
  • iOS底层原理 - Block本质探究

    千次阅读 2018-06-29 11:20:10
    iOS底层原理 - Block本质探究 本质 block 本质是一个OC对象,也存在 isa 指针。或者说Block 是封装了函数调用和函数调用环境的OC对象。 1.底层实现 编写一段最简单的OC代码顶一个block,代码如: int ...
  • cpp-block的实现原理

    2019-08-16 05:36:39
    block 本质也是一个OC对象,内部也有一个isa指针 block 是封装了函数调用以及函数调用环境的OC对象

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 187,698
精华内容 75,079
关键字:

block原理