iOS開發NSString的內存及copy和mutableCopy

私有類__NSCFConstantString,__NSCFString和NSTaggedPointerString

聲明一個對象,可以用父類類型聲明,子類來初始化,聲明只是決定了你使用的時候能用哪個類型的方法,但是這么寫是沒問題的:

UIView *view = [[UILabel alloc] init];
NSLog(@"%@", NSStringFromClass(view.class));

這里聲明用UIView聲明,初始化用UILabel初始化,打印的結果UILabel而不是UIView,但是view因為是使用UIVIew類型接收的,它僅能使用UIView的是一些方法和屬性而不能使用UIlabel的,當然,由于它實際就是一個UILabel類型的實例,它是可以直接轉換的:

 UILabel *realLabel = (UILabel *) view;

現在我們看下NSString的幾種情況:

NSString *test0 = @"Jeff";
NSString *test1 = [[NSString alloc] initWithString:@"Jeff"];
NSString *test2 = [NSString stringWithFormat:@"%@", @"Jeff"];
NSString *test3 = [[NSMutableString alloc] initWithString:@"Jeff"];
NSString *test4 = @"Jeff".copy;
NSString *test5 = @"Jeff".mutableCopy;
/**打印測試*/
NSArray<NSString *> *tests = @[test0, test1, test2, test3, test4, test5];
[tests enumerateObjectsUsingBlock:^(NSString *test, NSUInteger idx, BOOL *stop) {
    NSLog(@"test%ld的指針為:%p,類型為%@", idx, test, NSStringFromClass(test.class));
}];

先不說指針的打印,先說打印類型,由于NSMutableString本身是NSString的子類,因此全部用NSString接收是沒有問題的,只是如果真正的結果是可變字符串的時候打印類型會才變成NSMutableString(想象中)。
打印結果如下:

2018-06-07 15:07:34.074744+0800 CopyTest[12843:255981] test0的指針為:0x1070090d8,類型為__NSCFConstantString
2018-06-07 15:07:34.074832+0800 CopyTest[12843:255981] test1的指針為:0x1070090d8,類型為__NSCFConstantString
2018-06-07 15:07:34.074951+0800 CopyTest[12843:255981] test2的指針為:0xa0000006666654a4,類型為NSTaggedPointerString
2018-06-07 15:07:34.075055+0800 CopyTest[12843:255981] test3的指針為:0x60c00005a5e0,類型為__NSCFString
2018-06-07 15:07:34.075150+0800 CopyTest[12843:255981] test4的指針為:0x1070090d8,類型為__NSCFConstantString
2018-06-07 15:07:34.075242+0800 CopyTest[12843:255981] test5的指針為:0x60c00005a490,類型為__NSCFString

我們可以看到,它并沒有直接打印出NSString或者NSMutableString,而是打印出了3種類型,即_NSCFConstantString,__NSCFString和NSTaggedPointerString。而且內存地址也是有的相同有的不同,長短各異。
即是說,其實系統并不是用的NSString來作為當前的類型,而是內部有私有的類型來作了處理。

__NSCFConstantString

可以看到,此打印的結果的內存地址均相同,該內存地址實際上是在常量區,并不會釋放,它是不可變的,拿test0的情況單獨舉例:

NSString *test0 = @"Jeff";
NSLog(@"變化前內存地址:%p",test0);
test0 = @"Tom";
NSLog(@"變化后內存地址:%p",test0);
NSString *Jeff = @"Jeff";
NSLog(@"新建的Jeff內存地址:%p",Jeff);

打印結果:

2018-06-07 15:36:11.604766+0800 CopyTest[13606:278220] 變化前內存地址:0x105ed90b8
2018-06-07 15:36:11.604917+0800 CopyTest[13606:278220] 變化后內存地址:0x105ed90f8
2018-06-07 15:36:11.605046+0800 CopyTest[13606:278220] 新建的Jeff內存地址:0x105ed90b8

test0改變成@"Tom"后,在常量區為@"Tom"開辟了新了內存地址,并把test0的指針指向了它,于是test0的指向的地址就變了。但是@"Jeff"的地址已經生成過了,且并不會釋放掉,這樣Jeff變量等于@"Jeff"相當于把指針又指向了最初的@"Jeff"的內存地址,這樣它和第一個打印的地址是相同的。而它們的類型之前已經驗證過了,是__NSCFConstantString類型,此類型內存地址即在常量區,test0,test1,test4均為此種類型。__NSCFConstantString是不可變的,初始化為@"Jeff"就是@"Jeff",重新賦值為@"Tom"只是把指針指向了一個新的__NSCFConstantString地址,之前的@"Jeff"的地址沒有任何變化,看它的名字中帶"Constant"也能看出來它的性質。

