Diff應(yīng)用:從LCS到UICollectionView

前文

什么是Diff春畔?

日常編程中有時候會遇到對比字符串脱货,對比數(shù)組的情況,找出前后新舊數(shù)據(jù)的不同拐迁,可以稱之為Diff蹭劈。

什么是LCS?

Longest Common Subsequence的簡稱线召,最長公共子序列铺韧。

LCS有哪些應(yīng)用?

1.Git等版本控制缓淹,常用的git diff命令
2.一些對比軟件哈打,如Kaleidoscope塔逃,能進行圖片、文件料仗、文本的對比
3.Facebook iOS Snapshot Test框架湾盗,通過snapshot的方式,進行頁面UI測試
4.IGListKit一個基于UICollectionView的框架立轧,通過LCS衍生優(yōu)化算法格粪,進行UICollectionView的刷新

關(guān)于本文...

前幾部分都是LCS算法的一些簡單介紹,感興趣的可以看看氛改,也可直接看靠后的LCS算法在UICollectionView中的應(yīng)用帐萎。

傳統(tǒng)的LCS算法

以最常見的字符串對比為例,我們要從ADFGT變化到AFOXT找出LCS胜卤。從后向前進行對比疆导,T相同表明T是LCS的一部分,所以能進一步簡化為:

image

繼續(xù)向前對比ADFG和AFOX葛躏,發(fā)現(xiàn)G和X不同澈段,這意味著G只可能是字符串ADFG和AFO的LCS,也意味著X只可能是字符串ADF和AFOX的LCS舰攒,那么問題簡化為:

image

很容易能計算出败富,這種算法時間復雜度為O( 2^n ),當字符串或者數(shù)組很長時摩窃,會非常慢...

結(jié)合動態(tài)規(guī)劃的改進LCS算法

動態(tài)規(guī)劃常常能用來解決一些遞歸問題囤耳,LCS問題也是,使用一個二維數(shù)組就可以避開遞歸偶芍。

仍舊以ADFGT和AFOXT為例充择,舉例如下 A = "ADFGT" B = "AFOXT" m = A.length n = B.length

1.首先建立一個二維數(shù)組table[m+1][n+1],默認在i=0行和j=0列填充0匪蟀,如下圖:

image

2.在其他位置温自,任一[i][j]波岛,先計算max(table[i-1][j], table[i][j-1])笋敞,然后判斷A[i-1]和B[j-1]觉痛,相同的話此處填max+1,否則填max段化,最后填完所有空結(jié)果如下:

image

此時嘁捷,時間復雜度已經(jīng)是O( n^2 ),但是我們已經(jīng)算出兩個字符串LCS的長度是3了显熏,接下來需要利用table將LCS找出來雄嚣。

仍然選擇從table右下角向坐上角遍歷,中間會遇到三種情況

1.當i=m+1,j=n+1時缓升,發(fā)現(xiàn)此時的A[i]=B[i]鼓鲁,那么這個元素肯定是LCS的一部分,向左上角走港谊,直接將i-1,j-1

image

2.此時i=m骇吭,j=n時,發(fā)現(xiàn)A[i]!=B[i]歧寺,那么比較table[i][j]和table[i-1][j]燥狰,當兩者相同時向上走i-1,否則向左走j-1

image
image

3.按照前兩種策略一直向左上方走斜筐,知道遇到i=0碾局,j=0,結(jié)束搜索過程奴艾,下圖表明了整個線路,可以看到紅圈內(nèi)的就是LCS

image

得到正確的LCS為AFT内斯。

上面過程時間復雜度是O(n)蕴潦,結(jié)合構(gòu)造table的過程,整個過程時間復雜度是O( n^2 )俘闯,遠小于第一種遞歸算法潭苞。

4 LCS能做什么?

上面兩部分介紹了LCS問題的兩種算法真朗,為什么要算出LCS此疹?! 繼續(xù)看在上面ADFGT和AFOXT對比算出的table二維數(shù)組遮婶,會發(fā)現(xiàn)幾個有趣的地方:

  • 向左上走的單元蝗碎,都是兩個字符串重復的部分,即Reload/Move

  • 向上走的單元旗扑,都是舊數(shù)據(jù)中需要刪除的部分蹦骑,即Delete

  • 向左走的單元,都是新數(shù)據(jù)中需要插入的部分臀防,即Insert

這么以來眠菇,就很好理解table的作用了,我們也就可以在遍歷table中找到據(jù)的所有更新操作如下:

  • Reload:[0]A袱衷、[4]T

  • Insert:[2]O捎废、[3]X

  • Delete:[1]D、[3]G

  • Move:[3]F > [2]F

