iOS NSString詳解

一:__NSCFConstantString      __NSCFString        NSTaggedPointerString

二:weak修飾,字符串內存管理

三:NSTaggedPointerString講解

四:面試題

五:__NSCFString:Toll-free bridgin橋接機制(接上面一)

 

OC中的NSString不論是在編譯時還是在運行時都做了很多的優化,並不同於普通的對象,它是一個非常複雜的存在。 

一:__NSCFConstantString      __NSCFString        NSTaggedPointerString

這個證明需要再mrc環境下。

我們需要研究的NSString創建出來的實際的類型有:

__NSCFConstantString
__NSCFString
NSTaggedPointerString

那這三種類型分別在什麼時候創建出來,又是什麼意思呢?先看定義產生這三種類型分別是什麼創建方式下產生的。看代碼

//宏定義
#define HXLog(_var) ({ NSString *name = @#_var; NSLog(@"變量名=%@,類型=%@, 地址=%p,引用計數=%d,值=%@", name, [_var class], _var,(int)[_var retainCount], _var); })


        NSString *a0 = [[NSString alloc] init];
        NSString *a1 = @"12345678910";
        NSString *a2 = [NSString stringWithString:@"12345678910"];
        NSString *a3 = [[NSString alloc] initWithString:@"12345678910"];
        NSString *a4 = [[NSString alloc] initWithString:a0];

        NSString *a5 = [[NSString alloc] initWithFormat:@"1123456789"];
        NSString *a6 = [NSString stringWithFormat:@"123456789"];
        NSString *a7 = [NSString stringWithFormat:a5];

        HXLog(a0);
        HXLog(a1);
        HXLog(a2);
        HXLog(a3);
        HXLog(a4);
        HXLog(a5);
        HXLog(a6);
        HXLog(a7);
2018-09-03 11:31:35.196458+0800 lalal[2375:236529] 變量名=a0,類型=__NSCFConstantString, 地址=0x7fff85efa1d8,引用計數=-1,值=
2018-09-03 11:31:35.196648+0800 lalal[2375:236529] 變量名=a1,類型=__NSCFConstantString, 地址=0x100003060,引用計數=-1,值=12345678910
2018-09-03 11:31:35.196666+0800 lalal[2375:236529] 變量名=a2,類型=__NSCFConstantString, 地址=0x100003060,引用計數=-1,值=12345678910
2018-09-03 11:31:35.196679+0800 lalal[2375:236529] 變量名=a3,類型=__NSCFConstantString, 地址=0x100003060,引用計數=-1,值=12345678910
2018-09-03 11:31:35.196691+0800 lalal[2375:236529] 變量名=a4,類型=__NSCFConstantString, 地址=0x7fff85efa1d8,引用計數=-1,值=
2018-09-03 11:31:35.196736+0800 lalal[2375:236529] 變量名=a5,類型=__NSCFString, 地址=0x100600330,引用計數=2,值=1123456789
2018-09-03 11:31:35.196769+0800 lalal[2375:236529] 變量名=a6,類型=NSTaggedPointerString, 地址=0x1ea1f72bb30ab195,引用計數=-1,值=123456789
2018-09-03 11:31:35.196797+0800 lalal[2375:236529] 變量名=a7,類型=__NSCFString, 地址=0x100600220,引用計數=2,值=1123456789

可以看出來,a0,a1,a2, a3, a4類型都爲__NSCFConstantString類型,引用計數值爲-1。

a5和a7都爲__NSCFString類型,引用計數值爲1。

a6爲NSTaggedPointerString類型,引用計數值爲-1。

小結:

創建的字符串有三種類型:造成這種情況是由於 OC 對 NSString 的內存優化產生的。

  • __NSCFConstantString

從字面就可以看出,這是一個常量字符串,該類型的字符串是以字面量創建的,是在編譯期創建的,保存在常量區。通過a0,a1,a2, a3, a4的打印結果看出,當創建的字符串變量值在常量區存在時,變量會指向那個字符串,這是編譯期做的優化。

