前言
UITableview在iOS中的使用頻率是非常高的.通常,我們只需要通過設(shè)置代理,并且在代理方法tableView:cellForRowAtIndexPath:
調(diào)用dequeueReusableCellWithIdentifier:
獲取cell
并直接使用.但是從沒有細(xì)致得想過其中的過程與機(jī)制,并且知道最近面試(此次面試的問題)的時(shí)候,才發(fā)現(xiàn)自己表述得不太好.這種感覺就好像你每天都準(zhǔn)時(shí)吃飯(滑稽),突然有一天有人問你人為什么吃飯的時(shí)候自己卻沒能及時(shí)答上來的那種感覺.說明自己學(xué)得不夠扎實(shí).as we know,
如果你不能將知識(shí)通過簡潔的語言表達(dá)出來,那說明你還沒掌握這個(gè)知識(shí).
借此機(jī)會(huì),將這個(gè)知識(shí)點(diǎn)記錄下來
reuseIdentifier
對于reuseIdentifier,官方文檔是這樣解釋的:
The reuse identifier is associated with a
UITableViewCell
object that the table-view’s delegate creates with the intent to reuse it as the basis (for performance reasons) for multiple rows of a table view. It is assigned to the cell object ininitWithFrame:reuseIdentifier:
and cannot be changed thereafter.
AUITableView
object maintains a queue (or list) of the currently reusable cells, each with its own reuse identifier, and makes them available to the delegate in thedequeueReusableCellWithIdentifier:
method.
冒死翻譯一下:
這個(gè)復(fù)用標(biāo)識(shí)關(guān)聯(lián)UITableviewCell
對象,在tableview
代理中創(chuàng)建帶有”標(biāo)識(shí)符”來復(fù)用cell
對象,而且作為tableview
多行顯示的原型(性能的原因).通過initWithFrame:reuseIdentifier:
來指定一個(gè)cell
對象而且在調(diào)用這個(gè)方法之后就不能修改了.在UITableView
對象維護(hù)一個(gè)當(dāng)前復(fù)用的cell
隊(duì)列(或列表),并且每一個(gè)cell
都擁有自己的標(biāo)識(shí)符,并這些cell
能在代理對象的dequeueReusableCellWithIdentifier:
的方法中獲取.
在tableview
新建的時(shí)候,會(huì)新建一個(gè)復(fù)用池(reuse pool).這個(gè)復(fù)用池可能是一個(gè)隊(duì)列,或者是一個(gè)鏈表,保存著當(dāng)前的Cell
.pool中的對象的復(fù)用標(biāo)識(shí)符就是reuseIdentifier
,標(biāo)識(shí)著不同的種類的cell
.所以調(diào)用dequeueReusableCellWithIdentifier:
方法獲取cell
.從pool中取出來的cell
都是tableview
展示的原型.無論之前有什么狀態(tài),全部都要設(shè)置一遍.
過程
在UITableView
創(chuàng)建同時(shí),會(huì)創(chuàng)建一個(gè)空的復(fù)用池.之后UITableView
在內(nèi)部維護(hù)這個(gè)復(fù)用池.一般情況下,有兩種用法,一種是在取出一個(gè)空的cell的時(shí)候再新建一個(gè).一種是預(yù)先注冊cell.之后再直接從復(fù)用池取出來用,不需要初始化.
-
UITableview
第一次執(zhí)行tableView:cellForRowAtIndexPath:
.當(dāng)前復(fù)用池為空.
dequeueReusableCellWithIdentifier
調(diào)用中取出cell
,并檢測cell
是否存在.
目前并不存在任何cell
,于是新建一個(gè)cell
,執(zhí)行初始化, 并return cell
.
代碼如下:#define kInputCellReuseIdentifierPWD @"password_cell" UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kInputCellReuseIdentifierPWD]; if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kInputCellReuseIdentifierPWD]; // other initialization i.e. add an UIButton to cell's contentView } // custom cell. i.e. change cell's title cell.textLabel.text = @"It is awesome"; return cell;
上面的代碼中,你返回的cell
會(huì)被UITableView
添加到復(fù)用池中.第二次調(diào)用tableView:cellForRowAtIndexPath:
,當(dāng)前復(fù)用池中有一個(gè)cell
.這時(shí)候因?yàn)?code>UITableView上面還未填滿,而且復(fù)用池中唯一的那一個(gè)已經(jīng)在使用了.
所以取出來的Cell仍然是nil
.于是繼續(xù)新建一個(gè)cell并返回,復(fù)用池再添加一個(gè)cell
,當(dāng)前復(fù)用池中cell
的個(gè)數(shù)為2.
假如當(dāng)前tableview
只能容納5個(gè)cell
.那么在滾動(dòng)到第6個(gè)cell
時(shí),從tableview
的復(fù)用池取出來的cell
將會(huì)是第0行的那個(gè)cell
.以此類推,當(dāng)滾動(dòng)到第7行時(shí),會(huì)從復(fù)用池取出來第1行的那個(gè)cell
. 另外,此時(shí)不再繼續(xù)往復(fù)用池添加新的cell
.
-
另一個(gè)用法:在
UITableView
初始化的時(shí)候,注冊一個(gè)UITableViewCell
的類.[tableView registerClass:[UITableViewCell class] forCellWithReuseIdentifier:@"AssetCell"];
使用此方法之后,就不用再判斷取出來的cell
是否為空,因?yàn)槿〕鰜淼腸ell必定存在.調(diào)用dequeueReusableCellWithIdentifier:
方法時(shí),會(huì)先判斷當(dāng)前復(fù)用池時(shí)候有可用復(fù)用cell
.
如果沒有,tableview
會(huì)在內(nèi)部幫我們新建一個(gè)cell
,其他的跟方法一一樣.
這里有個(gè)動(dòng)畫可以很清晰地看到滑動(dòng)時(shí)的復(fù)用過程.圖片來自Scroll Views Inside Scroll Views
StoryBoard中的復(fù)用機(jī)制
與CSwater討論之后,覺得在StoryBoard中,復(fù)用機(jī)制如下:在對StoryBoard初始化UITableView時(shí),會(huì)再內(nèi)部調(diào)用registerClass:forCellWithReuseIdentifier:
注冊我們在storyboard上設(shè)置的cell.剩下的流程跟第二種方法一樣.
爬過的坑
從復(fù)用池中獲取cell的方法有兩個(gè):dequeueReusableCellWithIdentifier:forIndexPath:
和dequeueReusableCellWithIdentifier:
.還記得我們在第二個(gè)方法對使用使用復(fù)用池之前的情況嗎? 對,就是注冊一個(gè)cell.注冊之后我們調(diào)用dequeueReusableCellWithIdentifier:
獲取cell.
對于第二種情況,兩種方法都是沒問題的.但是,在調(diào)用registerClass:forCellReuseIdentifier:
之前,你必須注冊一個(gè)cell類.正如官方文檔所言:
IMPORTANT
You must register a class or nib file using theregisterNib:forCellReuseIdentifier:
orregisterClass:forCellReuseIdentifier:
method before calling this method.
總結(jié)
UITableViewCell
的復(fù)用機(jī)制是,在tableview
中存在一個(gè)復(fù)用池.這個(gè)復(fù)用池是一個(gè)隊(duì)列或一個(gè)鏈表.然后通過dequeueReusableCellWithIdentifier:
獲取一個(gè)cell
,如果當(dāng)前cell
不存在,即新建一個(gè)cell
,并將當(dāng)前cell
添加進(jìn)復(fù)用池中.如果當(dāng)前的cell數(shù)量已經(jīng)到過tableview
所能容納的個(gè)數(shù),則會(huì)在滾動(dòng)到下一個(gè)cell
時(shí),自動(dòng)取出之前的cell
并設(shè)置內(nèi)容.
擴(kuò)展知識(shí)
其實(shí),UITableViewCell
的復(fù)用機(jī)制并非什么黑魔法,而是設(shè)計(jì)模式中的一種創(chuàng)造型設(shè)計(jì)模式--對象池設(shè)計(jì)模式
The object pool pattern is a software creational design pattern that uses a set of initialized objects kept ready to use – a "pool" – rather than allocating and destroying them on demand. A client of the pool will request an object from the pool and perform operations on the returned object. When the client has finished, it returns the object to the pool rather than destroying it; this can be done manually or automatically. ---- wikipedia
大意是,對象池模式是一種創(chuàng)造型設(shè)計(jì)模式,使用一個(gè)已初始化對象的集合,并能隨時(shí)從”池”中拿出來使用.以此避免對象的創(chuàng)建銷毀所帶來的開銷.一個(gè)客戶端的池會(huì)請求池中的對象,然后在返回的對象上執(zhí)行操作.當(dāng)這個(gè)客戶端結(jié)束時(shí),代替原本銷毀操作,取而代之的是將對象返回到池中.這個(gè)操作可以手動(dòng)執(zhí)行,也可以自動(dòng)執(zhí)行.有一個(gè)明顯的有點(diǎn)就是面對數(shù)據(jù)量很大時(shí),能很好的改善性能.更多的請看維基百科.
參考文章
Object pool pattern
Scroll Views Inside Scroll Views
UITableViewDataSource Protocol Reference
UITableView