5 結(jié)合iOS UIColletionView

首先看一下當有數(shù)據(jù)源后致燥,有哪幾種方法刷新列表登疗?

1. reloadData,最直接最簡單的方式嫌蚤,當數(shù)據(jù)源很小谜叹,Cell樣式簡單時沒有問題匾寝,但是:

有非常復雜非常多Cell,Cell Subviews非常復雜的情況下荷腊,列如嵌套UIStackView艳悔、UICollectionView...直接調(diào)用ReloadData,意味著列表會重新計算Cell Size等各種layout attributes女仰,并渲染到屏幕上猜年,這肯定會消耗一部分CPU資源。

某些情況下疾忍,加載新數(shù)據(jù)插入列表最后乔外,調(diào)用ReloadData會重走一遍Cell周期,意味著cellWillDisplay一罩,cellDidEndDisplay回調(diào)杨幼,如果在會回調(diào)方法內(nèi)有很多邏輯處理的話,要格外小心聂渊。

2. performBatchUpdates或beginUpdates/endUpdates差购,可以計算Cell前后變化,調(diào)用insert/delete/reload/move等操作汉嗽。

  • 這么做可以少計算一些Cell欲逃,同時也可以添加一些動畫效果。

  • 但當數(shù)據(jù)源非常復雜時如何計算前后變化饼暑,如何判斷計算后的結(jié)果是不是最優(yōu)方案呢稳析,如果計算中稍有錯誤就會產(chǎn)生Cell和數(shù)據(jù)源不匹配的Crash。

因此弓叛,才希望能通過LCS計算出正確的insert/delete/reload/Move數(shù)組彰居,能在提高刷新效率的同時,保證計算的正確性撰筷。

6 此時存在的問題

現(xiàn)在有兩個問題擺在面前:

  • 第一個問題是上文通過一個二維數(shù)組將LCS算法時間復雜度降低到O( n^2 )裕菠,但是會發(fā)現(xiàn),當n很大的時候闭专,比如幾百幾千奴潘,這個復雜度也非常高。

  • 其次是Move操作影钉,我們會遇到一些問題画髓,把后一個字符串中的F和T調(diào)換一下字母順序,然后對比ADFGT和ATOXF平委,作出table圖如下:

image

會發(fā)現(xiàn)奈虾,按照前面介紹的遍歷table邏輯會有下面的刷新操作:

  • Reload:[0]A

  • Insert:[1]T、[2]O、[3]X

  • Delete:[1]D肉微、[3]G匾鸥、[4]T

  • Move:[2]F > [4]F

發(fā)現(xiàn)其中的T既有Delete也有Insert,但是卻沒有進行Move操作...稍微思考下發(fā)現(xiàn):

當前后數(shù)據(jù)存在多個LCS結(jié)果時碉纳,只會取其中一組勿负,其余的只能進行Insert/Delete操作

這是第二個問題。

7 進一步優(yōu)化方案 -- IGListKit Diff方案

那么有沒有既能降低時間復雜度劳曹,又能對Move操作進行一些優(yōu)化奴愉,而不是簡單調(diào)用Insert/Delete的方法呢?

有的铁孵。Instagram團隊的IGListKit框架锭硼,結(jié)合了Paul Heckel’s Diff(1978年)的一篇論文,進一步優(yōu)化蜕劝,使用額外一些內(nèi)存空間檀头,降低時間復雜度到O(n),并且能準確獲取所有Insert/Delete/Move操作岖沛。

簡化問題暑始,仍以ADFGT和ATOXF為例子,首先考慮的是降維處理烫止,避免使用二維數(shù)組,結(jié)合上面兩張路線路戳稽,不難發(fā)現(xiàn)馆蠕,最重要的是沿線路的幾個位置,其余位置并沒有走過惊奇。準確來說互躬,需要走一遍的距離是:

m + n - LCS.length

因此可以確定的說,一定會走過所有去重后的元素颂郎,仔細思考下吼渡,對于每個元素,我們需要的到底是什么乓序?

元素在新舊數(shù)據(jù)中的位置

我們需要一個數(shù)據(jù)結(jié)構(gòu)能夠快速映射出每個元素到它在新舊數(shù)據(jù)中的位置寺酪,這里IGListKit選擇使用一個無序去重unordered_map,以每一個元素作為key替劈,以entry為value寄雀,entry定義如下:

image

1. 正序遍歷新數(shù)據(jù),對應(yīng)的每個entry陨献,newCounter++盒犹,oldIndexes壓入NSNotFound

2. 因為棧后進先出,倒序遍歷舊數(shù)據(jù),對應(yīng)的每個entry急膀,oldCounter++沮协,oldIndexed壓入元素在舊數(shù)據(jù)中的位置

