NSString都存储在堆区吗?会不会存在栈区,或者数据区呢?
NSString用copy修饰还是strong修饰?NSString调用copy和mutableCopy会创建一个新的内存空间吗?NSMutableString用copy修饰会导致什么样的后果?
NSString和NSMutableString相信我们平时都用过n遍了,但NSString真的都是一个存储在堆区的对象吗?如果在不同区,对它们进行copy操作,内存地址又会是什么样呢?
几个需要注意的点:
内存地址由低到高分别为:程序区-->数据区-->堆区-->栈区
其中堆区分配内存从低往高分配,栈区分配内存从高往低分配
1.继承关系:
NSTaggedPointerString(栈区) ---> NSString
__NSCFConstantString(数据常量区) ---> __NSCFString (堆区) --->NSMutableString --->NSString
2.对于NSStringFromClass()方法,字符串较短的class,系统会对其进行比较特殊的内存管理,NSObject字符串比较短,直接存储在栈区,类型为NSTaggedPointerString,不论你NSStringFromClass多少次,得到的都是同一个内存地址的string;但对于较长的class,则为__NSCFString类型,而NSCFString存储在堆区,每次NSStringFromClass都会得到不同内存地址的string
3.__NSCFConstantString类型的字符串,存储在数据区,即使当前控制器被dealloc释放了,存在于这个控制器的该字符串所在内存仍然不会被销毁.通过快捷方式创建的字符串,无论字符串多长或多短,都是__NSCFConstantString类型,存储在数据区.
下面是实际代码测试的结果,对象内存地址和指针内存地址都作了相应注释:
NSString *str1 = @"abcdeefghijk";//该种方式无论字符串多长,都是__NSCFConstantString类型,存储在数据区,0x0000000104b2c400
NSString *strDigital = @"999";//即使很短,也是(__NSCFConstantString *)类型, 0x00000001092b1420,存储在数据区
NSString *str1Copy = [str1 copy];//__NSCFConstantString,NSString类型的str1进行copy是浅拷贝,并不会拷贝一份新的内存地址,0x0000000104b2c400,内存地址和str1相同
NSMutableString *str1MutableCopy = [str1 mutableCopy];//__NSCFString,NSString类型str1进行mutableCopy后是深拷贝,0x0000600000d7d590
NSMutableString *str1MutaCopyCopy = [str1MutableCopy copy];//__NSCFString,mutableString类型的str1MutableCopy进行mutableCopy后是深拷贝,0x0000600000373980
[str1MutableCopy appendString:@"12"];
[str1MutaCopyCopy appendString:@"34"];//本行代码崩溃,原因是mutableString进行copy虽然是深拷贝,但返回的是不可变类型,只有NSMutableString才有appendString方法,__NSCFString没有appendString方法;虽然用NSMutableString接收,但str1MutaCopyCopy的类型还是由实际运行过程决定
NSString * cfStringLongClass = NSStringFromClass([ViewController class]);//较长类情况下,该方法获得的字符串是类型__NSCFString存储在堆区,0x00006000003739a0
NSString *taggedStrShortClass = NSStringFromClass([NSObject class]);//较短类情况下,该方法获得的字符串是NSTaggedPointerString类型,存储在栈区,0x86b1d57ef6188519
NSString *taggedStrShortClassCopy = [taggedStrShortClass copy];//(NSTaggedPointerString *) $0 = 0xac71e37414f16209 @"NSObject" 内存地址不会变,浅拷贝,值也相同
NSString *taggedStrShortClassMutaCopy = [taggedStrShortClass mutableCopy];//(__NSCFString *) $2 = 0x00006000036f4030 @"NSObject",内存地址变化,深拷贝一份至堆区,值相同,
NSObject *obj = [[NSObject alloc]init];//对象存储在堆区,0x0000600000c61160
//总结:stringWithFormat方式,最终字符串的类型由字符串长度决定,少于10个字符类型为NSTaggedPointerString,否则为__NSCFString类型(长度的规律只适用于英文字母和数字,详见文末备注)
NSString *strFormatLong = [NSString stringWithFormat:@"%@",@"01234567a909"];// ; 当长度超过9时,为__NSCFString,0x00006000003f6680,堆区
NSString *strFormatShort = [NSString stringWithFormat:@"%@",@"012345678"];//当字符长度位数为9或以下时,strFormat为NSTaggedPointerString,0xfc16f577dc1ba4f7,存储在栈区
NSString *strShortConstantCopy = [@"999" copy];//__NSCFConstantString,0x00000001092b1420,数据区.NSString调用copy是浅拷贝,和strDigital内存地址相同
NSString *strShortConstantMutaCopy = [@"999" mutableCopy];//__NSCFString,0x0000600003fad410,堆区,NSMutable类型的string调用mutalbeCopy是深拷贝,返回一个b可变类型字符串;调用copy也是深拷贝,返回不可变字符串
//从以下两个对象的内存地址可以得出结论:stringWithFormat不论接收什么类型的字符串参数,也无论它在数据区栈区还是堆区,都遵守长度临界值9的规则
NSString *strShortConstantMutaCopyFormat = [NSString stringWithFormat:@"%@",strShortConstantCopy];//NSTaggedPointerString,0x8b25a274d8732690
NSString *strShortConstantCopyFormat = [NSString stringWithFormat:@"%@",strShortConstantMutaCopy];//NSTaggedPointerString,0x8b25a274d8732690
测试主要注重下面几方面:
1.NSString分别用copy和strong修饰
2.来源为可变字符串和来源为不可变字符串
3.修改字符串之前和修改字符串之后
注意:
1.不可变类型的字符串是无法更改内容的,这里说的修改其实是修改它的指针,让它指向另一块内存,所以不会影响self.string;
而可变字符串是可以更改内容的,对于他的修改是修改这块内存的内容.
2.copy修饰词起作用是在setter方法中发生的,如果通过_string_copy = [NSMutableString new];设置,得到的self.string_copy仍然
是浅拷贝
3.一个对象的类型是由运行时决定的,和@property中的声明无关,OC对于类型不匹配只会报警告,编译可以通过,但并不代表你执行
时候就不会出错.
4.NSString +copy ( 浅拷贝,返回不可变对象) NSString+mutableCopy(深拷贝,返回可变对象) NSMutableString+copy(深拷贝,返回不可变对象) NSMutableString+mutableCopy(深拷贝,返回可变对象)
依然先上代码,看运行结果,内存地址和相关注释如下:
@property (nonatomic, copy) NSString *string_copy;
@property (nonatomic, strong) NSString *string_strong;
@property (nonatomic, copy) NSMutableString *string_muta_copy;
@property (nonatomic, strong) NSMutableString *string_muta_strong;
@property (nonatomic, strong) UIView *string_obj;
NSString *string = [NSString stringWithFormat:@"%@",@"1234567890"];//p/x string (__NSCFString *) $0 = 0x0000600003e31800 @"1234567890"
self.string_copy = string;// p/x _string_copy (__NSCFString *) $1 = 0x0000600003e31800 @"1234567890"
self.string_strong = string;//p/x _string_strong (__NSCFString *) $2 = 0x0000600003e31800 @"1234567890"
self.string_muta_copy = string;//(__NSCFString *) $1 = 0x0000600003e31800 @"1234567890"
self.string_muta_strong = string;//(__NSCFString *) $2 = 0x0000600003e31800 @"1234567890"
self.string_obj = string;//(__NSCFString *) $2 = 0x0000600003e31800 @"1234567890",类型是由具体运行过程决定的,即便用UIView接收它,也并没有影响string_obj的类型
string = [NSString stringWithFormat:@"%@",@"abcdefghijklmn"];//p/x string (__NSCFString *) 0x0000600002d36dc0 @"abcdefghijklmn"
/*
一.来源是不可变字符串(修改前):
a.当string的来源是非mutable类型时,不论是copy还是strong修饰,最终都只会仅仅拷贝一个指针,并不拷贝这块值的内存,因为这块内存存储的字符串
本来就是非mutable,即不可修改类型的,深拷贝一份
(lldb) p/x string
(__NSCFString *) $0 = 0x0000600002d36da0 @"1234567890"
(lldb) p/x _string_copy
(__NSCFString *) $1 = 0x0000600002d36da0 @"1234567890"
(lldb) p/x _string_strong
(__NSCFString *) $2 = 0x0000600002d36da0 @"1234567890"
一.来源是不可变字符串(修改后):
--------> 此处开始修改源串string的值------>string = [NSString stringWithFormat:@"%@",@"abcdefghijklmn"];
(因为string本身是不可变的,所以:虽然说修改string的值,倒不如说实际上是修改string这个指针,让string这个指针指向另一块内存区域,
故结果是不会影响self.string_copy,self.string_strong所指向的那块内存)
(lldb) p/x string
(__NSCFString *) $3 = 0x0000600002d36dc0 @"abcdefghijklmn"
(lldb) p/x _string_copy
(__NSCFString *) $4 = 0x0000600002d36da0 @"1234567890"
(lldb) p/x _string_strong
(__NSCFString *) $5 = 0x0000600002d36da0 @"1234567890"
(lldb)
*/
NSMutableString *strMuta = [[NSMutableString alloc]initWithString:@"一二三四"];//p/x &strMuta (NSMutableString **) $7 = 0x00007ffee4d919a0
self.string_strong = strMuta;// p/x &_string_strong (NSString **) $6 = 0x00007f8c5fc0d788
self.string_copy = strMuta;//p/x &_string_copy (NSString **) $8 = 0x00007f8c5fc0d780
self.string_muta_strong = strMuta;//(__NSCFString *) $0 = 0x000060000032d500
self.string_muta_copy = strMuta;//(__NSCFString *) $2 = 0x0000600000d264e0
[self.string_muta_copy appendString:@"1234"];//崩溃,原因是:copy修饰的string_muta_copy在接收到字符串时,无论是可变还是不可变,都给你深拷贝一遍,NSMutableString调用copy方法虽是深拷贝,但返回的是不可变对象,不可变对象是没有appendString方法的
/*
二:来源是可变字符串(修改前)
0.未修改源muta字符串时,可以看出:copy修饰的string_copy拷贝了一份内存,即值和strMuta一样,但实际存储这个字符串值的内存不是同一块.
而strong修饰的string_strong只拷贝了一份指针,并没有拷贝存储这块值的内存,实际上只是指针不是同一个,指向的内存还是同一块
(lldb) p/x strMuta
(__NSCFString *) $0 = 0x000060000318ab20 @"一二三四"
(lldb) p/x _string_copy
(__NSCFString *) $1 = 0x0000600003f88120 @"一二三四"
(lldb) p/x _string_strong
(__NSCFString *) $2 = 0x000060000318ab20 @"一二三四"
*/
[strMuta appendString:@"1234"];
/*
二.来源是可变字符串(修改后)
1.修改源字符串strMuta后,由于string_copy指针指向新拷贝的那一块内存区域-->0x0000600003f88120,所以修改源串strMuta的那
块内存-->0x000060000318ab20不会影响copy修饰的字符串;而string_strong只是拷贝了一个指针,这个指针仍然指向-->0x000060000318ab20这
块区域,和源串是同一块内存,唯一不同的就是指针本身地址不同,而指针的内容是相同的,都存储着源串的内存地址,修改源串导致string_strong存储的值也
随之变化
lldb) p/x strMuta
(__NSCFString *) $3 = 0x000060000318ab20 @"一二三四1234"
(lldb) p/x _string_copy
(__NSCFString *) $4 = 0x0000600003f88120 @"一二三四"
(lldb) p/x _string_strong
(__NSCFString *) $5 = 0x000060000318ab20 @"一二三四1234"
*/
从上面代码运行的结果分析,不难得出:其实copy就是在setter方法里起了特殊作用,利用copy修饰的string,在setter方法中进行一次判断,a.如果来源是可变字符串,就深拷贝一份(为什么深拷贝?因为来源是可变字符串,这串字符就可能会被修改,如果不深拷贝一份的话,将来源串不小心被修改了,你的self.string也会跟着变化,如果发生了不可预见的结果,你肯定要怪xcode了,这个结果是你不希望发生的);b.如果来源是不可变字符串,就直接赋值,不拷贝内容(为什么只拷贝指针不拷贝内容?因为来源为不可变,你就算改变self.string也只能通过重新赋值来改变,赋值肯定是你自己操作的,如果因为重新赋值发生了不可预见的问题,那就是你自己的责任了)
从结论中我们可以推测NSMutableString不用copy修饰的原因了:如果你用copy去修饰一个NSMutableString的字符串属性string_muta_copy,如果将来你把一个不可变来源通过set方法赋值给这个属性,set方法内部会因为这个来源是不可变的进行一次copy,copy后再将新的对象赋给_string_muta_copy,这时候的拷贝是深拷贝,但返回的是不可变对象,而在你声明中你认为这个对象是可变的,编译时期也会被认为是可变的,你向他发送NSMutableString的方法消息不会报错,但到了运行时候回因为string_muta_copy是个不可变字符串而找不到方法.
对于NSMutableString类型的属性,我们在对它赋值时,最好确保是可变类型,防止以后你把它当做可变字符串,进行拼接操作,而实际上它却被你赋值了不可变字符串而出错
至此:大概讲述了NSString和NSMutableString调用copy和mutableCopy.修饰词用copy和strong的原因.
文章如有不对的地方还请尽快与我联系.我会尽快做出修改,以免错误误导到别人.QQ:422538722
另外:我们通过把代码编译成C++文件,可以看出copy修饰的属性和strong修饰的属性,在底层的处理是不同的,下面是源码节选:
//ViewController对象在底层实则是一个结构体,它的每个属性都将作为该类型结构体的一个成员
struct ViewController_IMPL {
struct UIViewController_IMPL UIViewController_IVARS;
NSString *_string_copy;
NSString *_string_strong;
NSMutableArray *_array_muta_copy;
NSMutableArray *_array_muta_strong;
NSArray *_array_copy;
NSArray *_array_strong;
};
//分别代表_string_copy和_string_strong在ViewController_IMPL结构体中的偏移量,通过偏移量+结构体内存地址就可以找到这个成员
extern "C" unsigned long OBJC_IVAR_$_ViewController$_string_copy;
extern "C" unsigned long OBJC_IVAR_$_ViewController$_string_strong;
// self.string_copy = string;
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)self, sel_registerName("setString_copy:"), (NSString *)string);
// self.string_strong = string;
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)self, sel_registerName("setString_strong:"), (NSString *)string);
//相当于_string_copy的getter方法,取_string_copy时,返回结构体里的该成员
static NSString * _I_ViewController_string_copy(ViewController * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_ViewController$_string_copy)); }
//OC中对copy修饰的属性所做的处理,这个方法下面会有详细说明
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
//相当于_string_copy的setter方法,可以看见,内部是调用了objc_setProperty方法做特殊处理的
static void _I_ViewController_setString_copy_(ViewController * self, SEL _cmd, NSString *string_copy) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct ViewController, _string_copy), (id)string_copy, 0, 1); }
//_string_strong的getter方法
static NSString * _I_ViewController_string_strong(ViewController * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_ViewController$_string_strong)); }
//_string_strong的setter方法,可以明显看到和copy修饰的属性处理不一样,没有调用objc_setProperty方法
static void _I_ViewController_setString_strong_(ViewController * self, SEL _cmd, NSString *string_strong) { (*(NSString **)((char *)self + OBJC_IVAR_$_ViewController$_string_strong)) = string_strong; }
objc-runtime源码里检索关键词,来到这里:
#define MUTABLE_COPY 2
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)
{
//当shouldCopy为真,且不等于2(MUTABLE_COPY)时,copy为YES,一般objc_setProperty方法调用时会给shouldCopy传0,1,2(一般来说,0代表不copy,1代表copy,2代表mutableCopy)
bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
//当shouldCopy为2时,mutableCopy为YES
bool mutableCopy = (shouldCopy == MUTABLE_COPY);
reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
对比上面传进来的参数:
objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct ViewController, _string_copy), (id)string_copy, 0, 1)
可以看出参数依次传入了self,_cmd,偏移量,string_copy,非atomic,copy为YES
构造好参数之后,实际上是调用了reallySetProperty函数,下面查看该函数:
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
//当offset为0时所做的处理,因为结构体第一个成员都是isa,如果offset为0代表是更改isa成员
if (offset == 0) {
object_setClass(self, newValue);
return;
}
//旧值
id oldValue;
id *slot = (id*) ((char*)self + offset);
//当copy为YES时,调用copyWithZone方法
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
//当mutableCopy为YES时,调用mutableCopyWithZone方法
newValue = [newValue mutableCopyWithZone:nil];
} else {
//如果objc_setProperty最后一个传0的话,则仅仅对该属性进行一次retain强引用
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
//设置新值newValue,释放旧值oldValue
if (!atomic) {
//非原子情况下的操作
oldValue = *slot;
*slot = newValue;
} else {
//原子情况下的操作
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
感谢:最后,非常感谢有小伙伴向我提出了字符长度影响NSString stringWithFormat得到的字符串类型的问题.实际上系统底层对字符串的处理是按照一个字符对应一个字节(8位),ASCII码恰好用8位存储一个字符,但这些字符仅限英文字母和数字,首位为0,1-7位刚好可表示128个字符,由于字符标准不统一,其它语言的字符不能用标准的ASCII码来存储.所以汉字日文等等,即使是一个字符也用__NSCFString来存储,而不是按照低于10位采用NSTaggedPointerString来优化存储的方式.