学做网站培训机构,深圳500强企业排行榜,图库,开发一平方赔多少钱理解引用计数
Objective-C 使用引用计数来管理内存#xff1a;每个对象都有个可以递增或递减的计数器。如果想使某个对象继续存活#xff0c;那就递增其引用计数#xff1a;用完了之后#xff0c;就递减其计数。计数变为 0时#xff0c;就可以把它销毁。 在ARC中#xf…理解引用计数
Objective-C 使用引用计数来管理内存每个对象都有个可以递增或递减的计数器。如果想使某个对象继续存活那就递增其引用计数用完了之后就递减其计数。计数变为 0时就可以把它销毁。 在ARC中所有与引用计数有关的方法都无法编译由于 ARC 会在编译时自动插入内存管理代码因此在编译时所有与引用计数相关的方法都会被 ARC 替换为适当的代码。
引用计数的工作原理
在引用计数架构下对象有个计数器用以表示当前有多少个事物想令此对象继续存活下去。这在 Objective-C 中叫做 “保留计数”也可以叫 “引用计数”。NSObject 协议声明了下面三个方法用于操作计数器以递增或递减其值
Retain 递增保留计数。release 递减保留计数。autorelease 待稍后清理 “自动释放池”时再递减保留计数。 查看保留计数的方法叫做 retainCount不推荐使用。
对象创建出来时其保留计数至少为 1。若想令其继续存活则调用 retain 方法。要是某部分代码不再使用此对象不想令其继续存活那就调用 release 或 autorelease 方法。最终当保留计数归零时对象就回收了也就是说系统会将其占用的内存标记为 “可重用”。此时所有指向该对象的引用也都变得无效。 应用程序在其生命周期中会创建很多对象这些对象都相互联系着。例如表示个人信息的对象会引用另一个表示人名的字符串对象还可能会引用其他个人信息对象比如在存放朋友的 set 中就是如此。这些相互关联的对象就构成了一张 “对象图”。对象如果持有指向其他对象的强引用,那么前者就 “拥有” 后者。对象想令其所引用的那些对象继续存活就可将其 “保留”。等用完了之后再释放。 按 “引用树” 回溯那么最终会发现一个 “根对象”。在 iOS 应用程序中它是 UIApplication 对象。是应用程序启动时创建的单例。 如下面这段代码
NSMutableArray *array [[NSMutableArray alloc] init];
NSNumber *number [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];//不能假设number对象一定存活
NSLog(number %, number);//***
//因为对象所占的内存在 “解除分配”之后只是放回 “可用内存池”。如果执行 NSLog 时尚未覆写对象内存那么该对象仍然有效这时程序不会崩溃。
//因过早释放对象而导致的 bug 很难调试。[array release];在上面这段代码中创建了一个可变数组 array然后创建了一个 NSNumber 对象 number并将其添加到数组中数组也会在 number 上调用retain 方法以期继续保留此对象这时number的引用计数至少为2。接着我们试着通过 release 方法释放了 number 对象因为数组对象还在引用着number对象因此它仍然存活但是不应该假设它一定存活。最后通过调用 release 方法释放了数组 array确保了内存的正确管理。上面这段代码在ARC中是无法编译的因为调用了release方法。 调用者通过 alloc 方法表达了想令该对象继续存活下去的意愿不过并不是说对象此时的保留计数必定是1。在 alloc 或 “initWithInt:” 方法的实现代码中也许还有其他对象也保留了此对象如初始化过程中的委托调用、通知其他对等所以其保留计数可能会大于 1。
为避免在不经意间使用了无效对象一般调用完 release 之后都会清空指针。这就能保证不会出现可能指向无效对象的指针这种指针通常称为 “悬挂指针”。 可以这样编写代码来防止此情况发生 NSNumber *number [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];
number nil;
属性存取方法中的内存管理
刚才那个例子中的数组通过在其元素上调用 retain 方法来保留那些对象。不光是数组其他对象也可以保留别的对象这一般通过访问 “属性”来实现而访问属性时会用到相关实例变量的获取方法及设置方法。若属性为 “strong 关系”则设置的属性值会保留。假设有个名叫 foo 的属性由名为 _foo 的实例变量所实现那么该属性的设置方法会是这样
- (void)setFoo:(id)foo {[foo retain];[_foo release];_foo foo;}当设置属性 foo 的新值时属性的设置方法会依次执行以下操作
保留新值使用 retain 方法保留新值确保其在设置方法之外仍然可用。释放旧值使用 release 方法释放旧值以减少其保留计数并在不再需要时释放内存。更新实例变量将实例变量 _foo 的引用指向新值。 上面这些操作的顺序很重要。如果在保留新值之前就释放了旧值并且旧值和新值指向同一个对象那么在释放旧值时可能会导致对象被系统回收而在后续保留新值时该对象已经不存在了。这就会导致 _foo 成为一个悬挂指针即指向已经释放的内存空间这样的指针是无效的并且可能导致应用程序崩溃或出现其他问题。
自动释放池
自动释放池autorelease是 Objective-C 内存管理的重要特性之一。 简单来说它允许开发者推迟对象的释放时间通常在下一个事件循环中才执行释放操作。 比如说有这个代码
- (NSString *)stringValue {NSString *str [[NSString alloc] initWithFormat:I am this: %, self];return str;
}在上面这段代码中str对象在stringValue方法的作用域中被alloc出来因此它的引用计数为1但是因为它的作用域是stringValue方法因此我们期望的是它在该方法结束前引用计数应为0但是因为缺少了释放操作因此该str对象的引用计数为1比期望值多1。 但是我们又不能直接在stringValue方法中将str对象释放否则还没等方法返回系统就把该对象回收了。这里应该用 autorelease它会在稍后释放对象从而给调用者留下了足够长的时间使其可以在需要时先保留返回值。换句话说此方法可以保证对象在跨越 “方法调用边界”method callboundary后一定存活。
因此我们需要改写 stringValue 方法使用 autorelease 来释放对象:
- (NSString *)stringValue {NSString *str [[NSString alloc] initWithFormat:I am this: %, self];return [str autorelease];
}改写后使用了 autorelease 方法将str对象放入自动释放池中以延迟其释放时间。这样在方法返回时对象的引用计数会保持预期的值而不会多出 1。 可以像下面这样使用 stringValue 方法返回的字符串对象
NSString *str [self stringValue];
NSLog(The string is: %, str);在第一段代码中NSString *str [self stringValue]; 返回的字符串对象 str 是一个被放入自动释放池的对象。因此尽管没有显式地调用 retain 方法但在 NSLog(“The string is: %”, str); 之后str 对象的引用计数不会被减少。这是因为该对象在自动释放池中直到下一个事件循环才会被释放。 但是如果你需要在稍后持有这个对象比如将它设置给一个实例变量那么你需要手动增加其引用计数以防止在自动释放池释放时对象被释放比如像这样假设在另一个地方创建了一个 ExampleClass 的实例并希望将stringValue返回的对象设置为该实例的 instanceVariable
ExampleClass *exampleObject [[ExampleClass alloc] init];
NSString *str [exampleObject stringValue];exampleObject.instanceVariable str;NSLog(The instance variable is: %, exampleObject.instanceVariable);则需要确保 str 对象不会在自动释放池被释放时被释放。因此我们需要手动增加 str 对象的引用计数以确保它不会被过早释放
//手动增加引用计数
exampleObject.instanceVariable [str retain];
//......//并且在稍后手动释放
[exampleObject.instanceVariable release];保留环
用引用计数机制时经常要注意的一个问题就是 “保留环”也就是呈环状相互引用的多个对象。这将导致内存泄漏因为循环中的对象其保留计数不会降为 0。对于循环中的每个对象来说至少还有另外一个对象引用着它。图里的每个对象都引用了另外两个对象之中的一个。在这个循环里所有对象的保留计数都是 1。 在垃圾收集环境中通常将这种情况认定为 “孤岛”。此时垃圾收集器会把三个对象全都回收走。而在 Objective-C 的引用计数架构中则享受不到这一便利。通常采用 “弱引用”来解决此问题或是从外界命令循环中的某个对象不再保留另外一个对象。这两种办法都能打破保留环从而避免内存泄漏。
引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后其保留计数至少为 1。若保留计数为正则对象继续存活。当保留计数降为 0 时对象就被销毁了。在对象生命期中其余对象通过引用来保留或释放对象。保留于释放操作分别会递增及递减保留计数。
以ARC简化引用计数
引用计数这个概念相当容易理解。需要执行保留与释放操作的地方也很容易就能看出来。所以 Clang 编译器项目带有一个 “静态分析器”。用于指明程序里引用计数出问题的地方。 比如再拿上一条中的这段代码举例子 if ([self shouldLogMessage]) {NSString *str [[NSString alloc] initWithFormat:I am this: %, self];NSLog(“str %”, str);}这段代码中alloc增加了str对象的引用计数但是在这个if块的作用域中它却缺少了释放操作因此str对象的引用计数比预期值多1导致了内存泄漏。因为上述这些规则很容易表述所以计算机可以简单地将其套用在程序上从而分析出有内存泄漏问题的对象。这正是 “静态分析器” 要做的事。
静态分析器还有更为深入的用途。既然可以查明内存管理问题那么应该也可以根据需要预先加入适当的保留或释放操作以避免这些问题。自动引用计数的思路就是源于此。 因此假如使用了ARC它就会自动将代码改写为这样 if ([self shouldLogMessage]) {NSString *str [[NSString alloc] initWithFormat:I am this: %, self];NSLog(“str %”, str);[message release];}使用 ARC 时一定要记住引用计数实际上还是要执行的只不过保留与释放操作现在是由 ARC 自动为你添加。 由于 ARC 会自动执行 retain、release 、autorelease 等操作所以直接在 ARC 下调用这些内存管理方法是非法的。具体来说不能调用下列方法
retainreleaseautoreleasedealloc 直接调用上述任何方法都会产生编译错误因为 ARC 要分析何处应该自动调用内存管理方法所以如果手工调用的话就会干扰其工作。 实际上ARC 在调用这些方法时并不通过普通的 Objective-C 消息派发机制而是直接调用其底层 C 语言版本。这样做性能更好因为保留及释放操作需要频繁执行所以直接调用底层函数能节省很多 CPU 周期。
使用 ARC 时必须遵循的方法命名规则
ARC 将内存管理语义在方法名中表示出来确立为硬性规定。 简单地体现在方法名上。若方法名以下列词语开头则其调用上述四种方法的那段代码要负责释放方法所返回的对象
allocnewcopymutableCopy 若方法名不以上述四个词语开头则表示其所返回的对象并不归调用者所有。 在这种情况下返回的对象会自动释放所以其值在跨越方法调用边界后依然有效。要想使对象多存活一段时间必须令调用者保留它才行。 我自己简单理解就是使用 “alloc”、“new”、“copy” 或者 “mutableCopy” 开头的方法方法内部的引用计数要自己手动管理release或者autorelease等而不使用这四个开头的方法的引用计数就是自动管理的。
除了会自动调用 “保留” 与 “释放” 方法外使用 ARC 还有其他好处它可以执行一些手工操作很难甚至无法完成的优化例如在编译器ARC 会把能够互相抵消的retain、release、autorelease 操作约简。如果发现在同一对象上执行了多次 “保留” 与 “释放” 操作那么 ARC 有时可以成对地移除这两个操作。 ARC 也包含运行期组件。此时所执行的优化很有意义大家看到之后就会明白为何以后的代码都应该用 ARC 来写了。前面讲到某些方法在返回对象前为其执行了 autorelease 操作而调用方法的代码可能需要将返回的对象保留比如像下面这种情况就是如此:
_myPerson [EOCPerson personWithName:Bob smith];调用 “personWithName:” 方法会返回新的 EOCPerson 对象而此方法在返回对象之前为其调用了 autorelease 方法。由于实例变量是个强引用所以编译器在设置其值的时候还需要执行一次保留操作。因此前面那段代码与下面这段手工管理引用计数的代码等效:
EOCPerson *tmp [EOCPerson personWithName:Bob Smith];
_myPerson [tmp retain];此时应该能看出来 “personWithName:” 方法里面的 autorelease 与上段代码中的 retain 都是多余的。为提升性能可将二者删去。但是在 ARC 环境下编译代码时必须考虑 “向后兼容性”backward compatibility以兼容那些不使用 ARC 的代码。 在 ARC 环境下编译器会尽可能地优化代码以提高性能和效率。在处理方法中返回自动释放的对象时编译器可以通过一些特殊的函数来优化代码从而避免不必要的 autorelease 和 retain 操作提升代码的执行效率。 具体来说在方法中返回自动释放的对象时编译器会替换对 autorelease 方法的调用改为调用 objc_autoreleaseReturnValue 函数。这个函数会检查方法返回后即将执行的代码如果发现需要在返回的对象上执行 retain 操作那么就会设置一个标志位而不会立即执行 autorelease 操作。类似地如果调用方法的代码需要保留返回的自动释放对象那么编译器会将 retain 操作替换为 objc_retainAutoreleasedReturnValue 函数该函数会检查之前设置的标志位如果已经设置则不会执行 retain 操作。 objc_autoreleaseReturnValue 函数检测方法调用者是否会立刻保留对象要根据处理器来定。
变量的内存管理语义
ARC 也会处理局部变量与实例变量的内存管理。默认情况下每个变量都是指向对象的强引用。 在编写设置方法setter时使用 ARC 会简单一些。如果不用 ARC 那么需要像下面这样来写:
- (void)setObject:(id)object {[_object release];_object [object retain];
}但是在这段代码中如果新值和实例变量已有的值相同那么在执行设置方法时会出现问题。具体来说当新值和旧值相同时首先会调用 [_object release] 来释放旧值此时如果旧值只有当前对象在引用那么旧值的引用计数会减少为0。接着会调用 [object retain] 来保留新值但是此时旧值的内存已经被释放掉了再次对其执行保留操作就会导致访问已释放的内存从而引发应用程序崩溃。 使用 ARC 之后就不可能发生这种疏失了。在 ARC 环境下与刚才等效的设置函数可以这么写
- (void)setObject:(id)object {_object object;
}ARC 会用一种安全的方式来设置先保留新值再释放旧值最后设置实例变量。用了 ARC 之后根本无须考虑这种 “边界情况”。
在应用程序中可用下列修饰符来改变局部变量与实例变量的语义 __strong: 默认语义保留此值。 __unsafe_unretained: 不保留此值这么做可能不安全因为等到再次使用变量时其对象可能已经回收了。 __weak: 不保留此值但是变量可以安全使用因为如果系统把这个对象回收了那么变量也会自动清空。 __autoreleasing: 把对象 “按引用传递” pass by reference给方法时使用这个特殊的修饰符。此值在方法返回时自动释放。 比方说想令实例变量的语义与不使用 ARC 时相同可以运用 __weak 或 __unsafe_unretained 修饰符:
interface EOCClass : NSObject {__weak id _weakObject;__unsafe_unretained id _unsafeUnretainedObject;
}
end我们经常会给局部变量加上修饰符用以打破由“块”所引入的“保留环”。块会自动保留其所捕获的全部对象而如果这其中有某个对象又保留了块本身那么就可能导致 “保留环”。可以用 __weak 局部变量来打破这种 “保留环”:
NSURL *url [NSURL URLWithString:http://www.example.com/];
EOCNetworkFetcher *fetcher [[EOCNetworkFetcher alloc] initWithURL:url];
EOCNetworkFetcher *__weak weakFetcher fetcher;[fetcher startWithCompletion:^(BOOL success) {NSLog(Finished fetching from %, weakFetcher.url);
}];在这段代码中我们使用了 __weak 修饰符来声明一个局部变量 weakFetcher它指向 fetcher 对象。通过使用 __weak 修饰符我们避免了块对 fetcher 对象的强引用从而打破了潜在的保留环。
ARC 如何清理实例变量
ARC 也负责对实例变量进行内存管理。要管理其内存ARC 就必须在 “回收分配给对象的内存”deallocate(也称为 “释放/回收/解除分配内存”) 时生成必要的清理代码。凡是具备强引用的变量都必须释放ARC 会在 dealloc 方法中插入这些代码。当手动管理引用计数时可以这样自己来编写 dealloc 方法
- (void)dealloc {[_foo release];[_bar release];[super dealloc];
}这段代码做了以下几件事情
释放 _foo 实例变量调用 release 方法来减少对 _foo 对象的引用计数如果引用计数为0则会释放 _foo 对象所占用的内存。释放 _bar 实例变量同样地调用 release 方法来减少对 _bar 对象的引用计数如果引用计数为0则会释放 _bar 对象所占用的内存。调用 super 的 dealloc 方法调用父类的 dealloc 方法来执行一些必要的清理工作确保对象的内存被正确释放。 使用 ARC 之后不需要再编写这种 dealloc 方法。不过如果有非 Objective-C 的对象比如 CoreFoundation 中的对象或是由 malloc() 分配在堆中的内存那么仍然需要清理。然而不需要像原来那样调用超类的 dealloc 方法。前文说过在 ARC 下不能直接调用 dealloc。ARC 会自动在 .cxx_destruct 方法中生成代码并运行此方法而在生成的代码中会自动调用超类的 dealloc 方法。ARC 环境下dealloc 方法可以像这样写:
- (void)dealloc {CFRelease(_coreFoundationObject);free(_heapAllocatedMemoryBlob);
}可以使用CFRelease() 函数来释放CoreFoundation 对象。 可以使用 free() 函数来释放通过 malloc() 函数分配在堆上的内存块_heapAllocatedMemoryBlob
覆写内存管理方法
不使用 ARC 时可以覆写内存管理方法。比方说在实现单例类的时候因为单例不可释放所以我们经常覆写 release 方法将其替换为 “空操作”(no-op)。但在 ARC 环境下不能这么做因为会干扰到 ARC 分析对象生命期的工作。而且由于开发者不可调用及覆写这些方法所以 ARC 能够优化 retain、release、autorelease 操作使之不经过 Objective-C 的消息派发机制。优化后的操作直接调用隐藏在运行期程序中的 C 函数。
有 ARC 之后程序员就无须担心内存管理问题了。使用 ARC 来编程可省去类中的许多 “样板代码”。ARC 管理对象生命期的办法基本上就是在合适的地方插入 “保留” 及 “释放”操作。 在 ARC 环境下变量的内存管理语义可以通过修饰符指明而原来需要手工执行 “保留” 及 “释放”操作。由方法所返回的对象其内存管理语义总是通过方法名来体现。ARC 将此确定为开发者必须遵守的规则。ARC 只负责管理 Objective-C 对象的内存。尤其要注意 CoreFoundation 对象不归 ARC 管理开发者必须适时调用 CFRetain/CFRelease。
在dealloc方法中只释放引用并解除监听
对象在经历其生命期后最终会为系统所回收这时就要执行 dealloc 方法了。在每个对象的生命期内此方法仅执行一次也就是当保留计数降为 0 的时候。然而具体何时执行则无法保证。也可以理解成: 我们能够通过人工观察保留操作与释放操作的位置。来预估此方法何时即将执行。但实际上程序库会以开发者察觉不到的方式操作对象从而使回收对象的真正时机和预期的不同。你决不应该自己调用 dealloc 方法运行期系统会在适当的时候调用它。而且一旦调用过 dealloc 之后对象就不再有效了后续方法调用均是无效的。 在 dealloc 方法中主要就是释放对象所拥有的引用。对象所拥有的其他非 Objective-C 对象也要释放。比如 CoreFoundation 对象就必须手工释放因为它们是由纯C 的API 所生成的。 在 dealloc 方法中通常还要做一件事那就是把原来配置过低观测行为都清理掉比如消息通知的回收。 delloc应该这样写
- (void)dealloc {CFRelease(coreFoundationObject);[[NSNotificationCenter defaultCenter] removeObserver:self];
}如果手动管理引用计数而不使用 ARC 的话那么最后还需调用 “[super dealloc]”。ARC 会自动执行此操作。 开销较大或系统内部稀缺的资源不应该于 dealloc 中释放引用。像是文件描述符、套接字、大块内存等都属于这种资源。不能指望 dealloc 方法必定会在某个特定的时机调用因为有一些无法预料的东西可能也持有此对象。 比方说如果某对象管理着连接服务器所用的套接字那么也许就需要这种 “清理方法”。此对象可能要通过套接字连接到数据库。对于对象所属的类其接口可以这样写:
#import Foundation/Foundation.hinterface EOCServerConnection : NSObject- (void)open:(NSString *)address;- (void)close;end这段代码提供了两个方法
open: 方法用于打开连接到服务器的套接字。它需要一个字符串类型的参数 address表示服务器的地址。close 方法用于关闭当前打开的连接。 这个类的设计允许用户通过 open: 方法打开连接然后使用完成后通过 close 方法关闭连接 在清理方法而非 dealloc 方法中清理资源还有个原因就是系统并不保证每个创建出来的对象的 dealloc 都会执行。极个别情况下当应用程序终止时仍有对象处于存活状态这些对象没有收到 dealloc 消息。由于应用程序终止之后其占用的资源也会返还给操作系统所以实际上这些对象也就等于是消亡了。不调用 dealloc 方法是为了优化程序效率。而这也说明系统未必会在每个对象上调用其 dealloc 方法。 如果对象管理着某些资源那么在 dealloc 中也要调用 “清理方法”以防止开发者忘了清理这些资源。 在系统回收对象之前必须调用 close 以释放其资源否则 close 方法就失去了意义了因此没有适时调用 close 方法就是编程错误我们应该在 dealloc 中补上这次调用以防泄漏内存。下面举例说明 close 与 dealloc 方法如何来写:
- (void)close {/*clean up resources*/_closed YES;
}- (void)dealloc {if (!_closed) {NSLog(ERROR: close was not called before dealloc!);[self close];}
}编写 dealloc 方法时还需要注意不要在里面随便调用其他方法。 调用dealloc 方法的那个线程会执行“最终的释放操作”令对象的保留计数降为 0而某些方法必须在特定的线程里比如主线程里调用才行。若在 dealloc 里调用了那些方法则无法保证当前这个线程就是那些方法所需的线程。通过编写常规代码的方式无论如何都没办法保证其会安全运行在正确的线程上因为对象处于 “正在回收的状态”为了指明此状况运行期系统已经改动了对象内部的数据结构。 在 dealloc 里也不要调用属性的存取方法因为有人可能会覆写这些方法并与其中做一些无法在回收阶段安全执行的操作。此外属性可能正处于 “键值观测” (KVO) 机制的监控之下该属性的观察者observer 可能会在属性值改变时 “保留” 或使用这个即将回收的对象。这种做法会令运行期系统的状态完全失调从而导致一些莫名其妙的错误。
在 dealloc 方法里应该做的事情就是释放指向其他对象的引用并取消原来订阅的“键值观测”KVO或 NSNOtificationCenter 等通知不要做其他事情。如果对象持有文件描述符等系统资源那么应该专门编写一个方法来释放此种资源。这样的类要和其使用者约定: 用完资源后必须调用 close 方法。执行异步任务的方法不应该在 dealloc 里调用; 只能在正常状态下执行的那些方法也不应在 dealloc 里调用因为此时对象已处于正在回收的状态了。
编写“异常安全代码” 时留意内存管理问题
许多时下流行的编程语言都提供了 “异常”这一特性。在当前的运行期系统中C 与 Objective-C 的异常相互兼容也就是说从其中一门语言里抛出的异常能用另外一门语言所编写的 “异常处理程序”来捕获。 Objective-C 的错误模型表明异常只应在发生严重错误后抛出不过有时仍然需要编写代码来捕获并处理异常。比如使用 Objective-C 来编码时或是编码中用到了第三方程序库而此程序库所抛出的异常又不受你控制时就需要捕获及处理异常了。此外有些系统库也会用到异常比如在使用 “键值观测”KVO功能时若想注销一个尚未注册的“观察者”便会抛出异常。 在 try 块中如果先保留了某个对象然后在释放它之前又抛出了异常那么除非 catch 块能处理此问题否则对象所占内存就将泄漏。 异常处理例程将自动销毁对象然而在手动管理引用计数时销毁工作有些麻烦。以下面这段使用手工引用计数的 Objective-C 代码为例
try {EOCSomeClass *object [[EOCSomeClass alloc] init];[object doSomethingThatMayThrow];[object release];
}
catch (...) {NSLog(Whoops, there was an error. Oh well...);
}这段代码使用了 Objective-C 中的异常处理机制尝试执行一些可能会抛出异常的代码并在发生异常时捕获并处理它。具体来说 try { … } catch (…) { … } 是 Objective-C 中的异常处理语法。try 块用于包含可能会抛出异常的代码而 catch 块则用于捕获异常并进行处理。 在 try 块中首先创建了一个 EOCSomeClass 类的对象 object然后调用了 doSomethingThatMayThrow 方法。这个方法可能会抛出异常。 在 try 块的最后调用了 [object release] 方法来释放 object 对象。这表明代码的编写者使用了手动内存管理。 如果在 try 块中的代码抛出了异常那么异常处理流程会跳转到对应的 catch 块中。在这个例子中catch 块中的代码会执行它打印了一条错误日志。由于 catch 块的参数是 …表示捕获所有类型的异常因此无论什么类型的异常都会被捕获并处理。 但如果 doSomethingThatMayThrow 抛出异常了呢由于异常会令执行过程终止并跳至catch 块因而其后的那行 release 代码不会运行。在这种情况下如果代码抛出异常那么对象就泄漏了。这么做不好。解决方法是使用 finally 块无论是否抛出异常其中代码都保证会运行且只运行一次。比方说刚才那段代码可改写如下:
EOCSomeClass *object;try {object [[EOCSomeClass alloc] init];[object doSomethingThatMayThrow];
}
catch (...) {NSLog(Whoops, there was an error. Oh well...);
}
finally {[object release];
}在 ARC 环境下问题会更严重。下面这段使用 ARC 的代码与修改前的那段代码等效:
try {EOCSomeClass *object [[EOCSomeClass alloc] init];[object doSomethingThatMayThrow];
}
catch (...) {NSLog(Whoope, there was an error. Oh well...);
}在 ARC 下由于不能手动调用 release 方法来释放对象因此无法像在手动管理内存时那样将释放操作放在 finally 块中。而且ARC 不会自动处理异常导致的内存释放因为这需要添加大量的额外代码来跟踪待清理的对象并在抛出异常时释放它们这可能会影响程序的性能并增加应用程序的大小。
虽然在 Objective-C 代码中抛出异常通常是在应用程序必须因异常状况而终止时才发生的但默认情况下 ARC 并不会为异常处理添加额外的代码。这是因为在应用程序即将终止时是否会发生内存泄漏已经无关紧要了。因此默认情况下ARC 不会为异常处理添加额外的代码。如果需要在 ARC 环境下处理异常并进行自动内存管理可以通过开启 -fobjc-arc-exceptions 编译器标志来实现。 但最重要的是在发现大量异常捕获操作时应考虑重构代码。
捕获异常时一定要注意将 try 块所创立的对象清理干净。在默认情况下ARC 不生成安全处理异常所需的清理代码。开启编译器标志后可以生成这种代码不过会导致应用程序变大而且会降低运行效率。
以弱引用避免保留环
对象图里经常会出现一种情况就是几个对象都以某种方式互相引用从而形成“环”。这种情况通常会泄漏内存因为最后没有别的东西会引用环中的对象。这样的话环里的对象就无法为外界所访问了但对象之间尚有引用这些引用使得它们都能继续存活下去而不会为系统所回收。最简单的保留环由两个对象构成它们互相引用对方。 例如这里有两个类
#import Foundation/Foundation.hclass EOCClassA;
class EOCClassB;interface EOCClassA : NSObjectproperty (nonatomic, strong) EOCClassB *other;endinterface EOCClassB : NSObjectproperty (nonatomic, strong) EOCClassA *other;end保留环会导致内存泄漏。如果只剩一个引用还指向保留环中的实例而现在又把这个引用移除那么整个保留环就泄漏了。 避免保留环的最佳方式就是弱引用。这种引用经常用来表示 “非拥有关系”。将属性声明为 unsafe_unretained 即可。修改刚才那段范例代码
#import Foundation/Foundation.hclass EOCClassA;
class EOCClassB;interface EOCClassA : NSObjectproperty (nonatomic, strong) EOCClassB *other;endinterface EOCClassB : NSObjectproperty (nonatomic, unsafe_unretained) EOCClassA *other;end修改之后EOCClassB 实例就不再通过 other 属性来拥有 EOCClassA 实例了。属性特质 (attribute) 中的 unsafe_unretained 一词表明属性值可能不安全而且不归此实例所拥有。如果系统已经把属性所指的那个对象回收了那么在其上调用方法可能会使应用程序崩溃。由于本对象并不保留属性对象因此其有可能为系统所回收。 还可以使用weak 属性特质刚刚的代码还可以修改为
property (nonatomic,weak) EOCClassA *other;unsafe_unretained 与 weak 属性在其所指的对象回收以后表现出来的行为不同。当指向 EOCClassA 实例的引用移除后unsafe_unretained 属性仍然指向那个已经回收的实例而 weak 属性则指向 nil。 一般来说如果不拥有某对象那就不要保留它当然 collection 例外。有时对象中的引用会指向另外一个并不归自己拥有的对象比如 Delegate 模式就是这样。
将某些引用设为 weak可避免出现 “保留环”。weak 引用可以自动清空也可以不自动清空。自动清空autonilling是随着 ARC 而引入的新特性由运行期系统来实现。在具备自动清空功能的弱引用上可以随意读取其数据因为这种引用不会指向已经回收过的对象。
以“自动释放池块”降低内存峰值
由前面的内容知道自动释放池用于存放那些需要稍后某个时刻释放的对象。 创建自动释放池所用语法如下:
autorelease {/...
}通常只有一个地方需要创建自动释放池那就是在 main 函数里。比如说iOS程序的main函数一般这样写
int main(int argc, char *argv[]) {autoreleasepool {return UIApplicationMain(argc, argv, nil, EOCAppDelegate);}
}从技术角度看不是非得有个“自动释放池块”才行。因为块的末尾恰好就是应用程序的终止处而此时操作系统会把程序所占的全部内存都释放掉。这个池可以理解成最外围捕捉全部自动释放对象所用的池。 下面这段代码中的花括号定义了自动释放池的范围。自动释放池于左花括号处创建并于对应的右花括号处自动清空。位于自动释放池范围内的对象将在此范围末尾处收到 release 消息。自动释放池可以嵌套。系统在自动释放对象时会把它放到最内层的池里。比方说:
autoreleasepool {NSString *string [NSString stringWithFormat:1 %i, 1];autoreleasepool {NSNumber *number [NSNumber numberWithInt:1];}
}这段代码创建了两个自动释放池。第一个自动释放池从第 1 行开始到第 10 行结束。在此范围内字符串 string 被创建并且在自动释放池的末尾被释放。 在第 5 行到第 8 行之间的范围内又创建了一个新的自动释放池。在这个内层自动释放池中数字 number 被创建然后在内层自动释放池的末尾被释放。 注意内层自动释放池的范围是嵌套在外层自动释放池的范围内的。因此number 对象的释放操作会在外层自动释放池的范围结束时执行而 string 对象的释放操作则会在整个自动释放池的范围结束时执行。
有如下代码
for (int i 0; i 100000; i) {[self doSomethingWithInt:i];
}如果 “doSomethingWithInt:”方法要创建临时对象那么这些对象很可能会放在自动释放池里。即便这些对象在调用完方法之后就不再使用了它们也依然处于存活状态因为目前还在自动释放池里等待系统稍后将其释放并回收。然而自动释放池要等线程执行下一次事件循环时才会清空。即执行 for 循环时会持续有新的对象创建出来并加入自动释放池中。所有这种对象都要等 for 循环执行完才会释放。这样一来在执行 for 循环时应用程序所占内存量就会持续上涨而等到所有临时对象都释放后内存用量又会突然下降。 或者比如说要从数据库中读数据
NSArray *databaseRecords /*...*/;
NSMutableArray *people [NSMutableArray new];for (NSDictionary *record in databaseRecords) {EOCPerson *person [[EOCPerson alloc] initWithRecord:record];[people addObject:person];
}若记录有很多条则内存中也会有很多不必要的临时对象它们本来应该提早回收的。增加一个自动释放池即可解决此问题。如果把循环内的代码包裹在“自动释放池块”中那么在循环中自动释放的对象就会放在这个池而不是线程的主池里面。例如:
NSArray *databaseRecords /*...*/;
NSMutableArray *people [NSMutableArray new];for (NSDictionary *record in databaseRecords) {autoreleasepool {EOCPerson *person [[EOCPerson alloc] initWithRecord:record];[people addObject:person];}
}新增的自动释放池块可以减少内存峰值因为系统会在块的末尾把某些对象回收掉。 是否应该用池来优化效率完全取决于具体的应用程序。所以尽量不要建立额外的自动释放池。 有一种老式写法是使用 NSAutoreleasePool 对象。但是这种写法并不会在每次执行 for 循环时都清空池通常用来创建那种偶尔要清空的池采用随着 ARC 所引入的新语法可以创建出更为 “轻量级”的自动释放池。现在可以改用自动释放池块把 for 循环中的语句包起来这样的话每次执行循环时都会建立并清空自动释放池。
自动释放池排布在栈中对象收到 autorelease 消息后系统将其放入最顶端的池里。合理运用自动释放池可降低应用程序的内存峰值。autoreleasepool 这种新式写法能创建出更为轻便的自动释放池。
用“僵尸对象”调试内存管理问题
Cocoa 提供了 “僵尸对象”Zombie Object这个非常方便的功能。启用这项调试功能之后运行期系统会把所有已经回收的实例转化成特殊的“僵尸对象”而不会真正回收它们。这种对象所在的核心内存无法重用因此不可能遭到覆写。僵尸对象收到消息后会抛出异常其中准确说明了发送过来的消息并描述了回收之前的那个对象。僵尸对象是调试内存管理问题的最佳方式。给僵尸对象发送消息后控制台会打印消息而应用程序则会终止。 也可以在Xcode 里打开此选项这样的话Xcode 在运行应用程序时会自动设置环境变量。开启方法编辑应用程序的 Scheme在对话框左侧选择 “Run”然后切换至 “Diagnostics” 分页最后勾选 “Enable Zombie Objects” 选项。 僵尸对象的工作原理涉及Objective-C的运行时程序库、Foundation框架和CoreFoundation框架的实现代码。当系统即将回收对象时如果启用了僵尸对象功能会执行一个额外的步骤即将对象转化为僵尸对象而不是立即回收。 僵尸类是从名为 NSZombie 的模板类里复制出来的。这些僵尸类没有多少事情可做只是充当一个标记。 僵尸类的作用会在消息转发例程中体现出来。 NSZombie 类以及所有从该类拷贝出来的类并未实现任何方法。此类没有超类因此和 NSObject 一样也是个“根类”该类只有一个实例变量叫做 isa 所有 Objective-C 的根类都必须有此变量。由于这个轻量级的类没有实现任何方法所以发给它的全部消息都要经过 “完整的消息转发机制”。
系统在回收对象时可以不将其真的回收而是把它转化为僵尸对象。通过环境变量 NSZombieEnable 可开启此功能。系统会修改对象的 isa 指针令其指向特殊的僵尸类从而使该对象变为僵尸对象。僵尸类能够相应所有的选择子响应方式为打印一条包含消息内容及其接收者的消息然后终止应用程序。
不要使用retainCount
每个对象的引用计数都有一个计数器其值表明还有多少个其他对象想令此对象继续存活。 NSObject 协议中定义了下列方法用于查询对象当前的保留计数
(NSUInteger)retainCount; 然而 ARC 已经将此方法废弃了。如果在 ARC 中调用编译器就会报错。但问题在于保留计数的绝对值一般都与开发者所应留意的事情完全无关。即便只在调试时才能调用此方法通常也还是无所助益的。 因为它返回的保留计数只是某个给定时间点上的值。该方法并未考虑到系统会稍后把自动释放池清空因而不会将后续的释放操作从返回值里减去这样的话此值就未必能真实反映实际的保留计数了。
开发者在期望系统于某处回收对象时应该确保没有尚未抵消的保留操作也就是不要令保留计数大于期望值。在这种情况下如果发现某对象的内存泄漏了那么应该检查还有谁仍然保留这个对象并查明其为何没有释放此对象。
即便只为调试此方法也不是很有用。由于对象可能处在自动释放池中所以其保留计数未必如想象般精确。那到底何时才应该用 retainCount 呢最佳答案是:绝对不要用尤其考虑到苹果公司在引入 ARC 之后已正式将其废弃就更不应该用了。
对象的保留计数看似有用实则不然因为任何给定时间点上的“绝对保留计数”absolute retain count都无法反映对象生命期的全貌。引入 ARC 之后retainCount 方法就正式废止了在 ARC 下调用该方法会导致编译器报错。
理解“块”这一概念
块可以实现闭包它与函数类似只不过是直接定义在另一个函数里的和定义它的那个函数共享同一个范围内的东西。块用 “^” 符号来表示后面跟着一对花括号括号里面是块的实现代码。 块的强大之处是在声明它的范围里。所有变量都可以为其所捕获。这也就是说那个范围里的全部变量在块里依然可用。比如下面这段代码所定义的块就使用了块以外的变量
int additional 5;
int (^addBlock)(int a, int b) ^(int a, int b){return a b addItional;
};
int add addBlock(2, 5);默认情况下为块所捕获的变量是不可以在块里修改的。声明变量的时候可以加上 __block 修饰符这样就可以在块内修改了。例:
NSArray *array [0, 1, 2, 3, 4, 5];
__block NSInteger count 0;
[array enumerateObjectsUsingBlock:^(NSNumber *number, NSUInteger idx, BooL *stop) {if([number compare:2] NSOrderedAscending) {count;}
}];这段范例代码也演示了 “内联块”的用法。传给 “numerateObjectsUsingBlock:” 方法的块并未先赋给局部变量而是直接内联在函数调用里了。这样可以把所有业务逻辑都放在一处。 如果块所捕获的变量是对象类型那么就会自动保留它。系统在释放这个块的时候也会将其一并释放。块本身可视为对象。块本身也和其他对象一样有引用计数。当最后一个指向块的引用移走之后块就回收了。回收时也会释放块所捕获的变量以便平衡捕获时所执行的保留操作。 如果将块定义在 OC 类的实例方法中那么除了可以访问类的所有实例变量之外还可以使用 self 变量。块总能修改实例变量所以在声明时无须加 _ _block 。不过如果通过读取或写入操作捕获了实例变量那么也会自动把 self 变量一并捕获了因为实例变量是与 self 所指代的实例关联在一起的。就是说在OC类的实例方法中使用块时可以轻松地访问和操作当前实例的变量和方法而不必过多关注__block的使用。这使得在块内部处理实例变量变得更加方便。 例
interface EOCClass
- (void)anInstanceMethod {void (^someBlock)() ^{_anInstanceVariable Something;NSlog(_anInstanceVariable %, _anInstanceVariable);};
}
end如果某个 EOCClass 实例正在执行 anInstanceMethod 方法那么 self 变量就指向此实例。由于块里没有明确使用 self 变量所以很容易就会忘记 self 变量其实也为块所捕获了。直接访问实例变量和通过 self 来访问是等效的。 self 也是个对象因而块在捕获它时也会将其保留。如果 self 所指代的那个对象同时保留了块那么这种情况通常就会导致 “保留环”。
块的内部结构
每个 OC 对象都占据着某个内存区域。每个对象所占的内存区域也有大有小。块本身也是对象在存放块对象的内存区域中首个变量是指向 Class 对象的指针isa。其余内存里含有块对象正常运转所需要的各种信息。 在内存布局中最重要的就是 invoke 变量这是个函数指针指向块的实现代码。descriptor 变量是指向结构体的指针每个块里都包含此结构体其中声明了块对象的总体大小还声明了 copy 与 dispose 这两个辅助函数所对应的函数指针。 块还会把它所捕获的所有变量都拷贝一份。这些拷贝放在 descriptor 变量后面捕获了多少个变量就要占据多少内存空间。
全局块、栈块及堆块
定义块的时候其所占的内存区域是分配在栈中的。这就是说块只在定义它的那个范围内有效。这里有段代码
void (^block)();
if () {block ^{NSLog(Block A);};
} else {block ^{NSLog(Block B);};
}
block();这个代码问题在于块的生命周期与其定义的范围有关。在这里两个块都分配在栈内存中而栈内存的生命周期通常与包含它的作用域相关。一旦超出 if 或 else 的作用域栈上的块可能会被释放而且内存区域可能被覆写。 这样的代码在编译时通常能够通过但在运行时可能会出现问题因为 block() 调用时块的定义范围可能已经结束栈上的内存可能已经被其他变量或数据覆写。 为解决此问题可以通过调用copy方法。这样的话就可以把块从栈复制到堆了。拷贝后的块可以在定义它的那个范围之外使用。而且一旦复制到堆上块就成了带引用计数的对象了。后续的复制操作都不会真的执行复制只是递增块对象的引用计数。而“分配在栈上的块”则无须明确释放因为栈内存本来就会自动回收。 应该改正为
void (^block)();
if () {block [^{NSLog(Block A);} copy];
} else {block [^{NSLog(Block B);} copy];
}
block();还有一类块叫做 “全局块”。这种块不会捕捉任何状态运行时也无须有状态来参与。块所使用的整个内存区域在编译期已经完全确定了因此全局块可以声明在全局内存里而不需要在每次用到的时候于栈中创建。 全局块的拷贝操作是个空操作因为全局块决不可能为系统所回收。这种块实际上相当于单例。下面是个全局块
void (^block)() ^{NSLog(This is a block);
}这完全是种优化技术
块是C、C、Objective-C 中的词法闭包。块可接受参数也可返回值。块可以分配在栈或堆上也可以是全局的。分配在栈上的块可拷贝到堆里这样的话就和标准的 Objective-C 对象一样具备引用计数了。
为常用的块类型创建typedef
每个块都具备其“固有类型”因而可将其赋给适当类型的变量。这个类型由块所接受的参数及其返回值组成。 与其他类型的变量不同在定义块变量时要把变量名放在类型之中而不要放在右侧。这种语法非常难记也非常难读。鉴于此我们应该为常用的块类型起个别名。 为了隐藏复杂的块类型需要用到C 语言中名为 “类型定义”的特性。typedef 关键字用于给类型起个易读的别名。 举个例子不使用typedef的话是这样声明一个块
void (^sumBlock)(int, int) ^(int a, int b) {int sum a b;NSLog(Sum: %d, sum);
};使用typedef
// 使用 typedef 创建块类型别名
typedef void (^SumBlock)(int, int);
// 使用块类型别名声明块变量
SumBlock sumBlock ^(int a, int b) {int sum a b;NSLog(Sum: %d, sum);
};这次代码读起来就顺畅多了与定义其他变量时一样变量类型在左边变量名在右边。可见使用 typedef 可以将复杂的块类型声明简化为易读的别名使代码更加清晰、易懂。通过项特性可以把使用块的 API 做得更为易用些。
定义方法参数所用的块类型语法又和定义变量时不同。若能把方法签名中的参数类型写成一个词那读起来就顺口多了。于是可以给参数类型起个别名然后使用词名称来定义
//创建一个名为EOCCompletionHandler的块类型别名
typedef void (^EOCCompletionHandler) (NSData *data, NSError *error);
//在方法签名中使用了先前创建的块类型别名EOCCompletionHandler作为参数类型
//startWithCompletionHandler方法接受一个块作为参数而这个块的类型就是 EOCCompletionHandler
- (void)startWithCompletionHandler:(EOCCompletionHandler)completion;现在看上去就简单多了而且易于理解。 使用类型定义还有个好处就是重构块的类型签名时会很方便。 比如给块再加一个参数那么只需要修改类型定义语句即可
typedef void (^EOCCompletionHandler) (NSData *data, NSTimeInterval duration, NSError *error);修改之后凡是使用了这个类型定义的地方都会无法编译而且报的是同一种错误于是开发者可据此逐个修复。
最好在使用块类型的类中定义这些 typedef而且还应该把这个类的名字加在由 typedef 所定义的新类型名前面这样可以阐明块的用途。还可以用 typedef 给同一个块签名类型创建数个别名。 比如Accounts 框架
typedef void (^ACAccountStoreSaveCompletionHandler)(BOOL success, NSError *error);如果有好几个类都要执行相似但各有区别的异步任务而这几个类又不能放入同一个继承体系那么每个类就应该有自己的 completion handler 类型。这几个 completion handler 的签名也许完全相同但最好还是在每个类里各自定义一个别名而不要同一个名称。反之若这些类能纳入同一个继承中则应该将类型定义语句放在超类中以供各子类使用。 以 typedef 重新定义块类型可令块变量用起来更加简单。定义新类型时应遵从现有的命名习惯勿使其名称与别的类型相冲突。不妨为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名那么只需要修改相应 typedef 中的块签名即可无须改动其他 typedef。
用handler块降低代码分散程度
为用户界面编码时一种常用的范式就是 “异步执行任务”。这种范式的好处在于处理用户界面的显示及触摸操作所用的线程不会因为要执行 I/O 或网络通信这类耗时的任务而阻塞。这个线程通常称为主线程。假设把执行异步任务的方法做成同步的那么在执行任务时用户界面就变得无法响应用户输入了。某些情况下如果应用程序在一定时间内无响应那么就会自动终止。iOS 系统上的应用程序就是如此“系统监控器”在发现某个应用程序的主线程已经阻塞了一段时间之后就会令其终止。 I/O 操作输入/输出操作 输入操作Input 从外部设备或文件中读取数据到计算机系统中。例如从键盘读取用户输入、从磁盘读取文件等。 输出操作Output 将计算机系统中的数据发送到外部设备或存储到文件中。例如将数据写入显示器显示、将结果写入磁盘文件等。 I/O 操作可能涉及到慢速的设备如硬盘、网络因此在进行这些操作时系统可能需要等待一段时间。 网络通信 指在不同计算机或设备之间传递数据的过程。 通过网络连接计算机系统可以通过一系列协议进行数据的发送和接收包括传输层协议如TCP或UDP和应用层协议如HTTP、FTP等。 网络通信包括从客户端到服务器的请求和响应文件传输远程过程调用RPC等。 在移动应用或网络应用中常见的 I/O 操作和网络通信包括 读写本地文件 通过文件系统进行读写例如保存应用程序数据、读取配置文件等。 数据库操作 与本地或远程数据库进行数据交互例如存储和检索用户信息。 HTTP 请求和响应 通过网络协议进行数据传输例如从服务器获取数据、上传文件等。 Socket 编程 直接在网络上建立连接进行实时通信例如聊天应用、在线游戏等。 异步方法在执行完任务之后需要以某种手段通知相关代码。实现此功能有很多方法。常用的技巧是设计一个委托协议令关注此事件的对象遵从该协议。对象成为 delegate 之后就可以在相关事件发生时例如某个异步任务执行完毕时得到通知了。委托模式有个缺点如果类要分别使用多个获取器下载不同数据那么就得在 delegate 回调方法里根据传入的获取器参数来切换。这么写代码不仅会令 delegate 回调方法变得很长而且还要把网络数据获取器对象保存为实例变量以便在判断语句中使用。 然而如果改用块来写的话代码会更清晰。块可以令这种 API 变得更紧致同时令开发者调用起来更加方便。改用块来写的好处是无须保存获取器也无须在回调方法里切换。每个 completion handler 的业务逻辑都是和相关的获取器对象一起来定义的。 比如像这样
#import Foundation/Foundation.h
// 定义块类型
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);
interface EOCNetworkFetcher : NSObject
- (id)initWithURL:(NSURL *)url;
// 使用块作为参数的方法
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)handler;
endimplementation EOCNetworkFetcher
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)handler {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{NSData *data [NSData dataWithContentsOfURL:[NSURL URLWithString:http://www.example.com]];dispatch_async(dispatch_get_main_queue(), ^{if (handler) {handler(data);}});});
}
end用块写出来的代码显然更为整洁。而且由于块声明在创建获取器的范围里所以它可以访问此范围内的全部变量。
这种写法还有其他用途比如现在很多基于块的 API 都使用块来处理错误。这又分为两种办法。可以分别用两个处理程序来处理操作失败的情况和操作成功的情况。也可以把处理失败情况所需的代码与处理正常情况所用的代码都封装到同一个 completion handler 块里。如果想采用两个独立的处理程序那么可以这样设计 API
#import Foundation/Foundation.h
// 声明块类型
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);
typedef void(^EOCNetworkFetcherErrorHandler)(NSError *error);interface EOCNetworkFetcher : NSObject
// 初始化方法声明
- (instancetype)initWithURL:(NSURL *)url;
// 方法声明接受两个块作为参数
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion failureHandler:(EOCNetworkFetcherErrorHandler)failure;
end调用方式如下:
EOCNetworkFetcher *fetcher [[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHander:^(NSData *data) {// Handle success
} failureHandler:^(NSError *error) {// Handle failure
}];另一种风格则像下面这样把处理成功情况和失败情况所用的代码全放在一个块里:
#import Foundation/Foundation.h
// 声明块类型接受两个参数NSData 和 NSError
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data, NSError *error);interface EOCNetworkFetcher : NSObject
// 初始化方法声明
- (instancetype)initWithURL:(NSURL *)url;
// 方法声明接受一个块作为参数
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion;
end调用方式如下
// 创建网络获取器实例
EOCNetworkFetcher *fetcher [[EOCNetworkFetcher alloc] initWithURL:url];[fetcher startWithCompletionHandler:^(NSData *data, NSError *error) {if (error) {// 处理失败情况NSLog(Error occurred: %, error);} else {// 处理成功情况NSLog(Data received: %, data);}
}];这种写法的缺点是由于全部逻辑都写在一起所以会令块变得比较长且比较复杂。然而也有好处那就是更为灵活。比方说在传入错误信息时可以把数据也传进来。而且还有一个优点是调用 API 的代码可能会在处理成功响应的过程中发现错误。
总体来说笔者建议使用同一个块来处理成功与失败情况苹果公司似乎也是这样设计其 API 的。
有时需要在相关时间点执行回调操作这种情况也可以使用 Handler 块。 基于 handler 来设计 API 还有个原因就是某些代码必须运行在特定的线程上。例如NSNotificationCenter 就属于这种 API它提供了一个方法调用者可以经由此方法来注册想要接收的通知等到相关事件发生时通知中心就会执行注册好的那个块。调用者可以指定某个块应该安排在哪个执行队列里。
- (id)addObserverForName:(NSString *)name object:(id)object queue:(NSOperationQueue *)queue usingBlock:(void(^)(NSNotification *))block;此处传入的 NSOperationQueue 参数就表示出触发通知时用来执行块代码的那个队列。这是个“操作队列”而非“底层 GCD 队列”不过两者语义相同。
在创建对象时可以使用内联的 handler 块将相关业务逻辑一并声明。在有多个实例需要监控时如果采用委托模式那么经常需要根据传入的对象来切换而若改用 handler 块来实现则可直接将块与相关对象放在一起。设计 API 时如果用到了 handler 块那么可以增加一个参数使调用者可通过此参数来决定应该把块安排在哪个队列上执行。
用块引用其所属对象时不要出现保留环
使用块时若不仔细思量则很容易导致“保留环”。比如说下面这个例子 // EOCNetworkFetcher.h#import Foundation/Foundation.htypedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);interface EOCNetworkFetcher : NSObjectproperty (nonatomic, strong, readonly) NSURL *url;- (instancetype)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion;-
end// EOCNetworkFetcher.m#import EOCNetworkFetcher.hinterface EOCNetworkFetcher ()property (nonatomic, strong, readwrite) NSURL *url;
property (nonatomic, copy) EOCNetworkFetcherCompletionHandler completionHandler;
property (nonatomic, strong) NSData *downloadedData;endimplementation EOCNetworkFetcher- (instancetype)initWithURL:(NSURL *)url {if ((self [super init])) {_url url;}return self;
}- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion {self.completionHandler completion;// Start the request// Request sets downloadedData property// When request is finished, p_requestCompleted is called
}- (void)p_requestCompleted {if (_completionHandler) {_completionHandler(_downloadedData);}
}end某个类可能会创建这个网络请求的实力并用其从url中下载数据
// EOCClass.mimplementation EOCClass {EOCNetworkFetcher *_networkFetcher;NSData *_fetchedData;
}- (void)downloadData {NSURL *url [[NSURL alloc] initWithString:http://www.example.com/something.dat];_networkFetcher [[EOCNetworkFetcher alloc] initWithURL:url];[_networkFetcher startWithCompletionHandler:^(NSData *data) {NSLog(Request URL % finished, _networkFetcher.url);_fetchedData data;}];
}end
在上面这段代码中当 EOCClass 类的实例调用 downloadData 方法时它会创建一个 EOCNetworkFetcher 的实例对象 _networkFetcher。 在 _networkFetcher 的 startWithCompletionHandler: 方法中传入了一个块作为参数该块会捕获 self即 EOCClass 实例因为它需要设置 EOCClass 实例中的 _fetchedData 实例变量。 这样块持有了 EOCClass 实例EOCClass 实例持有了 _networkFetcher 实例而 _networkFetcher 实例又持有了传入的块形成了保留环。
要打破保留环也很容易要么令 _networkFetcher 实例变量不再引用获取器要么令获取器的 completionHandler 属性不在持有 handler 块。在这个例子中应该等 completion handler 块执行完毕后再去打破保留环以便使获取器对象在 handler 块执行期间保持存活状态。比方说completion handler 块的代码可以这么修改
[_networkFetcher startWithCompletionHandler:^(NSData *data) {NSLog(Request for URL % finished, _networkFetcher.url);_fetchedData data;_networkFetcher nil;
];一般来说只要适时清理掉环中的某个引用即可解决此问题然而未必总有这种机会。若是 completion handler 一直不运行那么保留环就无法打破于是内存就会泄漏。 如果 completion handler 块所引用的对象最终又引用了这个块本身那么就会出现另一种形式的保留环。 举个例子
- (void)downloadData {NSURL *url [[NSURL alloc] initWithString:http://www.example.com/something.dat];EOCNetworkFetcher *networkFetcher [[EOCNetworkFetcher alloc] initWithURL:url];[networkFetcher startWithCompletionHandler:^(NSData *data) {NSLog(Request URL % finished, networkFetcher.url);_fetchedData data;}];
}在上面这个例子中downloadData 方法创建了一个 EOCNetworkFetcher 的实例 networkFetcher并设置了其 startWithCompletionHandler 方法的 completion handler 块。 在 completion handler 块中使用了 networkFetcher 实例的 url 属性。由于块内部引用了 networkFetcher 实例因此块会保留这个实例。 反过来networkFetcher 实例也持有了 completion handler 块因为 networkFetcher 的属性 completionHandler 是一个 copy 类型的块属性。 因此形成了一个保留环networkFetcher 持有 completion handler 块而 completion handler 块又持有 networkFetcher 实例。这会导致 networkFetcher 实例和其相关的对象无法被释放从而造成内存泄漏。 这个问题可以这样解决只需要将 p_requestCompleted 方法按如下方式修改即可
- (void)p_requestCompleted {if (_completionHandler) {_completionHandler(_downloadedData);}self.completionHandler nil;
}这样一来只要下载请求执行完毕保留环就解除了而获取器对象也将会在必要时为系统所回收。 注意要在 start 方法中把 completion handler 作为参数传进去。假如把 completion handler 暴露为获取器对象的公共属性那么就不便在执行完下载请求之后直接将其清理掉了因为既然已经把 handler 作为属性公布了那就意味着调用者可以自由使用它若是此时又在内部将其清理掉的话则会破坏“封装语义”。在这种情况下要想打破保留环只有一个办法可用那就是强迫调用者在 handler 代码里自己把 compleionHandler 属性清理干净。 如果块所捕获的对象直接或间接地保留了块本身那么就得当心保留环问题。一定要找个适当的时机解除保留环而不能把责任推给API 的调用者。
多用派发队列少用同步锁
派发队列
在 Objective-C 中派发队列是一种用来管理任务执行的队列系统。它基于GCD框架用于异步执行任务可以在串行或并发的队列上执行任务。派发队列可以是串行队列或并发队列。
串行队列串行队列中的任务一个接一个按顺序执行每个任务执行完毕后才会执行下一个任务。并发队列并发队列中的任务可以同时执行不需要等待前一个任务完成。
同步锁
同步锁是一种用于控制并发访问共享资源的机制。在多线程环境下当多个线程同时访问某个共享资源时可能会出现数据竞争的情况导致程序出现不确定的行为或错误。同步锁可以确保在某个线程修改共享资源时其他线程不会同时进行修改从而保证数据的一致性和正确性。 常见的同步锁机制包括
synchronized 块使用 synchronized 关键字创建临界区确保同一时间只有一个线程能够访问临界区中的代码。NSLock 类NSLock 是 Foundation 框架中提供的一个锁对象可以使用 lock 和 unlock 方法来实现对临界区的加锁和解锁操作。dispatch_semaphore 信号量通过信号量来实现对临界区的控制可以使用 dispatch_semaphore_wait 和 dispatch_semaphore_signal 函数来实现加锁和解锁操作。NSRecursiveLock 类NSRecursiveLock 是 NSLock 的子类允许同一个线程多次对锁进行加锁操作可以解决递归调用时可能出现的死锁问题。
在 GCD 出现之前如果有多个线程要执行同一份代码通常要使用锁来实现某种同步机制有两种办法第一种是采用内置的 “同步块”:
- (void)synchronizedMethod {synchronized(self) {//。。。}
}这种写法会根据给定的对象自动创建一个锁并等待块中的代码执行完毕。执行到这段代码结尾处锁就释放了。但是若是在 self 对象上频繁加锁那么程序可能要等另一段与此无关的代码执行完毕才能继续执行当前代码这样做其实并没有必要。 另一个办法是直接使用 NSLock 对象
_lock [[NSLock alloc] init];- (void)synchronizedMethod {[_lock lock];//...[_lock unlock];
}它可以创建一个 NSLock 对象 _lock然后在 synchronizedMethod 方法中使用 lock 方法来获取锁然后执行安全的代码。执行完安全代码后通过调用 unlock 方法释放锁。这种方式提供了更多的控制能力可以更灵活地管理锁的获取和释放 也可以使用 NSRecursiveLock 这种 “递归锁”重入锁线程能够多次持有该锁而不会出现死锁现象。
为什么要多用派发队列少用同步锁
使用同步锁的这几种方法有其缺陷。比方说在极端情况下同步块会导致死锁另外其效率也不见得很高而如果直接使用锁对象的话一旦遇到死锁就会非常麻烦。 因此我们可以使用 GCD 它能以更简单、更高效的形式为代码加锁。 在之前的学习中我们学习过atomic 特质它是用于修饰属性的原子性并且该特性是与锁这个机制紧密相连的而GCD与锁的机制不同它是通过队列和调度机制来管理任务的执行而不需要显式地使用锁来保护共享数据因此使用GCD不需要依赖属性的原子性。 而再回顾一下atomic特质它可以指定属性的存取方法。而开发者如果想自己来编写访问方法的话那么通常会这样写 - (NSString *)someString {synchronized(self) {return _someString;}
}- (void)setSomeString:(NSString *)someString {synchronized(self) {_someString someString;}
}刚才说过滥用 synchronized(self) 会很危险因为所有同步块都会彼此抢夺同一个锁。这么做虽然能提供某种程度的 “线程安全”thread safety但却无法保证访问该对象时绝对是线程安全的。 所以要用一种简单而高效的办法代替同步块或锁对象那就是使用 “串行同步队列”。将读取操作及写入操作都安排在同一个队列里即可保证数据同步
_syncQueue dispatch_queue_create(com.effectiveobjectivec.syncQueue, NULL);-(NSString *)someString {__block NSString *localSomeString;dispatch_sync(_syncQueue, ^{localSomeString _someString; });return localSomeString;
}- (void)setSomeString:(NSString *)someString {dispatch_sync(_suncQueue, ^{_someString someString; });
}在上面这段代码中创建了一个串行同步队列 _syncQueue用于处理对 someString 属性的读取和写入操作。 在someString 方法中通过调用 dispatch_sync 将读取操作安排在 _syncQueue 中执行。在串行队列中使用 dispatch_sync 可以确保读取操作按顺序执行并且在读取操作完成之前阻塞当前线程。读取操作执行完毕后将结果赋值给 localSomeString 变量然后返回。 setSomeString: 方法中同样使用 dispatch_sync 将写入操作安排在 _syncQueue 中执行。写入操作也会按照顺序执行。把设置操作与获取操作都安排在序列化的队列里执行这样的话所有针对属性的访问操作就都同步了。
设置代码也可以这样写
- (void)setSomeString:(NSString *)someString {dispatch_async(_syncQueue, ^{_someString someString;});
}上面这段代码用异步派发替代了同步派发意味着设置方法 setSomeString: 中的实例变量 _someString 的操作会在一个后台线程上执行而不会阻塞当前线程。这可以提高设置方法的执行速度并使得调用者不必等待设置操作完成。 但这么改有个坏处这种写法可能比原来慢因为执行异步派发时需要拷贝块。若拷贝块所用的时间明显超过执行块所花的时间则这种写法将比原来更慢。
多个获取方法可以并发执行而获取方法与设置方法之间不能并发执行 利用这个特点还能写出更快一些的代码来这次不用串行队列而改用并发队列:
_syncQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);- (NSString *)someString {__block NSString *localSomeString;dispatch_sync(_syncQueue, ^{localSomeString _someString;});return localSomeString;
}- (void)setSomeString:(NSString *)someString {dispatch_async(_syncQueue, ^{_someString someString;});
}在上面这段代码中_syncQueue 是一个并发队列通过 dispatch_get_global_queue 函数创建它允许多个任务同时在不同的线程上执行。 someString 方法用 dispatch_sync 函数将读取操作添加到 _syncQueue 中并且等待这个操作完成后再返回结果。这确保了在多个线程同时调用 someString 方法时能够安全地读取 _someString 的值。 setSomeString: 方法使用 dispatch_async 函数将写入操作添加到 _syncQueue 中这样就可以确保多个设置方法之间不会并发执行保证了数据的一致性和安全性。
像现在这样写代码还无法正确实现同步。所有读取操作与写入操作都会在同一个队列上执行不过由于是并发队列所以读取与写入操作可以随时执行。而我们恰恰不想让这些操作随意执行。此问题用一个简单的 GCD 功能即可解决它就是栅栏。 在并发队列中栅栏块的作用是确保在其前面的任务执行完毕后才会执行栅栏块而在其后的任务则会等待栅栏块执行完毕后才能继续执行。 下列函数可以向队列中派发块将其作为栅栏使用 dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block);上面这段代码向并发队列中添加栅栏块保证了栅栏块之前的任务并发执行而栅栏块本身及其后的任务则是顺序执行的。这样可以确保写入操作在读取操作之后进行从而避免了并发读取与写入操作导致的数据同步问题。 使用栅栏具体的实现代码
_syncQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);- (NSString *)someString {__block NSString *localSomeString;dispatch_sync(_syncQueue, ^{localSomeString _someString;});return localSomeString;
}- (void)setSomeString:(NSString *)someString {//通过 dispatch_barrier_async 函数将一个栅栏块提交到 _syncQueue 中执行dispatch_barrier_async(_syncQueue, ^{_someString someString;});
}在这个并发队列中读取操作是用普通的块来实现的而写入操作则是用栅栏块来实现的。读取操作可以并行但写入操作必须单独执行因为它是栅栏块。 这种做法肯定比使用串行队列要快。注意设置函数也可以改用同步的栅栏块来实现。
派发队列可用来表述同步语义synchronization semantic这种做法要比使用 synchronized 块或 NSLock 对象更简单。将同步与异步派发结合起来可以实现与普通加锁机制一样的同步行为而这么做却不会阻塞执行异步派发的线程。使用同步队列及栅栏块可以令同步行为更加高效。
多用GCD少用performSelector系列方法
什么是performSelector
performSelector 是 Objective-C 中的一个方法用于在对象上调用指定的方法并且可以延迟执行或在指定的线程上执行。
- (nullable id)performSelector:(SEL)aSelector;它会在当前线程中调用指定的方法 aSelector如果方法有返回值则返回该返回值如果方法没有返回值则返回 nil。它相当于直接调用选择子[object selectorName]; 还有一个带有 withObject: 参数的版本可以传递一个参数给指定的方法
- (nullable id)performSelector:(SEL)aSelector withObject:(nullable id)anObject;这种编程方式极为灵活经常可用来简化复杂的代码。
为何要多用GCD
但是使用performSelector的特性的代价是如果在 ARC 下编译代码那么编译器会发出如下警示信息
warning: performSelector may cause a leak because its selector
is unknown [-Warc-performSelector-leaks]原因在于编译器并不知道将要调用的选择子是什么因此也就不了解其方法签名及返回值甚至连是否有返回值都不清楚。而且由于编译器不知道方法名所以就没办法运用 ARC 的内存管理规则来判定返回值是不是应该释放。鉴于此ARC 采用了比较谨慎的做法就是不添加释放操作。然而这么做可能导致内存泄漏因为方法在返回对象时可能已经将其保留了。 有如下代码
SEL selector;if (/*some condition*/) {selector selector(newObject);
} else if (/*some other condition*/) {selector selector(copy);
} else {selector selector(someProperty);
}id ret [object performSelector:selector];if (selector selector(newObject) || selector selector(copy)) {[ret release]; // 手动释放返回的对象
}此代码中如果调用的是两个选择子之一那么 ret 对象应由这段代码来释放而如果是第三个选择子则无须释放。不仅在 ARC 环境下应该如此而且在非 ARC 环境下也应该这么做这样才算严格遵循了方法的命名规范。如果不使用 ARC那么在前两种情况下需要手动释放 ret 对象而在后一种情况下则不需要释放。这个问题很容易忽视而且就算用静态分析器也很难侦测到内存泄漏。performSelector 系列的方法之所以要谨慎使用这就是其中一个原因。 少使用performSelector系列方法的另一个原因在于该系列方法返回值只能是 void 或对象类型。尽管所要执行的选择子也可以返回 void但是 performSelector 方法的返回值类型毕竟是 id。如果想返回整数或浮点数等类型的值那么就需要执行一些复杂的转换操作了而这种转换很容易出错。 performSelector 还有如下几个版本可以在发消息时顺便传递参数
- (id)performSelector:(SEL)selector withObject:(id)object;
- (id)performSelector:(SEL)selector withObject:(id)objectA withObject:(id)objectB;比方说可以用下面这个版本来设置对象中名为 value 的属性值
id object /*an object with a property called value */;
id newValue /*new value for the property */;
[object performSelector:selector(setValue:) withObject:newValue];由于参数类型是 id所以传入的参数必须是对象才行。如果选择子所接受的参数是整数或浮点数那就不要采用这些方法了。
performSelector 系列方法还有个功能就是可以延后执行选择子或将其放在另一个线程上执行。下面列出了此方法中一些更为常用的版本
- (void)performSelector:(SEL)selector withObject:(id)argument afterDelay:(NSTimeInterval)delay;
- (void)performSelector:(SEL)selector onThread:(NSThread *)thread withObject:(id)argument waitUntilDone:(BOOL)wait;
- (void)performSelectorOnMainThread:(SEL)selector withObject:(id)argument waitUntilDone:(BOOL)wait;但是这些方法都无法处理带有两个参数的选择子。能够指定执行线程的那些方法也不是特别通用。如果要用这些方法就得把许多参数都打包到字典中然后在受调用的方法里将其提取出来这样会增加开销。而且还可能出 bug。 所以就需要改用其他替代方案使它不受这些限制。 最主要的替代方案就是使用块可以通过在 GCD 中使用 block 来实现。延后执行可以用 dispatch_after 来实现在另一个线程上执行任务则可通过 dispatch_sync 及 dispatch_async 来实现。 比如要要延后执行某项任务我们应该
dispatch_time_t time dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^{[self doSomething];
});要把任务放在主线程上执行应该
dispatch_async(dispatch_get_main_queue(), ^{[self doSomething];
});performSelector 系列方法在内存管理方面容易有疏失。它无法确定将要执行的选择子具体是什么因而ARC 编译器也就无法插入适当的内存管理方法。performSelector 系列方法所能处理的选择子太过局限了选择子的返回值类型及发送给方法的参数个数都受到限制。如果想把任务放在另一个线程上执行那么最好不要用 performSelector 系列方法而是应该把任务封装到块里然后调用GCD 的相关方法来实现。