從Swift看Objective-C的數(shù)組使用

狀態(tài)維護(hù)是個(gè)怎么說(shuō)都不夠的話題,畢竟?fàn)顟B(tài)的處理是我們整個(gè)App最核心的部分,也是最容易出bug的地方。之前寫過(guò)一篇以函數(shù)式編程的角度看狀態(tài)維護(hù)的文章,這次從Swift語(yǔ)言層面的改進(jìn),看看Objective C下該如何合理的處理數(shù)組的維護(hù)。

Objective C數(shù)組的內(nèi)存布局

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

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

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

arr初始化之后,以64位系統(tǒng)為例,其實(shí)際的內(nèi)存布局分為三塊:

第一塊是指針NSMutableArray* arr所處的位置,為8個(gè)字節(jié)。第二塊是數(shù)組實(shí)際的內(nèi)存區(qū)域所處的位置,為連續(xù)3個(gè)指針地址,各占8個(gè)字節(jié)一共24個(gè)字節(jié)。第三塊才是@1,@2,@3這些NSNumber對(duì)象真正的內(nèi)存空間。當(dāng)我們調(diào)用不同的API對(duì)arr進(jìn)行操作的時(shí)候,要分清楚實(shí)際是在操作哪部分內(nèi)存。

比如:

self.arr = @[@4];

是在對(duì)第一塊內(nèi)存區(qū)域進(jìn)行賦值。

self.arr[0] = @4;

是在對(duì)第二塊內(nèi)存區(qū)域進(jìn)行賦值。

[self.arr[0] integerValue];

是在訪問(wèn)第三塊內(nèi)存區(qū)域。

之前寫過(guò)一篇多線程安全的文章,我們知道即使在多線程的場(chǎng)景下,對(duì)第一塊內(nèi)存區(qū)域進(jìn)行讀寫都是安全的,而第二塊和第三塊內(nèi)存區(qū)域都是不安全的。

NSMutableArray為什么危險(xiǎn)?

在Objective C的世界里,帶Mutable的都是危險(xiǎn)分子。我們看下面代碼:

//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;執(zhí)行之后,我們的內(nèi)存模型是這樣的:

這行代碼實(shí)際上只是新生成了8個(gè)字節(jié)的第一類內(nèi)存空間給localArr,localArr實(shí)際上還是和arr共享第二塊和第三塊內(nèi)存區(qū)域,當(dāng)在thread 2執(zhí)行[localArr removeAllObjects];清理第二塊內(nèi)存區(qū)域的時(shí)候,如果主線程正在同時(shí)訪問(wèn)第二塊內(nèi)存區(qū)域_arr[1],就會(huì)導(dǎo)致crash了。這類問(wèn)題的根本原因,還是在對(duì)于同一塊內(nèi)存區(qū)域的同時(shí)讀寫。

Swift的改變

Swift對(duì)于上述的數(shù)組賦值操作,從語(yǔ)言層面做了根本性的改變。

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

var arr = [1, 2, 3]
var localArr = arr

print("arr: \(arr)")
print("localArr: \(localArr)")

arr += [4];

print("arr: \(arr)")
print("localArr: \(localArr)")

當(dāng)執(zhí)行到var localArr = arr的時(shí)候,arr和localArr的內(nèi)存布局還是和Objective C一致,arr和localArr都共享第二第三塊內(nèi)存區(qū)域,但是一旦出現(xiàn)寫操作(write),比如arr += [4];的時(shí)候,Swift就會(huì)針對(duì)原先arr的第二塊內(nèi)存區(qū)域,生成一份新的拷貝(copy),也就是所謂的copy on write,執(zhí)行cow之后,arr和localArr就指向不同的第二塊內(nèi)存區(qū)域了,如下圖所示:

一旦出現(xiàn)針對(duì)arr寫操作,系統(tǒng)就會(huì)將內(nèi)存區(qū)域2拷貝至一塊新的內(nèi)存區(qū)域4,并將arr的指針指向新開(kāi)辟的區(qū)域4,之后再發(fā)生數(shù)組的改變,arr和localArr就指向不同的區(qū)域,即使在多線程的環(huán)境下同時(shí)發(fā)生讀寫,也不會(huì)導(dǎo)致訪問(wèn)同一內(nèi)存區(qū)域的crash了。

上面的代碼,最后打印的結(jié)果中,arr和localArr中所包含的元素也不一致了,畢竟他們已經(jīng)指向各自的第二類內(nèi)存區(qū)域了。

