在Objective-C中,内存管理是一个重要的问题。为了解决这个问题,编译器提供了一些特殊的关键字和函数来帮助我们管理内存。其中,objc_autoreleaseReturnValue和objc_retainAutoreleasedReturnValue是两个常用的关键字。
1. objc_autoreleaseReturnValue:这个关键字用于将方法的返回值自动释放。当一个对象被创建时,它会被分配一段内存。当这个对象不再被使用时,这段内存就会被垃圾回收器回收。但是,如果这个对象在方法中被创建,然后又被返回,那么这段内存就不会被立即回收,而是会等待垃圾回收器下次运行时才被回收。为了避免这种情况,我们可以使用objc_autoreleaseReturnValue关键字。这个关键字会在方法返回时自动调用对象的release方法,从而释放这段内存。
2. objc_retainAutoreleasedReturnValue:这个关键字与objc_autoreleaseReturnValue类似,但是它的作用是在方法返回之前就释放内存。这意味着,如果你的方法中有多个return语句,那么每个return语句都会立即释放内存,而不是等到所有return语句都执行完毕后才统一释放。这样可以提高程序的性能。
总之,objc_autoreleaseReturnValue和objc_retainAutoreleasedReturnValue是Objective-C中用于管理内存的重要工具。通过合理地使用这两个关键字,我们可以有效地避免内存泄漏和其他内存管理问题。
首先,我们来分析一下这两种方法的区别。initWithFormat和stringWithFormat都是NSString的初始化方法,但它们的实现方式有所不同。
1. 实例方法创建对象:
```objc
NSString *str = [[NSString alloc] initWithFormat:@"111111111111"];
```
这段代码首先调用了alloc方法分配内存,然后调用initWithFormat方法进行初始化,最后释放内存。
2. 类方法对象:
```objc
NSString *str = [NSString stringWithFormat:@"111111111111"];
```
这段代码首先调用了stringWithFormat方法,然后自动释放返回的对象。
接下来,我们来看一下这两种方法在内存分配上的区别。当我们使用initWithFormat方法时,会先调用alloc方法分配内存,然后再调用initWithFormat方法进行初始化。在这个过程中,内存会不断增加。而当我们使用stringWithFormat方法时,会直接调用initWithFormat方法进行初始化,内存几乎没有变化。这是因为stringWithFormat方法内部实现了一个优化过的版本,它会在内部预先分配好足够的内存空间,然后再调用initWithFormat方法进行初始化。这样一来,内存的使用就变得更加高效了。
(instancetype)stringWithFormat:(NSString *)format {
/*编译器的模拟代码*/
id obj = objc_msgSend(NSString,@selector(alloc)); // 创建一个NSString对象
objc_msgSend(obj,@selector(initWithFormat:), @"111111111111"); // 用指定的格式初始化NSString对象
// 使用objc_retainAutoreleasedReturnValue函数来保留返回值的引用计数,并使用objc_autoreleaseReturnValue函数将其转换为自动释放的对象
id retainedObj = objc_retainAutoreleasedReturnValue(obj);
id autoReleasedObj = objc_autoreleaseReturnValue(retainedObj);
return autoReleasedObj; // 返回自动释放的对象
}
```
// Function to return objc_autoreleaseReturnValue(id obj)
id objc_autoreleaseReturnValue(id obj) {
// Try to prepare for optimized return with the given disposition (+0 or +1).
// Returns true if the optimized path is successful.
// Otherwise, the return value must be retained and/or autoreleased as usual.
static ALWAYS_INLINE bool prepareOptimizedReturn(ReturnDisposition disposition) {
ASSERT(getReturnDisposition() == ReturnAtPlus0);
// If caller accepts optimized return and disposition is provided
if (callerAcceptsOptimizedReturn(__builtin_return_address(0)) && disposition) {
setReturnDisposition(disposition);
return true;
}
return false;
}
if (prepareOptimizedReturn(ReturnAtPlus1)) {
return obj;
} else {
return objc_autorelease(obj);
}
}
// Define enum for Return Disposition: True if at+1, False otherwise.
enum ReturnDisposition : bool {
ReturnAtPlus0 = false, ReturnAtPlus1 = true };
// Getter function for Return Disposition.
static ALWAYS_INLINE ReturnDisposition getReturnDisposition() {
return (ReturnDisposition)(uintptr_t)tls_get_direct(RETURN_DISPOSITION_KEY);
}
// Setter function for Return Disposition.
static ALWAYS_INLINE void setReturnDisposition(ReturnDisposition disposition) {
tls_set_direct(RETURN_DISPOSITION_KEY, (void*)(uintptr_t)disposition);
}
```
在ARC中,原本对象生成之后是要注册到autoreleasepool中的。但是,非alloc/copy/mutableCopy/new开头的方法,使用了objc_retainAutoreleasedReturnValue函数返回注册到autorelease中的对象。这是因为它会检查使用该函数的方法或函数调用方执行命令列表,如果紧接着调用了方法或函数后紧接着调用objc_retainAutoreleasedReturnValue()函数,两者成对出现,编译器会做优化,使函数最优化程序运行,不将返回的对象注册到autoreleasePool中。
为了验证这个优化过程,我们可以创建一个代码示例:
```objective-c
id objc_retainAutoreleasedReturnValue(id obj) { if (acceptOptimizedReturn() == ReturnAtPlus1) { return obj; } return objc_retain(obj); }
static ALWAYS_INLINE ReturnDisposition acceptOptimizedReturn() { ReturnDisposition disposition = getReturnDisposition(); setReturnDisposition(ReturnAtPlus0); // reset to the unoptimized state return disposition; }
@interface MyClass : NSObject
@property (strong, nonatomic) id myObject;
@end
@implementation MyClass
@synthesize myObject;
- (void)doSomethingWithObjcRetainAutoreleasedReturnValue {
@autoreleasepool {
id obj = [self doSomething];
id retainedObj = objc_retainAutoreleasedReturnValue(obj);
}
}
- (id)doSomething {
return [[MyClass alloc] init];
}
@end
```
在这个示例中,我们创建了一个名为MyClass的类,其中有一个名为myObject的属性。我们在doSomethingWithObjcRetainAutoreleasedReturnValue方法中使用objc_retainAutoreleasedReturnValue函数返回一个已注册到autoreleasepool中的对象。然后,在doSomething方法中创建一个新的MyClass实例并将其赋值给myObject属性。最后,在doSomethingWithObjcRetainAutoreleasedReturnValue方法中调用objc_retainAutoreleasedReturnValue函数,传入myObject属性作为参数。
最上面第三段代码中5个字符串打印的地址之所以有如此大的差异,是因为它们的存储位置不同。其中,str1是在堆区;占用内存较大的字符串、且通过类生成的,不能再编译阶段确定,会放到堆区中,如果重复使用类初始化多个相同的字符串,但地址不一定相同。str2和str3是tagged Pointer;占用内存较小的字符串、且通过类生成的,会用tagged Pointer技术存储。具体可以看下面 。
内存分区如下:bss段( bss segment、 全局区)通常是指用来存放程序中未初始化的全局变量和静态变量的一块内存区域。如果不初始化全局变量和静态变量,编译器也会对它们进行一个隐式初始化(直接赋值就是显示初始化),赋给它们一个缺省值。BSS段在程序执行之前会清0,所以未初始化的全局变量(静态变量)已经是0了。所以这种情况还是存放在BSS段,一旦初始化就会从BSS段中回收掉,转存到data段(数据段)中。bss区-Block Started by Symbol(未初始化数据段):并不给该段的数据分配空间,仅仅是记录了数据所需空间的大小。数据段(data segment、常量区):存放程序已经初始化过的全局变量和静态变量 。
数据段分为只读数据段(常量区)和读写数据段。通常,只读数据段用于存放程序中已经初始化的全局变量和静态变量的一块内存区域。这些变量在结束程序时才会被收回。而读写数据段则用于存放程序执行代码的内存区域,这部分区域的大小在程序运行前就已经确定,并且通常属于只读。某些架构也允许代码段为可写,即允许修改程序。
在代码段中,还可能包含一些只读的常数变量,例如字符串常量等。这些常量放在只读数据段(data segment)中,也有叫做常量区的说法。堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可以动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上;当利用free等函数释放内存时,被释放的内存从堆中被剔除。堆向高地址扩展的数据结构是不连续的内存区域。程序员负责在何时释放内存(如用free或delete),在iOS的ARC程序中,系统自动管理计数器,计数器为0的时候,在当次的runloop结束后,释放掉内存。堆中的所有东西都是匿名的,这样不能按名字访问,而只能通过指针访问。
频繁的new/delete操作会造成内存空间的不连续性,从而产生大量的碎片,降低程序效率。栈又称堆栈,是用户存放程序临时创建的局部变量的地方。我们可以在函数括弧“{}”中定义变量(但不包括static声明的变量),这些变量会被压入发起调用的进程栈中。当函数被调用时,其参数也会被压入栈中。函数调用结束后,返回值会被存放回栈中。由于栈的后进先出特点,栈特别适合用来保存/恢复调用现场。因此,我们可以把堆栈看成一个寄存、交换临时数据的内存区。指针都存在栈区,用于指向分配在堆区的内存的地址(待验证)。
我们先来看看原有的对象为什么会浪费内存。假设我们要存储一个 NSNumber 对象,其值是一个整数。正常情况下,如果这个整数只是一个 NSInteger 的普通变量,那么它所占用的内存是与 CPU 的位数有关,在 32 位 CPU 下占 4 个字节,在 64 位 CPU 下是占 8 个字节的。而指针类型的大小通常也是与 CPU 位数相关,一个指针所占用的内存在 32 位 CPU 下为 4 个字节,在 64 位 CPU 下也是 8 个字节。
为了解决这个问题,苹果引入了 Tagged Pointer。Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate。它的内存并不存储在堆中,也不需要使用malloc和free进行内存管理。这样一来,程序在存储和访问这些小对象时就不再需要额外的逻辑,从而提高了运行效率。
在WWDC2013的《Session 404 Advanced in Objective-C》视频中,苹果对Tagged Pointer的特点进行了介绍:Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。因此,Tagged Pointer在内存读取上有着3倍的效率,创建时比以前快106倍。
通过引入Tagged Pointer,苹果成功地减少了64位机器下程序的内存占用,并提高了运行效率。这完美地解决了小内存对象在存储和访问效率上的问题。