这是我参与8月更文挑战的第22天,活动详情查看:8月更文挑战
写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。
目录如下:
- iOS 底层原理探索 之 alloc
- iOS 底层原理探索 之 结构体内存对齐
- iOS 底层原理探索 之 对象的本质 & isa的底层实现
- iOS 底层原理探索 之 isa - 类的底层原理结构(上)
- iOS 底层原理探索 之 isa - 类的底层原理结构(中)
- iOS 底层原理探索 之 isa - 类的底层原理结构(下)
- iOS 底层原理探索 之 Runtime运行时&方法的本质
- iOS 底层原理探索 之 objc_msgSend
- iOS 底层原理探索 之 Runtime运行时慢速查找流程
- iOS 底层原理探索 之 动态方法决议
- iOS 底层原理探索 之 消息转发流程
- iOS 底层原理探索 之 应用程序加载原理dyld (上)
- iOS 底层原理探索 之 应用程序加载原理dyld (下)
- iOS 底层原理探索 之 类的加载
- iOS 底层原理探索 之 分类的加载
- iOS 底层原理探索 之 关联对象
- iOS底层原理探索 之 魔法师KVC
- iOS底层原理探索 之 KVO原理|8月更文挑战
- iOS底层原理探索 之 重写KVO|8月更文挑战
- iOS底层原理探索 之 多线程原理|8月更文挑战
- iOS底层原理探索 之 GCD函数和队列
- iOS底层原理探索 之 GCD原理(上)
- iOS底层 - 关于死锁,你了解多少?
- iOS底层 - 单例 销毁 可否 ?
- iOS底层 - Dispatch Source
- iOS底层 - 一个栅栏函 拦住了 数
- iOS底层 - 不见不散 的 信号量
- iOS底层 GCD - 一进一出 便成 调度组
- iOS底层原理探索 - 锁的基本使用
- iOS底层 - @synchronized 流程分析
- iOS底层 - 锁的原理探索
- iOS底层 - 带你实现一个读写锁
以上内容的总结专栏
细枝末节整理
前言
之前,我们通过四篇文章,逐步展开探索了iOS开发中对于锁的概念作用以及常用的锁的使用,以及其底层的数据结构和原理流程。今天,我们开始对开发中也是用的很多的 -- Block ,探索其常规使用,使用中产生的问题,如何解决以及其底层是什么结构等问题。话不多说,这就开始今天的内容。
Block 是什么
- 关于闭包 先介绍一下什么是闭包。在 wikipedia 上,闭包的定义是:
In programming languages, a closure is a function or reference to a function together with a referencing environment—a table storing a reference to each of the non-local variables (also called free variables or upvalues) of that function.
翻译一下就是:
闭包是一个函数(或指向函数的指针),再加上该函数执行的外部的上下文变量(有时候也称作自由变量)。
- 苹果在关于Blcok编程主题中的简介如下:
Block objects are a C-level syntactic and runtime feature. They are similar to standard C functions, but in addition to executable code they may also contain variable bindings to automatic (stack) or managed (heap) memory. A block can therefore maintain a set of state (data) that it can use toimpact behavior when executed.
You can use blocks to compose function expressions that can be passed to API, optionally stored, and used by multiple threads. Blocks are particularly useful as a callback because the block carries both the code to be executed on callback and the data needed during that execution.
Blocks are available in GCC and Clang as shipped with the OS X v10.6 Xcode developer tools. You can use blocks with OS X v10.6 and later, and iOS 4.0 and later. The blocks runtime is open source and can be found in LLVM’s compiler-rt subproject repository. Blocks have also been presented to the C standards working group as N1370: Apple’s Extensions to C. As Objective-C and C++ are both derived from C,blocks are designed to work with all three languages (as well as Objective-C++). The syntax reflects this goal.
翻译如下:
块对象是 C 级语法和运行时功能。它们类似于标准 C 函数,但除了可执行代码之外,它们还可能包含变量绑定到自动(堆栈)或托管(堆)内存。因此,一个块可以维护一组状态(数据),它可以在执行时使用这些状态来影响行为。
您可以使用块来组合函数表达式,这些表达式可以传递给 API、可选择存储并由多个线程使用。块作为回调特别有用,因为块携带要在回调中执行的代码和执行期间所需的数据。
块在 GCC 和Clang中可用,随 OS X v10.6 Xcode 开发人员工具一起提供。您可以在 OS X v10.6 和更高版本以及 iOS 4.0 和更高版本中使用块。blocks 运行时是开源的,可以在LLVM 的 compiler-rt 子项目存储库中找到。块也已作为N1370: Apple's Extensions to C提交给 C 标准工作组。由于 Objective-C 和 C++ 都是从 C 派生而来的,因此块被设计为与所有三种语言(以及 Objective-C++)一起使用。语法反映了这个目标。
block 实际上就是 Objective-C 语言对于闭包的实现。
声明和使用Block
我们可以使用^运算符来声明块变量并指示块文字的开始。块本身包含在 中{},如本例所示(与 C 一样,;指示语句的结束):
int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
return num * multiplier;
};
下图解释了该示例:
请注意,该块能够使用来自定义它的同一范围内的变量。 如果将块声明为变量,则可以像使用函数一样使用它:
int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
return num * multiplier;
};
printf("%d", myBlock(7));
// prints "49"
声明Block引用
块变量保存对块的引用。您可以使用类似于用于声明指向函数的指针的语法来声明它们,不同之处在于您使用了^代替*。块类型与 C 类型系统的其余部分完全互操作。以下是所有有效的块变量声明:
void (^blockReturningVoidWithVoidArgument)(void);
int (^blockReturningIntWithIntAndCharArguments)(int, char);
void (^arrayOfTenBlocksReturningVoidWithIntArgument[10])(int);
块还支持可变参数 ( ...) 参数。不带参数的块必须void在参数列表中指定。
通过为编译器提供用于验证块的使用、传递给块的参数以及返回值的分配的完整元数据集,块被设计为完全类型安全。您可以将块引用转换为任意类型的指针,反之亦然。但是,您不能通过指针取消引用运算符 ( *)取消引用块引用,因此无法在编译时计算块的大小。
您还可以为块创建类型——当您在多个地方使用具有给定签名的块时,通常认为这样做是最佳实践:
typedef float (^MyBlockType)(float, float);
MyBlockType myFirstBlock = // ... ;
MyBlockType mySecondBlock = // ... ;
创建Block
您可以使用^运算符来指示块文字表达式的开头。它后面可能跟一个包含在(). 块的主体包含在{}. 下面的示例定义了一个简单的块并将其分配给先前声明的变量 ( oneFrom) — 这里块后面;是结束 C 语句的法线。
float (^oneFrom)(float);
oneFrom = ^(float aFloat) {
float result = aFloat - 1.0;
return result;
};
如果不显式声明块表达式的返回值,则可以从块的内容中自动推断出来。如果推断返回类型并且参数列表是void,那么您也可以省略(void)参数列表。如果或当存在多个 return 语句时,它们必须完全匹配(必要时使用强制转换)。
全局Block
在文件级别,您可以将块用作全局文字:
#import <stdio.h>
int GlobalInt = 0;
int (^getGlobalInt)(void) = ^{ return GlobalInt; };
直接使用 Block
在很多情况下,我们不需要声明块变量;相反,只需在需要作为参数的地方编写一个内联块文字。以下示例使用该qsort_b函数。qsort_b类似于标准qsort_r函数,但将块作为其最终参数。
char *myCharacters[3] = { "TomJohn", "George", "Charles Condomine" };
qsort_b(myCharacters, 3, sizeof(char *), ^(const void *l, const void *r) {
char *left = *(char **)l;
char *right = *(char **)r;
return strncmp(left, right, 1);
});
// myCharacters is now { "Charles Condomine", "George", "TomJohn" }
Cocoa 框架中的 Blocks
Cocoa 框架中的几个方法将块作为参数,通常要么对对象集合执行操作,要么在操作完成后用作回调。以下示例显示了如何将块与NSArray方法一起使用sortedArrayUsingComparator:。该方法接受一个参数——块。为了说明,在这种情况下,块被定义为一个NSComparator局部变量:
NSArray *stringsArray = @[ @"string 1",
@"String 21",
@"string 12",
@"String 11",
@"String 02" ];
static NSStringCompareOptions comparisonOptions = NSCaseInsensitiveSearch | NSNumericSearch |
NSWidthInsensitiveSearch | NSForcedOrderingSearch;
NSLocale *currentLocale = [NSLocale currentLocale];
NSComparator finderSortBlock = ^(id string1, id string2) {
NSRange string1Range = NSMakeRange(0, [string1 length]);
return [string1 compare:string2 options:comparisonOptions range:string1Range locale:currentLocale];
};
NSArray *finderSortArray = [stringsArray sortedArrayUsingComparator:finderSortBlock];
NSLog(@"finderSortArray: %@", finderSortArray);
/*
Output:
finderSortArray: (
"string 1",
"String 02",
"String 11",
"string 12",
"String 21"
)
*/
__block 变量
块的一个强大功能是它们可以在同一词法范围内修改变量。您可以使用__block存储类型修饰符来表示模块可以修改变量。使用 Cocoa修改Blocks 中显示的示例,您可以使用块变量来计算有多少字符串被比较为相等,如下面的示例所示。例如,在这种情况下,块被直接使用并currentLocale用作块内的只读变量:
NSArray *stringsArray = @[ @"string 1",
@"String 21", // <-
@"string 12",
@"String 11",
@"Strîng 21", // <-
@"Striñg 21", // <-
@"String 02" ];
NSLocale *currentLocale = [NSLocale currentLocale];
__block NSUInteger orderedSameCount = 0;
NSArray *diacriticInsensitiveSortArray = [stringsArray sortedArrayUsingComparator:^(id string1, id string2) {
NSRange string1Range = NSMakeRange(0, [string1 length]);
NSComparisonResult comparisonResult = [string1 compare:string2 options:NSDiacriticInsensitiveSearch range:string1Range locale:currentLocale];
if (comparisonResult == NSOrderedSame) {
orderedSameCount++;
}
return comparisonResult;
}];
NSLog(@"diacriticInsensitiveSortArray: %@", diacriticInsensitiveSortArray);
NSLog(@"orderedSameCount: %d", orderedSameCount);
/*
Output:
diacriticInsensitiveSortArray: (
"String 02",
"string 1",
"String 11",
"string 12",
"String 21",
"Str\U00eeng 21",
"Stri\U00f1g 21"
)
orderedSameCount: 2
Block 功能
一个Block是一个匿名的内联代码集合,它:
- 像函数一样有类型参数列表
- 具有推断或声明的返回类型
- 可以从定义它的词法范围中捕获状态
- 可以选择性地修改词法作用域的状态
- 可以与定义在同一词法范围内的其他块共享修改的潜力
- 词法范围(堆栈框架)被销毁后,可以继续共享和修改词法范围(堆栈框架)内定义的状态
您可以复制一个Block,甚至将其传递给其他线程以进行延迟执行(或者,在其自己的线程中,传递给运行循环)。编译器和运行时安排从块引用的所有变量在块的所有副本的生命周期内都被保留。尽管块可用于纯 C 和 C++,但块也始终是 Objective-C 对象。
__block 存储类型
您可以通过应用__block存储类型修饰符来指定导入的变量是可变的(即读写)。__block存储类似,但相互排斥的,对register,auto和static 存储类型的局部变量。
__block变量存在于变量的词法范围和在变量词法范围内声明或创建的所有块和块副本之间共享的存储中。因此,如果在帧内声明的块的任何副本在帧结束后仍然存在(例如,通过排队等待稍后执行),则存储将在堆栈帧被破坏后继续存在。给定词法范围内的多个块可以同时使用一个共享变量。
作为一种优化,块存储从堆栈开始——就像块本身一样。如果使用Block_copy (或在发送块时在 Objective-C 中copy)复制块 ,则变量将复制到堆中。因此,变量 的地址__block可以随时间变化。
对__block变量还有两个限制:它们不能是变长数组,并且不能是包含 C99 变长数组的结构。
以下示例说明了__block变量的使用:
__block int x = 123; // x lives in block storage
void (^printXAndY)(int) = ^(int y) {
x = x + y;
printf("%d %d\n", x, y);
};
printXAndY(456); // prints: 579 456
// x is now 579
如前所述,尝试x在块内分配新值会导致错误:
extern NSInteger CounterGlobal;
static NSInteger CounterStatic;
{
NSInteger localCounter = 42;
__block char localCharacter;
void (^aBlock)(void) = ^(void) {
++CounterGlobal;
++CounterStatic;
CounterGlobal = localCounter; // localCounter fixed at block creation
localCharacter = 'a'; // sets localCharacter in enclosing scope
};
++localCounter; // unseen by the block
localCharacter = 'b';
aBlock(); // execute the block
// localCharacter now 'a'
}
对象和Block变量
Block为 Objective-C 和 C++ 对象以及其他块作为变量提供支持。
### Objective-C 对象 复制块时,它会创建对块内使用的对象变量的强引用。如果在方法的实现中使用块:
- 如果通过引用访问实例变量,则会对 进行强引用
self; - 如果按值访问实例变量,则会对该变量进行强引用。
以下示例说明了两种不同的情况:
dispatch_async(queue, ^{
// instanceVariable is used by reference, a strong reference is made to self
doSomethingWithObject(instanceVariable);
});
id localVariable = instanceVariable;
dispatch_async(queue, ^{
/*
localVariable is used by value, a strong reference is made to localVariable
(and not to self).
*/
doSomethingWithObject(localVariable);
});
要覆盖特定对象变量的此行为,您可以使用__block存储类型修饰符对其进行标记。
Block 用法
块通常代表小的、自包含的代码段。因此,它们作为封装可以并发执行的工作单元的一种方式特别有用,或者在集合中的项目上执行,或者作为另一个操作完成时的回调。
块是传统回调函数的有用替代品,主要有两个原因:
- 它们允许您在稍后在方法实现的上下文中执行的调用点编写代码。 因此,块通常是框架方法的参数。
- 它们允许访问局部变量。 与其使用需要包含执行操作所需的所有上下文信息的数据结构的回调,不如直接访问局部变量。
调用Block
如果将块声明为变量,则可以像使用函数一样使用它,如以下两个示例所示:
int (^oneFrom)(int) = ^(int anInt) {
return anInt - 1;
};
printf("1 from 10 is %d", oneFrom(10));
// Prints "1 from 10 is 9"
float (^distanceTraveled)(float, float, float) =
^(float startingSpeed, float acceleration, float time) {
float distance = (startingSpeed * time) + (0.5 * acceleration * time * time);
return distance;
};
float howFar = distanceTraveled(0.0, 9.8, 1.0);
// howFar = 4.9
但是,您经常将块作为参数传递给函数或方法。在这些情况下,您通常会创建一个“内联”块。
使用Block作为函数参数
您可以像传递任何其他参数一样将块作为函数参数传递。然而,在许多情况下,您不需要声明块;相反,您只需在需要它们作为参数的地方内嵌实现它们。以下示例使用该qsort_b函数。qsort_b类似于标准qsort_r函数,但将块作为其最终参数。
char *myCharacters[3] = { "TomJohn", "George", "Charles Condomine" };
qsort_b(myCharacters, 3, sizeof(char *), ^(const void *l, const void *r) {
char *left = *(char **)l;
char *right = *(char **)r;
return strncmp(left, right, 1);
});
// Block implementation ends at "}"
// myCharacters is now { "Charles Condomine", "George", "TomJohn" }
请注意,该块包含在函数的参数列表中。
下一个示例显示了如何将块与dispatch_apply函数一起使用。dispatch_apply声明如下:
void dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t));
该函数将一个块提交到调度队列以进行多次调用。它需要三个参数;第一个指定要执行的迭代次数;第二个指定块提交到的队列;第三个是块本身,它又接受一个参数——迭代的当前索引。
您可以dispatch_apply简单地使用只是打印出迭代索引,如下所示:
#include <dispatch/dispatch.h>
size_t count = 10;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(count, queue, ^(size_t i) {
printf("%u\n", i);
});
使用Block作为方法参数
Cocoa提供了许多使用块的方法。您可以像传递任何其他参数一样将块作为方法参数传递。
以下示例确定出现在给定过滤器集中的数组中前五个元素中的任何一个的索引。
NSArray *array = @[@"A", @"B", @"C", @"A", @"B", @"Z", @"G", @"are", @"Q"];
NSSet *filterSet = [NSSet setWithObjects: @"A", @"Z", @"Q", nil];
BOOL (^test)(id obj, NSUInteger idx, BOOL *stop);
test = ^(id obj, NSUInteger idx, BOOL *stop) {
if (idx < 5) {
if ([filterSet containsObject: obj]) {
return YES;
}
}
return NO;
};
NSIndexSet *indexes = [array indexesOfObjectsPassingTest:test];
NSLog(@"indexes: %@", indexes);
/*
Output:
indexes: <NSIndexSet: 0x10236f0>[number of indexes: 2 (in 2 ranges), indexes: (0 3)]
*/
以下示例确定NSSet对象是否包含由局部变量指定的单词,如果包含,则将另一个局部变量 ( found)的值设置为YES(并停止搜索)。请注意,found它也被声明为一个__block变量,并且该块是内联定义的:
__block BOOL found = NO;
NSSet *aSet = [NSSet setWithObjects: @"Alpha", @"Beta", @"Gamma", @"X", nil];
NSString *string = @"gamma";
[aSet enumerateObjectsUsingBlock:^(id obj, BOOL *stop) {
if ([obj localizedCaseInsensitiveCompare:string] == NSOrderedSame) {
*stop = YES;
found = YES;
}
}];
// At this point, found == YES
要避免的模式
块文字(即 ^{ ... })是表示块的堆栈本地数据结构的地址。因此,堆栈本地数据结构的范围是封闭的复合语句,因此您应该避免以下示例中显示的模式:
void dontDoThis() {
void (^blockArray[3])(void); // an array of 3 block references
for (int i = 0; i < 3; ++i) {
blockArray[i] = ^{ printf("hello, %d\n", i); };
// WRONG: The block literal scope is the "for" loop.
}
}
void dontDoThisEither() {
void (^block)(void);
int i = random():
if (i > 1000) {
block = ^{ printf("got i at: %d\n", i); };
// WRONG: The block literal scope is the "then" clause.
}
// ...
}
总结
本篇我们主要简单介绍了Block的基本概念和用法,下一篇开始,我们通过常见的面试题,并结合实际开发中遇到的问题,开始深入的探索Block。大家,加油!!!