理解引用计数机制
在引用计数的机制下,每个对象都有一个计数器,可递增递减,用以表示当前有多少事物想让该对象继续保留下去。NSObject 协议声明了三个方法用于操作计数器:
- retain:递增计数器
- release:递减计数器
- autorelease : "autorelease pool" 清理时,再递减计数器。
retain 和 release
NSNumber *number = @1;//number引用计数递增1
[array addObject:number];////number引用计数递增1
对象A被创建出来时,会自动执行 retain ,引用计数递增。当对象B引用了对象A时,被引用者对象A递增。当对象A的引用计数为0时,对象会被系统回收。
NSMutableArray *array = [NSMutableArray array];//计数为1
NSNumber *number = @1;//计数为1
[array addObject:number];//计数为2
[number release];//计数为1
[array release];//0
如上,如果我们不需要对象了,就对它调用release,然而,此时number的计数仍然为1,number对象仍然存活。为了避免不经意间使用了无效指针,调用完release我们可以手动清空指针。(通常都是针对局部变量)
number = nil;
我们知道,程序在生命期间会创建很多对象,这些对象都相互联系着。所以,这些相互联系的对象就构成一个对象树,这个对象树的根节点是UIApplocation对象,它是程序启动时创建的单例。
autorelease
- (NSString *)temp{
NSString *tmp = @"A";//计数为1
return tmp;
}
如上,返回的temp对象的引用计数比我们期望的要多1,因为只有retain,没有对应的release操作。然而,我们又不能在方法中释放temp,否则这个方法就没有存在的意义了。
所以这里我们使用autorelease,它会在稍后释放对象,从而给调用者留下足够的时间,又不会因为没有释放而造成内存泄露。具体时间是当前线程的下一次时间循环。
- (NSString *)temp{
NSString *tmp = @"A";//计数为1
return [tmp autorelease];
}
ARC
为什么要使用ARC
使用 ARC 时,引用计数还是会执行的,只不过是 ARC 为你自动添加的,即上面所说的retain/release/ autorelease 方法。在对象被创建时 retain count +1,在对象被 release 时 retain count -1.当 retain count 为0 时,销毁对象。 程序中加入 autoreleasepool 的对象会由系统自动加上autorelease方法,如果该对象引用计数为0,则销毁。 因为ARC会自动执行这些方法,所以在ARC下调用这些方法是非法的,会产生编译错误。
那么ARC是为了解决什么问题诞生的呢?这个得追溯到MRC手动内存管理时代说起。
MRC下内存管理的缺点:
1.当我们要释放一个堆内存时,首先要确定指向这个堆空间的指针都被 release 了。(避免提前释放)
2.释放指针指向的堆空间,首先要确定哪些指针指向同一个堆,这些指针只能释放一次。(MRC下即谁创建,谁释放,避免重复释放)
3.模块化操作时,对象可能被多个模块创建和使用,不能确定最后由谁去释放。
4.多线程操作时,不确定哪个线程最后使用完毕。
内存管理语义
ARC下,我们创建一个属性,需要主动声明一下内存管理语义:
assign
适用于基本数据类型。赋值特性,不涉及引用计数,弱引用,仅仅是基本数据类型变量(scalar type,例如 CGFloat 或 NSlnteger 等) 在 setter 方法中的简单赋值操作。assign 其实也可以用来修饰对象,那么我们为什么不用它呢?因为用了就相当于回到 MRC 时代了:)。 被assign修饰的对象在释放之后,指针的地址还是存在的,也就是说指针并没有被置为nil。如果在后续的内存分配中,刚好分到了这块地址,程序就会崩溃掉。
strong
强引用,与MRC中retain类似,使用之后,计数器+1。
weak
弱引用 ,weak 修饰的对象在释放之后,指针地址会被置为nil,可以有效的避免野指针,其引用计数为1。在 ARC 中,在有可能出现循环引用的时候, 此时通过引用计数无法释放指针, 往往要通过让其中一端使用 weak 来解决,比如: delegate 代理属性。自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用 weak,自定义 IBOutlet 控件属性一般也使用 weak;当然,也可以使用strong。
weak 此特质表明该属性定义了一种“非拥有关系” (nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似, 然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。 。
readwrite
可读可写特性,需要生成getter方法和setter方法时使用。
readonly
只读特性,只会生成getter方法,不会生成setter方法,不希望属性在类外改变。
copy
表示拷贝特性,setter方法将传入对象复制一份,需要完全一份新的变量时。相当于strong,只不过在编译器生成的 setter 方法中会额外将传入对象复制一份。如果你自己实现了 setter 方法,不要忘了对变量进行 copy 操作,否则 copy 的声明就没有作用了。
nonatomic
非原子操作,不加同步,多线程访问可提高性能,但是线程不安全的。决定编译器生成的setter getter是否是原子操作。
atomic
原子操作,与nonatomic相反,系统会为setter方法加锁。 具体使用 @synchronized(self){//code } 。它是线程安全,需要消耗大量系统资源来为属性加锁 。(实际上并没有 atomic,只不过当没有 nonatomic 时就是 atomic,不过你要是写上去编译器也不会报错)。
内存泄露
即便有了ARC,也是有可能会内存泄露的。
两个类循环引用
//AViewController 对象
@interface AViewController : UIViewController
//aVC强引用了bVC
@property (nonatomic, strong) BViewController *bVC;
@end
/**************************************************/
//BViewController 对象
@interface BViewController : UIViewController
//bVC强引用了aVC
@property (nonatomic, strong) aViewController *aVC;
@end
如上的代码,A强引用了B,B也强引用了A,这样就导致了循环引用,ARC无法回收这两个对象,从而导致内存泄露。
那我们该怎么解决呢?我觉得两个对象最好不要互相引用,如果不得不互相引用,我们可以这么写:
@interface AViewController : UIViewController
//aVC强引用了bVC
@property (nonatomic, strong) BViewController *bVC;
@end
/**************************************************/
//BViewController 对象
@interface BViewController : UIViewController
//bVC弱引用了aVC
@property (nonatomic, weak) aViewController *aVC;
@end
delegate循环引用问题
delegate循环引用问题比较基础,原理和两个类循环引用一样,这里特地拿出来讲是因为 delegate 比较常用。具体可看下面的示例图。只需注意将代理属性修饰为weak即可。
@property (nonatomic, weak) id delegate;
Block
有这样一个场景:在网络工具类NetworkFetch中有一个网络请求的回调Block:
/**
* 本例取自《Effective Objective-C 2.0》
* NetworkFetecher 为自定义的网络获取器的类
*/
//EOCNetworkFetcher.h
#import <Foundation/Foundation.h>
typedef void (^EOCNetworkFetcherCompletionHandler)(NSData *data);
@interface EOCNetworkFetcher : NSObject
@property (nonatomic, strong, readonly) NSURL *url;
- (id)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion;
@end;
//EOCNetworkFetcher.m
#import "EOCNetworkFetcher.h"
@interface EOCNetworkFetcher ()
@property (nonatomic, strong, readwrite) NSURL *url;
@property (nonatomic, copy) (EOCNetworkFetcherCompletionHandler)completionHandler;
@property (nonatomic, strong) NetworkFetecher *networkFetecher;
@end;
@implementation EOCNetworkFetcher
- (id)initWithURL:(NSURL *)url{
if (self = [super init]) {
_url = url;
}
return self;
}
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion{
self.completionHandler = completion;
}
- (void)p_requestCompleted{
if (_completionHandler) {
_completionHandler(_downloaderData);
}
}
某个类中使用网络工具类发送请求并处理回调:
@implementation EOCClass {
EOCNetworkFetcher *_networkFetcher;
NSData *_fetcherData;
}
- (void)downloadData{
NSURL *url = [NSURL alloc] initWithString:@"/* some url string */";
_networkFetcher = [[EOCNetworkFetch alloc] initWithURL:url];
[_networkFetcher startWithCompletionHandler:^(NSData *data) {
NSLog(@"request url %@ finished.", _networkFetcher);
_fetcherData = data;
}]
}
@end;
很明显在使用block的过程中形成了循环引用:self 持有 networkFetecher;networkFetecher持有block;block持有 self。三者形成循环引用,内存泄露。
下面的例子也会造成内存泄露:
- (void)downloadData{
NSURL *url = [[NSURL alloc] initWithString:@"/* some url string */"];
NetworkFetecher *networkFetecher = [[NetworkFetecher alloc] initWithURL:url];
[networkFetecher startWithCompletionHandler:^(NSData *data){
NSLog(@"request url: %@", networkFetcher.url);
}];
}
networkFetecher持有block,block持有networkFetecher,形成内存孤岛,无法释放。
解决方案有两种:
将对象置为nil,消除引用,打破循环引用。这种方法容易漏掉某个该置nil的属性。
代码中任意地方
_networkFetecher = nil;
将强引用转换成弱引用,打破循环引用。
__weak __typeof(self) weakSelf = self;
如果想防止 weakSelf 被释放,例如当 block 回调是在子线程,block 执行的时候,主线程可能在block 执行到一半的时候就将 self 置空,所以可以再次强引用一次:
// 将强引用转换成弱引用,打破循环引用
__weak __typeof(self) weakSelf = self;
NSURL *url = [[NSURL alloc] initWithString:@"g.cn"];
_networkFetecher = [[NetworkFetecher alloc] initWithURL:url];
[_networkFetecher startWithCompletionHandler:^(NSData *data){
//如果想防止 weakSelf 被释放,可以再次强引用
__typeof(&*weakSelf) strongSelf = weakSelf;
if (strongSelf) {
//do something with strongSelf
}
}];
strongself是为了防止内存提前释放。具体原理可以假设A界面push到B界面,B界面执行Block如下:
self.title = @"B界面";
__weak typeof(self) weakSelf = self;
self.block = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", weakSelf.title);
});
};
self.block();
如果B界面10秒之后返回A界面会正常打印weakSelf.title为B界面
但如果B界面10秒之内返回A界面则会打印null,因为10秒之内返回,B界面执行dealloc销毁,内存提前销毁,B界面对应的self不存在,因此也不可能执行关于self的事项。
self.title = @"B界面";
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(self) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", strongSelf.title);
});
};
self.block();
而如果具有strongSelf,会使B界面所对应的self引用计数+1,即使10秒内返回A界面, B界面也不会立刻释放。并且strongSelf属于局部变量,存在与栈中,会随着Block的执行而销毁。
总之strongSelf就是为了保证Block中的事件执行正确。
performSelector
performSelector 就是在运行时执行一个selector。
- (id)performSelector:(SEL)selector;
[object methodName];
[object performSelector:@selector(methodName)];
如果有以下的代码:
SEL selector;
if (/* some condition */) {
selector = @selector(newObject);
} else if (/* some other condition */) {
selector = @selector(copy);
} else {
selector = @selector(someProperty);
}
id ret = [object performSelector:selector];
这段代码就相当于在动态之上再动态绑定。正是由于动态,编译器不知道即将调用的selector是什么,不了解方法签名和返回值,甚至是否有返回值都不懂,所以编译器无法用ARC的内存管理规则来判断返回值是否应该释放。因此,ARC采用了比较谨慎的做法,不添加释放操作,即在方法返回对象时就可能将其持有,从而可能导致内存泄露。
以本段代码为例,前两种情况(newObject, copy)都需要再次释放,而第三种情况不需要。这种泄露隐藏得如此之深,以至于使用static analyzer都很难检测到。如果把代码的最后一行改成:
[object performSelector:selector];
不创建一个返回值变量测试分析,简直难以想象这里居然会出现内存问题。所以如果你使用的selector有返回值,一定要处理掉。
NSNotificationcenter
这个比较常见,如果你在viewWillAppear 中监听了一个NSNotificationcenter,记得在 viewWillDisappear 中调用 removeObserver。同样的,如果你在viewDidLoad监听了一个NSNotificationcenter,记得在 dealloc 中调用 removeObserver。
不过,请注意,这个写法也是有存在隐患的,当你的类本来有内存泄漏的时候,你的类很可能不会调用dealloc 方法,所以你在 dealloc 中调用 removeObserver 可以说没什么卵用。
又或者,当你使用以下方法进行控制器的切换时:
+ (void)transitionWithView:(UIView *)view duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion;
viewWillDisappear 也不会调用,所以你想当然的想在viewWillDisappear 中调用 removeObserver 也没什么卵用。
所以正确的方式是使用上面说的方法 removeObserver 后,打 log 留意removeObserver 是否被调用到了。
NSTimer
在使用NSTimer addtarget时,为了防止target被释放而导致的程序异常,timer会持有self,所以这也是一处内存泄露的隐患。
看下面的例子:
#import "TestNSTimer.h"
@interface TestNSTimer ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation TestNSTimer
- (instancetype)init {
if (self = [super init]) {
_timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeRefresh:) userInfo:nil repeats:YES];
}
return self;
}
- (void)timeRefresh:(NSTimer*)timer {
NSLog(@"TimeRefresh...");
}
- (void)cleanTimer {
[_timer invalidate];
_timer = nil;
}
- (void)dealloc {
[super dealloc];
NSLog(@"销毁");
[self cleanTimer];
}
@end
为了防止内存泄漏,我们在delloc方法中调用cleanTimer。然而这并没有什么作用,因为TestNSTimer对象并没有正常释放,所以 delloc 无法执行,定时器仍然在无限的执行下去。当前类销毁执行dealloc的前提是定时器需要停止并滞空,而定时器停止并滞空的时机在当前类调用dealloc方法时,这样就造成了互相等待的场景,从而内存一直无法释放。
所以,如何解决?
如上的解决方案为将cleanTimer方法外漏,在外部调用,如viewDidDisappear
或viewWillDisappear
中清空计时器即可。
可是并不是特别优雅,要是其他开发者忘记调用cleanTimer,或者 viewDidDisappear
或viewWillDisappear 没有调用到,这个类就会一直存在内存泄漏,然后定时器也不会停止。
你可以参考这个链接使用这种取巧的方法[https://blog.callmewhy.com/2015/07/06/weak-timer-in-ios/]。
��所以我还是比较推荐用dispatch的timer,无须考虑这些事情:
@interface XXGCDTimer()
{
dispatch_source_t _timer;
}
@end
@implementation XXGCDTimer
-(void) startGCDTimer{
NSTimeInterval period = 1.0; //设置时间间隔
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), period * NSEC_PER_SEC, 0); //每秒执行
dispatch_source_set_event_handler(_timer, ^{
//在这里执行事件
NSLog(@"每秒执行test");
});
dispatch_resume(_timer);
}
-(void) pauseTimer{
if(_timer){
dispatch_suspend(_timer);
}
}
-(void) resumeTimer{
if(_timer){
dispatch_resume(_timer);
}
}
-(void) stopTimer{
if(_timer){
dispatch_source_cancel(_timer);
_timer = nil;
}
}
@end
具体可以使用 YYKit 中封装的 YYTimer ,和上面使用的原理一样。
非OC对象内存处理
对于一些非OC对象,使用完毕后其内存仍需要我们手动释放。举个例子,比如常用的滤镜操作调节图片亮度:
CIImage *beginImage = [[CIImage alloc]initWithImage:[UIImage imageNamed:@"yourname.jpg"]];
CIFilter *filter = [CIFilter filterWithName:@"CIColorControls"];
[filter setValue:beginImage forKey:kCIInputImageKey];
[filter setValue:[NSNumber numberWithFloat:.5] forKey:@"inputBrightness"];//亮度-1~1
CIImage *outputImage = [filter outputImage];
//GPU优化
EAGLContext * eaglContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
eaglContext.multiThreaded = YES;
CIContext *context = [CIContext contextWithEAGLContext:eaglContext];
[EAGLContext setCurrentContext:eaglContext];
CGImageRef ref = [context createCGImage:outputImage fromRect:outputImage.extent];
UIImage *endImg = [UIImage imageWithCGImage:ref];
_imageView.image = endImg;
CGImageRelease(ref);//非OC对象需要手动内存释放
在如上代码中的CGImageRef类型变量非OC对象,其需要手动执行释放操作CGImageRelease(ref),否则会造成大量的内存泄漏导致程序崩溃。其他的对于CoreFoundation框架下的某些对象或变量需要手动释放、C语言代码中的malloc等需要对应free等都需要注意。
代理未清空引起野指针
iOS 的某些API,delegate 声明为assign的,貌似是历史遗留问题。这样就会引起野指针的问题,可能会引起一些莫名其妙的crash。当一个对象被回收时,对应的delegate实体也就被回收,但是delegate的指针确没有被nil,从而就变成了游荡的野指针了。所以在delloc方法中要将对应的assign代理设置为nil,如:
- (void)dealloc{
self.myTableView.delegate = nil;
self.myTableView.dataSource = nil;
}
地图类处理
若项目中使用地图相关类,一定要检测内存情况,因为地图是比较耗费App内存的,因此在根据文档实现某地图相关功能的同时,我们需要注意内存的正确释放,大体需要注意的有需在使用完毕时将地图、代理等滞空为nil,注意地图中标注(大头针)的复用,并且在使用完毕时清空标注数组等。
- (void)clearMapView{
self.mapView.delegate =nil;
self.mapView.showsUserLocation = NO;
[self.mapView removeAnnotations:self.annotations];
[self.mapView removeOverlays:self.overlays];
[self.mapView setCompassImage:nil];
}
大次数循环内存暴涨问题
for (int i = 0; i < 100000; i++) {
NSString *string = @"Abc";
string = [string lowercaseString];
string = [string stringByAppendingString:@"xyz"];
NSLog(@"%@", string);
}
该循环内产生大量的临时对象,直至循环结束才释放,可能导致内存泄漏,解决方法为在循环中创建自己的autoReleasePool,及时释放占用内存大的临时变量,减少内存占用峰值。
for (int i = 0; i < 100000; i++) {
@autoreleasepool {
NSString *string = @"Abc";
string = [string lowercaseString];
string = [string stringByAppendingString:@"xyz"];
NSLog(@"%@", string);
}
}
检测内存泄漏
借助Xcode自带的Instruments工具(选取真机测试)
重写dealloc方法
简单暴力的重写dealloc方法,加入断点或打印判断某类是否正常释放。
dealloc 调用方式如下: 如果 a 持有对象 b ,b 持有c, c持有 d, 假设 a 是一个vc(其实只要是个对象都是一样的) 这时候 a.navigationxxxx popviewcon...... 这时候如果a “本身”没有内存泄漏,dealloc 回正常执行,
但在执行dealloc的,a 会驱使b 释放,b如果没有泄漏会执行b 的delloc 然后b 在dealloc 执行完之前首先驱使 c 释放,c 如果没有泄漏,在c的dealloc 执行完之前会首先驱使d 释放。这是整个释放链。
我们现在假设a 确实没内存泄漏,但是b有,则b 的delloc 不会执行,这样c、d的也不会执行,你就会看到,a的delloc 执行了,但是 它所持有的b, b持有的c, c 持有的d 都没有释放。
因此 单纯以一个delloc 来确定我整个类释放了是不准确的,你要保证你这个对象所有所持有的对象(系统对象应该不与考虑,即使你考虑了 系统控件/对象造成的对象你也解决不了)都执行了delloc 方法,你才可以保证的说:没有内存泄漏了。
网友评论