這也是為什么說(shuō)Swift是一種更加安全的語(yǔ)言,通過(guò)語(yǔ)言層面的修改,幫助開(kāi)發(fā)者避免一些難以調(diào)試的bug,而這一切都是對(duì)開(kāi)發(fā)者透明的,免費(fèi)的,開(kāi)發(fā)者并不需要做特意的適配。還是一個(gè)簡(jiǎn)單的=操作,只不過(guò)背后發(fā)生的事情不一樣了。

Objective C的領(lǐng)悟

Objective C還沒(méi)有退出歷史舞臺(tái),依然在很多項(xiàng)目中發(fā)揮著余熱。明白了Swift背后所做的事情,Objective C可以學(xué)以致用,只不過(guò)要多寫點(diǎn)代碼。

Objective C既然沒(méi)有COW,我們可以自己copy。

比如需要對(duì)數(shù)組進(jìn)行遍歷操作的時(shí)候,在遍歷之前先Copy:

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

比如當(dāng)我們需要修改數(shù)組中的元素的時(shí)候,在開(kāi)始修改之前先Copy:

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

self.arr = modifyArr;

比如當(dāng)我們需要返回一個(gè)可變數(shù)組的時(shí)候,返回一個(gè)數(shù)組的Copy:

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

只要是針對(duì)共享數(shù)組的操作,時(shí)刻記得copy一份新的內(nèi)存區(qū)域,就可以實(shí)現(xiàn)手動(dòng)COW的效果,這樣Objective C也能在維護(hù)狀態(tài)的時(shí)候,是多線程安全的。

Copy更健康

除了NSArray之外,還有其他集合類NSSet,NSDictionary等,NSString本質(zhì)上也是個(gè)集合,對(duì)于這些狀態(tài)的處理,copy可以讓他們更加安全。

宗旨是避免共享狀態(tài),這不僅僅是出于多線程場(chǎng)景的考慮,即使是在UI線程中維護(hù)狀態(tài),在一個(gè)較長(zhǎng)的時(shí)間跨度內(nèi)狀態(tài)也可能出現(xiàn)意料之外的變化,而copy能隔絕這種變化帶來(lái)的副作用。

當(dāng)然copy也不是沒(méi)有代價(jià)的,最明顯的代價(jià)是內(nèi)存方面的額外開(kāi)銷,一個(gè)含有100個(gè)元素的array,如果copy一份的話,在64位系統(tǒng)下,會(huì)多出800個(gè)字節(jié)的空間。這也是為什么Swift只有在write的時(shí)候才copy,如果只是讀操作,就不會(huì)產(chǎn)生copy額外的內(nèi)存開(kāi)銷。但綜合來(lái)看,這點(diǎn)內(nèi)存開(kāi)銷和我們程序的穩(wěn)定性比起來(lái),幾乎可以忽略不計(jì)。在維護(hù)狀態(tài)的時(shí)候多使用copy,讓我們的函數(shù)符合Functional Programming當(dāng)中的純函數(shù)標(biāo)準(zhǔn),會(huì)讓我們的代碼更加穩(wěn)定。

總結(jié)

學(xué)習(xí)Swift的時(shí)候,如果細(xì)心觀察,可以發(fā)現(xiàn)其他很多地方,也有Swift避免共享同一塊內(nèi)存區(qū)域的語(yǔ)法特性。要能真正理解這些語(yǔ)言背后的機(jī)制,說(shuō)到底還是在于我們對(duì)于memory layout的理解。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 章節(jié)導(dǎo)航:Swift開(kāi)發(fā)指南:使用Swift與Cocoa和Objective-C(Swift 4) - 1.入門S...
    Minecode閱讀 3,314評(píng)論 0 23
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,045評(píng)論 4 61
  • *面試心聲:其實(shí)這些題本人都沒(méi)怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個(gè)offer,總結(jié)起來(lái)就是把...
    Dove_iOS閱讀 27,581評(píng)論 30 472
  • 《驚夢(mèng)》 深宵閃電透窗明,子夜轟雷致魂驚, 卷地烈風(fēng)襲幽夢(mèng),漫天驟雨遏心寧。 ...
    尚硯草堂閱讀 295評(píng)論 0 0
  • 近期,在百度全民話題的網(wǎng)頁(yè)上,一個(gè)“你是否看好90后創(chuàng)業(yè)”的話題引起了眾多網(wǎng)友的關(guān)注,眾人對(duì)此話題也是各持其辭,見(jiàn)...
    幻曦夢(mèng)焰閱讀 226評(píng)論 1 1

友情鏈接更多精彩內(nèi)容