31. 在dealloc方法中只释放引用并解除监听
对象在引用计数为0时,会执行dealloc方法。
并不是引用计数一为0,就立刻执行
在dealloc方法中,主要做的事:
- 释放对象所拥有的引用,通过.cxxx_destruct方法
- 对象所拥有的其他非OC对象,比如由纯C的API生成的对象
- 监听行为清理掉(KVO和NSNotificationCenter)
- (void)dealloc{
CFRelease(coreFoundationObject);
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
在iOS系统,应用程序终止时会调用UIApplicationDelegate中的:
- (void)applicationWillTerminate:(UIApplication *)application
32. 编写“异常安全代码”时留意内存管理问题
在try块中,如果先保留了某个对象,然后在释放它之前又抛出了异常,那么,除非catch块能处理此问题,否则对象所占内存就讲泄露。
@try {
//这部分代码是有可能出现异常的错误
}
@catch(NSException *exception){
//只有出现了异常错误,才会执行
//通过打印exception.reason,可以知道错误原因
//还可以做一些操作,避免影响用户
}
@finally{
//不管有没有异常,都会执行
//即使前面代码块中有return,也会执行
}
-fobjc-arc-exceptions
打开,ARC会生成安全处理异常所用的附加代码。
不过打开后,会导致应用程序变大,而且会降低运行效率
可以在找错误时打开,平时开发用不到的时候,关闭
33. 以弱引用避免保留环
用unsafe_unretained修饰的属性特质,其语义同assign特质等价
但,assign通常只用于”整体类型“(int、float、结构体等),
unsafe_unretained则多用于对象类型。
weak,不拥有对象,并且在属性回收时,对象会自动设为nil
34. 以“自动释放池块”降低内存峰值
在OC的引用计数架构中,有一项特性叫做:“自动释放池”(aoturelease pool)
释放对象有两种方式:
- relase,立即递减
- autorelease,将其加入“自动释放池”中
自动释放池用于存放那些需要在稍后某个时刻释放的对象。
清空自动释放池时,系统会向其中的对象发送release消息
@autoreleasepool {
//...
}
一般情况下,无须担心自动释放池的创建问题。
系统会自动创建一些线程,比如主线程或GCD机制中的线程,这些线程默认都有自动释放池,每次执行“事件循环”(event lop)时,就会将其清空。因此,不需要自己来创建“自动释放池块”。
- 自动释放池于左花括号处创建,于对应的右花括号处自动清空
- 位于自动释放池范围内的对象,将在此范围末尾处收到release消息。
- 自动释放池可以嵌套,嵌套的好处:可以控制应用程序的内存峰值,使其不至过高
比如:
for (int i = 0; i < 10000000; i++) {
NSString *str = [NSString stringWithFormat:@"%d", i];
}
运行会发现:内存会过高,然后再突然下降
造成这种的原因是…
想不想知道,想知道还不好好看^ _ ^
for循环会创建很多临时变量,这些对象创建成功后会放在自动释放池里面
虽然,这些是临时变量,我们不使用他们,在方法调用完毕后,这些对象就没释放了,但他们依然存在自动释放池里面,等待系统稍后将其释放并回收。
然后,自动释放池要等线程执行下一次事件循环时才会清空,这就意味着在执行for循环时,会持续有新对象创建出来,并加入自动释放池中。所有这种对象都要等for循环执行完才会释放。
这样一来,在执行for循环时,应用程序所占内存量就会持续上涨,而等到所有临时对象都释放后,内存用量又会突然下降。
可以修改为:
for (int i = 0; i < 10000000; i++) {
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"%d", i];
}
}
奇迹出现了:
还是那么高!!!
说好的可以降低内存呢?
35. 用“僵尸对象”调试内存管理问题
向已经回收的对象发送消息是不安全的。这么做有时可以,有时又不可以。
具体是否可行,取决于对象所占内衣有没有为其他内容所覆写。
因此,应用程序只是偶尔崩溃。。。。。。
在没有崩溃的情况下,那块内存可能只复用了其中的一部分,所以对象中的某些二进制数据依然有效。
还有一种可能,内存地址已经被新的对象所占用,那么,消息会被发送给新的对象
如果新对象能处理,则不崩溃
如果新对象不能处理,则崩溃
例如:[NSDictionary lenth]
可能原来是个NSString对象,内存已经被一个NSDictionary对象覆盖,从而造成这种崩溃。
可以在编写程序的时候,打开NSZombieEnable功能
僵尸对象的工作原理:
它的实现根植于OC运行期程序库、Foundation框架、CoreFoundation框架中。系统在即将回收对象时,如果发现通过环境变量启用了僵尸对象功能,那么还将执行一个附加步骤:将对象转化为僵尸对象,而不彻底回收。
36. 不要使用retainCount
ARC中,不能编译、调用retainCount
MRC中,调用这个方法,得到的值不一定准
它返回的引用计数只是某个给定时间点上的值。该方法并未考虑到系统会稍后把自动释放池清空,因而不会将后续的释放操作从返回值里减去,因此,此值未必能够真是的反映实际的引用计数了。
由于对象可能处在自动释放池中,所以其引用计数未必如想象般精确。
第六章 块与大中枢派发
Block与GCD
在开发应用程序的时候,需要留意多线程问题
而多线程的核心就是:block与GCD
块block是一个 “词法闭包”,借由此机制,开发者可以将代码像对象一样传递
GCD是一种与块有关的技术
37. 理解“块”这一概念
使用块,可以实现闭包
块定义在函数里面,并且块与函数共享同一个范围内的东西。
使用“^”符合表示,后面跟着一对花括号,花括号里面是块的实现代码。
^{
}
块本身也是对象,块本身和其他对象一样,有引用计数
如果块定义在实例方法中,那么除了可以访问类的所有实例变量之外,还可以使用self变量。
块对象的内存布局:
翻看之前笔记,找到block定义:
struct __main_block_impl_0 {
void *isa;//isa指针
int Flags;
int Reserved;
void *FuncPtr;//函数地址
struct __main_block_desc_0* Desc;
int age;//函数调用的外部参数
};
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
}
基本一致,只是书上函数地址名为invoke,之前自己的笔记记录的是FuncPtr,应该是改为FuncPtr了
invoke变量,这是个函数指针,指向块的实现代码。
descriptor变量是指向结构体的指针,每个块里面都包含此结构,其中声明了块对象的总体大小,还声明了copy与dispose这两个辅助函数所对应的函数指针。辅助函数在拷贝及丢弃块对象时运行,其中会执行一些操作,比如:copy要保留捕获的对象,dispose则将之释放。
块还会把它所捕获的所有变量都拷贝一份。这些拷贝放在descriptor变量后面。比如例子中的age
注意块所在的内存区域:栈、堆、全局
38. 为常用的块类型创建typedef
每个块都有 固有类型,可以将其赋给适当类型的变量。
块的类型由接受的参数以及返回值组成
使用C语言中的“类型定义”特性,typedef为Block起个别名
例如:
int (^blockName)(BOOL flag, int value) = {
^(BOOL flag, int value){
return someInt;
}
}
该block接受两个类型分别为BOOL和int的参数,并返回类型为int的值。
return_type(^block_name)(parameters)
使用typedef定义block类型:
typedef int(^blockName)(BOOL flag, int value);
调用的时候:
SomeBlock bloc = ^(BOOL flag, int value){
};
使用typedef这项特性,可以使块的API做得更易用。
当block做函数的参数时,就可以使用这种操作。
- 在定义类型名的时候,最好把这个类的名字加在新的类名前面
39. 用handler块降低代码分散程度
- iOS系统发现某个应用程序的主线程已经阻塞了一段时间之后,就会令其终止。
- completion handler指的是:完成的回调
- 网络设计中,大量用到了block回调
40. 用块引用其所属对象时不要出现保留环
使用block的时候,要注意不要产生循环引用