微信讀書 iOS 性能優化總結

微信讀書作爲一款閱讀類的新產品,目前還處於快速迭代,不斷嘗試的過程中,性能問題也在業務的不斷累積中逐漸體現出來。最近的 1.3.0 版本發佈後,關於性能問題的用戶反饋逐漸增多,爲此,團隊開始做一些針對性的性能問題優化。本文將從發現問題、解決問題和預防問題三個方面進行總結。

如何發現性能問題

不同於一般的 bug,性能問題因爲並沒有統一的標準,而且與用戶的機器環境相關性較大,所以往往是在產品上線後才被發現,也導致解決問題的週期很長。微信讀書 1.3.0 版本之前,性能問題基本都來自於用戶反饋(包括測試人員),受限於測試時間和用戶反饋的積極性,性能問題往往到了比較嚴重的程度,開發人員才真正發現問題。

但是,移動應用要保證良好的用戶體驗,產品在性能方面的表現極其重要。爲了儘可能早、儘可能全面地收集產品的性能問題,就避免不了對產品做性能監控。我們主要從兩個維度進行了監控:

  1. 業務性能監控,是指在App本地,業務的開始和結束處打點上報,然後後臺統計達到監控目的;

  2. 卡頓監控。卡頓監控的實現一般有兩種方案:

    (1)主線程卡頓監控。通過子線程監測主線程的 runLoop,判斷兩個狀態區域之間的耗時是否達到一定閾值。具體原理和實現,這篇文章介紹得比較詳細。

    (2)FPS監控。要保持流暢的UI交互,App 刷新率應該當努力保持在 60fps。監控實現原理比較簡單,通過記錄兩次刷新時間間隔,就可以計算出當前的 FPS。

但是,在實際應用過程我們發現,無論是主線程監控,還是 FPS 監控,抖動都比較大。因此,微信團隊提出了一套綜合的判斷方法,結合了主線程監控、FPS監控,以及CPU使用率等指標,作爲判斷卡頓的標準。

微信卡頓監控

微信讀書接入了RDM(bugly)的卡頓監控(也是基於微信團隊的卡頓標準),通過下發配置,對現網用戶進行抽樣檢測,並上報卡頓的堆棧信息。這對於我們掌握現網用戶的卡頓狀況起到了非常大的幫助。

性能問題的解決方法

產生性能問題的原因多種多樣,因此解決的辦法也不盡相同,比較常用的大概有以下幾種:

1.優化業務流程

性能優化看似高深,真正落到實處纔會發現,最大的坑往往都隱藏在於業務不斷累積和頻繁變更之處。優化業務流程就是在滿足需求的同時,提出更加高效優雅的解決方案,從根本上解決問題。從實踐來看,這種方法解決問題是最徹底的,但通常也是難度最大的。微信讀書在優化閱讀中各種操作(如,書籤、劃想、想法等)性能時,就是從業務流程的角度來進行優化。如下圖:

閱讀劃線優化

2.合理的線程分配

由於 GCD 實在太方便了,如果不加控制,大部分需要拋到子線程操作都會被直接加到 global 隊列,這樣會導致兩個問題,1.開的子線程越來越多,線程的開銷逐漸明顯,因爲開啓線程需要佔用一定的內存空間(默認的情況下,主線程佔1M,子線程佔用512KB)。2.多線程情況下,網絡回調的時序問題,導致數據處理錯亂,而且不容易發現。爲此,我們項目定了一些基本原則。

  • UI 操作和 DataSource 的操作一定在主線程。
  • DB 操作、日誌記錄、網絡回調都在各自的固定線程。
  • 不同業務,可以通過創建隊列保證數據一致性。例如,想法列表的數據加載、書籍章節下載、書架加載等。

合理的線程分配,最終目的就是保證主線程儘量少的處理非UI操作,同時控制整個App的子線程數量在合理的範圍內。

3.預處理和延時加載

預處理,是將初次顯示需要耗費大量線程時間的操作,提前放到後臺線程進行計算,再將結果數據拿來顯示。

延時加載,是指首先加載當前必須的可視內容,在稍後一段時間內或特定事件時,再觸發其他內容的加載。這種方式可以很有效的提升界面繪製速度,使體驗更加流暢。(UITableView 就是最典型的例子)

這兩種方法都是在資源比較緊張的情況下,優先處理馬上要用到的數據,同時儘可能提前加載即將要用到的數據。在微信讀書中閱讀的排版是優先級最高的,所在在閱讀過程中會預處理下一頁、下一章的排版,同時可能會延時加載閱讀相關的其它數據(如想法、劃線、書籤等)。

4.緩存

