UITableView性能優化的一點感悟及計算UILabel高度的新方法

前言

在使用過程中發現,我們App的首頁在快速滑動時會出現掉幀,以及在上拉加載更多時會抖動,因爲首頁模塊是以前的同事寫的,很多代碼已不適應當前的需求,所以產生了優化的想法,優化主要分爲以下幾個方面:

1.緩存cell高度(發現了一種計算Label高度的新方法)

2.優化cellForRow方法

3.圖片加載優化

4.禁止tableView預估高度

5.刪除無用數據處理邏輯

緩存cell高度

在Feed流中,UITableViewCell的高度通常是變化的,需要根據返回的數據中的cell類型以及label的文字長度來計算高度,而在UITableView中func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell

是一個高頻調用的方法,爲了減少CPU的計算,儘可能減少掉幀,所以需要將高度進行緩存,在我們的項目中,首頁的數據是這樣一個操作流程後臺返回的JSON->FeedListModel->FeedsModel->各種cell的ViewModel(例如小圖片的cell對應的model-SmallImageCellViewModel,大圖片的cell對應的-BigImageCellViewModel)FeedListModel主要是包含了一些頁碼信息和FeedsModel數組FeedsModel儲存着後臺返回的cell所需的信息BigImageCellViewModel是cell對FeedsModel進行處理後得到cell所需的信息

優化以前,我們的高度是通過BigImageCellViewModel中計算屬性height去獲取的

var height: CGFloat {

      guard let title = title else {

return ((UIScreen.mainWidth - 30) * 9)/16.0 + 62

}

let constraintRect = CGSize(width: UIScreen.mainWidth - 30, height: 38.5)

let attributes = [NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: 16)]

let rect = title.boundingRect(with: constraintRect,

options: .usesLineFragmentOrigin,

attributes: attributes,

context: nil)

if let type = itemType, type == ItemType.sohuVideo {

return ((UIScreen.mainWidth - 30) * 9)/16.0 + rect.height + 62

}

return ((UIScreen.mainWidth - 30) * 9)/16.0 + rect.height + 62

}

這樣的話每次取值時,會需要通過計算然後返回height屬性,所以一開始我也是把計算屬性改成存儲屬性了,但是還是很耗時(後來才發現是因爲高度是存在BigImageCellViewModel中的,而每次數據更新後,由於業務需要會對當前的列表數據重新遍歷處理,生成新的BigImageCellViewModel,新的BigImageCellViewModel的高度自然是每次需要計算),在使用instruments分析時發現,在加載數據時,1s內有20%的時候是用於計算每個cell的高度,因爲計算cell高度時需要根據model.title確定cell中的標題Label顯示幾行,從而確定Label的高度,進而算出cell的高度,而計算Label高度一般都是使用這個方法,

@available(iOS 7.0, *)

open func boundingRect(with size: CGSize, options: NSStringDrawingOptions = [], attributes: [NSAttributedString.Key : Any]? = nil, context: NSStringDrawingContext?) -> CGRect

因爲即便是同一個字符串,字體大小一樣,字體不同時,高度會不一定一樣,這個方法會根據字符串和對應字體進行繪製計算後得到的高度,而且這個操作是在主線程進行的,所以會導致掉幀,然後我就網上查閱資料怎麼優化這個方法,網上這方面的資料比較少因爲這個方法的耗時本身是可接受範圍以內的,只是我們的height沒有真正緩存上導致這個方法測試時特別耗時,這種思路是思路一在子線程中調用這個方法,然後對height進行賦值,類似於這樣:

思路一 異步計算Label高度
var height:CGFloat = 70

let queue = DispatchQueue.global()

queue.async {

let labelRect = title.boundingRect(with: constraintRect,

options: .usesLineFragmentOrigin,

attributes: attributes,

context: nil)

height = labelRect.height + 50

}

就是通過先給height賦一個概率最大的值,然後通過異步計算後,得到一個準確值,再給height賦值上,但是在實際測試中發現,大部分cell取的是我們預設的高度默認值,這樣在下次reloadData時,cell的高度會取算出來的值,然後會導致tableView的contentSize變化,視圖抖動然後我就自己思考,其實我們的標題並不複雜,大部分是中文,其他是數字,標點符號,字母,然後我就測試了一下在UIFont.boldSystemFont(ofSize: 16)下,中文,數字,標點符號,字母的大小,然後測試發現中文 15pt 數字是8pt左右,主要的一些標點符號16pt 小寫字母大概8pt,大寫自貿銀11pt,就想能不能通過對標題字符串進行遍歷,判斷字符的類型來計算標題的總寬度,之後再將總寬度除以標題的最大寬度得到行數,然後計算得出cell高度,代碼如下:

