内存管理(二)

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

posted @ 2022-02-25 10:45  任淏  阅读(55)  评论(0编辑  收藏  举报