3. 由于map的查找速度是O(1),因此遍歷時間為O(m+n)

ADFGTATOXF為例卓嫂,經(jīng)過兩次遍歷慷暂,就能獲得下面的map數(shù)據(jù),oldIndexes左側(cè)表示棧底:

image

觀察上面的表格命黔,不難發(fā)現(xiàn):

  • oldIndexes棧中只有NSNotFound的表明是新元素呜呐,需要Insert

  • oldIndexes棧中只有數(shù)字的表明是要刪除的舊元素,需要Delete

  • oldIndexes棧中既有NSNotFound也有數(shù)字的元素悍募,需要Move蘑辑,可能需要Insert/Delete

那么接下來就是從map數(shù)據(jù)中得到最終結(jié)果,這里重新定義一個結(jié)構(gòu)體record如下坠宴,每一個新舊數(shù)據(jù)的index都對應(yīng)一個record洋魂,其中entry可以讓我們快速訪問到數(shù)據(jù)對應(yīng)的entry,index則用于記錄一個新數(shù)據(jù)在舊數(shù)據(jù)中的位置(如果存在的情況下)或一個舊數(shù)據(jù)在新數(shù)據(jù)中的位置(如果存在的情況下)喜鼓。

image

1.遍歷新數(shù)據(jù)副砍,對每一個元素對應(yīng)的entry的棧進行pop,記錄出棧的的數(shù)字庄岖。如果是NSNotFound表示該元素在舊數(shù)據(jù)中沒有出現(xiàn)豁翎,加入Insert數(shù)組。如果是數(shù)字則表示舊數(shù)據(jù)相同元素的位置隅忿,那么將新舊相同數(shù)據(jù)的record中的index互相設(shè)置心剥。

2.遍歷舊數(shù)據(jù),對每一個元素對應(yīng)的entry的棧進行pop背桐,記錄出棧的的數(shù)字优烧。如果不是NSNotFound,那么加入Delete數(shù)組链峭。

3.再次遍歷新數(shù)據(jù)畦娄,獲取每一個元素的record,如果index是數(shù)字且和新數(shù)據(jù)位置不同時弊仪,表示進行Move操作熙卡,如果和新數(shù)據(jù)位置相同,則表示進行了Reload操作励饵。

能得到最終Record如下再膳,從中不難得到我們需要的各種操作了。

image
  • Reload:[0]A

  • Insert:[2]O曲横、[3]X

  • Delete:[1]D喂柒、[3]G

  • Move:[2]F > [4]F不瓶、[4]T > [1]T

只通過五次遍歷就可以得到準確的Diff結(jié)果,在O(n)復雜度下完成且能避免多個LCS結(jié)果會產(chǎn)生的Move問題灾杰。最后只需要進一步封裝蚊丐,將結(jié)果包在UITableView的beginUpdates/endUpdates或UICollectionView的performBatchUpdates中即可。

在每次實際loadMore或者refresh時艳吠,調(diào)用封裝的performUpdate方法麦备,其中會首先計算前后數(shù)據(jù)源的diff,得到insert/delete/move操作的indexPath后昭娩,再使用系統(tǒng)的performUpdates方法凛篙。

IGListKit通過一個unorderedmap解決了所有問題,但使用這種方法有一些小小的弊端栏渺。unorderedmap是O(1)的查找速度呛梆,內(nèi)部使用哈希表實現(xiàn),相比map會使用更多的內(nèi)存空間磕诊。因此在內(nèi)存比較拘束下填物,可能會產(chǎn)生問題。

這部分的具體實現(xiàn)代碼參見:IGListDiff.mm

8 IGList數(shù)據(jù)流簡介

在這一步霎终,IGList有自己獨特的處理方式滞磺,簡單介紹下IGListKit對UICollectionView封裝,整體架構(gòu)組成如下圖:

image

中心調(diào)度器是Adapter莱褒,adapter作為UICollectionView的datasource击困,每次都會從sectionController中獲取cell,同時作為UICollectionView和UIScrollView的delegate广凸,負責向sectionController回調(diào)阅茶。

IGList的adapter對外提供datasource回調(diào),外界需要提供data數(shù)組炮障、sectionController的對象實例目派、以及empty view坤候,官方demo如下圖代碼所示:

image

其中PostSectionController作為具體section實現(xiàn)胁赢,繼承自IGListSectionController,想要添加cell到section中白筹,必須實現(xiàn)其中幾個方法智末。

在其中發(fā)現(xiàn)有幾點需要注意:

