iOS 底层分析

OC中的内存分配

2019-12-17  本文已影响0人  小心韩国人

今天我们研究一下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之前
在使用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方法,所以不会报错.

上一篇下一篇

猜你喜欢

热点阅读