OC中的内存分配
今天我们研究一下OC的内存分配,先从一段代码开始:
int a = 10;//全局变量
int c;
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
static int b = 20;//静态变量
static int d;
int e;//局部变量
int f = 20;
int g;
NSString *str = @"123";//字符串常量
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&g = %p\n&str = %p\n&obj = %p",&a,&b,&c,&d,&e,&f,&g,str,obj);
}
/**
内存地址由低到高排序:
&str = 0x102036020 //字符串常量
&a = 0x1020384a0 //已初始化全局变量
&b = 0x1020384a4 //已初始化静态变量
&d = 0x102038628 //未初始化静态变量
&c = 0x10203862c //未初始化全局变量
&obj = 0x600000576010 //堆区 alloc init 出来的对象
&g = 0x7ffeedbc9124 //栈区 局部变量 函数调用开销
&f = 0x7ffeedbc9128
&e = 0x7ffeedbc912c //先分配的 e ,地址最大
*/
@end
可以看到字符串常量的内存地址最低,局部变量的内存地址最高,并且静态变量和全局变量的内存地址是紧紧挨着的.事实上静态变量和全局变量都存放在内存中的数据段(.data区
),它们在内存中仅存在一份.并且栈内存的地址是从高到低分配,堆内存的地址是从低到高分配,所以越往后它们两只的内存地址会越来约接近.甚至堆栈溢出.
以上变量的内存关系的高低排序如下:
Tagged Pointer
从64bit开始 (iPhone5s) ,iOS引入了Tagged Pointer技术.用于优化NSNumber , NSDate , NSString
等小对象存储.
在没使用Tagged Pointer技术之前,一个NSNumber
对象会按照NSObject
存储的方式来存储:创建一个指针变量和一个NSNumber对象,然后让指针变量指向NSNumber
对象的内存地址.比如说NSNumber number = @10;
这行代码在没有使用Tagged Pointer技术之前是这样处理的:
在使用Tagged Pointer 之前,NSNumber等对象需要动态分配内存,维护引用计数等.并且从上图可以看到:一个
NSNumber
对象就需要24个字节来存储.非常的浪费性能.所以就引用了Tagged Pointer 技术.当使用 Tagged Pointer 技术之后,NSNumber 指针里面存储的数据就变成了:
Tag + Data
,也就是直接将数据存储在了指针中.我们来试一下:
NSNumber *number2 = @2;
NSNumber *number3 = @3;
NSNumber *number4 = @4;
NSNumber *number5 = @5;
NSLog(@"\n%p \n %p \n %p \n %p",number2,number3,number4,number5);
打印结果:
0xfc2f8f989ddaf722
0xfc2f8f989ddaf732
0xfc2f8f989ddaf742
0xfc2f8f989ddaf752
可以看到,2,3,4,5的值都存储在了他们的内存地址中:
值存储在了地址中
如果我们存储的值很大怎么办呢?我们试验一下:
可以看到,如果是个很大的数.当8个字节不够存储时,就不会使用Tagged Pointer技术来存储了,而是采用动态分配内存的方式来存储数据.
事实上
objc_msgSend
内部也是能够识别Tagged Pointer技术的.比如int x = [number2 intValue];
就能成功的把NSNumber转为int类型,他是怎么做到的呢?其实它的底部也是调用
objc_msgSend
的:底层调用 objc_msgSend
bjc_msgSend
内部会判断对象是不是Tagged Pointer类型,如果是Tagged Pointer类型就直接把想要的值从地址中抽取出来,不再走消息发送的流程;如果不是Tagged Pointer类型,才走正常的消息发送流程.
那我们怎么判断一个指针是否为Tagged Pointer呢?我们从runtime
源码中看答案:
rutime
是这么判断是否为Tagged Pointer指针的:
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
就是拿到指针后直接 & _OBJC_TAG_MASK
,最后判断是不是等于_OBJC_TAG_MASK
,我们再来看看_OBJC_TAG_MASK
是什么:
#if TARGET_OS_OSX && __x86_64__ 如果是x86架构
// 64-bit Mac - tag bit is LSB
# define OBJC_MSB_TAGGED_POINTERS 0 为0
#else
// Everything else - tag bit is MSB
# define OBJC_MSB_TAGGED_POINTERS 1 为1
#endif
#if OBJC_MSB_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63) //如果是iOS环境 _OBJC_TAG_MASK 就是1 左移 63位
#else
# define _OBJC_TAG_MASK 1UL // if TARGET_OS_OSX && __x86_64__ 如果是x86架构 _OBJC_TAG_MASK就是1
#endif
如果是iOS环境_OBJC_TAG_MASK
就是1左移63位,其实就是判断指针的最高位是不是等于1;如果是MAC环境_OBJC_TAG_MASK
就是1,其实就是判断指针的最低有效位是不是等于1.另外,堆空间对象的地址的最后一位肯定是0,因为内存对齐是16个字节对齐.
练习
下面代码,运行结果是什么?
@interface ViewController ()
@property (nonatomic,copy)NSString *name;
@end
------------------------------------------------------------------------------------
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i ++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abcdefghigk"];
});
}
崩溃
为什么会崩溃呢?原因很简单,我们调用的
self.name
本质就是:
- (void)setName:(NSString *)name{
if (_name != name) {
[_name rease];
_name = [name copy];
}
}
即使是ARC环境,但是ARC环境本质仍然是MRC转换来的.所以报错的原因就是[_name rease]
这行代码.当有多个线程同时执行到这行代码时就会报错,一条线程刚把name
释放掉,另一条线程又来释放一边所以就出现坏内存访问.
第一种解决解决办法就是使用atomic
.
第二种解决办法就是在setter
方法调用的前后加锁,解锁.
我们再换一种写法测试一下:
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"];
});
}
运行一下发现并没有报错.我们只是把abcdefghigk
换成了abc
而已,为什么会这样呢?因为abc
使用Tagged Pointer技术存储,而Tagged Pointer赋值是把值直接放到内存地址中,并没有什么setter
方法,也没有什么rease
方法,所以不会报错.