聊一聊 iOS 開發(fā)中的惰性計算

本文摘自:作者臧成威,美團網(wǎng) iOS 技術(shù)專家蜻牢,QCon 講師盾饮,國內(nèi) Functional Reactive Programming 技術(shù)愛好者.2015年加入美團,負責美團 iOS 發(fā)布工程系統(tǒng)的研發(fā)和流程優(yōu)化梳理汞斧。擅長多語言范式生真,對各種編程范式有著獨到的見解.

臧老師在美團組織過系統(tǒng)的 Functional Reactive Programming 培訓(xùn),參與人數(shù)總計達百人吏祸。

他最近在 InfoQ 旗下的 StuQ 開設(shè)課程:《ReactiveCocoa 編程思想與開發(fā)實戰(zhàn)》对蒲,第一期爆滿結(jié)束。于是他最近開了第二期課程,本周五即將上課齐蔽,感興趣的可以看文末的課程詳情两疚。

正文

首先給大家講一個笑話:

有一只小白兔,跑到蔬菜店里問老板:“老板含滴,有 100 個胡蘿卜嗎诱渤?”。老板說:“沒有那么多啊谈况∩酌溃”,小白兔失望的說道:“哎碑韵,連 100 個胡蘿卜都沒有赡茸。。祝闻≌嘉裕”。

第二天小白兔又來到蔬菜店問老板:“今天有 100 個胡蘿卜了吧联喘?”华蜒,老板尷尬的說:“今天還是缺點,明天就能好了豁遭“认玻”,小白兔又很失望的走了蓖谢。

第三天小白兔剛一推門捂蕴,老板就高興的說道:“有了有了,從前天就進貨的 100 個胡蘿卜到貨了闪幽∩侗妫”,小白兔說:“太好了沟使,我要買 2 根委可!”。腊嗡。。

不曉得笑話是否博您一笑拾酝,但是這里面確有一個點是和我們的主題惰性計算相關(guān)的燕少。試想一下,假設(shè)蔬菜店是一個電商蒿囤,你是老板客们,你掛商品數(shù)量的時候,是 100 個,1000 個底挫,還是真實的備貨 2 個恒傻?顯然做過淘寶的同學都知道這其中的玄機,就是先掛大的余量建邓,有賣出再補貨盈厘。所以,如果這個老板先回答有 100 個胡蘿卜官边,再等它要 2 個的時候把自己備貨的 2 個拿給它沸手,是不是就免去了 100 個胡蘿卜的物流?

在程序開發(fā)中注簿,我們也會經(jīng)常的遇到這樣的問題契吉,明明創(chuàng)建了很大的一個對象,但是其實只用了一個字段诡渴;明明創(chuàng)建了一個 500 個的數(shù)組捐晶,其實只用了第 0 個和第 1 個元素。遇到這類問題妄辩,我們可以嘗試使用惰性計算來解決惑灵。

關(guān)于惰性計算,或者惰性求值恩袱。想必大家第一反應(yīng)就是在 getter 里動態(tài)返回屬性了泣棋。例如有一個很大的屬性,你希望在有人調(diào)用的時候才創(chuàng)建畔塔,就可以這樣寫:

- (id)someBigProperty

{

if (_someBigProperty == nil) {

NSMutableArray *someBigProperty = [NSMutableArray array];

for (int i = 0; i < 100000; ++i) {

[someBigProperty addObject:@(i)];

}

_someBigProperty = [someBigProperty copy];

}

return _someBigProperty;

}

本文當然不拘泥于大家耳熟能詳?shù)闹R點進行闡述了潭辈。上述的代碼雖然也能勉強叫惰性求值,但并非足夠理想澈吨。為什么說是 “勉強叫” 呢把敢?大家想想上面的笑話,其實這樣做和老板的做法并無差別谅辣。首先店里沒有 100 個胡蘿卜修赞,就好像這個對象沒有_someBigProperty屬性一樣。一旦有人需要 100 個 “胡蘿卜”桑阶,就循環(huán) 100000 次創(chuàng)建這個_someBigProperty屬性柏副。然后可能使用者只需要第 0 個。

另外在實際項目中這樣的一個手段幾乎被大家嚴重的亂用了蚣录,為什么說是亂用呢割择?除了創(chuàng)建非常大的屬性、或者創(chuàng)建對象的時候有一些必要的副作用不能提前創(chuàng)建之外萎河,幾乎不應(yīng)該使用惰性求值來處理類似邏輯荔泳。原因如下:

如果真的是很大的屬性蕉饼,一般它比較重要,幾乎一定會被訪問玛歌,所以加上這個不如直接在 init 的時候創(chuàng)建昧港。