1.回調(diào)給adapter的data數(shù)據(jù)必須實現(xiàn)IGListDiffable協(xié)議,diff算法需要用到協(xié)議中的兩個方法徒河。

2. PostSectionController中collectionContext實際就是就是adapter系馆,也就是說實際上兩者是相互引用的關(guān)系。

3.每次系統(tǒng)的UICollectionView需要數(shù)據(jù)時顽照,作為dataSource的adapter都會問每一個sectionController由蘑,sectionController實際上又向adapter詢問闽寡,獲取某個index的cell...

其實IGListSectionController提供了很多property和method,比如inset尼酿、space的設(shè)置爷狈、selected、highlight方法裳擎、以及supplementaryViewSource涎永、displayDelegate、workingRangeDelegate鹿响、scrollDelegate這幾種很好用的delegate羡微。

其中最有意思的IGListWorkingRangeDelegate協(xié)議,設(shè)置working range范圍惶我,就可以控制sectionController提前回調(diào)下面的方法妈倔,IGListKit官方表示可以在這個時候做一些圖片下載,文字計算等工作指孤。具體實現(xiàn)文件可見IGListWorkingRangeHandler.mm启涯,其中使用了unordered_set實現(xiàn)。

image
image

9 總結(jié)

最初只是好奇而研究了IGListKit框架恃轩,卻發(fā)現(xiàn)針對某些問題解決的非常好结洼。通常在進行列表加載新數(shù)據(jù)選擇直接reloadData的操作,其中的一些問題和坑都遇到過叉跛,iOS的UITableView和UICollectionView會對所有cell重新計算布局再渲染松忍,使用基于LCS的Diff算法計算出其中差異cell,使用performUpdates進行小范圍的insert/delete/move操作不失為一種新的嘗試筷厘。

1.我們常常在UITableView或UICollectionView的delegate方法cellWillDisplay和cellDidEndDisplay中處理一些Cell的埋點鸣峭。比如展現(xiàn)和消失,但是在loadMore的reloadData后酥艳,會回調(diào)已經(jīng)在屏幕中Cell的代理方法摊溶,可能會導致埋點多發(fā)。

2.實現(xiàn)本地數(shù)據(jù)過濾充石,我們常常會遇到本地數(shù)據(jù)源很多格式的情況莫换,簡單的例子就是文本、圖片骤铃、視頻拉岁、問答等各種樣式。如果有某種篩選功能惰爬。簡單的reloadData能夠?qū)崿F(xiàn)喊暖,但使用insert/delete/move操作可以達到更好的動畫效果。

參考資料:

1. Longest Common Subsequence Diff Part 1

2. Diff in iOS Part 2

3. Open Sourcing IGListKit

4. Isolating Differences Between Files

  1. IGListKit Github

6.IGListKit Docs

7. 大規(guī)模重構(gòu)——重寫 Instagram Feed 的經(jīng)驗之談

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末撕瞧,一起剝皮案震驚了整個濱河市陵叽,隨后出現(xiàn)的幾起案子狞尔,更是在濱河造成了極大的恐慌,老刑警劉巖巩掺,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沪么,死亡現(xiàn)場離奇詭異,居然都是意外死亡锌半,警方通過查閱死者的電腦和手機禽车,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來刊殉,“玉大人殉摔,你說我怎么就攤上這事〖呛福” “怎么了逸月?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長遍膜。 經(jīng)常有香客問我碗硬,道長,這世上最難降的妖魔是什么瓢颅? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任恩尾,我火速辦了婚禮,結(jié)果婚禮上挽懦,老公的妹妹穿的比我還像新娘翰意。我一直安慰自己,他們只是感情好信柿,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布冀偶。 她就那樣靜靜地躺著,像睡著了一般渔嚷。 火紅的嫁衣襯著肌膚如雪进鸠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天形病,我揣著相機與錄音客年,去河邊找鬼。 笑死窒朋,一個胖子當著我的面吹牛搀罢,可吹牛的內(nèi)容都是我干的蝗岖。 我是一名探鬼主播侥猩,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼抵赢!你這毒婦竟也來了欺劳?” 一聲冷哼從身側(cè)響起唧取,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎划提,沒想到半個月后枫弟,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡鹏往,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年淡诗,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片伊履。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡韩容,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出唐瀑,到底是詐尸還是另有隱情群凶,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布哄辣,位于F島的核電站请梢,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏力穗。R本人自食惡果不足惜毅弧,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望当窗。 院中可真熱鬧形真,春花似錦、人聲如沸超全。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嘶朱。三九已至蛾坯,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間疏遏,已是汗流浹背脉课。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留财异,地道東北人倘零。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像戳寸,于是被迫代替她去往敵國和親呈驶。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

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