cache可能是所有性能優化中最常用的手段,但也是我們極不推薦的手段。cache建立的成本低,見效快,但是帶來維護的成本卻很高。如果一定要用,也請謹慎使用,並注意以下幾點:

  • 併發訪問 cache 時,數據一致性問題。
  • cache 線程安全問題,防止一邊修改一邊遍歷的 crash。
  • cache 查找時性能問題。
  • cache 的釋放與重建,避免佔用空間無限擴大,同時釋放的粒度也要依實際需求而定。

5.使用正確的API

使用正確的 API,是指在滿足業務的同時,能夠選擇性能更優的API。

  • 選擇合適的容器;
  • 瞭解 imageNamed: 與 imageWithContentsOfFile:的差異(imageNamed: 適用於會重複加載的小圖片,因爲系統會自動緩存加載的圖片,imageWithContentsOfFile: 僅加載圖片)
  • 緩存 NSDateFormatter 的結果。
  • 尋找 (NSDate *)dateFromString:(NSString )string 的替換品。
1
2
3
4
5
6
7
//#include <time.h>
time_t t;
struct tm tm;
strptime([iso8601String cStringUsingEncoding:NSUTF8StringEncoding], "%Y-%m-%dT%H:%M:%S%z", &tm);
tm.tm_isdst = -1;
t = mktime(&tm);
[NSDate dateWithTimeIntervalSince1970:t + [[NSTimeZone localTimeZone] secondsFromGMT]];
  • 不要隨意使用 NSLog().

  • 當試圖獲取磁盤中一個文件的屬性信息時,使用 [NSFileManager attributesOfItemAtPath:error:] 會浪費大量時間讀取可能根本不需要的附加屬性。這時可以使用 stat 代替 NSFileManager,直接獲取文件屬性:

1
2
3
4
5
6
7
8
9
#import <sys/stat.h>
struct stat statbuf;
const char *cpath = [filePath fileSystemRepresentation];
if (cpath && stat(cpath, &statbuf) == 0) {
    NSNumber *fileSize = [NSNumber numberWithUnsignedLongLong:statbuf.st_size];
    NSDate *modificationDate = [NSDate dateWithTimeIntervalSince1970:statbuf.st_mtime];
    NSDate *creationDate = [NSDate dateWithTimeIntervalSince1970:statbuf.st_ctime];
    // etc
}

如何預防性能問題

大部分性能問題可以通過程序員經驗和能力的提升得以減少,但是因爲團隊成員更新、業務累積,性能問題無法避免,如何在開發測試階段發現問題解決問題,是預防性能問題的關鍵。爲此,我們開發了一些比較有意思的工具,用於發現各種性能問題。

1. 內存泄露檢測工具

MLeakFinder是團隊成員zepo在github開源的一款內存泄露檢測工具,具體原理和使用方法可以參見這篇文章。在此之前,內存泄露引起的性能問題是很難被察覺的,只有泄露到了相當嚴重的程度,然後通過Instrument工具,不斷嘗試才得以定位。MLeakFinder能在開發階段,把內存泄露問題暴露無遺,減少了很多潛在的性能問題。

2. FPS/SQL性能監測工具條

該工具條是在DEBUG模式下,以浮窗的形式,實時展示當前可能存在問題的FPS次數和執行時間較長的SQL語句個數,是團隊成員tower的傑作。FPS監測的原理並不複雜,前文也有介紹,雖然並不百分百準確,但非常實用,因爲可以隨時查看FPS低於某個閾值時的堆棧信息,再結合當時的使用場景,開發人員使用起來非常便利,可以很快定位到引起卡頓的場景和原因。SQL語句的監測也非常實用,對於微信讀書,DB的讀寫速度是影響性能的瓶頸之一。因此在DEBUG階段,我們監測了每一條SQL語句的執行速度,一旦執行時間超出某個閾值,就會表現在工具條的數字上,點擊後可以進一步查詢到具體的SQL操作以及實際耗時。

這個工具幫助我們在開發階段發現了很多卡頓問題,尤其是一些不合理的SQL語句,例如:
在想法圏的優化過程中,利用這個工具,我們就發現想法圈第一次加載更多,執行的SQL語句耗時竟然達到了1000多毫秒。

1
_SELECT * FROM WRReview INNER JOIN WRUser ON WRReview.fromId = WRUser.vid WHERE WRReview.type & ? AND WRReview.createTime <= ? ORDER BY WRReview.createTime DESC , WRReview.itemId ASC  LIMIT ?_

通過explain,可以發現這條SQL效率之低:

