知识点:
- 内存消耗(例如,应用如何消耗内存)
- 内存管理模型(例如,iOS运行时如何管理内存)
- 语言架构 -- Objective-C 的架构及一些实用特性
- 在不影响用户体验的前提下,采用减少内存使用的最佳实践
2.1内存消耗
内存消耗指的是应用消耗的RAM。
2.1.1栈大小
应用中新创建的每个线程都有专用的栈空间,该空间由保留的内存和初始提交的内存组成。栈可以在线程存在期间自由使用。线程的最大栈空间很小,这就决定了以下限制。
- 可被递归调用的最大方法数:每个方法都有其自己的栈帧,并会消耗整体的栈空间。
main(){
method1();
}
method1(){
method2();
}
//main调用method1,method1调用method2,这样就存在三个栈帧了,且每个栈帧都会消耗一定字节的内存。
- 一个方法中最多可以使用的变量个数。所有的变量都会载入方法的栈帧中,并消耗一定的栈空间。
- 视图层级中可以嵌入的最大视图深度,渲染复合视图将在整个视图层级树中递归地调用layoutSubViews 和 drawRect 方法。如果层级过深,可能会导致栈溢出。
2.1.2堆大小
每个进程的所有线程共享同一个堆。 图中展示了可能出现在一个应用某个时刻的一个典型堆。 由main方法启动的主线程创建了UIApplocation。假设某个时间点的窗体包含了一个UITableview,当必须渲染表格中的一行时,UITableview调用了UITableViewDataSource的tableview:cellForRowAtIndex:方法。 通过名为photos的NSArray属性,数据源引用了全部的照片。如果处理不够谨慎,这个数组将会非常大,从而导致很高的峰值内存使用。解决方案之一是在数组中存储固定数量的图片,并在用户滚动视图时换入或换出图片。这个固定的数值将决定此应用的平局内存使用。 数组中的每一项都是HPPhoto类型,代表了一张照片。HPPhoto存储了与对象有关的数据,如照片的尺寸,创建日期,拥有者信息,标签,与照片关联的网络URL,对本地缓存的引用,等等; 与通过类创建的对象相关的所有数据都存放在堆中 类可能包含属性或值类型的实例变量,如int,char 或struct。但因为对象是在堆内创建的,所以他们只消耗堆内存。 当对象被创建并被赋值时,数据可能会从栈复制到堆。类似的,当值仅在方法内部使用时,它们也可能会被从堆复制到栈。这可能是个代价昂贵的操作。
[@interface](https://my.oschina.net/u/996807) AClass : NSObject
[@property](https://my.oschina.net/property) (nonatomic , assign) NSInteger anInteger;//通过传值进行传递
[@property](https://my.oschina.net/property) (nonatomic , copy) NSString *aString;//通过引用传递
[@end](https://my.oschina.net/u/567204)
其他的类中
-(AClass *)createAClassWithIntefer:(NSInteger) i string:(NSString *) s{
AClass *result = [AClass new];
result.anInteger = i;//i的值在栈上。但赋值给属性时,它必须被辅助到堆中,因为那是存储result的地方。
result.aString = s;//虽然NSString *通过引用传递,但这个属性被标记为copy。这以为着它的值必须被复制,这取决于[NSCopying copyWithzone:]方法的实现
return result;
}
-(void)someMethod:(NSArray *)item{
NSInteger total = 0;
NSMutableString *finalValue = [NSMutableString string];
for (AClass *obj in item){
total += obj.anInteger;//使用anInteger时,它的值必须先复制到栈然后才能进行进一步的处理。
[finalValue appendString:obj.aString];//aString使用时通过引用
}
}
2.2 内存管理模型
内存管理模型基于持有关系的概念。如果一个对象正处于被持有状态,那它占用的内存就不能被回收。 当一个对象创建于某个方法的内部时,那该方法就持有这个对象了。如果这个对象从方法返回,则调用者声称建立了持有关系。这个值可以“赋值”给其他变量,对应的变量同样会声称建立了持有关系。 一旦与某个对象相关的任务全部完成,那么就是放弃了持有关系。这一过程没有转移持有关系,而是分别增加或减少了持有者的数量。当持有者的数量降为零时,对象会被释放,相关的内存会被回收。这种持有关系计数通常被正式称为引用计数。
NSString *message = @"Object-C is a verbose yet awesome language";//创建对象,message建立了持有关系,引用计数为1
NSString *messageRetained = [message retain];//messageRetained建立了持有关系,引用计数增加为2
[messageRetained release];//放弃了持有关系,引用计数降为1
[message release];//放弃持有关系,引用计数降为0
NSLog(@"Value of message :%@",message);//严格来讲,此时message的值是未定义的。你仍然能像之前那样得到相同的值,因为它对应的内存还没有被回收或重置
方法是如何对引用计数产生影响的
//AClass类address方法的实现
-(NSString *)address{
NSString *result = [[NSString alloc]
initWithFormat:@"%@\n%@\n%@,%@",
self.line1,self.line2,self.city,self.state];//首次创建对象,result指向内存的引用计数为1
return result;
}
//其他类传入AClass的对象调用address
-(void)showPerson:(AClass *)p{
NSString *paddress = [p address];//通过paddress(指向result)指向内存的引用计数仍然是1。showPerson:方法通过address创建了对象,是对象的持有者。对象不应该被再次持有。
NSLog(@"Person's Address:%@",paddress);
[paddress release];//放弃持有关系,引用计数降为0
}
2.3自动释放对象
自动释放对象让你能够放弃对一个对象的持有关系,但延后对它的销毁。当在方法中创建一个对象并需要将其返回时,自动释放就显得非常有用。自动释放可以帮助在MRC中管理对象的生命周期。
在上面的代码中,没什么能表示address方法持有了返回的字符串。因此,方法调用者showPerson:也不应该释放返回的字符串,这可能会导致发生内存泄露。加入[paddress release]这行代码的目的是为了指明这种情况。
NSObject协议定义了可被用于延迟释放的autorelease消息。可在从方法中返回对象时使用它。
-(NSString *)address{
NSString *result = [[[NSString alloc]
initWithFormat:@"%@\n%@\n%@,%@",
self.line1,self.line2,self.city,self.state] autorelease];
return result;
}
可以用一下规则来分析代码: (1)持有的对象是alloc方法返回的。 (2)确保没有内存泄露,你必须在是去引用之前放弃持有关系。 (3)但是,如果使用了release,那么对象的释放将发生在返回之前,因而方法将返回一个无效的引用。 (4)autorelease表明你想要放弃持有关系,同时允许方法的调用者在对象被释放之前使用对象。 当创建一个对象并将其从非alloc方法返回时,应使用autolease。这样可以确保对象将被释放,并尽量在调用方法执行完成时立即释放
2.4自动释放池块
自动释放块是允许你放弃对一个对象的持有关系,但可避免它立即被回收的一个工具。当从方法返回对象时,这种功能非常管用。 它还能确保在块内创建的对象会在块完成时被回收。这在创建了多个对象的场景中非常有用。本地的块可以用来尽早的释放其中的对象,从而使内存用量保持在较低的水平。 自动释放池块用@autoreleasepool表示。
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
嵌套的@autoreleasepool块
@autoreleasepool{
// 一些代码
@autoreleasepool{
//更多代码
}
}
因为从一个方法进入另一个方法会传递控制权,所以在同一个方法内部使用嵌套的@autoreleasepool块并不常见。但是,被调用的方法也可能拥有自己的@autoreleasepool块,以提前执行对象的回收。
有一些特地情况下,创建自己的autoreleasepool块:
- 当你有一个创建了很多临时对象的循环时:在循环中使用autoreleasepool块可以为每个迭代释放内存。虽然迭代前后最终的内存使用相同,但你的应用的最大内存需求可以大大降低。
//不良代码 这段代码很糟糕,因为只有一个autoreleasepool,而且内存清理工作要在所有的循环迭代完成之后才能进行。
@autoreleasepool{
NSUInteger *userCount = userDatabase.userCount;
for {NSUInteger * i = 0; i < userCount; i++}{
Person *p = [userDatabase userAtIndex:i];
NSString *fname = p.name;
if(fname == nil){
fname = [self askUserForFirstNmae];
}
NSString *lname = p.lname;
if (lname == nil){
lname = [self askUserLastName];
}
//..
[userDataBase updateUser:p];
}
}
//好的代码 这个示例中有两个autoreleasepool,内层的autoreleasepool确保在每次循环迭代完成后清理内存,从而导致更少的内存需求
@autoreleasepool{
NSUInteger *userCount = userDatabase.userCount;
for {NSUInteger * i = 0; i < userCount; i++}{
@autoreleasepool{
Person *p = [userDatabase userAtIndex:i];
NSString *fname = p.name;
if(fname == nil){
fname = [self askUserForFirstNmae];
}
NSString *lname = p.lname;
if (lname == nil){
lname = [self askUserLastName];
}
//..
[userDataBase updateUser:p];
}
}
}
- 当你创建一个线程时:每个线程都将有它自己的autoreleasepool块栈。主线程用自己的autoreleasepool启动,因为它来自同一生成的代码。然而,对于任何自定义的线程,你必须创建自己的autoreleasepool。 自定义线程中的自动释放池块
{
NSThread *myThread = [[NSThread alloc]initWithTarget:self selector:@selector(myThreadStart:) object:nil];
[myThread start];
}
-(void)myThreadStart:(id)obj{
@autoreleasepool{
//新线程的代码
}
}
2.5自动引用计数
ARC是一种编译器特性,它评估了对象在代码中的生命周期,并在编译时自动注入合适的内存管理调用。编译器还会生成适合的dealloc方法。 自动引用计数完整规范:https://clang.llvm.org/docs/AutomaticReferenceCounting.html
ARC规则
- 不能实现或调用retain,release,autorelease或retainCount方法。这一限制不仅针对对象,对选择器同样有效。因此,[obj release]或@selector[retain]是编译时的错误。
- 可以实现dealloc方法,但不能调用他们,超类也无法调用,[super dealloc]是编译时的错误。但仍然可以对Core Foundation类型的对象调用CFRetain,CFRelease等相关方法。
- 不能调用NSAllocateObject和NSDeallocateObject方法。应使用alloc方法创建对象,运行时负责回收对象。
- 不能在C语言的结构体内使用对象指针。
- 不能在id类型和void *类型之间自动切换。
- 不能使用NSAutoreleasePool,要替换使用autorelease块。
- 不能使用NSZone内存区域。
- 属性的访问器名称不能以new开头,以确保与MRC的互操作性。
//未允许
//@property NSString *newTitle;
//允许
@property (getter = getNewTitile) NSString *newTitle;
2.6引用类型
- 强引用:强引用是默认的引用类型。被强引用指向的内存不会被释放。强引用会对引用计数加1,从而扩展对象的生命周期。
- 弱引用:弱引用是一种特殊的引用类型。它不会增加引用计数,因而不会扩展对象的生命周期。在启用了ARC的Objective-C编程中,弱引用格外重要。
2.6.1变量限定符
ARC为变量提供了4种生命周期限定符。
- __strong:这是默认的限定符,无需显示引入。只要有强引用指向,对象就会长时间驻留在内存中。可以将__strong理解为retain调用的ARC版本。
- __weak:这表明引用不会保持被引用对象的存活。当没有强引用指向对象时,弱引用会被置为nil。可将__weak看作是assign操作符的ARC版本,只是对象被回收时,__weak具有安全性----指针将自动被设置为nil。
- __unsafe_unretained:与__weak类似,只是当没有强引用指向对象时,__unsafe_unretained不会被置为nil。可将其看作assign操作符的ARC版本。
- __autoreleasing:__autoreleasing用于由引用使用id *传递的消息参数。它预期了autorelease方法会在传递参数的方法中被调用。
AClass *__strong a1 = [[AClass alloc] init];//创建对象后引用计数为1,并且对象在a1引用期间不会被回收
AClass *__weak a2 = [[AClass alloc]init];//创建对象后引用计数为0,对象会被立即释放,且a2将被设置为nil
AClass *__unsafe_unretained a3 = [[AClass alloc]init];//创建对象后引用计数为1,对象会被立即释放,但a3不会被设置为nil
AClass * __autoreleasing a4 = [[AClass alloc]init];//创建对象后引用计数1,当方法返回时对象会被立即释放
2.6.2属性限定符
- strong 默认符,指定了 __strong关系
- weak 指定了__weak关系
- assign 在ARC之前,assign是默认的持有关系限定符。在启用ARC之后,assign表示了__unsafe_unretained关系
- copy 暗指了__strong关系。此外,它还暗示了setter中的复制语义的常规行为。
- retain 指定了 __strong关系。
- unsafe_unretained 指定了__unsafe_unretained关系。 因为assign和unsafe_unretained只进行值复制而没有任何实质性检查,所以它们只应该用于值类型(BOOL,NSInteger,NSUInteger等)
@property (nonatomic , strong) NSString *Str;
@property (nonatomic , retain) NSString *oldSchool;//使用strong代替更好
@property (nonatomic , assign) NSString *Wrongdemo;//错误的将assign用于指针
@property (nonatomic , assign) BOOL ValueType;//正确使用了限定符
2.7实践环节
2.7.1照片模型
@interface HPPhoto : NSObject
@property (nonatomic , strong) NSURL *url;
@property (nonatomic , copy) NSString *title;
@property (nonatomic , strong) NSArray *comments;
//@property (nonatomic , strong) HPAlbum *album;
@end
@implementation HPPhoto
//打印对象释放
-(void)dealloc{
DDLogVerbose(@"HPPhoto dealloc-ed");
}
2.7.2方法实现&&输出分析
@interface ViewController ()
@property (strong, nonatomic) IBOutlet UILabel *resultLabel;
@end
- (IBAction)CreatStrongPhoto:(UIButton *)sender {
DDLogDebug(@"%s enter", __PRETTY_FUNCTION__);
HPPhoto * __strong photo = [[HPPhoto alloc]init];
DDLogDebug(@"Strong Photo:%@",photo);
photo.title = @"Strong Photo";
NSMutableString *ms = [[NSMutableString alloc]init];
[ms appendString:(photo == nil ?@"photo is nil" :@"Photo is not nil")];
[ms appendString:@"\n"];
if (photo != nil){
[ms appendString:photo.title];
}
self.resultLabel.text = ms;
DDLogDebug(@"%s exit", __PRETTY_FUNCTION__);
/* 打印 __strong引用确保了对象在其作用域内不会被销毁。对象只会在方法完成后被回收
16:07:27.516694+0800 HPhoto926[56675:1606211] -[ViewController CreatStrongPhoto:] enter
16:07:27:515 HPhoto926[56675:1605768] -[ViewController CreatStrongPhoto:] enter
16:07:27:516 HPhoto926[56675:1605768] Strong Photo:<HPPhoto: 0x604000225900>
16:07:27.536131+0800 HPhoto926[56675:1606212] Strong Photo:<HPPhoto: 0x604000225900>
16:07:27:516 HPhoto926[56675:1605768] -[ViewController CreatStrongPhoto:] exit
16:07:27.537587+0800 HPhoto926[56675:1606213] -[ViewController CreatStrongPhoto:] exit
16:07:27:516 HPhoto926[56675:1605768] HPPhoto dealloc-ed
16:07:27.538558+0800 HPhoto926[56675:1606211] HPPhoto dealloc-ed
*/
}
- (IBAction)CreatUnsafePhoto:(UIButton *)sender {
DDLogDebug(@"%s enter", __PRETTY_FUNCTION__);
HPPhoto * __unsafe_unretained photo = [[HPPhoto alloc]init];
DDLogDebug(@"Unsafe_unretained Photo:%@",photo);
photo.title = @"Strong Photo";
NSMutableString *ms = [[NSMutableString alloc]init];
[ms appendString:(photo == nil ?@"photo is nil" :@"Photo is not nil")];
[ms appendString:@"\n"];
if (photo != nil){
[ms appendString:photo.title];
}
self.resultLabel.text = ms;
DDLogDebug(@"%s exit", __PRETTY_FUNCTION__);
//EXC_BAD_ACCESS 崩溃 对象已释放
}
- (IBAction)CreatStrongToWeakPhoto:(UIButton *)sender {
DDLogDebug(@"%s enter", __PRETTY_FUNCTION__);
HPPhoto * __strong sphoto = [[HPPhoto alloc]init];
DDLogDebug(@"Strong Photo:%@",sphoto);
sphoto.title = @"Strong Photo,Assigned to Weak";
HPPhoto * __weak wphoto = sphoto;
DDLogDebug(@"Weak Photo:%@",wphoto);
NSMutableString *ms = [[NSMutableString alloc]init];
[ms appendString:(wphoto == nil ?@"photo is nil" :@"Photo is not nil")];
[ms appendString:@"\n"];
if (wphoto != nil){
[ms appendString:wphoto.title];
}
self.resultLabel.text = ms;
DDLogDebug(@"%s exit", __PRETTY_FUNCTION__);
/*虽然__weak引用不会增加引用计数,但之前创建的__strong引用确保了对象不会在方法结束前释放
16:58:41:739 HPhoto926[62474:1795569] -[ViewController CreatStrongToWeakPhoto:] enter
16:58:41.740738+0800 HPhoto926[62474:1819257] -[ViewController CreatStrongToWeakPhoto:] enter
16:58:41:739 HPhoto926[62474:1795569] Strong Photo:<HPPhoto: 0x60000003cbc0>
16:58:41.742364+0800 HPhoto926[62474:1819257] Strong Photo:<HPPhoto: 0x60000003cbc0>
16:58:41:739 HPhoto926[62474:1795569] Weak Photo:<HPPhoto: 0x60000003cbc0>
16:58:41.742984+0800 HPhoto926[62474:1819257] Weak Photo:<HPPhoto: 0x60000003cbc0>
16:58:41:739 HPhoto926[62474:1795569] -[ViewController CreatStrongToWeakPhoto:] exit
16:58:41.743577+0800 HPhoto926[62474:1819265] -[ViewController CreatStrongToWeakPhoto:] exit
16:58:41:739 HPhoto926[62474:1795569] HPPhoto dealloc-ed
16:58:41.743910+0800 HPhoto926[62474:1819267] HPPhoto dealloc-ed
*/
}
- (IBAction)CreatWeakPhoto:(UIButton *)sender {
DDLogDebug(@"%s enter", __PRETTY_FUNCTION__);
HPPhoto * __weak photo = [[HPPhoto alloc]init];
DDLogDebug(@"Weak Photo:%@",photo);
photo.title = @"Strong Photo";
NSMutableString *ms = [[NSMutableString alloc]init];
[ms appendString:(photo == nil ?@"photo is nil" :@"Photo is not nil")];
[ms appendString:@"\n"];
if (photo != nil){
[ms appendString:photo.title];
}
self.resultLabel.text = ms;
DDLogDebug(@"%s exit", __PRETTY_FUNCTION__);
/* __weak 引用对引用计数没有贡献。因为内存被分配在方法内且一个__weak引用指向这段内存,所以引用计数为0,对象被立即回收,甚至在其被用于紧邻的下一个语句前
16:53:22.929220+0800 HPhoto926[62474:1795870] -[ViewController CreatWeakPhoto:] enter
16:53:22:928 HPhoto926[62474:1795569] -[ViewController CreatWeakPhoto:] enter
16:53:22:928 HPhoto926[62474:1795569] HPPhoto dealloc-ed
16:53:22.936884+0800 HPhoto926[62474:1795879] HPPhoto dealloc-ed
16:53:22:928 HPhoto926[62474:1795569] Weak Photo:(null)
16:53:22.937509+0800 HPhoto926[62474:1795879] Weak Photo:(null)
16:53:22:928 HPhoto926[62474:1795569] -[ViewController CreatWeakPhoto:] exit
16:53:22.938307+0800 HPhoto926[62474:1795872] -[ViewController CreatWeakPhoto:] exit
*/
}
2.8僵尸对象
通常情况下,当引用计数降为0时对象会立即被释放,但这使得调试变得困难(例如上面的CreatUnsafePhoto方法,对象释放崩溃无报错)。如果开启了僵尸对象,那么对象就不会立即释放内存,而是被标记为僵尸。任何试图对其进行访问的行为都会被日志记录,因而你可以在对象的生命周期中跟踪对象在代码中被使用的位置。 设置NSZombieEnabled,Prodecut-->Scheme -->Edit Scheme.选择Run,然后Diagnostics标签页, 选择Zombie Objects。
开启僵尸对象后:上例的CreatUnsafePhoto方法会报
HPhoto926[82084:2447866] *** -[HPPhoto respondsToSelector:]: message sent to deallocated instance 0x600000435640
2.9内存管理法则
内存管理的4个基本规则:
- 你拥有所有自己创建的对象,如new,alloc,copy或mutableCopy。
- 你可以用MRC中的retain或者ARC中的__strong引用来拥有任何对象的持有关系。
- 在MRC中,当不在需要某个对象时,你必须立即使用release方法来放弃对该对象的持有关系。而在ARC中则无需任何特殊操作,持有关系会在对象是去最后的引用(如方法中的最后一行代码)时被抛弃。
- 一定不能抛弃原本并不存在持有关系的对象。
2.10循环引用
@interface HPAlbum : NSObject
@property (nonatomic , copy) NSString *name;
@property (nonatomic , strong) NSDate *creationTime;
@property (nonatomic , copy) HPPhoto *coverPhoto;//HPAlbum对coverPhoto有一个强引用,类型为HPPhoto
@property (nonatomic , copy) NSArray *photos;//它通过photos数组还持有了许多其他的HPPhoto对象。
@end
@interface HPPhoto : NSObject
@property (nonatomic , strong) NSURL *url;
@property (nonatomic , copy) NSString *title;
@property (nonatomic , strong) NSArray *comments;
@property (nonatomic , strong) HPAlbum *album;//HPPhoto通过强引用指向了它所属的相册
@end
假设一个相册中包含两张照片p1和p2,引用计数如下:
- p1在photos和coverPhoto中有强引用。引用计数为2.
- p2在photos中有强引用。引用计数为1.
- album在p1和p2中有强引用。引用计数为2.
这些对象从某个时间点后不在被使用,但他们的内存不会被释放,因为他们的引用计数都不会降为0.
2.10.1避免循环引用的规则
-
对象不应该持有它的父对象,应该用weak引用指向它的父对象。在上一个场景中,照片被alum所包含,我们可以将照片看成孩子。因此,从照片到相册的引用应该是弱引用。 (1)p1通过photos和coverPhoto被强引用。引用计数为2. (2)p2通过photos被强引用。引用计数为1. (3)alum不存在任何强引用。引用计数为0.
-
作为必然的结果,一个层级体系中的子对象应该保留祖先对象。
-
连接对象不应持有它们的目标对象。目标对象的角色是持有者。连接对象包括以下几种: (1)使用委托的对象。委托应该被当作目标对象,即持有者。 (2)包含目标和action的对象,这是由上一条规则推理得到的。例如,UIButton会调用它的目标对象上的action方法。按钮不应该保留它的目标。 (3)观察者模式中被观察的对象。观察者就是持有者,并会观察发生在被观察对象上的变化。
-
使用专用的销毁方法中断循环引用:双向链表中存在循环引用,环形链表中也存在循环引用。一旦明确对象不会再被使用时(当链表的表头超出作用范围),需要编写代码以打破链表的链接。创建一个(名为delinkde)方法切断其自身与链表中下一个节点的链接。通过访问者模式递归地执行这一过程,从而避免无线递归。
链表:https://zh.wikipedia.org/wiki/链表, http://www.jianshu.com/p/04c1cbccf62b
2.10.2循环引用的常见场景
1.委托 避免委托产生循环应用,在委托(当前示例为视图控制器)中建立对操作的强引用,并在操作中建立对委托的弱引用。
@interface HPDataListViewController ()<HPDataUpdateOpDelegate>
@property (nonatomic,strong) HPDataUpdateOp *updateOp;
@property (nonatomic,assign) BOOL refreshing;
- (IBAction)OnRefreshClicked:(UIButton *)sender;
@end
@implementation HPDataListViewController
- (IBAction)OnRefreshClicked:(UIButton *)sender {
DDLogDebug(@"%s enter",__PRETTY_FUNCTION__);
self.updateOp = [HPDataUpdateOp new];//1使用属性表示操作。视图控制器持有操作
self.updateOp.delegate = self;
[self.updateOp startUsingDelegate:self withSelector:@selector(onDataAvailable:)];
DDLogDebug(@"%s exit",__PRETTY_FUNCTION__);
}
-(void)onDataAvailable:(NSArray *)records{
DDLogDebug(@"%s called回调数据",__PRETTY_FUNCTION__);
//委托回调数据 records
self.updateOp = nil;//2当任务完成后将属性设置为nil。实现对操作对象的回收
}
-(void)dealloc{
DDLogDebug(@"%s called",__PRETTY_FUNCTION__);
if (self.updateOp != nil){
[self.updateOp cancel];//3若视图控制器即将被回收,则取消操作
}
}
@protocol HPDataUpdateOpDelegate <NSObject>
-(void)onDataAvailable:(NSArray *)records;
@end
@interface HPDataUpdateOp : NSObject
@property (nonatomic , weak) id <HPDataUpdateOpDelegate> delegate;//4操作保持对回调委托的弱引用
-(void)startUsingDelegate:(id) delegate withSelector:(SEL) selector;
-(void)cancel;
@end
#import "HPDataUpdateOp.h"
@implementation HPDataUpdateOp
-(void)startUsingDelegate:(id) delegate withSelector:(SEL) selector{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//执行网络调用,然后报告结果
NSLog(@"网络请求");
NSArray *records = nil;
dispatch_async(dispatch_get_main_queue(), ^{
id <HPDataUpdateOpDelegate> delegate = self.delegate;//5尝试获取对象的强引用
if (!delegate){//6如果原始对象仍然存在
return ;
}else{//7回调
[delegate onDataAvailable:records];
}
});
});
}
-(void)cancel{//8.取消操作显式地要求废弃回调对象
//取消执行中的网络请求
self.delegate = nil;
}
@end
2.块 与不正确地使用委托对象导致的问题类似,在使用块时,捕获外部变量也是导致循环引用的原因。
-(void)someMethod{
SomeViewController *vc = [[SomeViewController alloc]init];
__weak typeof(self) weakSelf = self;//1获得一个弱引用 ,typeof()只是对括号内的变量进行识别,返回值是变量的类型
[self presentViewController:vc animated:YES completion:^{
typeof(self) theSelf = weakSelf;//2通过弱引用获得强引用。注意,__strong 是隐式的,可以增加引用计数
if (theSelf != nil){//3只在不为nil时才继续
// theSelf.data = vc.data;处理后续操作
[self dismissViewControllerAnimated:YES completion:nil];
}
}];
}
block中何时用self,weakSelf,strongSelf:http://shoshino21.logdown.com/posts/732476-block-when-you-can-just-use-self-when-we-need-to-use-weakself-strongself
3.线程与计时器 不正确的使用NSTimer和NSThread对象也可能会导致循环引用。运行异步操作的典型步骤如下:
- 如果没有编写更高级的代码来管理自定义队列,则在全局队列上使用dispatch_async方法
- 在需要的时间和地点用NSThread开启异步执行。
- 使用NSTimer周期性地执行一段代码。
-(void)startCountDown{
self.timer = [NSTimer scheduledTimerWithTimeInterval:120 target:self selector:@selector(updateFeed:) userInfo:nil repeats:YES];
}
-(void)dealloc{
[self.timer invalidate];
}
上例中对象持有了计时器,同时计时器也持有了对象。如果代码属于一个视图控制器,由于用户的操作,试图控制器在应用中被创建多次。内存泄露的总量将会非常大。NSThread也会出现同样的问题。 别指望dealloc能够清理这些对象。如果建立了循环引用,那dealloc方法永远都不会被调用,计时器也永远都不会执行invalidated。因为运行循环会跟踪活跃的计时器对象和线程对象,所以仅在代码中置为nil并不能销毁对象。要想解决这个问题,可以创建一个自定义方法,以更加明确的方式执行清理操作。 解决办法:
- 主动调用invalidate
-(void)didMoveToParentViewController:(UIViewController *)parent{//1.当视图控制器进入或离开父视图控制器时,执行清理操作,这要比调用dealloc更加明确
if (parent == nil){
[self cleanup];
}
}
-(void)cleanup{
[self.timer invalidate];
}
通过拦截返回按钮执行清理
-(id)init{
if (self == [super init]){
self.navigationItem.backBarButtonItem.target = self;
self.navigationItem.backBarButtonItem.action = @selector(backButtonPressDetected:);//1拦截导航栏控制器对返回按钮的点击事件
}
return self;
}
-(void)backButtonPressDetected:(id) sender{
[self cleanup];//2在视图控制器弹出之前进行清理
[self.navigationController popViewControllerAnimated:YES];
}
- 将代码分离到多个类中:任务类执行具体动作,所有者类调用任务。
把控制器展示和刷新数据拆分为两个类,NPNewsFeedUpdateTask周期性执行,检查填充视图控制器的最新数据流
@interface NPNewsFeedUpdateTask : NSObject
@property (nonatomic , weak) id target;//1 弱引用,target会在这里实例化任务并持有它
@property (nonatomic , assign) SEL selector;
@property (nonatomic , strong) NSTimer *timer;
-(id)initWithTimeInterval:(NSTimeInterval) interval target:(id) target selector:(SEL)selector;
-(void)shutdown;
@end
#import "NPNewsFeedUpdateTask.h"
#import "HPNewsFeed.h"
@implementation NPNewsFeedUpdateTask
-(id)initWithTimeInterval:(NSTimeInterval) interval target:(id) target selector:(SEL)selector{//2分散到其他类中执行的自定义方法
if (self == [super init]){
self.target = target;
self.selector = selector;
self.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(fetchAndUpdate:) userInfo:nil repeats:YES];
}
return self;
}
-(void)fetchAndUpdate:(NSTimer *)timer{//3 定时器周期性执行方法
//检索feed
HPNewsFeed *feed = [self getFromServerAndCreateModel];
__weak typeof(self) weakSelf = self;//4在使用异步块时需要确保不会引入循环引用
dispatch_async(dispatch_get_main_queue(), ^{
__strong typeof(self) sself = weakSelf;
if (!sself){
return;
}
if (sself.target == nil){
return;
}
id target = sself.target;
SEL selector = sself.selector;
if ([target respondsToSelector:selector]){//5确保是正确的target和selector被调用
[target performSelector:selector withObject:feed];//回调更新数据
}
});
}
-(void)shutdown{//6对计时器调用invalidate,运行循环会终止对计时器的调用,于是计时器成为任务对象持有的唯一引用。
[self.timer invalidate];
self.timer = nil;
}
-(id)getFromServerAndCreateModel{
return [[NSObject alloc]init];
}
@end
HPNewsFeedViewController展示最新的feed流
@implementation HPNewsFeedViewController
- (void)viewDidLoad {
[super viewDidLoad];
//对任务对象进行初始化,其内部会触发计时器
self.updateTask = [[NPNewsFeedUpdateTask alloc]initWithTimeInterval:120 target:self selector:@selector(updateUsingFeed:)];
}
-(void)updateUsingFeed:(HPNewsFeed *) feed{//计时器周期性回调方法
//更新UI
}
-(void)dealloc{//负责调用任务对象的shutdown方法,其内部会销毁计时器。dealloc在此处是明确可用的,因为该对象没有被其他地方所引用
[self.updateTask shutdown];
}
当使用NSTimer和NSThread时,总是应该通过间接的层实现明确的销毁过程。这个间接层应使用弱引用,从而保证所拥有的对象能够在停止使用后执行销毁动作
2.10.3观察者
1.键-值观察 OC允许用addObserver:forKeyPath: options: context:方法在任何NSObject子类的对象上添加观察者。观察者会通过addObserver:forKeyPath: options: context:方法得到通知。removeObserver: forKeyPath: context:方法用于解除注册或移除观察者。 键-值观察方法addObserver:forKeyPath: options: context:不会维持观察对象,被观察对象及上下文对象的强引用。如有必要,需要自行维护对她们的强引用 这意味着观察者需要有足够长的生命周期才能够持续地监控变化。需要额外关注观察者的生命周期,而且要持续到所观察的内存被废弃之后。
用集中式的ObserverManager类来实现键-值观察,ObserverManager类会返回一个与持有者相关的ObserverObserveeHandle实例。当观察启动程序(示例为视图控制器)需要观察keyPath,它会调用addObserverToObject:forKey:方法并存储ObserverObserveeHandle,从而实现与视图控制器同时销毁
@interface ObserverObserveeHandle : NSObject
@property (nonatomic , strong)MyObserver *observer;
@property (nonatomic , strong)NSObject *obj;
@property (nonatomic , copy) NSString *keyPath;
-(id)initWithObserver:(MyObserver *)observer target:(NSObject *)obj keyPath:(NSString *)keyPath;
@end
@implementation ObserverObserveeHandle
-(id)initWithObserver:(MyObserver *)observer target:(NSObject *)obj keyPath:(NSString *)keyPath{
return [ObserverObserveeHandle new];
}
-(void)removeObserver{
[self.obj removeObserver:self forKeyPath:self.keyPath context:nil];
self.obj = nil;
}
-(void)dealloc{
[self removeObserver];
}
@end
@interface ObserverManager : NSObject
typedef void (^ block)(NSDictionary *);
+(ObserverObserveeHandle *)addObserverToObject:(NSObject *)obj forKey:(NSString *)keyPath block:(block)block;
@end
@implementation ObserverManager
NSMutableArray *observers;
+(ObserverObserveeHandle *)addObserverToObject:(NSObject *)obj forKey:(NSString *)keyPath block:(block)block{
MyObserver *observer = [[MyObserver alloc]init];
[obj addObserver:observer forKeyPath:keyPath options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
ObserverObserveeHandle *details = [[ObserverObserveeHandle alloc]initWithObserver:observer target:obj keyPath:keyPath];
[observers addObject:details];
return details;
}
@end
@interface ViewController ()
@property (strong, nonatomic) IBOutlet UILabel *resultLabel;
@property (strong, nonatomic) ObserverObserveeHandle *resultLabelMonitor;
@property (strong, nonatomic) UITextField *nameTextField;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
__weak typeof (self) weakSelf = self;
self.resultLabelMonitor = [ObserverManager addObserverToObject:self.nameTextField forKey:@"text" block:^(NSDictionary * changes) {
typeof(self) sSelf = weakSelf;
if (sSelf){
NSLog(@"Text changes %@",[changes objectForKey:NSKeyValueChangeNewKey]);
//需要时使用sSelf
sSelf.resultLabel.text = [changes objectForKey:NSKeyValueChangeNewKey];
}
}];
}
@end
2.通知中心 与键-值观察者类似,通知中心不会对观察者持有强引用。解决模式与键-值观察类似。
2.10.4返回错误
当用某个方法接受NSError **参数,并在发生错误时填充错误变量,则必须使用 __autoreleasing 限定符。
-(void)initWithObserver:(MyObserver *)observer error:(NSError *__autoreleasing *)error{
//处理如果发生错误
*error = [[NSError alloc]initWithDomain:@"transpose" code:123 userInfo:nil];
}
语法为:NSError *__autoreleasing *error 变量和属性的限定符有重要作用,可以帮助管理和精确控制对象的生命周期,确保它们既不会太长也不会太短。
2.11弱类型:id
在Cocoa framework中许多场景都需要id类型。但容易发生类型匹配错误,导致应用崩溃。
1.设置严格的选择器匹配 Build settings ->Strict Selector Matching->YES 配置当在id对象上发现多个选择器匹配时,编译器报错。 如果是处理类而不是对象,那么仍然无法发现这种问题
2.使用强类型
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
ClassA *item = (ClassA *)[self.input objectAtIndex:indexPath.row];
NSUInteger value = item.someProperty;
}
2.12对象寿命与泄露
对象在内存中活动的时间越长,内存不能被清理的可能性就越大。所以应当尽可能地避免出现长寿命的对象。 长寿命对象的常见形式是单例和全局变量。 合理使用全局变量,必须满足一下条件:
- 没有被其他对象所持有
- 不是常量
- 整个应用中只有一个,而不是每个组件一个。
2.13单例
单例模式是限制一个类只初始化一个对象的一种设计模式。在实践中,初始化常常在应用启动不久后执行,而且这些对象不会被销毁。 以下情况使用单例:
- 队列操作(如日志和埋点)
- 访问共享资源(如缓存)
- 资源池(如线程池和连接池(数据库))
单例通常会在应用启动时进行初始化,打算使用单例的组件需要等它们准备得当。这会增加应用的启动时间。 利用依赖注入尽量避免直接使用单例。 不合理的使用依赖:
-(void)someMethod{
MyObserver *obj = [MyObserver sharedInstance];//1.someMethod为operation方法使用了MyObserver类
NSString *someValue = [obj operation:@"some Parameter"];
}
潜在问题:
- 如果类MyObserver需要某些初始化,someMethod假设它已经完成了初始化。然而,事实上MyObserver并不为上游使用someMethod的方法所知,这可能导致MyObserver尚未初始化。
- 如果类MyObserver持有了一些资源,那么它会持续持有,哪怕sharedInstance后续不会再被调用。
@interface MyClass : NSObject
@property (nonatomic , strong) MyObserver *someClass;
-(instancetype)initWithSomeClass:(MyObserver *) someClass;//自定义需要传入MyObserver对象的初始化器
-(void)someMethod;
-(void)anotherMethodWithOtherClass:(AnOtherClass *)otherClass;
@end
@implementation MyClass
-(instancetype)initWithSomeClass:(MyObserver *)someClass{
if (self == [super init]){
self.someClass = someClass;
}
return self;
}
-(void)someMethod{//3通过self.someClass属性调用operation方法
NSString *someValue = [self.someClass operation:@"some paramters"];
}
-(void)anotherMethodWithOtherClass:(AnOtherClass *)otherClass{//4不同对象完成任务
NSString *someValue = [self.someClass operation:@"some paramters"];
NSString *anotherValue = [otherClass anotherOp:@"some paramters"];
}
@end
依赖注入:http://www.jianshu.com/p/0d72a945f2dd
2.14找到神秘的持有者
使用MRC的retain找出一个对象上的全部引用. 暂时禁用ARC,将下列代码添加到自定义类中
#if !__has_feature(objc_arc)
-(id)retain{
DDLogInfo(@"%s %@",__PRETTY_FUNCTION__,[NSThread callStackSymbols]);
return [super retain];
}
#endif
2.15最佳实践
- 避免大量的单例。具体来说,不要出现上帝对象(如职责特别多或状态信息特别多的对象)。
- 对子对象使用__strong
- 对父对象使用__weak
- 对使引用图闭合的对象(如委托)使用__weak
- 对数值属性(NSInteger,SEL,CGFloat等)而言,使用assign限定符
- 对于块属性,使用copy限定符
- 当声明使用NSError **参数的方法时,需要使用 __autoreleasing,并要注意用正确的语法:NSError * __autoreleasing * 。
- 避免在块内引用外部的变量。在块外面将它们weakify,并在块内再将它们strongify。参见libextobjc库来了解@weakify 和 @strongify
- 进行必要清理是遵循下列准则: 1.销毁计时器 2.移除观察者(移除对通知的注册) 3.解除回调(将强引用的委托设置为nil)
2.16生产环境的内存使用情况
使用Instruments进行内存分析。
跟踪可用和已用的内存:
#import "ZJMemoryAbakyzer.h"
#import <mach/mach.h>
@implementation ZJMemoryAbakyzer
vm_size_t getUsedMemory(){
task_basic_info_data_t info;
mach_msg_type_number_t size = sizeof(info);
kern_return_t kerr = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t) &info, &size);
if (kerr == KERN_SUCCESS){
return info.resident_size;
}else{
return 0;
}
}
vm_size_t getFreeMemory(){
mach_port_t host = mach_host_self();
mach_msg_type_number_t size = sizeof(vm_statistics_data_t) / sizeof(integer_t);
vm_size_t pagesize;
vm_statistics_data_t vmstat;
host_page_size(host, &pagesize);
host_statistics(host, HOST_VM_INFO, (host_info_t) &vmstat, &size);
return vmstat.free_count * pagesize;
}
@end
获取设备各类硬件信息:http://www.jianshu.com/p/f69fba15aa3a