《Effective Objective-C》阅读笔记(下)

目录

内存管理

理解引用计数

引用计数工作原理

自动释放池

保留环

以ARC简化引用计数

使用ARC时必须遵循的方法命名规则

变量的内存管理语义

ARC如何清理实例变量

在dealloc方法中只释放引用并解除监听

编写"异常安全代码"时留意内存管理问题

以弱引用避免保留环

以"自动释放池块"降低内存峰值

用"僵尸对象"调试内存管理问题

不要使用retainCount

块与大中枢派发

理解"块"这一概念

块的基础知识

块的内部结构

全局块、栈块及堆块

为常用的块类型创建typedef

用handler块降低代码分散程度

用块引用其所属对象时不要出现保留环

多用派发队列,少用同步锁

​编辑

多用GCD,少用performSelector系列方法

掌握GCD及操作队列的使用时机

[通过Dispatch Group机制,根据系统资源状况来执行任务](#通过Dispatch Group机制,根据系统资源状况来执行任务)

使用dispatch_once来执行只需运行一次的线程安全代码

不要使用dispatch_get_current_queue

系统框架

熟悉系统框架

多用枚举块,少用for循环

对自定义其内存管理语义的collection使用无缝桥接

构建缓存时选用NSCache而非NSdictionary

精简initiakize与load的实现代码

别忘了NSTimer会保留其目标对象


内存管理

理解引用计数

OC使用引用计数来管理内存。

引用计数工作原理

NSObject协议声明了下面三个方法来操作计数器,以递增或递减其值:

retain 递增保留计数

release 递减保留计数

autorelease 待清理"自动释放池",再递减保留计数

对象被创建后保留计数的变化过程如下图所示:

一般调用完release后都会清空指针,这就能保证不会出现可能指向无效对象的指针。

自动释放池

有时可以不调用release,改为调用autorelease,此方法会在稍后递减计数,通常是下一次"事件循环"时递减。

使用自动释放池autorelease可以延长对象生命期,使其在跨越方法调用边界后依然可以存活一段时间。

保留环

引用计数经常要注意的一个问题是"保留环","保留环"会导致内存泄漏,因为循环中的对象保留计数不会降为0,通常采用"若引用"来解决这一问题。

以ARC简化引用计数

ARC,即自动引用计数,所做的事情就是自动管理引用计数。由于ARC自动执行retain、release、autorelease等操作,所以不能调用上述方法

ARC在调用这些方法时,并不通过消息派发机制,而是直接调用其底层C语言版本,这样性能更好。

使用ARC时必须遵循的方法命名规则

若方法名以下列词语开头,则其返回的对象归调用者所有

若方法名不以上述四个词语开头,则表示其所返回的对象不归调用者所有。

变量的内存管理语义

在应用程序中,可用下列修饰符来改变局部变量与实例变量的语义:

通过__weak局部变量可以打破由块所引入的"保留环"。

ARC如何清理实例变量

ARC会借助OC特性来生成清理例程,来自动清理OC对象,所以ARC环境下通常无需再编写dealloc方法

不过如果有非OC的对象,那么仍然需要清理,dealloc方法可以这样写:

在dealloc方法中只释放引用并解除监听

对象在经历其生命期后,最终会为系统所回收,这时就要执行dealloc方法了。dealloc方法中要做的主要就是释放对象所拥有的引用。如果对象拥有其他非OC对象,那就要手工释放。

dealloc方法中还要把配置的观测行为清理掉,比如用NSNotificationCenter订阅的通知,dealloc可以这样写:

有一些开销较大或系统内稀缺的资源不在dealloc中释放,比如文件描述符、套接字、大块内存等。通常的做法是:实现另一个方法,当对象使用完后,就调用此方法。

这种对象所属的类,接口可以这样写:

objectivec 复制代码
#import <Foudation/Foudation.h>
@interface EOCServerConnection : NSObject
- (void)open:(NSString*)address;
- (void)close;
@end

该类与开发者的约定是:想打开连接,就调用"open:"方法,连接完毕就调用close方法。

close与dealloc方法可以这样写:

objectivec 复制代码
#import <Foudation/Foudation.h>
@interface EOCServerConnection : NSObject
- (void)open:(NSString*)address;
- (void)close;
@end

还要注意:dealloc中不要随便调用其他方法,执行异步任务的方法不应在dealloc里调用,只能在正常状态下执行的方法不应在dealloc里调用,属性的存取方法不应在dealloc里调用。

编写"异常安全代码"时留意内存管理问题

在try块中,如果先保留了某个对象,在释放它以前又抛出了异常,那么除非catch块能处理此问题,否则对象所占内存就将泄漏。

在手动管理引用计数时,要使用@finally块,并在块中释放对象,这样无论是否抛出异常,其中的代码都保证会运行且只运行一次。

当自动管理引用计数时,需要打开-fobjc-arc-exceptions标志,该标志默认关闭,只有在打开时,才会自动管理异常的引用计数,不过降低运行效率。

当遇到大量异常捕获操作时,就要考虑重构代码,用NSError式错误信息传递法来取代异常。

以弱引用避免保留环

最简单的保留环就是两个对象互相引用对方。

保留环会导致内存泄漏

如果只剩一个引用还指向保留环中的实例,而现在又把这个引用移除,那么整个保留环就泄漏了。

避免保留环最佳方式是弱引用,这种引用经常用来表示"非拥有关系",将属性声明为unsafe_unretained或weak即可。

两者都会对属性弱引用,区别只是对象回收后的行为:

以"自动释放池块"降低内存峰值

嵌套自动释放池,可以控制应用的内存峰值,使其不致过高。

比如这段代码:

如果方法创建临时对象,那这些对象即便不再使用,也依然存活,直到系统将其释放并回收,意味着所有临时对象都要等for循环执行完才会释放。这会导致程序所占内存量持续上涨。

当循环长度取决于用户输入时更如此,比如:

这时,增加一个自动释放池,把循环内的代码包裹在"自动释放池块"中,那么循环中自动释放的对象就会放在这个池,内存峰值就会降低

用"僵尸对象"调试内存管理问题

Cocoa提供了"僵尸对象"这一功能,当功能启用后,系统会把所有已经回收的实例转化为"僵尸对象",而不会真正回收它们 。这种对象内存不可能遭到覆写,并且收到消息后会抛出异常,准确说明发送过来的消息,并描述回收之前的那个对象。僵尸对象是调试内存管理问题的最佳方式。

将NSZombieEnabled环境变量设为YES,即可开启此功能。开启功能后,系统会修改对象的isa指针,指向特殊的僵尸类,从而使对象变为僵尸对象,他会相应所有选择子,响应方式为:打印一条包含信息内容及其接收者的消息,然后终止应用程序

不要使用retainCount

ARC中已经废弃了retainCount这一方法,而在不启用ARC时,也不应该使用这一方法。

这个方法之所以无用,原因在于:它所返回的保留计数只是某个给定时间点上的值。

块与大中枢派发

理解"块"这一概念

块可以实现闭包

块的基础知识

块与函数类似,只不过是直接定义在另一个函数里的,和定义它的函数共享同一个范围内的东西,用符号"^"来表示,后面跟一对花括号。

块类型的语法结构如下:

块的强大之处是:在声明它的范围内,所有变量都可以为其所捕获,但是默认情况下,不可以在块里修改。

如果声明时加上__block修饰符,这样就可以在块内修改了。

如果块捕获了对象类型,就会自动保留它,就会导致一个问题,块本身也具有引用计数,这样就很容易导致循环引用。

块的内部结构

栈的内存布局如图:

这里invoke变量是个函数指针,指向块的实现代码,descriptor是指向结构体的指针,结构体中声明了块对象的总体大小,还声明了copy与dispose两个辅助函数对应的函数指针。

全局块、栈块及堆块

定义块时,所占内存区域是分配在栈中的,离开相应范围后,栈内存可能被覆写。

将块对象发送copy消息就可以拷贝,把块从栈复制到堆。此后再调用copy,只会递增块对象引用计数

除了栈块和堆块,还有全局块,这种块不会捕捉任何状态,也不需要,块所使用的内存区域在编译器就已经确定了。这种块可以声明在全局内存中,不需要在栈中创建。

全局块的拷贝是个空操作,它实际上相当于单例。

为常用的块类型创建typedef

块类型的语法结构如下:

为了简化,可以使用typedef为块起一个已读的别名,比如这样:

现在可以直接使用新类型了。

当块作为参数时,也可以简化,比如:

简化后:

使用类型定义还有个好处,就是在打算重构块时,会很方便,只要修改类型定义处即可

用handler块降低代码分散程度

为用户界面编码时,经常异步执行任务,如果使用委托模式,那么处理数据的代码总是在受委托者处实现,如下图:

而如果改用块来写的话,代码就会变得更加清晰。

如此一来,代码就变得好懂许多了。

除此之外,委托模式还有一个缺点:如果类要分别使用多个获取器下载不同数据,那就得在delegate回调方法里根据传入参数来切换。

如果使用块,就可以将实现代码与委托者内联在一起。

在设计API时,如果用到了handler块,那就可以增加一个参数,使调用者可以通过此参数来决定应该把块安排在哪个队列上执行

用块引用其所属对象时不要出现保留环

使用块很容易导致"保留环"

有两种常见情况。第一种如下例:

当使用block捕获self时,就可能造成这种情况,这时应该等闭包执行完后,再打破保留环

另一种情况是互相引用,这时常见的方法是不保留引用,设定一套机制,令获取器对象自己设法保持存活。

多用派发队列,少用同步锁

在GCD出现之前,有两种办法来实现同步机制,第一种是"同步块"(@synchronized),第二种是直接使用NSLock对象。这两种方法都有其缺陷,替代方案就是GCD。

有种简单而高效的办法可以代替同步块或锁对象,那就是使用"串行同步队列"。GCD在底层实现。可以做许多优化。

比如属性的存取:

将属性的访问操作都放在串行队列中,就可以实现同步。

如果把设置方法改为异步派发,就可以进一步优化

然而,获取方法与设置方法不能并发执行,但多个获取方法可以并发执行。将并发队列与栅栏块结合,可以进一步优化。

在队列中,栅栏块必须单独执行,不能与其他块并行。

实现代码也很简单:

objectivec 复制代码
_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, ^{
      _someString = someString;
    });
}

