《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引用,以保证实例在执行期间持续存活。

相关推荐
m0_6786933312 分钟前
深度学习笔记25-RNN心脏病预测(Pytorch)
笔记·rnn·深度学习
我的golang之路果然有问题27 分钟前
快速了解GO+ElasticSearch
开发语言·经验分享·笔记·后端·elasticsearch·golang
凤年徐44 分钟前
【数据结构初阶】顺序表的应用
c语言·开发语言·数据结构·c++·笔记·算法·顺序表
Digitally1 小时前
如何轻松地将数据从 iPhone传输到iPhone 16
ios·iphone
I烟雨云渊T2 小时前
iOS 电子书听书功能的实现
macos·ios·cocoa
半导体守望者3 小时前
英福康INFICON VGC501, VGC502, VGC503 单通道、双通道和三通道测量装置
经验分享·笔记·功能测试·自动化·制造
Timmer丿3 小时前
kafka学习笔记(三、消费者Consumer使用教程——配置参数大全及性能调优)
笔记·学习·kafka
Timmer丿3 小时前
kafka学习笔记(三、消费者Consumer使用教程——消费性能多线程提升思考)
笔记·学习·kafka
保持学习ing3 小时前
黑马Java面试笔记之 消息中间件篇(Kafka)
java·笔记·面试·kafka
二流小码农3 小时前
鸿蒙开发:UI界面分析利器ArkUI Inspector
android·ios·harmonyos