@property 的 atomic、nonatomic支子、copy创肥、strong 等描述在有 getter 方法的屬性上會失效,后人修改代碼的時候可能只改了 @property 聲明译荞,并不會記得改 getter瓤的,于是隱患就這樣埋下了。

代碼含有了隱私操作吞歼,尤其 getter 中再混雜了各種邏輯圈膏,使得程序出現(xiàn)問題非常不好排查。后人哪會想到someObj.someProperty這樣一個簡簡單單的取屬性發(fā)生了很多奇妙的事篙骡。

很多人的 getter 寫得并不是完全標準稽坤,例如上述代碼會導(dǎo)致多線程訪問的時候,出現(xiàn)很多神奇的問題糯俗。一旦形成習慣尿褪,后續(xù)的很多稀奇古怪的 crash 就接踵而至了。

代碼多得湘,本來代碼只需要在init方法中創(chuàng)建用上一兩行杖玲,結(jié)果用了至少 7 行的一個 getter 方法才能寫出來,想想一個程序輕則數(shù)百個屬性淘正,都這么搞摆马,得多出多少行代碼?另外代碼格式幾乎完全一樣鸿吆,不符合 DRY 原則囤采。好的程序員不應(yīng)該總是寫重復(fù)的代碼,不是么惩淳?

性能損耗蕉毯,對于屬性取值可能會非常的頻繁,如果所有的屬性取值之前都經(jīng)過一個if判斷思犁,這不是平白浪費的性能代虾?

我們回到正題。既然簡單改寫一下 getter 不但解決不了問題還有這么多隱患激蹲,那我們該如何能夠正確優(yōu)雅的把惰性計算寫好褐着?下面給大家一些建議。

觀察上面的代碼托呕,你會發(fā)現(xiàn) _someBigProperty 是一個非常規(guī)則的 NSArray含蓉,它的 item 內(nèi)容與下標相等。我們可以看出 item 的結(jié)果與 index 存在如下關(guān)系:

f(x) = x

類似的可以有很多项郊,例如> 100的為@“world”馅扣,0 <= x <= 100的為@“hello”;item 為下標的平方着降;item 為下標的數(shù)值轉(zhuǎn)換成的字符串等差油。所以這類NSArray,基本需要一個 count 和一個函數(shù)就可以構(gòu)成了任洞。那我們現(xiàn)在就基于NSArray這個類簇蓄喇,實現(xiàn)一個特殊的類吧!

關(guān)于類簇交掏,相信很多同學都有所了解妆偏,大概的說法是不可以直接繼承一個NSArray、NSNumber盅弛、NSString這樣的類钱骂。如果要繼承需要實現(xiàn)全部的必要方法,在NSArray這個類簇來說挪鹏,就是如下的方法:

@interface NSArray<__covariant ObjectType> : NSObject

@property (readonly) NSUInteger count;

- (ObjectType)objectAtIndex:(NSUInteger)index;

- (instancetype)init NS_DESIGNATED_INITIALIZER;

- (instancetype)initWithObjects:(const ObjectType [])objects count:(NSUInteger)cnt NS_DESIGNATED_INITIALIZER;

- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;

@end

當然除了NSArray類的基本方法见秽,還有NSCopying、NSMutableCopying讨盒、NSSecureCoding這些協(xié)議需要實現(xiàn)解取,另外NSFastEnumberation協(xié)議已經(jīng)默認實現(xiàn)完成,不需要額外處理返顺。與惰性計算無關(guān)的細節(jié)大家可以自己填補禀苦,對于本例,我們只需要關(guān)心這幾個方法的實現(xiàn):

typedef id(^ItemBlock)(NSUInteger index);

@interface ZDynamicArray : NSArray

- (instancetype)initWithItemBlock:(ItemBlock)block count:(NSUInteger)cnt;

- (id)objectAtIndex:(NSUInteger)index;

- (NSUInteger)count;

@end

按照上文的說法创南,對于這樣一個特殊的NSArray伦忠,我們真正要儲存的數(shù)據(jù)只有一個 count 值外加一個函數(shù),所以我們用這兩個作為init參數(shù)稿辙。實現(xiàn)也很簡單:

@interface ZDynamicArray()

@property (nonatomic, readonly) ItemBlock block;

@property (nonatomic, readonly) NSUInteger cnt;

@end

@implementation ZDynamicArray

- (instancetype)initWithItemBlock:(ItemBlock)block count:(NSUInteger)cnt

{

if (self = [super init]) {

_block = block;

_cnt = cnt;

}

return self;

}

- (NSUInteger)count

{

return self.cnt;

}

- (id)objectAtIndex:(NSUInteger)index