多用GCD,少用performSelector系列方法

使用performSelector方法,可以动态绑定消息,如下图:

但是这种方法不仅可能导致内存泄漏,还很难传达一些非对象类型参数。

避免这些限制和缺陷,方法就是使用块,而perform方法提供的那些线程功能,都可以通过在大中枢派发机制中使用块来执行,延后执行可以用dispatch_after来实现,在另一个线城上执行任务可以通过dispatch_sync及dispatch_async来实现

掌握GCD及操作队列的使用时机

有时使用NSOperationQueue要比使用GCD技术要更合适。

用NSOperationQueue类的"addOperationWithBlock:"方法搭配NSBlockOperation类使用操作队列,语法与GCD非常类似,但使用下来有以下好处:

通过Dispatch Group机制,根据系统资源状况来执行任务

Dispatch group是GCD的一项特性,能够把任务分组。

与dispatch group相关的有以下几个函数:

objectivec 复制代码
dispatch_group_tdispatch_group_create// 创建分组
void dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_bloack_t block);//管理分组
void dispatch_group_enter(dispatch_group_t group);
void dispatch_group_leave(dispatch_group_t group);//管理分组

有两个方法可以用于等待dispatch group执行完毕:

objectivec 复制代码
long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);

二者的区别是:前者会阻塞当前线程,而后者只会在完成任务后进行通知。

