二.内存管理

知识点:

  • 内存消耗(例如,应用如何消耗内存)
  • 内存管理模型(例如,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

转载于:https://my.oschina.net/u/2319073/blog/1548070

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值