本篇文章并不打算過(guò)多的講解技術(shù)實(shí)現(xiàn)的細(xì)節(jié),大部分都是點(diǎn)到為止。我個(gè)人覺(jué)得技術(shù)細(xì)節(jié)雖然很重要,但是它只是實(shí)現(xiàn)一個(gè)功能的手段,更為重要的是實(shí)現(xiàn)功能的思路和方向。只有理清了思路,選定了方向,接下來(lái)的實(shí)現(xiàn)就應(yīng)該是水到渠成了。
好了,下面我們?cè)撜f(shuō)說(shuō)優(yōu)化的事。我們看一下微信,基礎(chǔ)功能就是聊天,能看到經(jīng)常發(fā)生變化的就是消息列表,和聊天時(shí)候的聊天界面了,幾乎只要你在使用它的時(shí)候這兩個(gè)頁(yè)面都會(huì)發(fā)生變化。所以針對(duì)性能上最重要的兩個(gè)界面,一個(gè)是消息列表,一個(gè)是聊天界面。
一.消息列表頁(yè)面的優(yōu)化:
首先我們發(fā)消息時(shí)候觀察一下消息列表的特性,當(dāng)發(fā)送一條消息時(shí)候,消息的數(shù)量會(huì)變化,列表會(huì)出現(xiàn)在最上邊的位置,列表內(nèi)的內(nèi)容會(huì)發(fā)生變化。從消息列表的特性,我們就可以分析出要優(yōu)化的點(diǎn)了。通過(guò)這些點(diǎn),我們做了一些優(yōu)化:
1.如果列表消息從沒(méi)顯示過(guò)需要刷新列表,創(chuàng)建好一個(gè)cell后,將cell插入到第一位上,cell插入的性能要高于刷新tableview的性能。
2.如果消息已經(jīng)顯示過(guò)了,但是并不是第一位,則需要刷新列表。
3.如果消息已經(jīng)顯示,并且是第一位,則只需要cell的內(nèi)容變化。
4.只修改cell里的內(nèi)容,不進(jìn)行刷新cell整體,這里要注意的是,一定要最小化刷新。刷新點(diǎn)越小,性能損耗越小。我們項(xiàng)目架構(gòu)是MVVM,采用了ReactiveCocoa框架,針對(duì)每個(gè)cell上的可變化的控件數(shù)據(jù)進(jìn)行了監(jiān)聽(tīng),每一個(gè)cell上對(duì)應(yīng)一個(gè)vm,這樣當(dāng)vm上的數(shù)據(jù)變化時(shí)候,cell上的數(shù)據(jù)也就跟著變了。做到了最小化刷新。
5.避免使用autolayout計(jì)算位置,這個(gè)很重要,在性能要求高的情況下,autolayout計(jì)算會(huì)很耗時(shí)間,尤其在算tableview高度的時(shí)候可見(jiàn)一斑。可喜的是消息列表的高度是固定的,所以在計(jì)算高度時(shí)候我們并未花費(fèi)時(shí)間。
6.使用(__kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath而不使用-(nullable __kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier來(lái)查找cell,因?yàn)橄逻叺姆椒〞?huì)多查一次,耗時(shí)更長(zhǎng)一點(diǎn)。
7.cellForRowAtIndexPath方法只負(fù)責(zé)創(chuàng)建cell,willDisplayCell方法才給cell進(jìn)行賦值操作。從方法名字就可以看出來(lái)原因
8.當(dāng)來(lái)消息轟炸時(shí)候,必然會(huì)是不同的人發(fā)來(lái)的消息,會(huì)導(dǎo)致tableview不可避免的刷新,如果不加處理必然會(huì)卡頓,要知道,機(jī)器也是有瓶頸的。這里我們做的優(yōu)化是根據(jù)cpu的使用率選擇性的刷新tableview。后來(lái)我們發(fā)現(xiàn)微信也是有這個(gè)現(xiàn)象,并不是實(shí)時(shí)的刷新,我們猜測(cè)也是類似處理。
9.重繪制系統(tǒng)控件,相信你也發(fā)現(xiàn)了消息列表里,主要有兩個(gè)控件,一個(gè)是頭像,一個(gè)是label。而系統(tǒng)的UIImageView用來(lái)顯示頭像,未免有點(diǎn)重。我們的處理是使用UIView,設(shè)置View得layer.content來(lái)處理。針對(duì)layer層做的setImageWithUrl的第三方庫(kù)也不少,大家可以自行查詢。另一個(gè)就是label,如果你能集成UIView自己繪制一個(gè)label,我想也許會(huì)有一點(diǎn)效果。
以上我們對(duì)消息列表做的優(yōu)化,寫的并不全,只記得大概有這些吧。
二.聊天界面的優(yōu)化:
聊天界面的優(yōu)化算是比較繁瑣的了,但是優(yōu)化點(diǎn)跟回話列表的優(yōu)化差不多。上邊提到回話列表里最耗時(shí)的tableview的高度是固定的,而聊天界面的幾乎每條消息的高度都可能不一樣,所以我們?cè)趦?yōu)化聊天界面時(shí)候最重要的一點(diǎn)就是計(jì)算tableviewcell的高度。而我們?cè)谟?jì)算tableview的高度是怎么做的呢?
其實(shí)這個(gè)網(wǎng)上千篇一律的優(yōu)化tableview的方式是大同小異的。
主要有兩個(gè)準(zhǔn)則:
第一個(gè)是能在后臺(tái)線程執(zhí)行的都放在后臺(tái)線程里。
第二個(gè)計(jì)算高度要放在顯示之前。
上邊提到我們使用的ReactiveCocoa基于事件流來(lái)處理消息,這個(gè)確實(shí)是為開(kāi)發(fā)帶來(lái)了很大的好處,處理層次非常清晰,開(kāi)發(fā)效率的提升,代碼復(fù)雜度降低,多人員開(kāi)發(fā)分工分明,低耦合等這些好處充分讓你感受到編程的樂(lè)趣,之后我們會(huì)出一篇關(guān)于架構(gòu)的文章,這里只是稍提一嘴。當(dāng)然即使你不用ReactiveCocoa也沒(méi)關(guān)系,思路都是一樣的。首先是你從底層異步接收到消息,進(jìn)行消息的處理,包括數(shù)據(jù)庫(kù)的處理,提供給View顯示的對(duì)象處理,這里我們是用的VM層,其中也包括了消息的高度的計(jì)算,這些都是放在線程隊(duì)列里,當(dāng)所有的處理完成,然后進(jìn)行主線程消息列表更新,進(jìn)行插入一個(gè)cell和向上滾動(dòng)一條消息的距離。我們之前把VM的創(chuàng)建放在了主線程,發(fā)現(xiàn)在32位機(jī)器上掉了10fps,這是讓人不能忍受的事情。當(dāng)然還有一點(diǎn)是盡量保證你的對(duì)象不要太大,如果太大也會(huì)影響性能,我們的vm就比較大,這也是以后性能上的隱患。優(yōu)化tableView的另外一點(diǎn)就是cell的制作了,要說(shuō)的跟上邊第九條提到的也差不多,這里我也總結(jié)了一下幾個(gè)原則:
1.巧妙的選擇控件。比如上邊提到的,如果只是顯示一個(gè)圖片,那么用View的layer.content性能自然是好一些。圖片加點(diǎn)擊也可以用view,然后監(jiān)聽(tīng)view的touch事件,像button這種重量級(jí)的控件在性能為主的app面前,我對(duì)他們都是棄之如敝履。
2.減少使用layer層的cornerRadius,mask等圓角的繪制,這會(huì)引發(fā)離屏渲染,增加cpu的占用率。如果業(yè)務(wù)需要的話,我們可以通過(guò)UIBezierPath來(lái)drawInRect它。
3.避免設(shè)置透明
4.避免autolayout設(shè)定控件位置
5.盡可能的減少視圖的層級(jí),如果你能把所有的控件都繪制到一個(gè)View上,可想而知性能會(huì)爆棚。
三.其他優(yōu)化
還有兩大優(yōu)化點(diǎn)不容忽視。
一是數(shù)據(jù)持久層的優(yōu)化,就是所謂的數(shù)據(jù)庫(kù)優(yōu)化。
二就是內(nèi)存優(yōu)化。
關(guān)于持久層的優(yōu)化嚴(yán)格來(lái)性能上通常不會(huì)有太大出入,只不過(guò)我們之前是直接針對(duì)底層sqlite數(shù)據(jù)庫(kù)很簡(jiǎn)單的封裝,后來(lái)用了fmdb,發(fā)現(xiàn)在插入性能上有了明顯的提升。幾乎用過(guò)的小伙伴都說(shuō)好。持久化這邊我們將fmdb,jsonModel進(jìn)行封裝了一個(gè)庫(kù),提供了友好的api,通常像這樣使用[message findAll],[message save]等基本上都不需要寫sql語(yǔ)句。回頭我們會(huì)開(kāi)源化。關(guān)于sql優(yōu)化的我們總結(jié)了幾點(diǎn):
1.大批量插入時(shí)候,用事務(wù)可以幾何倍的減少插入時(shí)間,原因是會(huì)把sql都加入內(nèi)存,然后一次性提交。
2.針對(duì)經(jīng)常變化的表、字段避免使用索引,當(dāng)然索引帶來(lái)的查詢性能也是幾何倍的增加,這里要說(shuō)的一點(diǎn)是like并不會(huì)使用到索引
3.巧妙使用sql語(yǔ)句增刪改查,能夠用一條語(yǔ)句解決的事就不要使用兩條,比如之前我們發(fā)現(xiàn)一個(gè)設(shè)置已讀消息,是把這個(gè)人的消息取出來(lái),然后再設(shè)置進(jìn)去,后來(lái)我們直接進(jìn)行了這個(gè)人的update語(yǔ)句,發(fā)現(xiàn)這塊的處理性能高了很多。
4.當(dāng)單條數(shù)據(jù)小于 20K 時(shí),數(shù)據(jù)越小 SQLite 讀取性能越高;單條數(shù)據(jù)大于 20K 時(shí),直接寫為文件速度會(huì)更快一些。
5.ibireme.com提到過(guò)在官網(wǎng)下載源碼編譯的sqlite性能會(huì)高幾倍,我確實(shí)編譯了一下,但是編譯出來(lái)的是dylib,現(xiàn)在的系統(tǒng)是tbd,我沒(méi)轉(zhuǎn)過(guò)去,并且pods里依賴的也是tbd的,所以也就作罷了,主要放棄原因還是發(fā)現(xiàn)我們將很多數(shù)據(jù)庫(kù)操作的部分去掉了,cpu占用率也沒(méi)有發(fā)生變動(dòng)。如果有搞定的同學(xué)記得分享一下。
數(shù)據(jù)庫(kù)的大概能記住的也就這幾點(diǎn)吧。
大最后說(shuō)一嘴,多用Instuments來(lái)檢測(cè)你的方法執(zhí)行時(shí)間,以及CPU GPU占用率,這能夠很好的幫你優(yōu)化你的程序。