從Swift看Objective-C的數組使用

來源:互聯網
上載者:User

標籤:編程   code   border   改進   效果   觀察   ras   modify   指標   

  狀態維護是個怎麼說都不夠的話題,畢竟狀態的處理是我們整個App最核心的部分,也是最容易出bug的地方。之前寫過一篇以函數式編程的角度看狀態維護的文章,這次從Swift語言層面的改進,看看Objective C下該如何合理的處理數組的維護。

Objective C數組的記憶體布局

要瞭解NSArray,NSSet,NSDictionary這些集合類的使用方法,我們需要先弄明白其對應的記憶體布局(Memory Layout),以一個NSMutableArray的property為例:

12345 //declare@property (nonatomic, strong) NSMutableArray*                 arr; //initself.arr = @[@1, @2, @3].mutableCopy;

arr初始化之後,以64位系統為例,其實際的記憶體布局分為三塊:

第一塊是指標NSMutableArray* arr所處的位置,為8個位元組。第二塊是數組實際的記憶體地區所處的位置,為連續3個指標地址,各佔8個位元組一共24個位元組。第三塊才是@1,@2,@3這些NSNumber對象真正的記憶體空間。當我們調用不同的API對arr進行操作的時候,要分清楚實際是在操作哪部分記憶體。

比如:

1 self.arr = @[@4];

是在對第一塊記憶體地區進行賦值。

1 self.arr[0] = @4;

是在對第二塊記憶體地區進行賦值。

1 [self.arr[0] integerValue];

是在訪問第三塊記憶體地區。

之前寫過一篇多安全執行緒的文章,我們知道即使在多線程的情境下,對第一塊記憶體地區進行讀寫都是安全的,而第二塊和第三塊記憶體地區都是不安全的。

NSMutableArray為什麼危險?

在Objective C的世界裡,帶Mutable的都是危險分子。我們看下面代碼:

12345678910111213141516 //main threadself.arr = @[@1, @2, @3].mutableCopy; for (int i = 0; i < _arr.count; i ++) {    NSLog(@"element: %@", _arr[i]);} //thread 2NSMutableArray* localArr = self.arr; //get result from serverNSArray* results = @[@8, @9, @10]; //refresh local arr[localArr removeAllObjects];[localArr addObjectsFromArray:results];

NSMutableArray* localArr = self.arr;執行之後,我們的記憶體模型是這樣的:

這行代碼實際上只是新產生了8個位元組的第一類記憶體空間給localArr,localArr實際上還是和arr共用第二塊和第三塊記憶體地區,當在thread 2執行[localArr removeAllObjects];清理第二塊記憶體地區的時候,如果主線程正在同時訪問第二塊記憶體地區_arr[1],就會導致crash了。這類問題的根本原因,還是在對於同一塊記憶體地區的同時讀寫。

Swift的改變

Swift對於上述的數組賦值操作,從語言層面做了根本性的改變。

Swift當中所有針對集合類的操作,都符合一種叫copy on write(COW)的機制,比如下面的代碼:

12345678910 var arr = [1, 2, 3]var localArr = arr print("arr: \(arr)")print("localArr: \(localArr)") arr += [4]; print("arr: \(arr)")print("localArr: \(localArr)")

當執行到var localArr = arr的時候,arr和localArr的記憶體布局還是和Objective C一致,arr和localArr都共用第二第三塊記憶體地區,但是一旦出現寫操作(write),比如arr += [4];的時候,Swift就會針對原先arr的第二塊記憶體地區,產生一份新的拷貝(copy),也就是所謂的copy on write,執行cow之後,arr和localArr就指向不同的第二塊記憶體地區了,如所示:

一旦出現針對arr寫操作,系統就會將記憶體地區2拷貝至一塊新的記憶體地區4,並將arr的指標指向新開闢的地區4,之後再發生數組的改變,arr和localArr就指向不同的地區,即使在多線程的環境下同時發生讀寫,也不會導致訪問同一記憶體地區的crash了。

上面的代碼,最後列印的結果中,arr和localArr中所包含的元素也不一致了,畢竟他們已經指向各自的第二類記憶體地區了。

這也是為什麼說Swift是一種更加安全的語言,通過語言層面的修改,協助開發人員避免一些難以調試的bug,而這一切都是對開發人員透明的,免費的,開發人員並不需要做特意的適配。還是一個簡單的=操作,只不過背後發生的事情不一樣了。

Objective C的領悟

Objective C還沒有退出曆史舞台,依然在很多項目中發揮著餘熱。明白了Swift背後所做的事情,Objective C可以學以致用,只不過要多寫點代碼。

Objective C既然沒有COW,我們可以自己copy。

比如需要對數組進行遍曆操作的時候,在遍曆之前先Copy:

1234 NSMutableArray* iterateArr = [self.arr copy];for (int i = 0; i < iterateArr.count; i ++) {    NSLog(@"element: %@", iterateArr[i]);}

比如當我們需要修改數組中的元素的時候,在開始修改之前先Copy:

1234567 self.arr = @[@1, @2, @3].mutableCopy;    NSMutableArray* modifyArr = [self.arr mutableCopy];[modifyArr removeAllObjects];[modifyArr addObjectsFromArray:@[@4, @5, @6]]; self.arr = modifyArr;

比如當我們需要返回一個可變數組的時候,返回一個數組的Copy:

1234567 - (NSMutableArray*)createSamples{        [_samples addObject:@1];    [_samples addObject:@2];         return [_samples mutableCopy];}

只要是針對共用數組的操作,時刻記得copy一份新的記憶體地區,就可以實現手動COW的效果,這樣Objective C也能在維護狀態的時候,是多安全執行緒的。

Copy更健康

除了NSArray之外,還有其他集合類NSSet,NSDictionary等,NSString本質上也是個集合,對於這些狀態的處理,copy可以讓他們更加安全。

宗旨是避免共用狀態,這不僅僅是出於多線程情境的考慮,即使是在UI線程中維護狀態,在一個較長的時間跨度內狀態也可能出現意料之外的變化,而copy能隔絕這種變化帶來的副作用。

當然copy也不是沒有代價的,最明顯的代價是記憶體方面的額外開銷,一個含有100個元素的array,如果copy一份的話,在64位系統下,會多出800個位元組的空間。這也是為什麼Swift只有在write的時候才copy,如果只是讀操作,就不會產生copy額外的記憶體開銷。但綜合來看,這點記憶體開銷和我們程式的穩定性比起來,幾乎可以忽略不計。在維護狀態的時候多使用copy,讓我們的函數符合Functional Programming當中的純函數標準,會讓我們的代碼更加穩定。

總結

學習Swift的時候,如果細心觀察,可以發現其他很多地方,也有Swift避免共用同一塊記憶體地區的文法特性。要能真正理解這些語言背後的機制,說到底還是在於我們對於memory layout的理解。

  

從Swift看Objective-C的數組使用

相關文章

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.