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