對於 initWithString 實例方法以及 stringWithString 類方法,編譯器會給出redundant警告,原因是該方法創建字符串等同於直接複製字符串字面量

那 retainCount爲-1是什麼情況

首先retainCount是NSUInteger的類型,其實上面的打印是將它作爲int類型打印。所以它其實不是-1,它的實際值是4294967295。在objc的retainCount中.如果對象的retainCount爲這個值,就意味着“無限的retainCount”,這個對象是不能被釋放的。

所有的 __NSCFConstantString對象的retainCount都爲-1,這就意味着 __NSCFConstantString不會被釋放,使用第一種方法創建的NSString,如果值一樣,無論寫多少遍,都是同一個對象。而且這種對象可以直接用 == 來比較

文字常量區存放常量字符串,程序結束後由系統釋放,也就是說指向常量表的指針不受引用計數管理。所以對於NSCFConstantString類型的變量,OC 的內存管理策略對其無效。

  • __NSCFString (>=10位是__NSCFString類型)
    表示這是一個對象類型的字符串,在運行時創建,存儲在堆區,服從OC 的對象內存管理策略。該類型的字符串由 Format創建,無論是實例方法還是類方法且其長度不能太小(內容若包含中文字符,不論長度大小,都是NSCFString),否則創建的是NSTaggedPointerString類型,例如上例的變量 a5 與 a6。

  • NSTaggedPointerString (從上面也可以看出來,0-9位是taggedpointer類型)
    對於64位程序,爲了節省內存和提高運行速度,蘋果引入了 Tagged Point 技術NSTaggedPointerString是對NSCFString優化後的存在,在運行時創建時對字符串的內容和長度做出判斷,若字符串內容是由ASCII字符構成且長度較小(大概十個字符以內),這時候創建的字符串就是NSTaggedPointerString類型,字符串直接存儲在指針裏,引用計數同樣爲-1,不適用對象的內存管理策略。

Tagged Pointer指針的值不再是地址了,而是真正的值。所以,實際上它不再是一個對象了,它只是一個披着對象皮的普通變量而已。所以,它的內存並不存儲在堆中,OC 對象的內存管理方式對其無效。

 

對上述a0-a7進行copy,所得到的類型還都是原來的類型,並不會改變,並且地址都不會改變,因爲原來就是一個不可變的,因爲copy的還是不可變的,所以就沒有開闢新的對象。

但是進行mutablecopy,會發現

__NSCFConstantString->__NSCFString;    NSTaggedPointerString->__NSCFString;   __NSCFString 依舊是__NSCFString類型

同時理解了上面三個類型,也就知道了一些關於string的內存管理的些許知識,比方說,看下面,用weak來修飾,看釋放的時間。

 

二:weak修飾,字符串內存管理

在arc環境下,

__weak id weaka1 = nil;
__weak id weaka2 = nil;
__weak id weaka3 = nil;
__weak id weaka4 = nil;

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    NSLog(@"viewWillAppear:\n weaka1:%@ \n weaka2:%@  \n weaka3:%@ \n weaka4:%@",[weaka1 class],[weaka2 class],[weaka3 class], [weaka4 class]);
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    NSLog(@"viewWillDisappear:\n weaka1:%@ \n weaka2:%@  \n weaka3:%@\n weaka4:%@",[weaka1 class],[weaka2 class],[weaka3 class], [weaka4 class]);
}

- (void)viewDidLoad {
    [super viewDidLoad];

    NSString *a1 = @"string";
    NSString *a2 = [NSString stringWithFormat:@"stirng"];
    NSString *a3 = [NSString stringWithFormat:@"stirng strings"];
    NSString *a4 = [a1 mutableCopy];

    weaka1 = a1;
    weaka2 = a2;
    weaka3 = a3;
    weaka4 = a4;
}
2018-09-03 15:03:42.779454+0800 newstestt[5026:514384] viewWillAppear:
 weaka1:__NSCFConstantString 
 weaka2:NSTaggedPointerString  
 weaka3:__NSCFString 
 weaka4:(null)
