1. Objective-C 内存办理办法

Objective-C 运用引证计数来办理内存。每个目标都有一个相关的引证计数。Objective-C 供给了以下办法来办理目标的引证计数:

  • alloc:分配内存并将引证计数设置为 1。

  • retain:添加引证计数。

  • release:削减引证计数。

  • autorelease:将目标添加到当时的 autorelease pool,稍后主动开释。

手动办理内存(MRC)的一般进程是:

  • 当创立一个新的目标时,初始的引证计数为1.

  • 为确保目标的存在,每当创立一个引证到该目标时,经过给目标发送 retain 音讯,为引证计数加1.

  • 当不再需求目标时,经过给目标发送 release 音讯,为引证计数减1.

  • 当目标的引证计数为0时,系统就知道这个目标不再运用了,经过给目标发送 dealloc 音讯,毁掉目标并收回内存。

Objective-C 内存圈套
而在主动引证计数(ARC)环境下,编译器会主动刺进恰当的 retain 和 release 调用,从而简化内存办理。

2. 循环引证

引证计数这种办理内存的办法尽管很简略,可是有一个比较大的瑕疵,即它不能很好的处理循环引证问题。

循环引证是指两个或多个目标彼此引证,导致它们的引证计数永久不会变为 0. 这会导致内存走漏。

如下图所示:目标 A 和目标 B,彼此引证了对方作为自己的成员变量,只要当自己毁掉时,才会将成员变量的引证计数减 1。因为目标 A 的毁掉依赖于目标 B 毁掉,而目标 B 的毁掉与依赖于目标 A 的毁掉,这样就造成了咱们称之为循环引证(Reference Cycle)的问题。

Objective-C 内存圈套
处理循环引证问题主要有两个办法,第一个办法是我清晰知道这儿会存在循环引证,在合理的位置主动断开环中的一个引证,使得目标得以收回。

更常见的办法是运用弱引证。弱引证尽管持有目标,可是并不添加引证计数。

例如,在运用 block 时,假如 ViewController 有 block 成员变量,而 block 捕获了 self,就会导致循环引证。

@property (nonatomic, copy) void (^myBlock)(void);
self.myBlock = ^{
    [self updateUI];
};

处理办法是运用 __weak 润饰符创立一个弱引证:

@property (nonatomic, copy) void (^myBlock)(void);
__weak typeof(self) weakSelf = self;
self.myBlock = ^{
    [weakSelf updateUI];
};

3. 多线程操作目标的溃散

在刚刚接触 iOS 开发的时候,咱们知道特点的默许原子性是 atomic. 可是 atomic 在保障 getter、setter 操作原子性的一起,会影响功用。所以一般状况下,咱们要将特点声明为 nonatomic.

而在多线程环境下,假如多个线程一起修正同一个 nonatomic 目标,可能会导致溃散。

咱们能够经过 runtime 源码的 setter 函数,来一探终究。

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }
    id oldValue;
    id *slot = (id*) ((char*)self + offset);
    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }
    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }
    objc_release(oldValue);
}

这儿咱们重视20行开端的关于 nonatomic/atomic 的不同:

当特点的原子性为 atomic 时,会对特点赋值操作加入锁,以此保障多线程状况下的写操作的安全,一起也会导致功用的丢失。

多次 release 原始值

现在咱们结合源码来看一下,两个线程一起修正同一个 nonatomic 目标时,可能会导致溃散的状况。

Objective-C 内存圈套
此时,假如不一起确保这两个赋值操作的原子性,就有概率导致 *slot 中的原始值被 release 两次,而这样就会导致 crash 的发生。

在这种场景下,能够运用 atomic 来润饰特点,以确保安稳性。

atomic 是全能药吗?

当然不是。咱们看下以下代码。

@property (atomic, strong) NSArray* array;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // thread 1
    for (int i = 0; i < 10000; i ++) {
        if (i % 2) {
            self.array = @[@(1), @(2), @(3)];
        } else {
            self.array = @[@(1)];
        }
    }
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // thread 2
    for (int i = 0; i < 10000; i ++) {
        if (self.array.count == 3) {
            NSLog(@"object at index 2: %@", [self.array objectAtIndex:2]);
        }
    }
});

即使咱们将 array 的原子性设置为 atomic,一起在拜访 objectAtIndex: 之前加上判别,thread 2 仍是会 crash. 原因是因为17、18两行代码之间 array 所指向的内存区域被 thread 1 修正了。

atomic 经过加锁确保了关于特点 getter、setter 操作的原子性。getter、setter 操作的是特点的指针值,关于特点指针所指向的内存地址并不能起到维护作用。

为了防止这种状况,能够运用锁(如 `@synchronized`、`NSLock` 等)来确保同一时间只要一个线程能够拜访目标。

例如,运用 `@synchronized` 维护上述数组的拜访:

@property (nonatomic, strong) NSArray* array;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // thread 1
    for (int i = 0; i < 10000; i ++) {
        @synchronized (self) {
            if (i % 2) {
                self.array = @[@(1), @(2), @(3)];
            } else {
                self.array = @[@(1)];
            }
        }
    }
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // thread 2
    for (int i = 0; i < 10000; i ++) {
        @synchronized (self) {
            if (self.array.count == 3) {
                NSLog(@"object at index 2: %@", [self.array objectAtIndex:2]);
            }
        }
    }
});

4. try-catch 内存走漏

