iOS内存管理

2018/8/19 posted in  iOS基础概念

本文不是对内存管理的应用介绍,而是对内存里一些比较难以理解的概念做重新梳理。
本文将会解决的如下疑问
1. 内存管理研究的对象(这是几个基本的问题)
2. 自动释放在ARC里理解(自动释放在ARC里似乎被大家遗忘或是被误解)
3. 自动释放池的原理实现
4. ARC里有些系统的API为什么不需要做__weak避免循环引用?
5. 为什么NSTimer容易造成内存泄漏,怎么解决?
6. 如何检查内存泄漏的实践

一:内存管理研究的对象(基本问题)

简单回忆下内存管理内容
对内存的管理采用引用计数。释放计数release,autorelese.在知道明确的释放位置时,使用release,在是自己创建当要传递给别人时,不知道明确的释放位置时加入autoreleasePool里。让系统帮着在合适的时机释放(其实这个合适的时机时在一个运行循环的结束)。当一个对象的引用计数0时,对象释放。而MARC与ARC的区别,是ARC编译器为我们做了在合适的时机加入+1与-1,当然这个工作是用了更底层的api。

比较重要的几个概念

  • 野指针
    指指针变量没有进行初始化或者指向的空间已经释放。(没进行初始化,有可能指向的内存地址就是非法的,指向的空间已经释放,这个指针也可能被其他占用)。这种情况在MRC时代很常见。在ARC很少见。因为ARC里weak会自动给释放的对象的引用设置为nil。OC里对nil发消息是没有任何反应的。比如 利用unsafe_unretained修饰的对象被释放了(这个不会主动置null)。

  • 内存泄露

    1. 在栈区对象引用置为nil。在MRC里是会内存泄露的,因为堆区的对象没有收到release。当在ARC里是不会有内存泄露的,ARC做了优化。ARC里只要没有强引用了就会释放(这块因为在看一些文章时,有的没有说明具体环境,给人造成一些误解。经过实践)
    2. 循环引用
    3. 使用C的API
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGColorRef colorRef = CGColorCreate(colorSpace, (CGFloat[]){0,0,0,0.8});
    //这里就有内存泄露。计即使在arc下,也需要释放
    //CGColorRelease(colorRef);
  • 僵尸对象
    堆中已经释放的对象retainCount = 0;

  • 空指针
    指针为nil.不指向任何一个堆区对象。

二:ARC里的Autorelease

ARC修饰符回忆
1. ARC,__weak在对象被释放时,指向它的弱引用会自动被置为nil;
2. ARC里的修饰符,__strong,__weak,__autoreleasing
如果是从MRC转过来,这样思考,__stong就是retain,__autoreleasign就是autorelease。只是编译器给我们添加了。而__weak,只是引用,不做任何操作。

当在实际的开发中,autoreleasing好像不存在似的_autoreleasing其实在ARC里也是存在的。与MRC里的autorelease作用相同如下做出解释

在ARC下,编译器会检查方法名是否以alloc/new/copy/mutableCopy开头,如果不是,则自动将返回的对象注册到autorelease pool中。

@interface RJBObject : NSObject

+ (NSString *)newHelloWorldString;
+ (NSString *)helloWorldString;

@end

@implementation RJBObject

+ (NSString *)newHelloWorldString {
    return [[NSString alloc] initWithCString:"HelloWorld" encoding:NSUTF8StringEncoding];
}

+ (NSString *)helloWorldString {
    return [[NSString alloc] initWithCString:"HelloWorld" encoding:NSUTF8StringEncoding];
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        __weak NSString *helloWorldString = [RJBObject helloWorldString];
        __weak NSString *newHelloWorldString = [RJBObject newHelloWorldString];
        
        //assigning retained object to weak variable; 
        //object will be released after assignment 

        NSLog(@"%@", helloWorldString);//输出HelloWorld
        NSLog(@"%@", newHelloWorldString);//输出null
        
    }
    return 0;
}

从上面可以看出。ARC给非创建的函数的返回值添加了自动释放池。

在一些特殊的情况下,程序员也可以手动给某些方法加上其他标记,来覆盖被编译器隐式加上的标记。
比如

函数之间如果想要传递一个对象,不仅可以通过返回值,也可以通过将一个对象

三:自动释放池

自动释放池就是一个数据结构,里面存有要被自动释放的对象的引用(实际就是搞一个存储地方即池子,标记需要在池子释放时同时释放的对象而已,那么释放时机就是池子的释放时机了),在自动释放池要释放的时候,会向这些对象发送release。

那么自动释放池释放时机?

  1. 自动释放池“自动释放”时,是在一个运行循环结束时。在与runloop联系时,是在runloop收到afterWaiting时(线程苏醒),将需要放入释放池的对象,在这个运行循环里放到这池子里面。在runloop收到beforeWaiting时(线程即将进入睡眠),将池子倒掉。

  2. 当然可以“手动释放”池子。自己创建的释放池,可以在出去(运行到{}后)池子时就释放掉。手动创建的自动释放池灵活应用可以避免内存泄露,避免不断循环并创建对象导致的内存峰值,避免可能将栈区搞溢出。

特别说明
主线程,GCD创建的线程都是会主动创建一个自动释放池的。而采用NSThread是不会主动创建一个自动释放池的。所以要特别注意,采取这种方式创建的线程要注意内存泄露。具体的实现源码AutoreleasePoolPage实现的一个双向链表实现的栈。具体的源码分析。我们将在专门的一章进行说明。

四:为什么系统的某些blockAPI不会循环引用?

如下:一
以前以为不会循环引用可能是系统做了哪些工作。哎,当初真是笨啊,糊里糊涂的死记,并没有理解其本质。还是回到循环引用的本质就能理解。不说了。

[UIViewanimateWithDuration:durationanimations:^{ 
    [self.superviewlayoutIfNeeded]; 
}];

如下:二

 dispatch_async(dispatch_get_main_queue(), ^{
        [self.navigationController pushViewController:self.blockvc animated:YES];
    });

上面两例都不会造成循环引用,根本原因就时block捕获self,当self幷不强引用block;

如下:三
NStimer的两种版本

 [NSTimer bk_scheduledTimerWithTimeInterval:10 block:^(NSTimer *timer) {
              [Weak(self) fetchChatRoomInfo];
        } repeats:YES];
- (void)viewDidLoad {
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timeTick) userInfo:nil repeats:YES];
}

- (void)timeTick {
    
}

block版本的通过设置弱引用可以防止循环引用
target版本因为设置target时将self强引用了(即计时器会保留它的target对象)。在timer处于有效期间,会一直对self持有强引用。而self又对timer有强引用。这就导致了,大家常说的NSTimer导致的内存泄漏的原因。

那么如何去解决这个问题?
首先想到的是打破环
手动调用timer的invalidate,但在程序中,我们很难保证一定就会调用到这个使定时期无效的代码。有人会想,我将无效的代码放在dealloc里不就可以保证了吗。哈哈,当什么时候调用dealloc呢,是在释放的时候。因为循环引用了,dealloc永远不会调用。

最优的思路
将NSTimer进行block化。然后用blcock的API,通过设置weak避免循环引用。就能解决了。
以下是block化的代码(实际是将NSTimer对target的强引用变成对类对象的强引用,而类对象本来就不会释放。就无所谓了。这里就给我们提供了一个绝妙的思路,在设计API时采取这种方式是不是更好呢!)

@interface NSTimer (Block)

+ (NSTimer *)rjb_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                          block:(void(^)(void))block
                                        repeats:(BOOL)repeats;


@end

@implementation NSTimer (Block)
+ (NSTimer *)rjb_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                          block:(void(^)())block
                                        repeats:(BOOL)repeats {
    return [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(rjb_blockinvoke:) userInfo:[block copy] repeats:YES];
}

+ (void)rjb_blockinvoke:(NSTimer *)timer {
    void (^block)() = timer.userInfo;
    if(block){
        block();
    }
}

@end

四:内存泄露检测的实践

下面我以我在我自己的项目中的真实记录

  1. 静态分析analyze

  2. Leak checks
    在时间线里有红色x的就是有内存泄露。下面代码是哪个泄露的函数实现

Xnip2018-08-23_15-21-18

哈哈,以我们自己的项目里,可以看出,用c的API导致的内存泄露很多,项目里的人员对于c里的内存管理不是很理解。

calloc申请的内存明显没有释放,添加free(map_chars)即可

+ (NSString *)_encryptWithString:(NSString *)source {
    NSString *materialString = [source stringByAppendingString:(NSString *)k_material];
    NSString *encryptString = [self md5WithString:materialString];
    NSUInteger len = encryptString.length;
    char *map_chars = (char *)calloc((len+1), sizeof(char));
    md5Map([encryptString UTF8String], map_chars, (int)len);
    NSString *mapString = [NSString stringWithCString:map_chars encoding:NSUTF8StringEncoding];

    return mapString;
}
vars因为是copyIvarList产生的.是非object-c对象,所以要手动释放。添加free(vars)

- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder{
    if (self = [super init]) {
        unsigned int outCount = 0;
        Ivar *vars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            Ivar var = vars[i];
            const char *name = ivar_getName(var);
            NSString *key = [NSString stringWithUTF8String:name];
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
    }
    return self;
}

五:常见的内存泄露场景

  1. NSTimer初始化时指定self为target。即self引用timer,timer引用self。
  2. block的循环引用
  3. 调用c的API忘记调用release
  4. 在通知中心里,在对象销毁前不将该对象从通知中心移除,当发送通知时,就会造成奔溃(野指针)。