思路二 計算Label高度的新方法 通過遍歷字符串來計算高度
-(CGFloat)calculateTotalWidthInBold16 {

CGFloat totalWidth = 0;

for (int i = 0; i < self.length; i++) {

unichar character = [self characterAtIndex:i];

//中 佔15pt 數字 佔7 英文 a 8.2 A 10.1 B 10.6 , ? 16.6pt

if ([[NSCharacterSet decimalDigitCharacterSet] characterIsMember:character]) {//數字

totalWidth += 8;

} else if ([[NSCharacterSet lowercaseLetterCharacterSet] characterIsMember:character]) {//小寫字母

totalWidth += 10;

} else if ([[NSCharacterSet uppercaseLetterCharacterSet] characterIsMember:character]) {//大寫字母

totalWidth += 12;

} else if ([[NSCharacterSet punctuationCharacterSet] characterIsMember:character]) {//標點符號

totalWidth += 17;

} else if (character >= 0x4E00 && character <= 0x9FA5) {

totalWidth += 15;

} else {

totalWidth += 15;

}

}

return totalWidth + 5;

}

在不緩存高度的情況下,這個方法能夠很快得計算出高度,讓tableview達到平均55幀以上的幀率,但是缺點是需要對使用的字體下進行測試,在UIFont.boldSystemFont(ofSize: 16)字體下,中文是固定的15pt,但是數字,小寫字母,大寫字母的長度不是固定的,所以如果需要做到非常準確,需要對每個數字,字母在這個字體下的長度進行測試。

在緩存高度的情況下,與boundingRect方法相比,這個方法也能夠提高計算速度,只是收益不那麼明顯

優化cellForRow方法

因爲tableView的cellForRow方法也是一個調用頻率特別高的方法,所以應該避免在cellForRow對cell進行約束脩改,frame變化等操作,

open func cellForRow(at indexPath: IndexPath) -> UITableViewCell? // returns nil if cell is not visible or index path is out of range

主要是把這部分代碼註釋掉了,這部分操作主要是爲了隱藏最後一個cell的分割線,但是我們是預加載的,其實很少能看到最後一個cell的底部,所以其實沒有必要

default: //feed流

let cellViewModel = viewModel.viewModels.value[indexPath.row]

let cell = configFeedCell(tableView: tableView, cellViewModel: cellViewModel, indexPath: indexPath)

// cell.saHorizontalSpace = (15, 15)

// if viewModel.isInfrontOfFeedSpacAble(indexPath: indexPath) {

// cell.saSeparaptorLineStyle = .bottom

// } else if cellViewModel as? FeedSpacAble != nil {

// cell.saSeparaptorLineStyle = .bottom

// } else {

// cell.saSeparaptorLineStyle = .none

// }

return cell

圖片加載優化

主要使用charles進行抓包,看項目有沒有加載比較大的圖片,我們項目首頁的三張圖片的資訊使用的是大圖,一張圖片長達4M,所以我改成小圖了

禁止tableView預估高度

因爲tableView會根據estimatedRowHeight*行數來計算contentSize,並且在滑動時進行修正,所以會發生抖動,所以可以通過以下代碼,禁用預估高度,因爲iOS11以後預估高度的值不爲0,所以需要顯式賦值爲0

tableView.estimatedRowHeight = 0

tableView.estimatedSectionHeaderHeight = 0

tableView.estimatedSectionFooterHeight = 0

刪除無用數據處理邏輯
主要註釋了代碼中沒有用的數據處理邏輯

總結

以上其實只是針對我們項目一些比較基本的優化的地方,當然還有很多地方可以進行優化,例如將cell中view的佈局進行緩存,減少不必要的計算,還有將一些Label通過異步渲染的方式繪製在cell中,減少view的層級,將一部分渲染的工作放在子線程中,但是這樣會對我們的項目改動過大,所以暫時沒有采用

PS: 最近加了一些iOS開發相關的QQ羣和微信羣,但是感覺都比較水,裏面對於技術的討論比較少,所以自己建了一個iOS開發進階討論羣,歡迎對技術有熱情的同學掃碼加入,加入以後你可以得到:

1.技術方案的討論,會有在大廠工作的高級開發工程師儘可能抽出時間給大家解答問題

2.每週定期會寫一些文章,並且轉發到羣裏,大家一起討論,也鼓勵加入的同學積極得寫技術文章,提升自己的技術

3.如果有想進大廠的同學,裏面的高級開發工程師也可以給大家內推,並且針對性得給出一些面試建議

羣已經滿100人了,想要加羣的小夥伴們可以掃碼加這個微信,備註:“加羣+暱稱”,拉你進羣,謝謝了 !
167d5c441623126a?w=674&h=896&f=jpeg&s=84496

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章