從 Swift 看 Objective-C 的數組使用

SanCavenagh 7年前發布 | 8K 次閱讀 Swift Apple Swift開發 Objective-C

狀態維護是個怎么說都不夠的話題,畢竟狀態的處理是我們整個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位系統為例,其實際的內存布局分為三塊:

第一塊是指針 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 

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

這行代碼實際上只是新生成了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就指向不同的第二塊內存區域了,如下圖所示:

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

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

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

Objective C的領悟

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

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

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

NSArray* iterateArr = [self.arrcopy];
for (int i = 0; i 

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

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

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

- (NSMutableArray*)createSamples
{    
    [_samplesaddObject:@1];
    [_samplesaddObject:@2];
 
    return [_samplesmutableCopy];
}

只要是針對共享數組的操作,時刻記得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的理解。

 

 

來自:http://ios.jobbole.com/91928/

 

 本文由用戶 SanCavenagh 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!