2018-09-03 15:03:44.432565+0800 newstestt[5026:514384] viewWillDisappear:
 weaka1:__NSCFConstantString 
 weaka2:NSTaggedPointerString  
 weaka3:(null)
 weaka4:(null)

這個結果說明,

a1是__NSCFConstantString,字符串常量,放在常量區,對其retain或者release不影響它的引用計數,程序結束後釋放。用字面量語法創建出來的string就是這種,所以在出了viewDidLoad方法以後在其他地方也能打印出值,根本沒有釋放。不由我們控制

a2是NSTaggedPointerString,Tagged Point,標籤指針,蘋果在64位環境下對NSString和NSNumber做的一些優化,簡單來說就是把對象的內容存放在了指針裏,這樣就不需要在堆內存裏在開闢一塊空間存放對象了,一般用來優化長度較小的內容。這個根本不是對象,所以也不受引用計數的計算。所以也無所謂釋放和不釋放之說。

a3 和a4 都是__NSCFString 類型, 這種string就和普通的對象很像了,儲存在堆上,有正常的引用計數,需要程序員分配釋放。所以weaka3 = a3時,會打印出null,cstr出了方法作用域在runloop結束時就被autoreleasepool釋放了。只是這裏有一點需要說明,就是系統創建的stringWithFormat類型和我們創建的以 alloc, copy, init,mutableCopy和new這些方法打頭的方法,返回的都是 retained return value,例如[[NSString alloc] initWithFormat:],而其他的則是unretained return value,例如 [NSString stringWithFormat:]。對於前者調用者是要負責釋放的,對於後者(系統穿件的那些)就不需要了。而且對於後者ARC會把對象的生命週期延長,確保調用者能拿到並且使用這個返回值,但是並不一定會使用 autorelease,在worst case 的情況下才可能會使用,因此調用者不能假設返回值真的就在 autorelease pool中。從性能的角度,這種做法也是可以理解的。如果我們能夠知道一個對象的生命週期最長應該有多長,也就沒有必要使用 autorelease 了,直接使用 release 就可以。如果很多對象都使用 autorelease 的話,也會導致整個 pool 在 drain 的時候性能下降。

所以可以看到,a3剛開始沒多久就釋放了,但是a4過了好一會才釋放。當然這個在mrc下,自己釋放a3即可,因爲是自己創建的。也會得到同樣的效果。

 

三:NSTaggedPointerString講解

爲什麼字面量常量蘋果不使用NSTaggedPointerString呢?

【譯】採用Tagged Pointer的字符串中文版的 翻譯有些晦澀,看了下英文版的描述比較易懂些:

although a string like @"a" could be stored as a tagged pointer, constant strings are never tagged pointers. Constant strings must remain binary compatible across OS releases, but the internal details of tagged pointers are not guaranteed.

原因是常量字符串需要在跨系統上保持二進制兼容,而 tagged pointers在技術上並不能保證這個。因此對於這種短的字符串字面量還是使用\ __NSCFConstantString類型。

下面一個問題,tagged pointers在內存上分配在哪個區?
其實如果我們仔細在XCode中多點兩下,就可以看到其實tagged pointers是沒有isa指針的,說明它根本不是一個對象。究其原因這個要說到tagged pointers是爲什麼被創造出來

一般來說,對象所佔內存是和CPU位數相關的。在32位的時候,比如一個NSNumber對象佔用的空間是4(對象指針)+4(對象的值)=8字節,升級到64位的時候,邏輯不變的話,佔用的空間直接翻倍,變成8+8=16字節,這樣會產生十分嚴重的效率問題:爲了存儲和訪問一個NSNumber對象,我們需要在堆上爲其分配內存,另外還要維護它的引用計數,管理它的生命期。這些都給程序增加了額外的邏輯,造成運行效率上的損失。

