狀態之始
我們第一眼接觸新事物所觸發的思考方式,決定了以后我們看待這樣事物的角度,進而影響更深層次的理解和行為。
編程相對于人類歷史的進程而言,不過是個六七歲孩童偶然撿到的新玩具,因為新鮮好玩到現在都還愛不釋手。這個玩具于我們的大腦會產生怎么樣的化學反應是個未知數,每個個體都不同。你第一眼見到色彩或形狀直接關系到你的興趣點或是以后會怎樣去把玩這個玩具。
小朋友拿到新玩具往往急不可耐去動手試玩,成年人面對編程的時候應該理智的去建立自己的知識觀。編程到底是件什么樣的玩具,它的本質是什么?
編程是和我們平行的宇宙,有自己的世界和規則。它的基礎元素是名詞和動詞。名詞即數據(data),動詞即行為(action)。理解這兩個基礎元素是建立編程世界觀的基石。
我們經常會談論data,只不過形式各異。data是個寬泛的概念集,它可以是:變量,狀態,model,數據,屬性等等。同行在談論app架構如何設計modellayer的時候,我反應:哦,是在說怎么維護app的狀態。
所以你看,將概念歸類很重要,理解狀態的本質和表現形式很重要,怎么去維護狀態很重要。
狀態生命周期
每一個變量的誕生,無論身份如何都有一個生命周期。
函數內部變量:
inti=0
生于函數內部,存在內存棧上。一旦函數結束,i也隨著結束生命。
類的屬性:
@property(nonatomic,assign)intcount;
生于某個類的實例,存在實例的內存堆上。一旦對象被銷毀,count也被回收。
全局變量:
intindex=0;
生于程序初始化之刻,存于內存data區。程序被退出,index才會隨之消失。
Model實例:
User*user=[Usernew];
model實例的誕生一般散落在各個模塊,注重架構的程序員會把model的創建都放在同一個layer或者module。model一般依附于cache或者某個業務對象,生命周期較之一般狀態更難把控確定。
狀態還有更多的表現形式,無論其形式如何,明確我們所創造每一個狀態的生命周期,對于書寫好代碼至關重要。生命周期越短,能夠訪問狀態的對象越少,我們的代碼就越可控,越安全。你所寫app當中的每一個狀態是否安全?
安全的狀態
狀態是否安全十分重要,如果條件允許,我們總是應該嘗試盡可能創造“無害”的狀態。
狀態的安全性可以從兩個角度去理解。
訪問權限安全:
一個狀態生命周期越短,能夠訪問(read和write)它的對象越少,我們可以認為這個狀態越安全。
在類當中創建新的property的時候,將property定義在.m當中是個好習慣。放在.h當中意味著任何對象都可以訪問。
確實需要被其他對象訪問(read)之時,我們應該吝嗇的只提供get方法。
當你覺得實在需要被外部對象修改(write)狀態的時候,這很有可能是一個代碼開始降級的消極信號,我們需要反復審視這個“需求”的合理性,在找不到其他設計來規避之時,可以惶恐的提供一個set方法。但必須記住,這個set方法被調用的越頻繁,這個狀態越危險。
ifelse或者switch,是bug很容易生長的土壤。當我們嘗試在if語句中判斷狀態的時候,不穩定的狀態會讓我們原本以為清晰簡單的判斷,變得不可控而且難以調試。
每次書寫ifelse之時,謙卑謹慎的去審視我們所依賴狀態的安全性,會讓我們的代碼更健康,更容易發現問題癥結所在。
近幾年炙手可熱的函數式編程強調“無狀態”,無狀態并不是禁止我們去定義變量,聲明狀態。狀態是編程宇宙中的基礎元素,沒有狀態談何邏輯。無狀態是指將狀態“鎖在”函數的內部,使其生命周期僅存于某個函數個體之內。所以函數成了函數式編程當中的第一公民,函數可以返回狀態,不過這個狀態更像一個結果,一個數學公式運算的結果,每次公式運算都是一份新的結果,一個結果決不被多個對象共同持有。無狀態其實是在強化狀態的安全性。
多線程安全:
多線程訪問較之于多對象訪問是另一個維度,一個和人腦運行方式迥異的維度。
多線程問題復雜度在于執行的時序不確定性,結合狀態被write的場景,如果不仔細設計,很容易讓你的代碼變得一團糟。甚至有時候debug多線程狀態問題,所費時間不亞于開發投入的時間。
多線程read狀態不需要過于擔心,read操作幾乎沒有副作用。需要謹慎對待的是write操作。write和read在多線程的場景下,同時發生在集合類(比如數組)對象之時,代碼會變得十分脆弱。數組類對象是我們代碼當中常用的狀態,也是很多疑難雜癥bug產生的源頭。比如如下代碼:
-(void)initTableArray
{
if(_tableArr==nil){
_tableArr=@[].mutableCopy;
}
}
-(void)renderTableArray
{
for(NSObject*itemin_tableArr){
//render
}
}
-(void)insertTableItem:(NSObject*)item
{
[_tableArraddObject:item];
}
上面三段代碼分別對應數組狀態的三種操作:創建狀態,讀取狀態,修改狀態。看似簡單的代碼,如果放在多線程的場景之下問題很容易變得復雜起來。
多線程并發下,_tableArr可能會被創建多次。
多線程并發下,_tableArr在遍歷之時可能被修改,直接導致crash。
多線程并發下,_tableArr中item插入的順序變得不確定。
此時我們需要“鎖技”來應對數組類狀態,鎖可以讓多線程場景下,我們的狀態得以“原子”的粒度被訪問或被修改。OC這類高級語言使得加鎖變得輕而易舉:
-(void)initTableArray
{
@synchronized(self){
if(_tableArr==nil){
_tableArr=@[].mutableCopy;
}
}
}
-(void)renderTableArray
{
@synchronized(self){
for(NSObject*itemin_tableArr){
//render
}
}
}
-(void)insertTableItem:(NSObject*)item
{
@synchronized(self){
[_tableArraddObject:item];
}
}
如果覺得synchronized性能不夠好,可以換成dispatch_semaphore_t,但絕大部分業務場景下,這點性能的損耗是無法被感知的。
我們還可以采用“縮短狀態生命周期”的方式,來規避多線程帶來的風險。比如:
-(void)renderTableArray
{
NSMutableArray*arr=[selfcreateNewRanderArr];
@synchronized(self){
for(NSObject*iteminarr){
//render
}
}
}
-(NSMutableArray*)createNewRanderArr
{
return@[].mutableCopy;
}
每次渲染的array都是重新生成的,不會被其他對象訪問修改,render之后array就可以被廢棄。通過這種方式我們也可以盡量避免多個線程同時修改狀態,所引入的不穩定性。
清理狀態
對于函數內部的臨時變量,函數退出之時,狀態也就隨著被清理。更多的場景下,狀態由我們自己生成,并存放于heap上。如果不手動清理,狀態就會一直存在,并帶來可能的風險。
如果可以,我們應該總是盡可能縮短一個狀態的生命周期,減少狀態暴露給其他對象的機會。適時的清理狀態會讓我們的代碼更加健壯。
狀態皆有其所依賴的業務場景。購物車里的商品在完成購買之后就失去了依附的業務環境,用戶的購買記錄在用戶退出登錄之后也應該被清除,更不應該影響到下一個登錄的新用戶。
所以在使用新狀態描述業務的時候,我們總是需要考慮以下收尾工作:
-(void)onUserLogout
{
//clearstate
}
結束語
每一個新的狀態就像程序王國里的新子民,其所扮演的角色,影響范圍,生命周期都需要被程序員以上帝視角反復的推敲設計。