你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 從Swift看Objective

從Swift看Objective

編輯:IOS開發基礎

54.jpg

作者:MrPeakTech(公眾號)

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

Objective C數組的內存布局

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

//declare
@property (nonatomic, strong) NSMutableArray*                 arr;

//init
self.arr = @[@1, @2, @3].mutableCopy;

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

QQ截圖20161222111540.png

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

比如:

self.arr = @[@4];

是在對第一塊內存區域進行賦值。

self.arr[0] = @4;

是在對第二塊內存區域進行賦值。

[self.arr[0] integerValue];

是在訪問第三塊內存區域。

之前寫過一篇多線程安全的文章,我們知道即使在多線程的場景下,對第一塊內存區域進行讀寫都是安全的,而第二塊和第三塊內存區域都是不安全的。

NSMutableArray為什麼危險?

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

//main thread
self.arr = @[@1, @2, @3].mutableCopy;

for (int i = 0; i < _arr.count; i ++) {
    NSLog(@"element: %@", _arr[i]);
}

//thread 2
NSMutableArray* localArr = self.arr;

//get result from server
NSArray* results = @[@8, @9, @10];

//refresh local arr
[localArr removeAllObjects];
[localArr addObjectsFromArray:results];

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

232.png

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

Swift的改變

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

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

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就指向不同的第二塊內存區域了,如下圖所示:

306.png

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

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

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

Objective C的領悟

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

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

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

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

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

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

self.arr = modifyArr;

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

- (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的理解。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved