• 内存管理(二)


    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操作,因此,不会崩溃。

  • 相关阅读:
    用命令创建MySQL数据库
    Linux下安装mysql
    MySQL字符集及校对规则的理解
    Mybatis 高级结果映射 ResultMap Association Collection
    查看linux系统版本命令
    hdu 1217 Arbitrage (最小生成树)
    hdu 2544 最短路(两点间最短路径)
    hdu 3371 Connect the Cities(最小生成树)
    hdu 1301 Jungle Roads (最小生成树)
    hdu 1875 畅通工程再续(prim方法求得最小生成树)
  • 原文地址:https://www.cnblogs.com/r360/p/15934951.html
Copyright © 2020-2023  润新知