1
2
3
SEARCH TABLE WRReview
SEARCH TABLE WRUser USING INTEGER PRIMARY KEY (rowid=?)
USE TEMP B-TREE FOR ORDER BY

  • 沒有建立合適的索引,導致WRReview全表掃描。
  • 排序字段沒有索引,導致SQLite需要再一次B-TREE排序。
  • 兩字段排序,性能更低。

優化:給WRReview的 fromId createTime 兩個字段增加了索引,並去掉一個排序字段:

1
SELECT * FROM WRReview INNER JOIN WRUser ON WRReview.fromId = WRUser.vid WHERE WRReview.type & ? ORDER BY WRReview.createTime DESC  LIMIT ?

Explain的結果:

1
2
SCAN TABLE WRReview USING INDEX WRReview_createTime
SEARCH TABLE WRUser USING INTEGER PRIMARY KEY (rowid=?)

SQL執行時間直接降了一個數量級,到100毫秒左右。

3. UI / DataSource主線程檢測工具。

該工具是爲了保證所有的UI的操作和 DataSource 操作一定是在主線程進行,同樣是由tower同學貢獻。實現原理是通過 hook UIView 的 -setNeedsLayout-setNeedsDisplay-setNeedsDisplayInRect 三個方法,確保它們都是在主線程執行。子線程操作UI可能會引起什麼問題,蘋果說得並不清楚,實際開發中我們遇到幾種神奇的問題似乎都是跟這個有關。

  • app 突然丟動畫,似乎 iOS 系統也有這個 bug。雖然沒有確切的證據,但使用這個工具,改完所有的問題後,bug 也好了(不止一次是這樣)。

  • UI 操作偶爾響應特別慢,從代碼看沒有任何耗時操作,只是簡單的 push 某個 controller。

  • 莫名的 crash,這當然是因爲 UI 操作非線程安全引起的。

更多時候,子線程操作 UI 也並不一定會發生什麼問題,也正因爲不知道會發生什麼,所以更需要我們警惕,這個工具替我們掃除了這些隱患。雖然,蘋果表示,現在部分的 UI 操作也已經是線程安全了,但畢竟大部分還不是。DataSource 的監測是因爲我們業務定下的原則,保證列表 DataSource 的線程安全。

4. 排版引擎自動化檢測工具

排版引擎是微信讀書最核心的功能,排版引擎檢測工具原本是爲了檢驗排版引擎改進過程中準確性,防止因爲業務變更,而影響原來的排版特性。實現原理是結合自動化腳本和 App 本身的排版引擎,給書庫中的每一本書建立一個鏡像,鏡像的內容包括書籍的每一章每一頁的截圖,然後分析同一頁碼的兩個不同版本的圖片差異,就可以知道不同版本的排版引擎渲染效果。但是我發現,只要稍加改進,排版後記錄每個章節排版耗時,就可以知道每個版本變化後同一個章節的耗時變化,以此作爲排版引擎的性能指標。這個工具保證了微信讀書,即使在快速迭代過程中也不會丟失閱讀的核心體驗。雖然這個工具無法在其它項目中複用,但是提醒了我們,可以通過自動化工具來保證產品最核心功能的體驗。

5. 書源檢測工具

微信讀書爲了支持正版版權,目前書源完全依賴於後臺,不允許本地導入。書源的優劣的直接影響排版的效果和性能。爲了解決了部分書籍無法打開或者亂碼的問題,我們藉助了後臺同學的書源檢測工具。對線上所有 epub 書籍進行掃描,按照章節大小進行排序。對於章節內容特別大的書籍重點檢測,重新排版,解決了一批 epub 書籍無法打開的問題。同時針對章節內容亂碼的問題,對所有 txt 的書籍進行了一次全量掃描,發現了一些問題,但還無法準確找出所有亂碼的章節,這一點還在努力改善中。

優化成果

  1. 整體使用感受上,已經可以明顯區分兩個版本的性能差異,這一點也可以通過每天的用戶反饋數據中得到驗證。1.3.0 和 1.3.1分別發佈一週後反饋的卡頓數從 10 個降到了 3 個,從總體反饋比例的 2.8% 降到 0.8%。
  2. 某些關鍵業務,耗時也有明顯改善。
  3. 極端案例的修復。超大的epub書籍已通過後臺進行拆分,解決了無法打開書籍的情況。
  4. 針對低端機型,去掉了某些動畫,交互更加流暢。

總結

通過上述介紹,我們可以看出,性能問題普遍存在,無可避免,與其花費大量時間,查找線上版本的性能問題,不如提高整體團隊成員性能優化意識,藉助性能查找工具,將性能問題儘早暴露在開發階段,達到預防爲主的效果。


本文轉載自:


http://wereadteam.github.io/2016/05/03/WeRead-Performance/



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