内存管理(二)

本文详细解析了iOS程序的内存布局,包括方法区、常量区、静态区、堆和栈的分配原则,并深入探讨了TaggedPointer技术在优化小对象存储中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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];的内存存储形式如下:

在这里插入图片描述

其中0xb000a1,b是表示小数据类型(NSNumber),a是十进制的10,1代表是小对象

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

前面两个地址,最后一位是5,转换为2进制是0b0101,也就是最后一位有效位是1,因此,前两个是Tagged Pointer类型。
后一个指针,最后一位是0,转为为2进制,最后一位是0,因此不是Tagged Pointer类型。是指针类型。

在iOS平台:

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方法,在执行的时候,系统可以识别出是Tagged Pointer类型,还是指针。
如果是小对象类型,则直接从地址里面取出数据
如果是非小对象类型,则根据num1的isa指针,找到类对象,去类对象的方法列表中寻找intValue方法。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

参考资料:
iOS - 老生常谈内存管理(五):Tagged Pointer
iOS内存管理


面试题

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


OC对象的内存管理

  • 在iOS中,使用引用计数来管理OC对象内存
  • 一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间。
  • 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1。
内存管理的经验总结
  • 当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease释放它。
  • 想拥有某个对象,就让他的引用计数+1;不想再拥有某个对象,就让他的引用计数-1。
MRC环境下

int类型的age,不需要做retain、release操作,直接赋值、取值即可。
MJDog类型的dog对象,才需要做release、retain操作。

release旧值,是一个人要换一条狗,要把之前拥有的那条狗释放。
retain新值,是要拥有新的狗。
if(_dog != dog){
}
才会执行换狗操作。
因为,如果不加判断条件,那么下列代码:

YZDog *dog = [[YZDog alloc] int];//dog:1
YZPerson *person = [[YZPerson alloc] init];
[person setDog:dog];//dog:2
[dog release];//dog:1
[person setDog:dog];//执行这句代码,先释放,再添加。在释放的时候,就是0,再添加,就是野指针错误。

在这里插入图片描述
以下代码,跟上图代码一起看
-(void)dealloc
{
[_dog release];
_dog = nil;
[super dealloc];
}


copy

copy顾名思义,就是复制。
copy的目的:产生一个副本对象,跟原对象互不影响。
修改了原对象,不会影响副本对象;
修改了副本对象,不会影响原对象。

NSString、NSDictionary、NSArray等Foundation框架中的某些对象具备了copy能力。
iOS提供了两种拷贝方法:
copy:不可变拷贝,产生不可变的副本;
mutableCopy:可变拷贝,产生可变的副本。

深拷贝和浅拷贝
深拷贝:内容拷贝,产生新的对象;
浅拷贝:指针拷贝,没有产生新的对象,只是引用计数器+1.
在这里插入图片描述

NSString本身就是不可变对象,使用copy,产生的还是不可变对象,既然两个都是不可变对象,那么,使用同一个内存地址,也就是指针copy(浅拷贝)就可以满足要求,就不需要再新建一块地址存储副本对象了。

在MRC环境下:

YZPerson *person = [[YZPerson alloc] init];
person.array = @[@"jack", @"rose"];
[person release];


@property (copy, nonatomic) NSArray *array;
相当于做了一下set方法:
- (void)setArray:(NSArray *)array
{
    if (_array != array) {
        [_array release];
        _array = [array copy];
    }
}


@property (retain, nonatomic) NSArray *array;
相当于做了一下set方法:
- (void)setArray:(NSArray *)array
{
    if (_array != array) {
        [_array release];
        _array = [array retain];
    }
}

在使用copy的时候,相当于对:
person.array = @[@"jack", @"rose"];
[person setArray:@[@"jack", @"rose"]];
进入set方法:
_array = [@[@"jack", @"rose"] copy];
等于
person.array = [@[@"jack", @"rose"] copy];

也就是,使用copy修饰属性,相当于对传入的值进行了copy操作,然后赋值给原属性。

面试题

问:以下代码会有什么问题?

@property (copy, nonatomic) NSMutableArray *array;

YZPerson *person = [[YZPerson alloc] init];
person.array = [NSMutableArray array];
[person.array addObject:@"jim"];
[person release];

答:
会崩溃,报错信息:
[__NSArray0 addObject:]: unrecognized selector sent to instance 0x100501190
NSArray没有addobject:方法。

原因:
array虽然定义为NSMutableArray,但使用的是copy修饰,那么,person.array创建出来的对象是:
person.array = [[NSMutableArray array] copy];
也就是
person.array创造出来的是一个不可变对象。
不可变对象array,没有addObject:方法。

不存在使用mutableCopy修饰的属性。
@property (mutableCopy, nonatomic) NSString *name;

对于一个普通自定义类,如果想使用copy,则需要遵守NSCopying协议

@interface YZPerson : NSObject<NSCopying>
@property (assign, nonatomic) int age;
@property (assign, nonatomic) float weight;
@end

@implementation YZPerson
//实现copyWithZone:方法
- (id)copyWithZone:(NSZone *)zone
{
    YZPerson *person = [YZPerson allocWithZone:zone];
    return person;
}
@end

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值