{

if (self.block) {

return self.block(index);

} else {

return nil;

}

}

@end

瞧昆码,就這么簡單的寫好了。讓我們試一下吧邻储!

ZDynamicArray *array = [[ZDynamicArray alloc] initWithItemBlock:^id(NSUInteger index) {

return @(index);

} count:100000];

for (id v in array) {

NSLog(@"%@", v);

}

NSLog(@"%@", array[15]);

一個看似 10w 數(shù)據(jù)的數(shù)組赋咽,其實占用空間微乎其微,但是作用和最開始那樣的代碼效果一樣吨娜。很不錯吧脓匿。大家也可以動手實踐,寫一些自己需要用到的惰性計算代碼宦赠,例如一個Model的數(shù)組陪毡,并非所有的Model都需要用到米母,我們也可以做成這樣的一個數(shù)組,等用到的時候再從NSDicitonary轉(zhuǎn)換成Model毡琉。就像這樣:

NSArray *downloadData = @[@{}, @{}, @{}, @{}];

NSArray *modelArray = [[ZDynamicArray alloc] initWithItemBlock:^id(NSUInteger index) {

return [SomeModel modelFromDictionary:downloadData[index]];

} count:downloadData.count];

當然這可能有一定的風險铁瞒,因為傳統(tǒng)的寫法會更早一步的發(fā)現(xiàn)某些數(shù)據(jù)不正確,然后惰性計算桅滋,會把這個發(fā)現(xiàn)問題的時間延后慧耍。這就需要更多更好的錯誤處理機制。ReactiveCocoa這個著名的 FRP 庫為我們提供了更多編程的可能丐谋,它在很多處理上都是惰性計算的芍碧,同時它又做了很好的異常處理工作。學習它可以讓你編程思路更廣号俐。

全文完泌豆。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市萧落,隨后出現(xiàn)的幾起案子践美,更是在濱河造成了極大的恐慌,老刑警劉巖找岖,帶你破解...
    沈念sama閱讀 222,000評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件陨倡,死亡現(xiàn)場離奇詭異,居然都是意外死亡许布,警方通過查閱死者的電腦和手機兴革,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蜜唾,“玉大人杂曲,你說我怎么就攤上這事≡啵” “怎么了擎勘?”我有些...
    開封第一講書人閱讀 168,561評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長颖榜。 經(jīng)常有香客問我棚饵,道長,這世上最難降的妖魔是什么掩完? 我笑而不...
    開封第一講書人閱讀 59,782評論 1 298
  • 正文 為了忘掉前任噪漾,我火速辦了婚禮,結(jié)果婚禮上且蓬,老公的妹妹穿的比我還像新娘欣硼。我一直安慰自己,他們只是感情好恶阴,可當我...
    茶點故事閱讀 68,798評論 6 397
  • 文/花漫 我一把揭開白布诈胜。 她就那樣靜靜地躺著豹障,像睡著了一般。 火紅的嫁衣襯著肌膚如雪耘斩。 梳的紋絲不亂的頭發(fā)上沼填,一...
    開封第一講書人閱讀 52,394評論 1 310
  • 那天,我揣著相機與錄音括授,去河邊找鬼。 笑死岩饼,一個胖子當著我的面吹牛荚虚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播籍茧,決...
    沈念sama閱讀 40,952評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼版述,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了寞冯?” 一聲冷哼從身側(cè)響起渴析,我...
    開封第一講書人閱讀 39,852評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎吮龄,沒想到半個月后俭茧,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,409評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡漓帚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,483評論 3 341
  • 正文 我和宋清朗相戀三年母债,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片尝抖。...
    茶點故事閱讀 40,615評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡毡们,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出昧辽,到底是詐尸還是另有隱情衙熔,我是刑警寧澤,帶...
    沈念sama閱讀 36,303評論 5 350
  • 正文 年R本政府宣布搅荞,位于F島的核電站红氯,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏取具。R本人自食惡果不足惜脖隶,卻給世界環(huán)境...
    茶點故事閱讀 41,979評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望暇检。 院中可真熱鬧产阱,春花似錦、人聲如沸块仆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至庄敛,卻和暖如春俗壹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背藻烤。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評論 1 272
  • 我被黑心中介騙來泰國打工绷雏, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人怖亭。 一個月前我還...
    沈念sama閱讀 49,041評論 3 377
  • 正文 我出身青樓涎显,卻偏偏與公主長得像,于是被迫代替她去往敵國和親兴猩。 傳聞我的和親對象是個殘疾皇子期吓,可洞房花燭夜當晚...
    茶點故事閱讀 45,630評論 2 359

推薦閱讀更多精彩內(nèi)容