問題引入:
NSString都存儲在堆區(qū)嗎?會不會存在棧區(qū),或者數(shù)據(jù)區(qū)呢?
NSString用copy修飾還是strong修飾?NSString調(diào)用copy和mutableCopy會創(chuàng)建一個新的內(nèi)存空間嗎?NSMutableString用copy修飾會導致什么樣的后果?
一.各類型字符串的關(guān)系和存儲方式
NSString和NSMutableString相信我們平時都用過n遍了,但NSString真的都是一個存儲在堆區(qū)的對象嗎?如果在不同區(qū),對它們進行copy操作,內(nèi)存地址又會是什么樣呢?
幾個需要注意的點:
內(nèi)存地址由低到高分別為:程序區(qū)-->數(shù)據(jù)區(qū)-->堆區(qū)-->棧區(qū)
其中堆區(qū)分配內(nèi)存從低往高分配,棧區(qū)分配內(nèi)存從高往低分配
1.繼承關(guān)系:
NSTaggedPointerString(棧區(qū)) ---> NSString
__NSCFConstantString(數(shù)據(jù)常量區(qū)) ---> __NSCFString (堆區(qū)) --->NSMutableString --->NSString
2.對于NSStringFromClass()方法,字符串較短的class,系統(tǒng)會對其進行比較特殊的內(nèi)存管理,NSObject字符串比較短,直接存儲在棧區(qū),類型為NSTaggedPointerString,不論你NSStringFromClass多少次,得到的都是同一個內(nèi)存地址的string;但對于較長的class,則為__NSCFString類型,而NSCFString存儲在堆區(qū),每次NSStringFromClass都會得到不同內(nèi)存地址的string
3.__NSCFConstantString類型的字符串,存儲在數(shù)據(jù)區(qū),即使當前控制器被dealloc釋放了,存在于這個控制器的該字符串所在內(nèi)存仍然不會被銷毀.通過快捷方式創(chuàng)建的字符串,無論字符串多長或多短,都是__NSCFConstantString類型,存儲在數(shù)據(jù)區(qū).
下面是實際代碼測試的結(jié)果,對象內(nèi)存地址和指針內(nèi)存地址都作了相應注釋:
NSString *str1 = @"abcdeefghijk";//該種方式無論字符串多長,都是__NSCFConstantString類型,存儲在數(shù)據(jù)區(qū),0x0000000104b2c400
NSString *strDigital = @"999";//即使很短,也是(__NSCFConstantString *)類型, 0x00000001092b1420,存儲在數(shù)據(jù)區(qū)
NSString *str1Copy = [str1 copy];//__NSCFConstantString,NSString類型的str1進行copy是淺拷貝,并不會拷貝一份新的內(nèi)存地址,0x0000000104b2c400,內(nèi)存地址和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存儲在堆區(qū),0x00006000003739a0
NSString *taggedStrShortClass = NSStringFromClass([NSObject class]);//較短類情況下,該方法獲得的字符串是NSTaggedPointerString類型,存儲在棧區(qū),0x86b1d57ef6188519
NSString *taggedStrShortClassCopy = [taggedStrShortClass copy];//(NSTaggedPointerString *) $0 = 0xac71e37414f16209 @"NSObject" 內(nèi)存地址不會變,淺拷貝,值也相同
NSString *taggedStrShortClassMutaCopy = [taggedStrShortClass mutableCopy];//(__NSCFString *) $2 = 0x00006000036f4030 @"NSObject",內(nèi)存地址變化,深拷貝一份至堆區(qū),值相同,
NSObject *obj = [[NSObject alloc]init];//對象存儲在堆區(qū),0x0000600000c61160
//總結(jié):stringWithFormat方式,最終字符串的類型由字符串長度決定,少于10個字符類型為NSTaggedPointerString,否則為__NSCFString類型(長度的規(guī)律只適用于英文字母和數(shù)字,詳見文末備注)
NSString *strFormatLong = [NSString stringWithFormat:@"%@",@"01234567a909"];// ; 當長度超過9時,為__NSCFString,0x00006000003f6680,堆區(qū)
NSString *strFormatShort = [NSString stringWithFormat:@"%@",@"012345678"];//當字符長度位數(shù)為9或以下時,strFormat為NSTaggedPointerString,0xfc16f577dc1ba4f7,存儲在棧區(qū)
NSString *strShortConstantCopy = [@"999" copy];//__NSCFConstantString,0x00000001092b1420,數(shù)據(jù)區(qū).NSString調(diào)用copy是淺拷貝,和strDigital內(nèi)存地址相同
NSString *strShortConstantMutaCopy = [@"999" mutableCopy];//__NSCFString,0x0000600003fad410,堆區(qū),NSMutable類型的string調(diào)用mutalbeCopy是深拷貝,返回一個b可變類型字符串;調(diào)用copy也是深拷貝,返回不可變字符串
//從以下兩個對象的內(nèi)存地址可以得出結(jié)論:stringWithFormat不論接收什么類型的字符串參數(shù),也無論它在數(shù)據(jù)區(qū)棧區(qū)還是堆區(qū),都遵守長度臨界值9的規(guī)則
NSString *strShortConstantMutaCopyFormat = [NSString stringWithFormat:@"%@",strShortConstantCopy];//NSTaggedPointerString,0x8b25a274d8732690
NSString *strShortConstantCopyFormat = [NSString stringWithFormat:@"%@",strShortConstantMutaCopy];//NSTaggedPointerString,0x8b25a274d8732690
二.NSString為什么用copy,copy修飾詞到底做了什么工作?NSMutableString用copy修飾又如何?
測試主要注重下面幾方面:
1.NSString分別用copy和strong修飾
2.來源為可變字符串和來源為不可變字符串
3.修改字符串之前和修改字符串之后
注意:
1.不可變類型的字符串是無法更改內(nèi)容的,這里說的修改其實是修改它的指針,讓它指向另一塊內(nèi)存,所以不會影響self.string; 而可變字符串是可以更改內(nèi)容的,對于他的修改是修改這塊內(nèi)存的內(nèi)容.
2.copy修飾詞起作用是在setter方法中發(fā)生的,如果通過_string_copy = [NSMutableString new];設置,得到的self.string_copy仍然是淺拷貝.
3.一個對象的類型是由運行時決定的,和@property中的聲明無關(guān),OC對于類型不匹配只會報警告,編譯可以通過,但并不代表你執(zhí)行時候就不會出錯.
4.NSString +copy ( 淺拷貝,返回不可變對象) NSString+mutableCopy(深拷貝,返回可變對象) NSMutableString+copy(深拷貝,返回不可變對象) NSMutableString+mutableCopy(深拷貝,返回可變對象)
依然先上代碼,看運行結(jié)果,內(nèi)存地址和相關(guān)注釋如下:
@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修飾,最終都只會僅僅拷貝一個指針,并不拷貝這塊值的內(nèi)存,因為這塊內(nèi)存存儲的字符串
本來就是非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這個指針指向另一塊內(nèi)存區(qū)域,
故結(jié)果是不會影響self.string_copy,self.string_strong所指向的那塊內(nèi)存)
(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調(diào)用copy方法雖是深拷貝,但返回的是不可變對象,不可變對象是沒有appendString方法的
/*
二:來源是可變字符串(修改前)
0.未修改源muta字符串時,可以看出:copy修飾的string_copy拷貝了一份內(nèi)存,即值和strMuta一樣,但實際存儲這個字符串值的內(nèi)存不是同一塊.
而strong修飾的string_strong只拷貝了一份指針,并沒有拷貝存儲這塊值的內(nèi)存,實際上只是指針不是同一個,指向的內(nèi)存還是同一塊
(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指針指向新拷貝的那一塊內(nèi)存區(qū)域-->0x0000600003f88120,所以修改源串strMuta的那
塊內(nèi)存-->0x000060000318ab20不會影響copy修飾的字符串;而string_strong只是拷貝了一個指針,這個指針仍然指向-->0x000060000318ab20這
塊區(qū)域,和源串是同一塊內(nèi)存,唯一不同的就是指針本身地址不同,而指針的內(nèi)容是相同的,都存儲著源串的內(nèi)存地址,修改源串導致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"
*/
從上面代碼運行的結(jié)果分析,不難得出:
其實copy就是在setter方法里起了特殊作用,利用copy修飾的string,在setter方法中進行一次判斷;a.如果來源是可變字符串,就深拷貝一份(為什么深拷貝?因為來源是可變字符串,這串字符就可能會被修改,如果不深拷貝一份的話,將來源串不小心被修改了,你的self.string也會跟著變化,如果發(fā)生了不可預見的結(jié)果,你肯定要怪xcode了,這個結(jié)果是你不希望發(fā)生的);b.如果來源是不可變字符串,就直接賦值,不拷貝內(nèi)容(為什么只拷貝指針不拷貝內(nèi)容?因為來源為不可變,你就算改變self.string也只能通過重新賦值來改變,賦值肯定是你自己操作的,如果因為重新賦值發(fā)生了不可預見的問題,那就是你自己的責任了)
從結(jié)論中我們可以推測NSMutableString不用copy修飾的原因了:如果你用copy去修飾一個NSMutableString的字符串屬性string_muta_copy,如果將來你把一個不可變來源通過set方法賦值給這個屬性,set方法內(nèi)部會因為這個來源是不可變的進行一次copy,copy后再將新的對象賦給_string_muta_copy,這時候的拷貝是深拷貝,但返回的是不可變對象,而在你聲明中你認為這個對象是可變的,編譯時期也會被認為是可變的,你向他發(fā)送NSMutableString的方法消息不會報錯,但到了運行時候回因為string_muta_copy是個不可變字符串而找不到方法.
對于NSMutableString類型的屬性,我們在對它賦值時,最好確保是可變類型,防止以后你把它當做可變字符串,進行拼接操作,而實際上它卻被你賦值了不可變字符串而出錯
至此:大概講述了NSString和NSMutableString調(diào)用copy和mutableCopy.修飾詞用copy和strong的原因.
另外:我們通過把代碼編譯成C++文件,可以看出copy修飾的屬性和strong修飾的屬性,在底層的處理是不同的,下面是源碼節(jié)選:
//ViewController對象在底層實則是一個結(jié)構(gòu)體,它的每個屬性都將作為該類型結(jié)構(gòu)體的一個成員
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結(jié)構(gòu)體中的偏移量,通過偏移量+結(jié)構(gòu)體內(nèi)存地址就可以找到這個成員
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時,返回結(jié)構(gòu)體里的該成員
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方法,可以看見,內(nèi)部是調(diào)用了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修飾的屬性處理不一樣,沒有調(diào)用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; }
關(guān)于objc_setProperty:
objc-runtime源碼里檢索關(guān)鍵詞,來到這里:
#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方法調(diào)用時會給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);
}
對比上面?zhèn)鬟M來的參數(shù):
objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct ViewController, _string_copy), (id)string_copy, 0, 1)
可以看出參數(shù)依次傳入了self,_cmd,偏移量,string_copy,非atomic,copy為YES
構(gòu)造好參數(shù)之后,實際上是調(diào)用了reallySetProperty函數(shù),下面查看該函數(shù):
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
//當offset為0時所做的處理,因為結(jié)構(gòu)體第一個成員都是isa,如果offset為0代表是更改isa成員
if (offset == 0) {
object_setClass(self, newValue);
return;
}
//舊值
id oldValue;
id *slot = (id*) ((char*)self + offset);
//當copy為YES時,調(diào)用copyWithZone方法
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
//當mutableCopy為YES時,調(diào)用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得到的字符串類型的問題.實際上系統(tǒng)底層對字符串的處理是按照一個字符對應一個字節(jié)(8位),ASCII碼恰好用8位存儲一個字符,但這些字符僅限英文字母和數(shù)字,首位為0,1-7位剛好可表示128個字符,由于字符標準不統(tǒng)一,其它語言的字符不能用標準的ASCII碼來存儲.所以漢字日文等等,即使是一個字符也用__NSCFString來存儲,而不是按照低于10位采用NSTaggedPointerString來優(yōu)化存儲的方式.
原文鏈接:
NSTaggedPointerString,__NSCFConstantString,__NSCFString和NSString的關(guān)系?NSString為什么用copy?