在 Objective-C 中,反常处理(try-catch)其实并不常用。原因之一是 try-catch 不能捕获 OC 中的许多反常,比方第3节说的多线程 crash。另外一个原因,是可能会导致内存走漏。

咱们看下内存走漏的比如。

先定义一个简略的类,在目标开释时,打印 dealloc.

@implementation TestObject
- (void)dealloc {
    NSLog(@"dealloc TestObject");
}
@end

在 ViewController.m 中编写 try-catch,并在 @try 中创立 TestObject 目标,并主动抛出反常。

@try {
    NSLog(@"@try");
    TestObject* obj = [[TestObject alloc] init];
    @throw [[NSException alloc] initWithName:@"TestExceptionName" reason:@"TestExceptionReason" userInfo:nil];
} @catch (NSException *exception) {
    NSLog(@"@catch exception: %@", exception);
} @finally {
    NSLog(@"@finally");
}

履行后打印日志如下。

OcMemoryDemo[99627:1472760] @try
OcMemoryDemo[99627:1472760] @catch exception: TestExceptionReason
OcMemoryDemo[99627:1472760] @finally

日志没有打印 `dealloc TestObject`,说明 @try 中创立的 TestObject 目标没有被开释。

第1节中咱们了解到,alloc 会设置目标引证计数初始值为1. 因此 TestObject 目标内存没有被开释的原因,肯定是对应的 release 没有履行。

咱们把 try-catch 的比如代码还原为 MRC 的形式,就了解了。

@try {
    NSLog(@"@try");
    TestObject* obj = [[TestObject alloc] init];
    @throw [[NSException alloc] initWithName:@"TestExceptionName" reason:@"TestExceptionReason" userInfo:nil];
    [obj release];
} @catch (NSException *exception) {
    NSLog(@"@catch exception: %@", exception);
} @finally {
    NSLog(@"@finally");
}

可见,因为 TestObject 是 @try 中的局部变量,编译器会在 @try 最下面主动添加 release 代码。因为 @try 在 release 履行前就抛出了反常,所以 TestObject 目标的引证计数没有减1.

要破解这个问题,有两个办法。

第一个办法是不在 @try 中声明局部变量。@try 中需求用到的变量,都在 @try 外部都声明好。这种办法需求注意,@try 中尽量不要调用其他函数,不然调用链一旦深化,就很难控制其他局部变量的开释。

第二个办法,咱们能够经过给 ViewController.m 文件加上 -fobjc-arc-exceptions 参数来进行修复,防止出现内存走漏。可是这会导致编译器添加碎片逻辑用于开释内存,简略的状况,可能会多出一倍的汇编代码量;关于复杂状况,编译器会刺进更多的无用代码,导致生成的二进制代码变得很大,所以要慎用。

5. 内存剖析东西

针对 OC 内存办理,Xcode 供给了强壮的内存剖析东西,能够协助咱们检测和处理内存走漏和循环引证问题,如 Instruments 和 Memory Graph Debugger.

Instruments

Instruments 是一个强壮的功用剖析东西箱,能够用来检测内存走漏、循环引证等问题。

首先编写一段简略的测验代码。该代码中的 firstArray 和 secondArray 彼此引证了对方,构成了循环引证。

NSMutableArray* firstArray = [NSMutableArray array];
NSMutableArray* secondArray = [NSMutableArray array];
[firstArray addObject:secondArray];
[secondArray addObject:firstArray];

要运用 Instruments 来检测循环引证,首先在 Xcode 中挑选 Product > Profile,然后挑选 Leaks.

Objective-C 内存圈套
在 Leaks 东西中运转代码,咱们看到东西检测到了咱们的循环引证。

Objective-C 内存圈套
并且 Leaks 以图形办法展示了循环引证的两个目标。一起,发生循环引证的堆栈也能够在右侧展开,方便咱们定位问题代码。

Memory Graph Debugger

Leaks 检测的是一个相当于打包后的 app,假如要在运转时调试,则需求借助 Memory Graph Debugger.

Memory Graph Debugger 是 Xcode 中的一个实用东西,能够协助咱们查看应用程序的内存图,找出循环引证和其他内存问题。

要运用 Memory Graph Debugger,首先在 Xcode 中运转应用程序,然后点击 Debug Navigator 中的 Memory Graph 图标。

Objective-C 内存圈套
Memory Graph Debugger 会显示应用程序的内存目标图,包括目标之间的引证联系。咱们能够经过底部挑选按钮,直接挑选出内存走漏的目标。

Objective-C 内存圈套
点击列表上的某一个目标,Memory Graph Debugger 也会用图形展示两个目标循环引证的联系。

Objective-C 内存圈套
有了这些东西,当咱们开发完一个功用后,能够经过他们检测一下代码,能有效防止循环引证、内存走漏等问题。

6. 代码标准

在 Xcode 东西的协助外,良好的代码标准也能够防止一些常见的内存问题。

在编写代码时,应该遵从一定的标准和约定,如及时开释不再需求的目标、block 防止循环引证、delegate 运用 weak 润饰等。

另外,咱们也能够调查苹果官方框架的写法,了解并模仿,同样能够防止许多内存问题。

7. 总结

内存办理是 Objective-C 编程中的一个重要主题。本文介绍了内存走漏、野指针溃散等问题,以及怎么运用内存剖析东西进行优化。要防止这些问题,咱们需求了解 Objective-C 的内存办理办法,注意循环引证和多线程操作目标的问题,并善于运用内存剖析东西。经过这些办法,咱们能够编写出更高效、更安稳的 Objective-C 代码。