OC中的内存分配

作者: 小心韩国人 | 来源:发表于2019-12-17 23:31 被阅读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方法,所以不会报错.

    相关文章

      网友评论

        本文标题:OC中的内存分配

        本文链接:https://www.haomeiwen.com/subject/ekdenctx.html