未必一定要使用dispatch group,使用异步派发加单个队列也可以实现该效果。然而,使用dispatch group,GCD会根据系统资源状况来调度这些任务。若不采用group,则不仅性能有所下降,还需要额外代码来防止死锁

使用dispatch_once来执行只需运行一次的线程安全代码

在GCD中,有一种很好的方式实现单例,采用以下函数:

objectivec 复制代码
void dispatch_once(dispatch_once_t *token, dispatch_block_t block);

此函数接受一个参数,只要这个参数相同,那么这个块里的代码便只执行一次。

可以这样实现单例:

这个参数应该声明在static或global作用域中。

不要使用dispatch_get_current_queue

dispatch_get_current_queue这个函数已经被弃用了。使用这个函数时,容易造成死锁。并且这个函数的行为也总是与预计不符。因为队列存在一套层级体系。

所以使用该函数检查当前队列是否为执行同步派发所用的队列,并不总是奏效。这个函数所返回的不一定是API指定的那个,有可能是API内部的那个同步队列。

为了解决这个问题,最好的办法是通过GCD提供的功能来设定"队列特有数据",通过队列特有数据来解决不可重入导致的死锁。

系统框架

熟悉系统框架

许多系统框架可以直接使用,最重要的是Foundation与CoreFoundation。

