標籤:編程 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; //init self.arr = @[@1, @2, @3].mutableCopy; |
arr初始化之後,以64位系統為例,其實際的記憶體布局分為三塊:
第一塊是指標NSMutableArray* arr所處的位置,為8個位元組。第二塊是數組實際的記憶體地區所處的位置,為連續3個指標地址,各佔8個位元組一共24個位元組。第三塊才是@1,@2,@3這些NSNumber對象真正的記憶體空間。當我們調用不同的API對arr進行操作的時候,要分清楚實際是在操作哪部分記憶體。
比如:
是在對第一塊記憶體地區進行賦值。
是在對第二塊記憶體地區進行賦值。
1 |
[self.arr[0] integerValue]; |
是在訪問第三塊記憶體地區。
之前寫過一篇多安全執行緒的文章,我們知道即使在多線程的情境下,對第一塊記憶體地區進行讀寫都是安全的,而第二塊和第三塊記憶體地區都是不安全的。
NSMutableArray為什麼危險?
在Objective C的世界裡,帶Mutable的都是危險分子。我們看下面代碼:
12345678910111213141516 |
//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;執行之後,我們的記憶體模型是這樣的:
這行代碼實際上只是新產生了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的數組使用