在查找資料的過程中也發現了蘋果官方的明確說法(摘自深入理解Tagged Pointer):

我們也可以在WWDC2013的《Session 404 Advanced in Objective-C》視頻中,看到蘋果對於Tagged Pointer特點的介紹:

  • Tagged Pointer專門用來存儲小的對象,例如NSNumber和NSDate
  • Tagged Pointer指針的值不再是地址了,而是真正的值。所以,實際上它不再是一個對象了,它只是一個披着對象皮的普通變量而已。所以,它的內存並不存儲在堆中,也不需要malloc和free。
  • 在內存讀取上有着3倍的效率,創建時比以前快106倍。

由此看來,NSTaggedPointerString根本不是對象,是分配在棧區的。

 

四:面試題

4.1:寫一個NSString類的實現

+ (id)initWithCString:(const char *)nullTerminatedCString encoding:(NSStringEncoding)encoding;

實現如下:

+ (id) stringWithCString: (const char*)nullTerminatedCString 

            encoding: (NSStringEncoding)encoding

{

  NSString  *obj;

  obj = [self allocWithZone: NSDefaultMallocZone()];

  obj = [obj initWithCString: nullTerminatedCString encoding: encoding];

  return AUTORELEASE(obj);

}

4.2:判斷兩個NSString的字面量是否相同,爲什麼要用isEqualToString來判斷,而不能用==來判斷呢?

     並且關於字符串有非常多的問題,比方說:判斷兩個NSString的字面量是否相同,爲什麼要用isEqualToString來判斷,而不能用==來判斷呢? 可能大多數人會回答:因爲==判斷的是兩個指針是否相等,而NSString是分配到堆上的,每次創建的時候,指針指向的地址的不同的,所以不能用==來判斷。但是這樣的回答不完整。這個題感覺有點毛病,字面量本身就只有一種方式,@""; 直接創建等於賦值,這種方式創建的類型都是__NSCFConstantString,本身也不會有引用計數,所以它就是一個對象,這個是可以用==來判斷的,我想題的意思應該是NSString創建的字符串,是否相等,要用isEqualToString來判斷,因爲字符串的創建方式不同,類型不同,地址不同,單純從==來判斷的話,不準確。

深入理解Tagged Pointer字符串深度剖析

 

五:__NSCFString:Toll-free bridgin橋接機制

Toll-free bridging,簡稱爲TFB,是一種允許某些ObjC類與其對應的CoreFoundation類(Core Foundation框架 (CoreFoundation.framework) 是一組C語言接口,它們爲iOS應用程序提供基本數據管理和服務功能)之間可以互換使用的機制。比如 NSString與CFString是橋接(bridged)的, 這意味着可以將任意NSString當做CFString使用,也可以將任意的CFString當做NSString使用。

官網也有相關描述:There are a number of data types in the Core Foundation framework and the Foundation framework that can be used interchangeably. This capability, called toll-free bridging, means that you can use the same data type as the parameter to a Core Foundation function call or as the receiver of an Objective-C message。

原理(拿NSString舉例)大概是:NSString是一個抽象類,每當你創建一個NSString實例,實際上是創建的NSString的一個私有子類實例。其中一個私有子類就是NSCFString,其是CFString類的在ObjC中的對應類。NSCFString實現了作爲NSString需要的所有方法。

我的理解:總之,你知道有Toll-Free Bridging橋接機制,然後NSCFString是NSString的私有子類,實現了它的所有方法。詳細解釋看官網

而爲什麼要有CFString呢?

官網解釋:

CFString provides a suite of efficient string-manipulation and string-conversion functions. It offers seamless Unicode support and facilitates the sharing of data between Cocoa and C-based programs

 

Toll-Free Bridging的橋接機制、 Toll Free Bridging的內部原理

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章