1. Objective-C 内存办理办法
Objective-C 运用引证计数来办理内存。每个目标都有一个相关的引证计数。Objective-C 供给了以下办法来办理目标的引证计数:
-
alloc:分配内存并将引证计数设置为 1。
-
retain:添加引证计数。
-
release:削减引证计数。
-
autorelease:将目标添加到当时的 autorelease pool,稍后主动开释。
手动办理内存(MRC)的一般进程是:
-
当创立一个新的目标时,初始的引证计数为1.
-
为确保目标的存在,每当创立一个引证到该目标时,经过给目标发送 retain 音讯,为引证计数加1.
-
当不再需求目标时,经过给目标发送 release 音讯,为引证计数减1.
-
当目标的引证计数为0时,系统就知道这个目标不再运用了,经过给目标发送 dealloc 音讯,毁掉目标并收回内存。
而在主动引证计数(ARC)环境下,编译器会主动刺进恰当的 retain 和 release 调用,从而简化内存办理。
2. 循环引证
引证计数这种办理内存的办法尽管很简略,可是有一个比较大的瑕疵,即它不能很好的处理循环引证问题。
循环引证是指两个或多个目标彼此引证,导致它们的引证计数永久不会变为 0. 这会导致内存走漏。
如下图所示:目标 A 和目标 B,彼此引证了对方作为自己的成员变量,只要当自己毁掉时,才会将成员变量的引证计数减 1。因为目标 A 的毁掉依赖于目标 B 毁掉,而目标 B 的毁掉与依赖于目标 A 的毁掉,这样就造成了咱们称之为循环引证(Reference Cycle)的问题。
处理循环引证问题主要有两个办法,第一个办法是我清晰知道这儿会存在循环引证,在合理的位置主动断开环中的一个引证,使得目标得以收回。
更常见的办法是运用弱引证。弱引证尽管持有目标,可是并不添加引证计数。
例如,在运用 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 目标时,可能会导致溃散的状况。
此时,假如不一起确保这两个赋值操作的原子性,就有概率导致 *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.
在 Leaks 东西中运转代码,咱们看到东西检测到了咱们的循环引证。
并且 Leaks 以图形办法展示了循环引证的两个目标。一起,发生循环引证的堆栈也能够在右侧展开,方便咱们定位问题代码。
Memory Graph Debugger
Leaks 检测的是一个相当于打包后的 app,假如要在运转时调试,则需求借助 Memory Graph Debugger.
Memory Graph Debugger 是 Xcode 中的一个实用东西,能够协助咱们查看应用程序的内存图,找出循环引证和其他内存问题。
要运用 Memory Graph Debugger,首先在 Xcode 中运转应用程序,然后点击 Debug Navigator 中的 Memory Graph 图标。
Memory Graph Debugger 会显示应用程序的内存目标图,包括目标之间的引证联系。咱们能够经过底部挑选按钮,直接挑选出内存走漏的目标。
点击列表上的某一个目标,Memory Graph Debugger 也会用图形展示两个目标循环引证的联系。
有了这些东西,当咱们开发完一个功用后,能够经过他们检测一下代码,能有效防止循环引证、内存走漏等问题。
6. 代码标准
在 Xcode 东西的协助外,良好的代码标准也能够防止一些常见的内存问题。
在编写代码时,应该遵从一定的标准和约定,如及时开释不再需求的目标、block 防止循环引证、delegate 运用 weak 润饰等。
另外,咱们也能够调查苹果官方框架的写法,了解并模仿,同样能够防止许多内存问题。
7. 总结
内存办理是 Objective-C 编程中的一个重要主题。本文介绍了内存走漏、野指针溃散等问题,以及怎么运用内存剖析东西进行优化。要防止这些问题,咱们需求了解 Objective-C 的内存办理办法,注意循环引证和多线程操作目标的问题,并善于运用内存剖析东西。经过这些办法,咱们能够编写出更高效、更安稳的 Objective-C 代码。