许多常见任务都能用框架来做,例如音频与视频处理、网络通信、数据管理等。

多用枚举块,少用for循环

遍历collection有四种方式,最基本的是for循环,其次是NSEnumerator类和for...in...快速便利法,最新、最先进的是"块枚举法"。

以数组为例,NSArray中定义了这个方法

objectivec 复制代码
- (void)enumerateObjectsUsingBlock:(void(^)id object, NSUInteger idx, BOOL *stop)block

通过这种方法遍历collection,可以直接从块里获取更多信息。遍历数组时可以知道当前下标,遍历字典可以同时获取键值,还可以修改块的方法签名以免进行类型转换操作

对自定义其内存管理语义的collection使用无缝桥接

使用无缝桥接技术,可以在Foudation框架中的OC对象与CoreFoudation框架中的C语言数据结构之间来回转换。

使用无缝桥接,可以将collection转换为具备特殊内存管理语义的collection。

比如字典的键为"拷贝",而值为"保留",这时就只能使用无缝桥接来改变语义。

CF框架中的可变字典叫CFMutableDictionary,可通过下列方法来制定键和值的内存管理语义:

构建缓存时选用NSCache而非NSdictionary

构建缓存时,应选用NSCache而非NSdictionary。

NSCache的优势在于:当系统资源快要耗尽时,它可以自动删减缓存

NSCache对象可以设置上限来限制缓存中的对象总个数和成本。

NSCache经常与NSData和NSPurgeableData搭配使用。将NSPurgeableData与NSCache搭配使用,可以实现自动清除数据的功能。

精简initiakize与load的实现代码

类的初始化有两个方法,分别是load和initialize

load方法的问题在于,执行时系统处于"脆弱状态",在执行时必然执行超类的load方法,如果依赖程序库,那库里所有相关类的load方法必定会先执行,但是根据某个库,无法判定各个类的载入顺序。因此,在load方法里使用其他类是不安全的。

load方法务必实现精简一些,因为整个程序在执行load时都会阻塞。

initialize方法会在程序首次调用该类之前调用且只调用一次。它与load不同之处在于:

1.initiakize为惰性调用

2.执行initialize时,系统处于正常状态,也能确保一定是"线程安全的环境"中执行。

3.如果某个类为实现initialize,就会运行超类的实现代码。

initialize方法在实现时也尽量精简。

别忘了NSTimer会保留其目标对象

使用重复执行模式的计时器,很容易引入"保留环"

如果创建本类的实例,并调用startPolling方法,创建计时器时,由于目标对象是self,所以要保留此实例。然而,实例也保留了计时器。因此就产生了保留环。

这时可以拓展NSTimer的功能,用"块"来打破保留环。不过需要创建分类,将实现代码加入分类中。

objectivec 复制代码
- (void)startPolling{
    __weak EOCClass *weakSelf = self;
    _pollTimer = [NSTimer eoc_scheduledTimerWithTimeInterval:5.0 block: ^{
    EOCClass *strongSelf = weakSelf;
    [strongSelf p_doPoll];
    } repeats:YES];
}

这段代码先定义了一个弱引用,令其指向self,然后捕获这个引用,而不是原本的self变量,这样self就不会被计时器所保留块开始执行时,立即生成strong引用,以保证实例在执行期间持续存活。

相关推荐
程序员林北北6 分钟前
IOS基础面试题
macos·ios·cocoa
极客BIM工作室1 小时前
AI探索笔记:线性回归
人工智能·笔记·线性回归
s_little_monster3 小时前
【Linux】缓冲区和文件系统
linux·运维·服务器·经验分享·笔记·学习·学习方法
StickToForever3 小时前
第5章 软件工程(二)
经验分享·笔记·学习·职场和发展
StickToForever3 小时前
第5章 软件工程(一)
经验分享·笔记·学习·职场和发展
Magnetic_h4 小时前
《Effective Objective-C》阅读笔记(上)
笔记·学习·macos·ios·objective-c·cocoa
剑走偏锋o.O4 小时前
Vue.js 学习笔记:TodoList 待办事项小案例
vue.js·笔记·学习
初九之潜龙勿用5 小时前
结束,起点。有感于2024博客之星评选
笔记
Magnetic_h6 小时前
《Effective Objective-C》阅读笔记(中)
笔记·学习·ios·objective-c