大家好,关于深入探究Objective-C字符串内存管理:以一个实践案例解析很多朋友都还不太明白,今天小编就来为大家分享关于的知识,希望对各位有所帮助!
- (void)字符串测试{
NSString *string1=@"字符串";
NSString *string2=[NSString stringWithString:@"string"];
NSString *string3=[[NSString alloc] initWithString:@"string"];
NSString *string4=[NSString stringWithFormat:@"string"];
NSString *string5=[[NSString alloc] initWithFormat:@"string"];
NSLog(@"1----%p",string1);
NSLog(@"2----%p",string2);
NSLog(@"3----%p",string3);
NSLog(@"4----%p",string4);
NSLog(@"5----%p",string5);
}2018-04-27 17:36:17.419547+0800 StringTest[19381:2393524] 1----0x10dd070e8
2018-04-27 17:36:17.419789+0800 StringTest[19381:2393524] 2----0x10dd070e8
2018-04-27 17:36:17.420025+0800 StringTest[19381:2393524] 3----0x10dd070e8
2018-04-27 17:36:17.420166+0800字符串测试[19381:2393524] 4----0xa00676e697274736
2018-04-27 17:36:17.420258+0800字符串测试[19381:2393524] 5----0xa00676e697274736
这里使用不同的方法来创建NSString 对象。
它们之间有什么区别?我们先看一下存储区域的划分:
存储区域
堆栈
它由编译器自动分配和释放,用于存储函数参数值、局部变量值等。它的操作就像数据结构中的堆栈一样。堆
一般来说,程序员分配和释放它。如果程序员不释放它,当程序结束时它可能会被操作系统回收。它在数据结构上与堆不同,但分配方式类似于链表。全局区域(静态区域)(static)
全局变量和静态变量存储在一起(全局变量静态存储)。已初始化的全局变量和静态变量位于一个区域中,未初始化的全局变量和未初始化的静态变量位于另一相邻区域中。
程序结束后系统释放的区域。字面常数面积
常量字符串放置在这里,程序结束后由系统释放。代码区
存储函数体的二进制代码。内存分区string1是通过文字创建的,它是常量区中存储的常量。如果其他对象存储相同的内容,则指针指向相同的地址。内存空间不会被初始化,因此内存使用后不会被释放。 string2通过类方法初始化字符串创建,并通过copy@"string"返回一个字符串,并且这个副本是浅拷贝,会指向同一个地址。其实就相当于创建一个字面量的方法,完全是多余的,所以这段代码会有警告:Using "initWithString:" with aliteral is Redundant。 String3通过实例方法初始化字符串创建。与string2类似,相当于文字创建,也会有警告。 String4是通过类方法初始化格式创建的。它需要初始化一个动态内存空间并将其存储在堆中。内存使用后需要释放。 String5通过实例方法初始化格式创建,与string4类似。 initWith.和stringWith.这两个实例方法和类方法的内存分配是相同的,但是内存释放是不同的。有什么区别?我们继续看第二段代码:
@interfaceViewController()
@property (非原子,弱) NSString *string1;
@property (非原子,弱) NSString *string2;
@结尾
@实现ViewController
- (void)viewDidLoad {
[超级viewDidLoad];
[自我字符串测试];
NSLog(@"%@",_string1);
NSLog(@"%@",_string2);
}
- (void)字符串测试{
NSString *string1=[NSString stringWithFormat:@"string string1"];
NSString *string2=[[NSString alloc] initWithFormat:@"string string2"];
self.string1=string1;
self.string2=string2;
}
@end 这里的两个字符串属性修改为weak而不是strong,这样就可以通过打印string1和string2的值来分析它们的内存释放情况。一般来说,仅当需要避免循环引用时才使用weak修饰符。运行时将布局注册的类并将弱对象放入哈希表中。使用weak所指向的对象的内存地址作为key,当这个对象的引用计数达到0时就会执行dealloc。因此,当weak修饰的变量所引用的对象被丢弃时,会经历以下步骤:
1、从弱表中获取地址为废弃对象键值的记录。
2. 将记录中包含的weak修饰符变量的地址赋值为nil。
3. 删除弱表中的记录。
4、从引用计数表中删除地址为废弃对象键值的记录。
这会消耗相应的CPU资源。这里加weak只是为了方便分析。
这段代码的输出是什么?
由于_string1和_string2都是弱引用,因此ARC下的string1和string2对象在调用stringTest方法后超出范围后就会被销毁。你可能不假思索地回答:结果都是空,但我们来看看实际情况。
2018-04-28 11:36:20.445170+0800 StringTest[22110:2816244] 字符串string1
2018-04-28 11:36:20.445294+0800 StringTest[22110:2816244](空)
为什么string1 仍然有值? string1引用的对象不应该在方法范围之外被销毁吗?其实这涉及到iOS内存管理的另一个知识点:自动释放池autoreleasepool。
autorelease
我们知道autorelease是一种自动内存回收机制,autorelease对象会被添加到autoreleasepool中。 autoreleasepool 中的对象不会立即释放。一般情况下,创建的对象超出其作用域时就会被释放。但是,如果将对象添加到autoreleasepool中,则该对象将等到autoreleasepool被销毁后才被释放。这允许对象在超过其指定的生存范围时自动恢复。正确释放。通过类似于+ (instancetype)stringWithFormat:(NSString *)string 的方法创建的string1 对象被添加到autoreleasepool 中,是一个autorelease 对象。因为如果我们不手动添加autoreleasepool,autorelease对象会在当前runloop迭代结束时被丢弃,这意味着string1直到循环结束才会被释放(因为autoreleasepool是在每次runloop循环期间生成或丢弃的) )
因此,我们有两种方式重写释放string1的代码:
第一种方式:手动添加@autoreleasepool,@autoreleasepool {}后对象就会被释放
- (void)viewDidLoad {
[超级viewDidLoad];
@autoreleasepool {
[自我字符串测试];
}
NSLog(@"%@",_string1);
NSLog(@"%@",_string2);
}第二种方式:模拟runloop迭代
- (void)viewDidLoad {
[超级viewDidLoad];
[自我字符串测试];
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"%@",_string1);
NSLog(@"%@",_string2);
});
}打印结果是一样的:
2018-04-28 14:18:10.769891+0800 StringTest[22742:2893498](空)
2018-04-28 14:18:10.770081+0800 StringTest[22742:2893498](空)
现在我们大致知道,通过stringWithFormat创建的对象会被添加到自动释放池中,并且是自动释放的对象,而通过initWithFormat创建的对象不会被添加到自动释放池中。这不仅适用于NSString 类,也适用于其他类。 ARC下生成的对象不能调用autorelease。为了区分生成的对象是否是自动释放的,ARC建立了硬性规则。这些规则简单地反映在方法名称中。
使用以alloc/new/copy/mutableCopy开头的方法意味着生成的对象由调用者持有,这些自生成并持有的对象通过release来释放(另外,这也是用new前缀。原因是,因为该属性的getter方法以new开头,根据硬规则内存可能不正确)。其他名称类似于字符串数组字典的方法会生成不属于调用者所拥有的对象。在这种情况下,该对象会自动释放。也就是说
NSString *string1=[NSString stringWithFormat:@"string string1"];相当于
NSString *string1=[[[NSString alloc] initWithFormat:@"string string1"] autorelease];眼尖的朋友一定注意到了,上面第二段代码中string1和string2的初始化值分别是@"string string1",@。 "string string2",我专门为了字符串的长度而写的。那么这个长度对打印结果有什么影响呢?将第二段代码修改为:
- (void)viewDidLoad {
[超级viewDidLoad];
[自我字符串测试];
NSLog(@"%@",_string1);
NSLog(@"%@",_string2);
}
- (void)字符串测试{
NSString *string1=[NSString stringWithFormat:@"string1"];
NSString *string2=[[NSString alloc] initWithFormat:@"string2"];
self.string1=string1;
self.string2=string2;
}2018-04-28 16:10:17.572271+0800 StringTest[23270:2961270] string1
2018-04-28 16:10:17.572456+0800 StringTest[23270:2961270] string2
您是否注意到string2 还没有被释放?刚才不是说了init创建的对象不会被添加到autoreleasepool中吗?为什么改变字符串值后不释放?这实际上与autoreleasepool无关。我们可以尝试像以前一样手动添加@autoreleasepool,但结果不会改变。这里又来一个知识点:
Tagged Pointer
我们通过lldb查看string1的信息:
(lldb) p 字符串1
(NSTaggedPointerString *) $1=0xa31676e697274737 @"string1"
您可以看到奇怪的类,例如NSTaggedPointerString,它是标记指针对象。那么它有什么作用呢?
假设您要存储一个值为整数的NSNumber 对象。一般情况下,如果这个整数只是NSInteger的一个普通变量,那么它占用的内存和CPU位数有关。在32位CPU下占用4字节,在64位CPU下占用8字节。的。指针类型的大小通常与CPU位数有关。指针占用的内存在32位CPU下为4字节,在64位CPU下为8字节。因此,普通的iOS程序从32位机迁移到64位机后,虽然逻辑没有改变,但NSNumber、NSDate等对象占用的内存会增加一倍。为了存储和访问NSNumber 对象,我们需要在堆上为其分配内存,并维护其引用计数并管理其生命周期。这些给程序增加了额外的逻辑,造成运行效率的损失。
各路大神图片
为了改善上面提到的内存占用和效率问题,Apple 提出了Tagged Pointer 对象。由于NSNumber 和NSDate 等变量的值占用的内存大小往往不需要8 个字节,以整数为例,4 个字节可以表示的有符号整数数量可达21 亿个以上(2 ^31)。所以我们可以把一个对象的指针拆成两部分,一部分直接存储数据,另一部分作为特殊标记,表明这是一个不指向任何地址的特殊指针。所以,事实上,它不再是一个对象,它只是一个披着对象皮的普通变量。因此,它的内存并不存放在堆中,也不需要malloc和free。假设你调用NSNumber的integerValue,它会从数据部分提取值并返回。这样,每次访问对象时,就节省了真实对象的内存分配,也节省了间接获取值的时间。此外,引用计数可以是空指令,因为不需要释放内存。对于常用的类来说,这将是一个巨大的性能提升。引入Tagged Pointer对象后,64位CPU下的NSNumber内存图如下:
各路大神图片
编写示例代码:- (void)numberTest {
NSNumber *number=[NSNumber numberWithInt:1];
NSLog(@"%p",数字);
}字符串测试[1309:126876]0xb000000000000012
这里的指针地址包含了对象的特殊标记以及指针指向的内容:地址0xb00000000000012中b的最高四位是NSNumber对象的特殊标记,最低四位2用来标记数值类型,2代表int类型(3:long、4:float、5:double)。剩余的56 位用于存储值本身的内容。也就是说,当值超过56位存储限制时,NSNumber将使用真正的64位内存地址来存储该值,然后使用指针指向该内存地址。
- (void)numberTest {
NSNumber *number1=[NSNumber numberWithInt:1];
NSNumber *number2=[NSNumber numberWithLong:2];
NSNumber *number3=[NSNumber numberWithFloat:3];
NSNumber *number4=@(pow(2, 54));
NSNumber *normalNumber=@(pow(2, 55));
NSLog(@"%pn%pn%pn%pn%p",number1,number2,number3,number4,normalNumber);
}0xb000000000000012
0xb000000000000023
0xb000000000000034
0xb400000000000005
0x604000038800
可以清楚地看到,当数值为2^55或者更大时,会在内存中分配一个NSNumber对象来存储,然后用指针指向内存地址。可见Tagged Pointer可以与普通类共存,即对某些值使用Tagged Pointer,对其他值使用普通指针。
那么NSString对象也适合Tagged Pointer。
- (void)字符串测试{
NSString *string1=[NSString stringWithFormat:@"11"];
NSString *string2=[NSString stringWithFormat:@"a"];
NSLog(@"%pn%p",字符串1,字符串2);
}0xa000000000031312
0xa000000000000611
与NSNumber 一样,地址中a 的最高四位是NSString 对象的特殊标记,而最低四位用于标记字符串的长度,其余56 位用于存储字符串的内容(字符串的内容转换为ASCII码存储)。我们可以猜测,当字符串需要的内存小于56位时,就会使用Tagged Pointer,而使用真正的NSString对象。事实真的是这样吗?
- (void)字符串测试{
NSString *string1=[NSString stringWithFormat:@"1234567"];
NSString *string2=[NSString stringWithFormat:@"12345678"];
NSLog(@"%@---%pn%@---%p",[字符串1类],字符串1,[字符串2类],字符串2);
}NSTaggedPointerString---0xa373635343332317
NSTaggedPointerString---0xa007a87dcaecc2a8
正如你所看到的,string2内存有64位,但它仍然使用Tagged Pointer存储。只是编码方式不同而已。具体编码方法可以参考这篇博客。以下是不同字符串长度的编码方法的简要列表:
1:如果长度在0到7之间,则直接以八位编码存储字符串。
2:如果长度为8或9,则使用六位编码存储字符串,使用编码表“eilotrm.apdnsIc ufkMShjTRxgC4013bDNvwyUL2O856P-B79AFKEWV_zGJ/HYX”。
3:如果长度为10或11,则使用五位编码存储字符串,使用编码表“eilotrm.apdnsIc ufkMShjTRxgC4013”
- (void)字符串测试{
NSString *string1=[NSString stringWithFormat:@"123456789"];
NSString *string2=[NSString stringWithFormat:@"1234567890"];
NSLog(@"%@---%pn%@---%p",[字符串1类],字符串1,[字符串2类],字符串2);
}NSTaggedPointerString---0xa1ea1f72bb30ab19
__NSCFString---0x600000420060
当长度大于9时,使用真正的NSString对象来存储。现在string2释放的问题就很清楚了:string2被赋值为@"string string2"(长度:14),超出作用域后正常释放。 string2 被分配了一个值@"string2"(长度:7),并且在超出范围时不会被释放。另外,当字符串内容包含中文或特殊字符(非ASCII字符)时,只能使用NSString对象来存储。文字字符串不使用标记指针。
总结
我这里用一个问题来总结一下上面关于内存的内容:
在64位架构下,下面代码的输出结果是什么?
- (void)viewDidLoad {
[超级viewDidLoad];
[自我字符串测试];
NSLog(@"%@",_string1);
NSLog(@"%@",_string2);
NSLog(@"%@",_string3);
NSLog(@"%@",_string4);
}
- (void)字符串测试{
NSString *string1=@"1234567890";
NSString *string2=[NSString stringWithFormat:@"1"];
NSString *string3=[[NSString alloc] initWithFormat:@"2"];
NSString *string4=[[NSString alloc] initWithFormat:@"1234567890"];
_字符串1=字符串1;
_string2=字符串2;
_string3=字符串3;
_string4=字符串4;
深入探究Objective-C字符串内存管理:以一个实践案例解析的介绍就聊到这里吧,感谢你花时间阅读本站内容,更多关于、深入探究Objective-C字符串内存管理:以一个实践案例解析的信息别忘了在本站进行查找哦。
【深入探究Objective-C字符串内存管理:以一个实践案例解析】相关文章:
2.米颠拜石
3.王羲之临池学书
8.郑板桥轶事十则
用户评论
这个标题听起来很有意思啊,我想看看小Demo能展现些什么 NSString 的内存机制。
有19位网友表示赞同!
最近在学Objective-C,对内存管理一直不太理解,这篇博客正好可以学习下!
有14位网友表示赞同!
String对象占内存吗?这应该是个不错的入门话题。
有11位网友表示赞同!
通过案例来讲解内存问题感觉更容易理解。期待这个小Demo。
有5位网友表示赞同!
希望能看懂NSString的本质和一些常见的内存泄漏场景。
有10位网友表示赞同!
学习编程真是个不断探索的过程,这次来看看NSString是如何管理内存的!
有6位网友表示赞同!
希望这个Demo能讲解清楚强引用、弱引用等概念。
有9位网友表示赞同!
一直对苹果平台的内存模型比较好奇,文章能不能深入一点?
有8位网友表示赞同!
这篇博客听起来很有实用价值,我需要学习一下NSString的应用技巧。
有8位网友表示赞同!
学习Objective-C还是要关注内存管理,这个标题很吸引人!
有5位网友表示赞同!
希望能看到一些代码实战案例,帮助我更直观地理解。
有20位网友表示赞同!
我对苹果平台开发越来越感兴趣了,希望这篇博客能进一步解答我的疑惑。
有9位网友表示赞同!
我已经开始学习iOS开发了,这种类型的文章对我非常有用!
有14位网友表示赞同!
希望能了解一下NSString的使用技巧,以及如何避免内存泄漏问题。
有9位网友表示赞同!
最近项目里遇到一些内存问题,这个博客或许能提供一些解决方案。
有15位网友表示赞同!
我一直想深入理解Objective-C的内存管理机制,这篇文章正 Hit The Nail!
有10位网友表示赞同!
看了标题感觉很有深度,期待作者能够讲清楚细节部分。
有7位网友表示赞同!
学习编程的关键是掌握核心知识点,这篇博客帮助我了解 NSString 的内存特性!
有19位网友表示赞同!