__NSCFString

測試中可以看出,test3和test5為此類型,test3寫法上我們其實是期望它最終類型是NSMutableString類型(雖然接受是用父類NSString接收的),可以看出,系統內部實際上是用__NSCFString類型來處理NSMutableString的,新建一個任何的NSOBject類型并打印地址,可以看出其地址和test3類似,而實際上也正是如此,__NSCFString和對象一樣,內存地址是在堆中,而不是常量區,它的內存管理和常規對象類似。test5的情況后面討論copy和mutableCopy再提。NSMutableString是使用__NSCFString來處理,但不能認為所有的__NSCFString類型均是NSMutableString類型。

NSTaggedPointerString

有興趣的可以去看下【譯】采用Tagged Pointer的字符串
個人覺得其實只要知道此類型的具體的表現即可,此類型字符串地址存放在棧,為不可變的類型,test2如果把@"Jeff"改成一個很長的字符串成(可以嘗試使勁復制粘貼),你會發現它又變成了一個__NSCFString類型,但是它還是不可變字符串,這也是為什么說不能認為所有的__NSCFString類型均是NSMutableString類型的原因。

類型小結:

1.NSString對應的私有類主要為 __NSCFConstantString類型,部分情況為NSTaggedPointerString,本來應該為NSTaggedPointerString但因長度過長,也可能為__NSCFString類型。即NSString在私有類體現上可能為3種中的任意一種。
另外,如果字符串私有類型為__NSCFConstantString或者NSTaggedPointerString,那么它實際上應該就體現為一個NSString類型。
2.NSMutableString在私有類體現上均為__NSCFString,但__NSCFString并不一定體現為NSMutableString類型
3.鑒于同一個字符串如"Jeff"的表現可能是任何一種情況,因此我們不能直接用內存地址來判斷兩個字符串是否相同,而需要用系統提供的isEqualToString方法來判斷。

copy和mutableCopy

示例:

NSString *string = @"Jeff";//基礎不可變字符串
NSString *stringCopy = string.copy;
NSString *stringMCopy = string.mutableCopy;
NSMutableString *mutableString = [NSMutableString stringWithString:@"Jeff"];//基礎可變字符串
NSString *mutableStringCopy = mutableString.copy;
NSString *mutableStringMCopy = mutableString.mutableCopy;

NSArray<NSString *> *tests = @[string, stringCopy, stringMCopy, mutableString, mutableStringCopy, mutableStringMCopy];
for (NSString *test in tests) {
    NSLog(@"指針為:%p,類型為%@", test, NSStringFromClass(test.class));
}

打印結果:

2018-06-07 16:48:49.683133+0800 CopyTest[15592:335287] 指針為:0x10c1ef088,類型為__NSCFConstantString
2018-06-07 16:48:49.683265+0800 CopyTest[15592:335287] 指針為:0x10c1ef088,類型為__NSCFConstantString
2018-06-07 16:48:49.683373+0800 CopyTest[15592:335287] 指針為:0x6080000512e0,類型為__NSCFString
2018-06-07 16:48:49.683458+0800 CopyTest[15592:335287] 指針為:0x608000051280,類型為__NSCFString
2018-06-07 16:48:49.683573+0800 CopyTest[15592:335287] 指針為:0xa0000006666654a4,類型為NSTaggedPointerString
2018-06-07 16:48:49.683691+0800 CopyTest[15592:335287] 指針為:0x608000051340,類型為__NSCFString

分析:

1.聲明了一個NSString和一個NSMutableString(備注寫了"基礎xx字符串")并在其基礎上做操作。為了避免接收類型錯誤,這里還是均用父類NSString來接收copy和mutableCopy的結果。我們發現string和stringCopy內部私有類型為__NSCFConstantString,然后,可變字符串mutableString通過點copy出來的mutableStringCopy私有類型為NSTaggedPointerString類型。而__NSCFConstantString和NSTaggedPointerString具體體現其實就是NSString,即為不可變字符串,區別只是string和stringCopy內存地址相同,在常量區,mutableStringCopy在棧。
2.基礎不可變字符串string通過點mutableCopy出來的stringMCopy、基礎可變字符串mutableString、以及mutableString.mutableCopy產生的mutableStringMCopy均為__NSCFString類型,但__NSCFString并不能確定其是否是體現為NSMutableString類型,這里可以強制轉換下并隨便調用下NSMutableString的某個方法驗證其確實就是NSMutableString類型。

結論:

1.NSString類型實例通過點copy得到一個內存地址相同的NSString,地址在常量區
2.NSString類型實例通過點mutableCopy得到一個NSMutableString類型
3.NSMutableString通過點copy得到一個NSString類型,但內存地址和1中有所不同,地址在棧區
4.NSMutableString通過mutableCopy得到一個新的NSMutableString類型
5.NSMutableString類型實例內存地址均在堆區

聲明為屬性時,NSString里copy和strong的區別和注意事項

賦值原理

很多人可能知道,NSString屬性一般用copy,為了防止接收NSMutableString后被修改,但是沒有深入理解的話,屬性雖然寫成copy了,但可能還是會犯一些小錯誤。
原理上來說,copy和strong的區別,其實是在于set方法,如下:

@interface ViewController ()
@property(nonatomic, copy) NSString *stringCopy;
@property(nonatomic, strong) NSString *stringStrong;

@end

@implementation ViewController
- (void)setStringCopy:(NSString *)stringCopy {
    _stringCopy = stringCopy.copy;
}

- (void)setStringStrong:(NSString *)stringStrong {
    _stringStrong = stringStrong;
}

系統內部會根據不同的前綴按照上面的形式來做set方法的處理,這里我重寫出來,區別在于,copy修辭的屬性,set方法會使用里屬性變量會接收傳進來的值的copy對象,而不是像strong,直接接收字符串對象,而我們賦值的時候是可以接收一個不可變的NSMutableString類型的字符串的,因為它是NSString的子類。strong修辭的stringStrong屬性,接收一個NSStirng字符串類型,自然是沒有太大的問題,因為接收的字符串是不會變化一直存在的,好比:

NSString *stringTest = @"test1";
self.stringStrong = stringTest;
stringTest = @"test2";

這里只是stringTest重新指向了一個新的常量區的內存地址(@"test2"),而stringStrong并沒有變化,還是之前指向的地址(@"test1"),但如果接收的是一個堆里的NSMutableString,那就不同了,例如:

NSMutableString *test = [NSMutableString stringWithString:@"test"];
self.stringStrong = test;
[test appendString:@"appendString"];

首先是新建了一個可變的字符串,然后stringStrong=test,因為是strong修辭的,所以屬性直接指向了它,最后這個可變字符串添加了額外的"appendString",因為屬性指向了這個可變字符串,所以屬性的值也跟著變化了。同樣的,我們用stringCopy來執行同樣的邏輯:

NSMutableString *test = [NSMutableString stringWithString:@"test"];
self.stringCopy = test;
[test appendString:@"appendString"];

因為self.stringCopy = test;內部實際上是把_strongCopy賦值為test.copy了,它是一個不可變字符串類型,且是一個區別于變量test的新的內存地址,因此test后面即使修改了,也對stringCopy不會有影響,這也是為什么字符串一般需要用copy的原因。

注意事項!!

重寫字符串屬性set方法一定要記得先copy再賦值!

上面我們已經知道,copy和strong修辭NSString,其原因在于set方法的不同,而很多時候我們重寫set方法都是直接接收入參,例如這樣:

@interface ViewController ()
@property(nonatomic, copy) NSString *name;
@property(nonatomic, copy) NSString *father;

@end

@implementation ViewController
- (void)setName:(NSString *)name {
    _name = name;
 // _name = name.copy;//應該這么寫
    if ([name isEqualToString:@"Jeff"]) {
        _father = @"Tom";
    }
}

這里只是打個比方,只是說明有些時候會重寫set方法來執行一些特定邏輯,但是因為重寫的時候,直接用的是_name=name,這樣做其實copy的修辭已經沒有意義了,這個隨便測試一下就能知道結果。當name屬性接收一個可變字符串,且可變字符串有變動,name屬性也還是會一起變動,這里_name賦值應該是_name=name.copy,而不是直接賦值,直接賦值和strong沒有區別。重寫set方法的時候一定要注意。

構造方法也一樣,需要傳入copy后的字符串賦值

如上面的例子,如果僅用name來構造控制器ViewController,且寫成這樣:

- (instancetype)initWithName:(NSString *)name {
    self = [super init];
    if (self) {
        _name = name;
//       _name = name.copy;//應該這么寫
    }

    return self;
}

這樣寫也是有問題的,因為這里賦值name屬性用的是屬性的常量_name,沒有調用set方法,所以必須把賦值改為_name=name.copy,否則,構造的時候傳入的是個不可變字符串,且在構造完后立馬修改它,屬性也會跟著改變。
這里如果寫成self.name = name走set方法來賦值屬性本身是可以避免這種情況,但不建議,構造方法一般還是直接操作屬性的變量_name合適。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容