筆者由於在iOS開發過程中做過一些優化的工作,對iOS效能優化有一些粗淺的認識,一直想把自己這些經驗,簡單總結一下。於是最近在工作閒暇時間,準備針對iOS開發的效能優化寫一系列文章。
作為整個系列的第一篇,我打算針對iOS的優化中的一些總體原則做一些總結。因為我覺得無論列表流暢度優化也好、啟動時間優化也好還是說其他方面的優化,都有一些共性的原則,只有掌握了這些總體性的原則,才能夠更好的做優化,給我們具體的優化任務指明方向,讓我們少繞彎路。後面如果時間允許,我可能會寫一些關於列表流暢度、啟動時間和記憶體優化等方面的文章。
在第一篇“優化總體原則”裡面。我對優化總體原則總結出包括不要提前過度優化、要找到效能瓶頸、要在不同效能指標間權衡、要理解優化任務的底層執行機制和要有技術保障體系五大原則,其中具體闡述每一個原則的時候並不侷限於效能優化方面,會發散到其他的相關領域,會對一些延伸的領域做一些簡單的探討,希望能夠對讀者有一些啟示。以下是第一篇的主要內容。
一、不要提前過度優化
這個原則包括優化的過程中需要避免的兩個陷阱,即提前優化和過度優化。
-
提前優化指的是在開發的起始階段就把效能優化作為一個重要的任務來考慮,在沒有實際資料指標的基礎上,為了效能提前做的些盲目優化工作。
當然這個觀點可能會引起爭議,因為在某些開發領域,一些效能指標以歷史的經驗來說,的確有很大概率甚至必然會有效能瓶頸的問題,因此在架構初期就需要考慮效能的問題。 因此如果把“不要提前優化“這個觀點推廣所有開發領域上的話,我認為可能不一定合適。但是如果把此觀點約束在iOS開發這一領域內,我個人認為還是成立的。因為在目前階段iOS平臺裝置效能普遍較好,蘋果無論是硬體層面還是系統層面對效能方面都做了大量的優化。所以我認為效能方面並不是iOS開發過程中需要首要考慮的因素。相比效能, 我個人認為在iOS開發的初始階段,以下幾個方面是更重要的,是需要首先考慮的。
首先需要考慮的是架構的選擇,這裡的架構指的是Native架構、web架構、Native和web混合架構和跨平臺的架構。這裡面我個人的意見是首先應該儘量避免使用web架構,從Facebook早期的失敗經驗可以看出,web和Native相比的確存在諸多效能、體驗等方面的問題。連大廠都無法徹底改善webview的問題,何況我們。但是在一些和使用者體驗相比,對動態化需求更加迫切的應用場景下,是可以選擇web架構的,比如大家都一直在吐槽某鐵路售票軟體。Native架構的優勢是產品體驗好,對大多數iOS開發者技術棧友好,缺點是由於蘋果對熱更新做了嚴格限制,導致一些動態化的方案無法使用。Native和web混合架構主要是在Native架構上,在一些運營需求十分強烈的場景下(如電商等場景),某些模組使用web開發,這樣既可以在App大部分場景下使用Native架構,保證使用者體驗,又滿足了部分場景動態化的需求。跨平臺的架構主要是可以減少多端開發的成本,使用一套程式碼完成iOS和Android兩個平臺的開發,目前主流的框架有ReactNative、Weex和Xamarin等。這些跨平臺架構的願景都很美好,但實際使用過程中,個人覺得現階段並不比使用Native架構節省人力,其中會遇到許多已知的未知的坑,當然作為新的技術我們應該持開發的心態,但在使用時候也需要全面的評估,尤其作為一個可能有很長生命週期的應用,在使用非官方推薦的開發架構也好、開源庫也好,如果後期無人維護的話,自己團隊是不是有實力去接盤,如果不能,使用蘋果官方推薦的技術棧則更穩妥些。
其次需要考慮的是開發語言的選擇,這個方面其實在選擇了架構之後,也將可選的開發語言範圍縮小到幾個。而且實際專案開發中並不難選,因為團隊開發人員的技術棧幾乎決定了使用的開發語言,如果使用了大部分團隊成員都不熟悉的語言,我相信即使所選語言在很多方面都有壓倒性的優勢,專案的推進也不會十分順利。但是排除團隊技術棧的因素,不同的開發語言的確是各有千秋。大家都知道iOS開發過程中,如果使用Native架構,官方的開發語言是objc和swift。objc是早期iOS開發的官方推薦語言,優點是其動態性,十分靈活,可以實現許多“黑魔法”,缺點語法略怪異(當然對於iOS開發者,使用久了也不覺得怪了),另外是對一些高階的語言特性支援的不是很好(記得最初使用objc開發iOS應用的時候,因為一些特殊的需求。由於objc不支援namespace,給團隊造成了很大的困擾)。swift是蘋果近年來主推的開發語言, 其吸收了許多其他語言的先進特性,也比較容易上手。關於兩種開發語言的具體技術細節,大家有興趣可以自己檢視一些資料瞭解下。雖然蘋果一直在力推swift,但是目前在國內iOS開發領域,由於一些使用者基數大的主流App,均是在swift出現前使用objc編寫的,而且大多經過了數年的版本迭代,加之早起swift ABI的不穩定和版本之間升級需要較多工作,還有swift和objc混編的一些問題,導致目前國內主流App大多仍使用objc作為開發語言。在一些創業公司,或者新的專案中,才有部分開發者使用swift,當然如果目光放長遠的話,未來一定是swift的天下,這兩年objc在每年的語言排名中逐年下降也側面印證了這一點。 除了官方推薦的objc和是swift之外,如果使用跨平臺等其他架構,還可以使用如js、c#等語言,有興趣的可以自行了解下。
再次需要考慮的是開發過程中具體的程式碼架構的選擇,這裡只簡單談談Native架構下的程式碼架構選擇。 目前iOS開發中常用的架構有MVC 、MVVM、VIPER、MVP等。關於這些架構,網上目前有很多的介紹,大家如果對具體細節有興趣可以自行查閱。這裡我只想補充一點,大家在學習和實踐時,不要盲目跟風新技術,比如MVVM等架構未必比MVC好很多,MVC也未必是一個過時的框架。要知道很多新架構帶來的擴充套件性和解耦行都是通過引入間接層來實現的,隨之而來的可能是更多的膠水程式碼和更復雜的程式碼結構。希望大家在選擇的時候能夠根據專案的特點和團隊自身的狀況,選擇最適合自己團隊和專案的程式碼架構。
除了上面說的三點,還有一些其他的關鍵點需要大家在專案初期考慮,比如如何在團隊內部達成統一的程式碼風格?一些關鍵的技術如何選型?如何保證程式碼結構清晰、簡單、擴充套件性好等等。效能問題可以在專案後期開始考慮,如果真的發現明顯的效能問題再優化也來得及。比如專案一開始憑直覺感覺某一個模組可能會有效能問題,就盲目使用多執行緒,而不是根據實際情況具體問題具體分析。會導致程式複雜且容易出現執行緒安全問題。
-
過度優化是指為了優化效能,過度增加系統複雜度和維護成本,使得開發週期變長。雖然可能效能上帶來了一定的提升,但是和過度優化而導致的這些缺點來比,這麼做顯而易見是得不償失的。
筆者在工作過程中,發現許多同學在效能優化過程中,都容易陷入這種過度優化的陷阱。比如這一個簡單的設定介面,一共只有十幾個靜態的cell, 如果去考慮圓角效能、離屏渲染、圖片快取、高度快取、非同步渲染甚至快取佈局資訊,這些無疑是陷入了過度優化的陷阱,在這個應用背景下,簡單快速的實現功能才是第一要務。在目前蘋果的開發框架和平臺上,一般如果出現效能問題,以我的實際經驗來說,問題大部分是出在業務邏輯上面,所以遇到問題首先需要在業務邏輯上找問題,一些過度的極限的優化,完全是沒有必要的。
其實不光是效能優化,我發現許多同學在日常開發中,處處都有過度設計的情況。比如設計模式中的design happy這一陷阱,許多初學者在剛開始學習設計模式的時候,十分痴迷設計模式在解決不同問題時,對程式碼的解耦性和可擴充套件性上的威力,在開發過程中會時時刻刻想著應該用什麼設計模式。結果導致很多的過度設計,其實我們寫程式碼過程中,如果能遵守基本的SOLID原則,大部分情況下就可以寫出高質量的程式碼。
另外一個例子是元件化。近期iOS元件化是一個十分流行的話題,有許多團隊提出了不同的元件化方案。實際專案中,團隊在是否採用元件化方式開發的選擇上,我希望要結合專案特點和團隊組織架構形式具體問題具體分析,不要盲目跟風。在產品功能相對單一、開發人員較少、並行開發需求不強烈的情況下,推行元件化,不但增加系統複雜度,而且增加開發人員學習成本高,使得開發成本變大,我個人覺得這種規模的應用初期需要更多考慮的是如何快速上線、快速迭代和保證App質量。因此如果能夠進行清晰的分層,嚴格遵守簡單統一的架構模式即可。元件化比較適合從功能形態上可以清晰劃分若干模組的產品,比如美團、58同城、淘寶和攜程等產品,內部有多個業務模組,而且這些公司開發此類“航母”App的時候,會從組織架構把不同業務劃分給不同的開發團隊,為了能夠保證不同團隊之間能夠獨立並行開發和發版,最大程度上減少程式碼的依賴程度,這個時候應用元件化則是最佳實踐。
上面是對不要提前過度優化原則的詳細闡述,並引申到相關開發領域,做了一些不成熟的探討。下面介紹效能優化總體原則的第二個。
二、要找到效能瓶頸
在做優化前,一定要首先找到效能瓶頸有哪些,依效能嚴重程度逐個解決。不要盲目優化,否則最後可能花了很大的力氣,優化掉的可能只是效能損耗很小的一部分。這一原則我覺得尤為重要,因為我在工作中遇見過包括我在內,很多不進行效能瓶頸查詢,全憑主觀猜測進行效能優化的情況。 在尋找效能瓶頸過程中,也需要注意以下問題。
-
不要主觀猜測,讓效能評測資料說話。
這一點十分重要,要時刻記住要以事實說話,不要以為某個函式使用的演算法的時間複雜度是O(n2)就覺得一定會有效能問題,非要費很大力氣優化到O(n*logn), 殊不知你的輸入資料可能只有幾十或者幾百個,即使O(n2)也不會有多大的效能問題。也不要以為某個方法僅僅呼叫了系統庫的一個簡單get方法,就不會有什麼效能問題,殊不知這個get方法裡可能包含一些十分耗時的操作(比如磁碟IO)。因此在遇到效能問題的時候,一定不要憑主觀猜測,實地跑一下效能資料,讓資料告訴我們效能瓶頸究竟在哪裡。
-
要使用恰當的效能評測工具。
對於開發版本的效能優化,Xcode提供的instruments絕對是最好的尋找效能瓶頸的工具,沒有之一。instruments有豐富的效能評測工具,包括常用的Core Animation、Time Profiler、Leaks和Allocations等等。這些工具在分析函式執行時間、fps和記憶體等方面給我提供了十分便捷的功能。在使用instruments過程中需要注意:
-
要使用真機,而不是模擬器。模擬器的CPU比iOS機器要快很多,所以在模擬器上,CPU相關的操作會更快。因為Mac的GPU和iOS裝置上的GPU不同,所以模擬器需要在CPU上通過軟體去模擬iOS裝置上的GPU,所以GPU相關的操作會更慢。因此如果使用模擬器去進行效能優化的話,評測裝置和真實使用者裝置效能表現的不一致,會導致優化的效果大打折扣。這裡面記憶體是一個例外,在做記憶體優化的時候,使用模擬器和真機一般差別不大,可以使用模擬器進行記憶體的優化。
-
要使用Release配置而不是Debug配置,因為在release包的時候,編譯器會做一些優化以提高效能。自己的工程程式碼可能也會在release下做一些優化,比如去除log資訊和一些debug功能等。我們關心的是release下的效能,因為使用者最終也是使用的release的安裝包。所以測試的時候要一定要記住在release配置下進行,instruments進行效能評測的時候,預設是在release下進行的。但是工程程式碼裡面的優化則需要自己注意。筆者就曾經為此付出過很大代價,因為沒有注意工程程式碼裡面的一些debug功能,導致優化過程中錯誤的認為動態庫是影響啟動時間的罪魁禍首,花了很大力氣把動態庫修改為靜態庫,白白浪費了很多時間。
-
要使用效能相對差的機器進行評測,因為我們需要保證的是我們的應用在效能差的機器上也有良好的表現。如果有條件,最好能夠覆蓋多個機型,和我們傳統上的認識不同,機型越新效能不一定越高,例如iPad3在動畫和渲染效能上比iPad2差。
-
要覆蓋不同系統版本,因為在iOS系統上,一般系統版本越高,同一機器效能大體上趨於差,所以如果只覆蓋低版本的機型,可能在高版本上表現的效能會不盡如人意。
除了使用instruments,還可以使用log等方式進行查詢效能瓶頸。
對於線上的App,查詢效能瓶頸的工具主要是Application Performance Management(APM)。目前各大公司基本都有自己的APM,主要是對線上App進行效能監控以及預警。因為在開發和測試階段,由於使用人數相對比較有限,很難覆蓋所有的業務場景。而在App釋出出去後,大量使用者在使用過程中的效能表現,會給我們的App帶來更全面的效能評測資料,因此線上App的效能監控是十分重要的。
-
-
要抓重點,有的放矢。找到效能損耗大的前N個問題,依重要程度和解決的難易程度解決,這樣才能花最少的精力,解決最大問題。
三、要在不同效能指標間權衡,達到總體最優
在已經找到效能瓶頸的時候,解決效能問題的方法則需要具體問題具體分析,要在不同效能指標間權衡,以達到總體最優。
這需要我們要有整體的意識,App的效能可以分為很多類,不同的效能指標對使用者體驗造成的影響也不盡相同,比如fps主要影響的是使用者的滑動體驗,頁面載入時間和應用啟動時間影響的是使用者等待時間上的體驗。我們在優化的過程中,要牢記我們的目標是希望App的整體體驗最優,而不是某一單項的效能指標最優。不同優化指標之間可能是呈正相關,比如優化了滑動過程中大量函式的耗時時間,使得fps效能提升,可能會導致App的耗電量變少。同時,不同優化指標也可能是負相關、相互制約的,比如為了流暢性做了過多的cache,會導致記憶體效能下降,甚至導致因為memory warning導致被系統kill掉,這無疑對App的整體體驗造成了負面的影響。因此實際優化過程中需要我們反覆權衡利弊和取捨,達到整體的效能最優。
四、要理解優化任務的底層執行機制
如果不理解優化任務的底層執行機制,可能很難達到更好的優化效果。
比如在做啟動時間優化的時候,如果你不知道iOS中App的啟動時間是由main之前和main之後兩部分時間組成的,此時如果你的App是因為main函式之前的部分佔用了過多的啟動時間,可能你花了大量的精力去優化main之後的時間卻沒有達到好的優化效果。如果你不知道App啟動過程的執行機制,就無法知道去檢查是否連結了過多的自定義的動態庫或者去load函式裡面確認是否有耗時的操作等等。還有在做fps優化的時候,如果不瞭解卡頓的底層原因是什麼、一個view從建立到顯示過程中經歷那些步驟、CPU和GPU在這個過程中都扮演什麼角色,則很難做到絲滑般的順暢?還有在做記憶體優化的時候,如果不瞭解記憶體分為哪幾類、系統對App和不同型別extension的記憶體限制機制的不同、超過限制系統會採取什麼操作等等,也很難把記憶體優化做好。因此只有深入瞭解底層機制才能更好的有針對性的提出更優的解決方案。
其實不只是在效能優化方面,在開發過程中很多情況下,瞭解底層的原理會讓你變得更高效,更容易解決遇到的各種問題。在這裡分享一個我印象比較深的一次經歷。記得有一次在開發某個功能的時候,需要用到level db資料庫,在開發過程中做單元測試的時候發現,level db的本地儲存檔案在不斷刪除和寫入的過程中,越變越大,甚至達到1G大小。當時第一印象以為是在使用上出了問題,所以上層業務邏輯上查詢問題,結果查了很久都沒有找到問題。但如果我在使用level db的時候去多瞭解一下其底層的實現原理,瞭解LSM(Log-Structured-Merge Tree)的原理,遇到這個問題的時候就不會認為這是一個bug,也不會浪費了大把的時間來做無用功。所以建議大家不要抱怨每天的工作過於簡單枯燥,在開發過程中多去挖掘一些深層次的東西,不但讓你的技術的深度不斷加深,也會對你的編碼效率有很大的提升。
五、要有技術保障體系
效能優化不能一勞永逸,我個人覺得更是一場持久戰。不僅需要你能夠在某個特定時期做專項優化的攻堅戰,還要為打好持久戰做出好的後勤補給,為了能使App長期保持好的效能,不僅僅需要開發人員有良好的開發技能,還需要有一些技術保障和體系。下面簡單羅列我能想到的幾個方面。
-
要有好的測試保障,這裡的測試保障不僅僅指的是測試人員的手動測試,更需要的自動化測試。要建立針對不同效能指標的專項自動化測試,建立一套從定時執行測試到測試結果的輸出等一套完整的自動化測試體系,能夠為效能的保證提供堅實的資料支撐。
-
引入關鍵效能指標上線准入制度。在開發階段,為了不將有效能問題的程式碼帶到線上,可以將比如啟動時間、FPS、安裝包大小等指標作為關鍵指標,上線前進行自動化測試,如指標不達標,不允許上線。
-
對於線上App使用APM進行監控,發現線上的效能問題,有及時的預警機制,能夠隨時解決線上的效能問題。
-
開發過程中,程式碼中需要有對效能保障的設計。 比如可以設計可複用的高效能控制元件,這樣其他開發人員在開發類似功能時,可以簡單複用,不僅提升了效能,而且大大節省了開發的時間。還有比如為了防止App隨著版本迭代導致啟動時載入的服務越來約多,導致啟動變慢,可以設計App啟動器,把這些任務統一放到主介面載入完成後再執行,並且在組內開發人員中形成硬性的規範,但凡啟動時期不必須的服務,要麼不要執行,要麼統一放到啟動器的主介面載入完成的回撥中執行。
-
定期做一些效能優化方面的技術分享,不僅僅可以提高組內同學的開發技能,還可以活躍組內的技術氣氛。
以上我對iOS效能優化的總體原則做的總結,希望能夠對大家有一點點啟示。其中可能很多想法並不成熟,也希望大家能夠多多批評互相探討,共同進步。