内存管理(二)
iOS程序的内存布局
简而言之,就是一张图:
当然,一般我们也可以把内存分为五大区域:
方法区(程序代码区)、常量区、静态区(全局区)、堆、栈
可以看出,上图中的数据段包含了五大区域中的常量区和静态区。
其实质是一样的,只是叫法不一样。
从打印结果来看,相同的字符串是同一个地址。新建的str1,str2地址不同,但是指向的地址是一样的。
举个例子:
#import "ViewController.h"
int a = 10;//初始化的全局变量,存储在全局区(静态区)
int b;//未初始化的全局变量,存储在全局区(静态区)
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
static int c = 5;//初始化的静态变量,存储在全局区(静态区)
static int d;//未初始化的静态变量,存储在全局区(静态区)
int e;//局部变量,存储在栈,不管有没有初始化
int f = 20;//局部变量,存储在栈,不管有没有初始化
//存储在数据段里面的 常量区
NSString *str1 = @"123";
NSString *str2 = @"123";
//obj是一个指针变量,存储在栈;其里面的内容是[[NSObject alloc] init]的地址
//[[NSObject alloc] init]是一个对象,存储在堆中;其地址存储在obj指针变量中;
//obj取到的是指针变量内部存储的值,也就是[[NSObject alloc] init]的地址
//&obj取到的是指针变量本身的地址
NSObject *obj = [[NSObject alloc] init];
NSLog(@"\n&a = %p \n&b = %p \n&c = %p \n&d = %p \n&e = %p \n&f = %p \n&str1 = %p \n&str2 = %p \n&obj = %p", &a, &b, &c, &d, &e, &f, &str1, &str2, &obj);
}
/**
字符串常量
&str2 = 0x7ffee00d5970
&str1 = 0x7ffee00d5978
已初始化的全局变量、静态变量
&a = 0x10fb29d98
&c = 0x10fb29d9c
未初始化的全局变量、静态变量
&d = 0x10fb29e60
&b = 0x10fb29e64
栈
&obj=0x7ffee00d5968
栈,先大后小
&e = 0x7ffee00d5984
&f = 0x7ffee00d5980
*/
@end
结果:
&a = 0x10fb29d98
&b = 0x10fb29e64
&c = 0x10fb29d9c
&d = 0x10fb29e60
&e = 0x7ffee00d5984
&f = 0x7ffee00d5980
&str1 = 0x7ffee00d5978
&str2 = 0x7ffee00d5970
&obj = 0x7ffee00d5968
该例子很好的证明了顶图中关于内存的分配问题。
Tagged Pointer
tagged:标记
Pointer:指针
从64位开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储。
在没有使用Tagged Pointer之前,NSNumber等对象需要动态分配内存、维护引用计数器等。
NSNumber指针存储的是堆中NSNumber对象的地址值。
代码NSNumber *number = [NSNumber numberWithInt:10];的内存存储形式如下:
number指针在64位上占8个字节;
number指针指向的对象至少占16个字节;
也就是一共至少占用24个字节。
而,我们只是要存储一个int类型的值,int类型的值只需要4个字节。
因此,为了节省空间,有了Tagged Pointer概念。
题外话:int的存储
{
NSObject *obj = [[NSObject alloc] init];
NSObject *obj2 = [[NSObject alloc] init];
NSLog(@"%d, %d", malloc_size((__bridge const void *)(obj)), malloc_size((__bridge const void *)(obj2)));
NSLog(@"%p, %p", &obj, &obj2);
int a = 10;
int b = 12;
NSLog(@"%d, %d", sizeof(a), sizeof(b));
NSLog(@"%p, %p", &a, &b);
}
结果:
16, 16
0x7ffee973cfa8, 0x7ffee973cfa0
4, 4
0x7ffee973cf9c, 0x7ffee973cf98
因为是局部变量,根据前面学习的可以知道,是存储在栈,并且栈是从大到小存储,打印结果也验证了结果。
obj与obj2是指针,指针占8个字节,也就是
0x7ffee973cfa8开始,往下数8个字节,用于存储obj的数据。
0x7ffee973cfa0开始,往下数8个字节,用于存储obj2的数据。
a和b是int类型,占4个字节
0x7ffee973cf9c开始,往下数4个字节,用于存储a的数据。
0x7ffee973cf98开始,往下数4个字节,用于存储b的数据。
使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag+Data(标记+值),也就是将数据直接存储在了指针中。
Tag标记是为了区分是NSString或者NSNumber等类型。
如何判断一个指针是否为Tagged Pointer?
在iOS平台,最高有效位是1(第64bit)
在Mac平台,最低有效位是1
在Mac平台:
NSNumber *num1 = [NSNumber numberWithInt:3];
NSNumber *num2 = [NSNumber numberWithInt:7];
NSNumber *num3 = @(0xFFFFFFFFFFFFFFFF);
NSLog(@"%p, %p, %p", num1, num2, num3);
NSLog(@"%@, %@, %@", [num1 class], [num2 class], [num3 class]);
结果:
0x46a004c55674e085, 0x46a004c55674e485, 0x101804d50
__NSCFNumber, __NSCFNumber, __NSCFNumber
NSNumber *num1 = [NSNumber numberWithInt:3];
NSNumber *num2 = [NSNumber numberWithInt:7];
NSNumber *num3 = @(0xFFFFFFFFFFFFFFFF);
NSLog(@"%p, %p, %p", num1, num2, num3);
NSLog(@"%@, %@, %@", [num1 class], [num2 class], [num3 class]);
结果:
0x46a004c55674e085, 0x46a004c55674e485, 0x101804d50
__NSCFNumber, __NSCFNumber, __NSCFNumber
NSNumber *num1 = [NSNumber numberWithInt:3];
NSNumber *num2 = [NSNumber numberWithInt:7];
NSNumber *num3 = @(0xFFFFFFFFFFFFFFFF);
NSLog(@"%p, %p, %p", num1, num2, num3);
NSLog(@"%@, %@, %@", [num1 class], [num2 class], [num3 class]);
结果:
0x84a7f9ace2ec63f7, 0x84a7f9ace2ec63b7, 0x60000335a740
__NSCFNumber, __NSCFNumber, __NSCFNumber
前面两个地址,第一位是8,转换为2进制是0b1000,也就是第一位有效位是1,因此,前两个是Tagged Pointer类型。
后一个指针,第一位是6,转为为2进制是0b0100,第一位是0,因此不是Tagged Pointer类型。是指针类型。
为何要区分平台,这是因为,源码就是这么做的,查看源文件
最后跟踪到:
#if TARGET_OS_OSX && __x86_64__//Mac上
// 64-bit Mac - tag bit is LSB
# define OBJC_MSB_TAGGED_POINTERS 0
#else//iOS上
// Everything else - tag bit is MSB
# define OBJC_MSB_TAGGED_POINTERS 1
#endif
#if OBJC_MSB_TAGGED_POINTERS//iOS
# define _OBJC_TAG_MASK (1UL<<63)
#else//Mac
# define _OBJC_TAG_MASK 1UL
#endif
也就是,Mac是找的最后一位。iOS上找的是第一位。
在Mac平台,可以使用
BOOL isTaggedPointer(id pointer)
{
return (long)(__bridge void *)pointer & 1;
}
判断是否是Tagged Pointer类型。
当指针不够存储数据时,才会使用动态分配内存的方式来存储数据。
也就是数据太大时,还是按之前的方法存储数据。
objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销。
比如:
NSNumber *num1 = [NSNumber numberWithInt:3];
[num1 intValue];
其实是转化为:
objc_msgSend(num1, @selector(intValue));
一般情况下,在objc_msgSend方法中,根据num1的isa指针,找到类对象,去类对象的方法列表中寻找intValue方法。
其实,objc_msgSend方法,在执行的时候,系统可以识别出是Tagged Pointer类型,还是指针。然后根据不同的类型,做响应的操作。
面试题
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i<1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abcdefghijk"];
});
}
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i<1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abc"];
});
}
两者运行结果有何不同?
首先看self.name = [NSString stringWithFormat:@"abcdefghijk"];
崩溃,并且崩溃在objc_release的地方。
是什么原因导致崩溃的呢?
我们知道,
self.name = [NSString stringWithFormat:@"abcdefghijk"];
其实是调用了
[self setName:[NSString stringWithFormat:@"abcdefghijk"]];
而setName:的实现是:
- (void)setName:(NSString *)name
{
if (_name != name) {
[_name release];//老的释放掉
_name = [name copy];//传入的值copy后赋值给_name
}
}
由于是async异步操作,self.name = [NSString stringWithFormat:@"abcdefghijk"];即[_name release];有可能会被多条线程同时操作。导致,线程n把_name释放掉,线程n+1又要执行_name的释放,从而造成_name已经被释放两次,第二次访问的时候,_name已经释放过,造成坏内存访问。
解决方法一:atomic
@property (copy, atomic) NSString *name;
从而:
- (void)setName:(NSString *)name
{
//加锁操作
if (_name != name) {
[_name release];
_name = [name copy];
}
//解锁操作
}
解决方法二:
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i<1000; i++) {
dispatch_async(queue, ^{
//加锁
self.name = [NSString stringWithFormat:@"abcdefghijk"];
});
//解锁
}
self.name = [NSString stringWithFormat:@"abc"];
为何没有崩溃呢?
从类型可以看出来,
内容多的name类型是__NSCFString
内容少的name类型是NSTaggedPointerString
这就是原因所在。
内容少的name,由于类型是NSTaggedPointerString,在赋值的时候
是直接在指针里面取值,而不需要release操作,因此,不会崩溃。