iOS性能調(diào)優(yōu)總結(jié)
性能調(diào)優(yōu)的方式:
1、通過專門的性能調(diào)優(yōu)工具
2、通過優(yōu)化代碼
1. 性能調(diào)優(yōu)工具:
1.1 靜態(tài)分析工具Analyze
相信IOS開發(fā)者在App進行Build或Archive時,會產(chǎn)生很多編譯警告,這些警告是編譯時產(chǎn)生的,靜態(tài)分析的過程也類似,在XCode Product菜單下,點擊Analyze對App進行靜態(tài)分析。
Analyze主要分析以下四種問題:
1. 邏輯錯誤:訪問空指針或未初始化的變量等;
2. 內(nèi)存管理錯誤:如內(nèi)存泄漏等;
3. 聲明錯誤:從未使用過的變量;
4. Api調(diào)用錯誤:未包含使用的庫和框架。
1.2 內(nèi)存泄漏分析工具–Leaks
點擊XCode的Product菜單Profile啟動Instruments,使用Leaks開始動態(tài)分析。
選擇Leaks,會自動啟動Leaks工具和IOS模擬器,Leaks啟動后會開始錄制,隨著對模擬器運行的App的操作,可以在Leaks中查看內(nèi)存占用的情況。
注意:其中黑色頭像代表了最可能的位置
注:如果你的項目使用了ARC,隨著你的操作,不斷開啟或關(guān)閉視圖,內(nèi)存可能持續(xù)上升,但這不一定表示存在內(nèi)存泄漏,ARC釋放的時機是不固定的。
開啟ARC后,內(nèi)存泄漏的原因:
開啟了ARC并不是就不會存在內(nèi)存問題,蘋果有句名言:ARC is only for NSObject。
在IOS 中使用malloc分配的內(nèi)存,ARC是不會處理的,需要自己進行處理。
例子中的 CGImageRef 也是一個Image的指針,ARC也不會進行處理。
1.3 不合理內(nèi)存分析工具–Allocation
關(guān)于內(nèi)存的問題,除了內(nèi)存泄漏以外,還可能存在內(nèi)存不合理使用的情況,也會導(dǎo)致IOS內(nèi)存警告。
內(nèi)存的不合理使用往往比內(nèi)存泄漏更難發(fā)現(xiàn),內(nèi)存泄漏可以更多借助于工具的判斷,
而內(nèi)存的不合理運用更多需要開發(fā)者結(jié)合代碼、架構(gòu)來進行分析。
明確說明一下兩者的區(qū)別:
- 內(nèi)存泄漏:是指內(nèi)存被分配了,但程序中已經(jīng)沒有指向該內(nèi)存的指針,導(dǎo)致該內(nèi)存無法被釋放,產(chǎn)生內(nèi)存泄漏。
- 內(nèi)存不合理運用:蘋果官方稱這種情況為Abandoned Memory,也就是存在已分配內(nèi)存的引用,但實際上程序中不會使用,比如圖片等對象加入了緩存,但緩存中的對象一直沒有被使用。
- XCode提供的Instruments中的Allocation工具可以用來幫你了解內(nèi)存的分配情況,當你的App收到內(nèi)存警告時,首先應(yīng)該用Allocation進行內(nèi)存分析,了解哪些對象占用了太多內(nèi)存。
1.4 干掉僵尸對象 Zombies
僵尸對象,也就是我們會遇到的EXC_BAD_ACCESS錯誤,由于內(nèi)存已經(jīng)被釋放,而這個對象仍舊保留這那個壞地址而導(dǎo)致的。
在MRC的開發(fā)中,這個錯誤比較常見,ARC下面在使用到C++的代碼也會遇到。
不過這個工具比較簡單,遇到這類錯誤,打開這個位于Instruments下的工具,直接就能幫你定位到,這里就不贅述了。
【重點】1.5 性能提升工具 Time Profile
既然是性能調(diào)優(yōu),那么怎么提升代碼的運行效率其實才是我們程序員最直接的訴求,而這個工具可以輔助我們辦到這件事。
Time Profiler分析原理:
它按照固定的時間間隔來跟蹤每一個線程的堆棧信息,通過統(tǒng)計比較時間間隔之間的堆棧狀態(tài),來推算某個方法執(zhí)行了多久,
并獲得一個近似值。它將各個方法消耗的時間統(tǒng)計起來,形成了我們直接定位需要進行優(yōu)化的代碼的好幫手。
選擇Time Profiler工具開始測試,這時會自動啟動模擬器和Time Profiler錄制。
先進行一些App的操作,讓Time Profiler收集足夠的數(shù)據(jù),尤其是你覺得那些有性能瓶頸的地方。
需要關(guān)注的:顯示的堆棧查看;看到cpu運行的時間都消耗在哪里;通過對應(yīng)用的操作,可以在詳細面板中看到那些最耗時的操作是哪些圖標為黑色頭像的就是Time Profiler給我們的提示,有可能存在性能瓶頸的地方,可以逐漸向下展開,找到產(chǎn)生的根本原因。
Time Profiler參數(shù)設(shè)置
這里邊幾個選項的含義如下:
-
Separate by Thread:
每個線程應(yīng)該分開考慮。只有這樣你才能揪出那些大量占用CPU的”重”線程
-
Invert Call Tree:
從上倒下跟蹤堆棧,這意味著你看到的表中的方法,將已從第0幀開始取樣,這通常你是想要的,只有這樣你才能看到CPU中耗費時間最深的方法.也就是說FuncA{FunB{FunC}} 勾選此項后堆棧以C->B-A 把調(diào)用層級最深的C顯示在最外面
-
Hide Missing Symbols:
如果dSYM無法找到你的app或者系統(tǒng)框架的話,那么表中看不到方法名只能看到十六進制的數(shù)值,如果勾線此項可以隱藏這些符號,便于簡化數(shù)據(jù)
-
Hide System Libraries:
勾選此項你會顯示你app的代碼,這是非常有用的. 因為通常你只關(guān)心cpu花在自己代碼上的時間不是系統(tǒng)上的
-
Show Obj-C Only:
只顯示oc代碼,如果你的程序是像OpenGl這樣的程序,不要勾選側(cè)向因為他有可能是C++的 Flatten Recursion: 遞歸函數(shù), 每個堆棧跟蹤一個條目 Top Functions: 一個函數(shù)花費的時間直接在該函數(shù)中的總和,以及在函數(shù)調(diào)用該函數(shù)所花費的時間的總時間。因此,如果函數(shù)A調(diào)用B,那么A的時間報告在A花費的時間加上B花費的時間,這非常真有用,因為它可以讓你每次下到調(diào)用堆棧時挑最大的時間數(shù)字,歸零在你最耗時的方法。
上面的參數(shù)在實踐中合理設(shè)置,也沒有什么太多技巧,就是通過數(shù)據(jù)的隱藏、顯示讓我們更關(guān)注于想找到的數(shù)據(jù)。
2. 性能調(diào)優(yōu)的代碼優(yōu)化:
下面介紹一下在開發(fā)中可以直接進行的代碼優(yōu)化的方面。
2.1 views設(shè)置為不透明(opaque=yes)
(opaque)這個屬性給渲染系統(tǒng)提供了一個如何處理這個view的提示。如果設(shè)為YES, 渲染系統(tǒng)就認為這個view是完全不透明的,這使得渲染系統(tǒng)優(yōu)化一些渲染過程和提高性能。
如果設(shè)置為NO,渲染系統(tǒng)正常地和其它內(nèi)容組成這個View。默認值是YES。如果這個屬性為NO,GPU會利用圖層顏色合成公式去合成真正的色值,這在ScrollView或者動畫中是很消耗性能的。
2.2 不要阻塞主線程
永遠不要使主線程承擔過多。因為UIKit在主線程上做所有工作,渲染,管理觸摸反應(yīng),回應(yīng)輸入等都需要在它上面完成。
一直使用主線程的風險就是如果你的代碼真的block了主線程,你的app會失去反應(yīng)。
大部分阻礙主進程的情形是你的app在做一些牽涉到讀寫外部資源的I/O操作,比如存儲或者網(wǎng)絡(luò)。
通常建議這些操作都使用GCD的方式直接異步執(zhí)行,并將UI相關(guān)操作在主線程進行回調(diào),像這樣:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 切換到全局隊列,異步執(zhí)行耗時操作 dispatch_async(dispatch_get_main_queue(), ^{
// 切換到主線程,更新你的UI。 }); });
2.3 提前調(diào)整ImageView中的圖片大小(同圖片和動畫的渲染)
如果要在UIImageView中顯示一個圖片,你應(yīng)保證圖片的大小和UIImageView的大小相同。
因為在運行中縮放圖片是很耗費資源的,特別是UIImageView嵌套在UIScrollView中的情況下。
如果圖片是從遠端服務(wù)加載的你不能控制圖片大小,比如在下載前調(diào)整到合適大小的話,你可以在下載完成后,最好是用background thread,縮放一次,然后在UIImageView中使用縮放后的圖片。
這個類比到圖片和動畫的渲染中,是通用的。
具體方法參考上面的GCD操作。
2.4 正確使用容器的特性
Arrays: 有序的一組值。使用index來查找很快,使用value 查找很慢, 插入/刪除很慢。
Dictionaries: 存儲鍵值對。 用鍵來查找比較快。
Sets: 無序的一組值。用值來查找很快,插入/刪除很快。
2.5 大文件傳輸使用gzip
大量app依賴于遠端資源和第三方API,你可能會開發(fā)一個需要從遠端下載XML, JSON, HTML或者其它格式的app。
問題是我們的目標是移動設(shè)備,因此你就不能指望網(wǎng)絡(luò)狀況有多好。一個用戶現(xiàn)在還在edge網(wǎng)絡(luò),下一分鐘可能就切換到了3G。不論什么場景,你肯定不想讓你的用戶等太長時間。
減小文檔的一個方式就是在服務(wù)端和你的app中打開gzip。這對于文字這種能有更高壓縮率的數(shù)據(jù)來說會有更顯著的效用。
當然,現(xiàn)在蘋果已經(jīng)自動支持了,你只需要告訴你們服務(wù)端的同學(xué),傳輸大文件的時候記得用gzip就完了。
2.6 View的重用和懶加載
更多的view意味著更多的渲染,也就是更多的CPU和內(nèi)存消耗,對于那種嵌套了很多view在UIScrollView里邊的app更是如此。
重用就是模仿UITableView和UICollectionView的操作: 不要一次創(chuàng)建所有的subview,而是當需要時才創(chuàng)建,當它們完成了使命,把他們放進一個可重用的隊列中。
當需要使用View的時候,去可重用隊列里面找一找有沒有可以被復(fù)用的View。
這里我的一份框架中曾經(jīng)使用過類似的方法去創(chuàng)建一個圖片瀏覽器,大家可以稍做參考。View的重用
懶加載就是在程序啟動時并不進行加載,只有當用到這個對象的時候,才進行加載。
這個不僅在屬性中可以進行這樣的使用,在View上面也是一樣,不過實現(xiàn)稍有不同。
懶加載會消耗更少內(nèi)存,但是在View的顯示上會稍有滯后。
2.7 Cache
一個極好的原則就是,緩存所需要的,也就是那些不大可能改變但是需要經(jīng)常讀取的東西。
我們能緩存些什么呢?一些選項是,遠端服務(wù)器的響應(yīng),圖片,甚至計算結(jié)果,比如UITableView的行高。
NSURLConnection默認會緩存資源在內(nèi)存或者存儲中根據(jù)它所加載的HTTP Headers。你甚至可以手動創(chuàng)建一個NSURLRequest然后使它只加載緩存的值。
下面是一個可用的代碼段,你可以可以用它去為一個基本不會改變的圖片創(chuàng)建一個NSURLRequest并緩存它:
+ (NSMutableURLRequest *)imageRequestWithURL:(NSURL *)url {
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;
// this will make sure the request always returns the cached image
request.HTTPShouldHandleCookies = NO;
request.HTTPShouldUsePipelining = YES;
[request addValue:@"image/*" forHTTPHeaderField:@"Accept"];
return request;
}
注意你可以通過 NSURLConnection 獲取一個URL request, AFNetworking也一樣的。這樣你就不必為采用這條tip而改變所有的networking代碼了。
如果你需要緩存其它不是HTTP Request的東西,你可以用NSCache。
2.8 記得處理內(nèi)存警告
一旦系統(tǒng)內(nèi)存過低,iOS會通知所有運行中app。在官方文檔中是這樣記述:
- 如果你的app收到了內(nèi)存警告,它就需要盡可能釋放更多的內(nèi)存。最佳方式是移除對緩存,圖片object和其他一些可以重創(chuàng)建的objects的strong references.
幸運的是,UIKit提供了幾種收集低內(nèi)存警告的方法:
在app delegate中使用applicationDidReceiveMemoryWarning:的方法在你的
自定義UIViewController的子類(subclass)中覆蓋didReceiveMemoryWarning 注冊并接收
UIApplicationDidReceiveMemoryWarningNotification 的通知
一旦收到這類通知,你就需要釋放任何不必要的內(nèi)存使用。
例如,UIViewController的默認行為是移除一些不可見的view, 它的一些子類則可以補充這個方法,刪掉一些額外的數(shù)據(jù)結(jié)構(gòu)。一個有圖片緩存的app可以移除不在屏幕上顯示的圖片。
這樣對內(nèi)存警報的處理是很必要的,若不重視,你的app就可能被系統(tǒng)殺掉。
然而,當你一定要確認你所選擇的object是可以被重現(xiàn)創(chuàng)建的來釋放內(nèi)存。一定要在開發(fā)中用模擬器中的內(nèi)存提醒模擬去測試一下。
2.9 重用大的開銷對象
這里的大開銷是指一些初始化很慢的objects,如:NSDateFormatter和NSCalendar。但是,你又不可避免地需要使用它們,比如從JSON或者XML中解析數(shù)據(jù)。
想要避免使用這個對象的瓶頸你就需要重用他們,可以通過添加屬性到你的class里或者創(chuàng)建靜態(tài)變量來實現(xiàn)。
注意如果你要選擇第二種方法,對象會在你的app運行時一直存在于內(nèi)存中,和單例(singleton)很相似。
下面的代碼說明了使用一個屬性來延遲加載一個date formatter. 第一次調(diào)用時它會創(chuàng)建一個新的實例,以后的調(diào)用則將返回已經(jīng)創(chuàng)建的實例:
// in your .h or inside a class extension
@property (nonatomic, strong) NSDateFormatter *formatter;
// inside the implementation (.m)
// When you need, just use self.formatter -
(NSDateFormatter *)formatter {
if (! _formatter) {
_formatter = [[NSDateFormatter alloc] init];
_formatter.dateFormat = @"EEE MMM dd HH:mm:ss Z yyyy";// twitter date format
}
return _formatter;
}
還需要注意的是,其實設(shè)置一個NSDateFormatter的速度差不多是和創(chuàng)建新的一樣慢的!所以如果你的app需要經(jīng)常進行日期格式處理的話,你會從這個方法中得到不小的性能提升。
2.10 避免反復(fù)的處理數(shù)據(jù)
許多應(yīng)用需要從服務(wù)器加載功能所需的常為JSON或者XML格式的數(shù)據(jù)。在服務(wù)器端和客戶端使用相同的數(shù)據(jù)結(jié)構(gòu)很重要。在內(nèi)存中操作數(shù)據(jù)使它們滿足你的數(shù)據(jù)結(jié)構(gòu)是開銷很大的。
比如你需要數(shù)據(jù)來展示一個tableview,最好直接從服務(wù)器取array結(jié)構(gòu)的數(shù)據(jù)以避免額外的中間數(shù)據(jù)結(jié)構(gòu)改變。
類似的,如果需要從特定key中取數(shù)據(jù),那么就使用鍵值對的dictionary。
2.11 正確設(shè)定背景圖片
在View里放背景圖片就像很多其它iOS編程一樣有很多方法:
- 使用UIColor的 colorWithPatternImage來設(shè)置背景色;
- 在view中添加一個UIImageView作為一個子View。
- 如果你使用全畫幅的背景圖,你就必須使用UIImageView因為UIColor的colorWithPatternImage是用來創(chuàng)建小的重復(fù)的圖片作為背景的。這種情形下使用UIImageView可以節(jié)約不少的內(nèi)存:
// You could also achieve the same result in Interface Builder
UIImageView *backgroundView = [ [UIImageView alloc] initWithImage:[UIImage imageNamed:@"background"]];
[self.view addSubview:backgroundView];
- 如果你用小圖平鋪來創(chuàng)建背景,你就需要用UIColor的colorWithPatternImage來做了,它會更快地渲染也不會花費很多內(nèi)存:
self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"background"]];
2.12 試試蘋果最新的WKWebView來處理web
UIWebView很有用,用它來展示網(wǎng)頁內(nèi)容或者創(chuàng)建UIKit很難做到的動畫效果是很簡單的一件事。
但是你可能有注意到UIWebView并不像驅(qū)動Safari的那么快。這是由于以JIT compilation 為特色的Webkit的Nitro Engine的限制。
所以想要更高的性能你就要調(diào)整下你的HTML了。
第一件要做的事就是盡可能移除不必要的javascript,避免使用過大的框架。能只用原生js就更好了。另外,盡可能異步加載例如用戶行為統(tǒng)計script這種不影響頁面表達的javascript。最后,永遠要注意你使用的圖片,保證圖片的符合你使用的大小。使用Sprite sheet提高加載速度和節(jié)約內(nèi)存。
當然,上面是針對你在使用UIWebView的情況下,需要盡量減少使用web的特性,而蘋果最近剛推出的Safari的底層框架WKWebView也許能幫我們規(guī)避掉很多這樣的性能問題。
2.13 優(yōu)化你的TableView
Table view需要有很好的滾動性能,不然用戶會在滾動過程中發(fā)現(xiàn)動畫的瑕疵。
為了保證tableview平滑滾動,確保你采取了以下的措施:
正確使用reuseIdentifier來重用cells
盡量使所有的view opaque,包括cell自身
避免漸變,圖片縮放,后臺選人 緩存行高
如果cell內(nèi)現(xiàn)實的內(nèi)容來自web,使用異步加載,
緩存請求結(jié)果 使用shadowPath來畫陰影
減少subviews的數(shù)量
盡量不適用cellForRowAtIndexPath:,如果你需要用到它,只用一次然后緩存結(jié)果
使用正確的數(shù)據(jù)結(jié)構(gòu)來存儲數(shù)據(jù) 使用rowHeight, sectionFooterHeight 和 sectionHeaderHeight來設(shè)定固定的高,不要請求delegate
2.14 選擇正確的數(shù)據(jù)存儲方式
存儲方式:
使用NSUerDefaults,使用XML, JSON,或者 plist 使用NSCoding存檔,使用類似SQLite的本地SQL數(shù)據(jù)庫 使用 Core Data
-
NSUserDefaults的問題是什么?
雖然它很nice也很便捷,但是它只適用于小數(shù)據(jù),比如一些簡單的布爾型的設(shè)置選項,再大點你就要考慮其它方式了
-
XML這種結(jié)構(gòu)化檔案呢?
總體來說,你需要讀取整個文件到內(nèi)存里去解析,這樣是很不經(jīng)濟的。使用SAX又是一個很麻煩的事情。
NSCoding?不幸的是,它也需要讀寫文件,所以也有以上問題。 -
使用SQLite 或者CoreData比較好
使用這些技術(shù)你用特定的查詢語句就能只加載你需要的對象。
在性能層面來講,SQLite和Core Data是很相似的。他們的不同在于具體使用方法。Core Data代表一個對象的graphmodel,但SQLite就是一個DBMS。
Apple在一般情況下建議使用Core Data,但是如果你有理由不使用它,那么就去使用更加底層的SQLite吧。
如果你使用SQLite,你可以用FMDB(https://github.com/ccgus/fmdb)這個庫來簡化SQLite的操作,這樣你就不用花很多經(jīng)歷了解SQLite的C API了。
2.15 把Xib換成Storyboard吧
當你加載一個XIB的時候所有內(nèi)容都被放在了內(nèi)存里,包括任何圖片。如果有一個不會即刻用到的view,你這就是在浪費寶貴的內(nèi)存資源了。
Storyboards就是另一碼事兒了,storyboard僅在需要時實例化一個view controller.
當加載XIB時,所有圖片都被緩存,如果你在做OS X開發(fā)的話,聲音文件也是。Apple在相關(guān)文檔中的記述是:
當你加載一個引用了圖片或者聲音資源的nib時,nib加載代碼會把圖片和聲音文件寫進內(nèi)存。
在OS X中,圖片和聲音資源被緩存在named cache中以便將來用到時獲取。在iOS中,
僅圖片資源會被存進named caches。取決于你所在的平臺,使用NSImage 或UIImage 的`imageNamed:`方法來獲取圖片資源。
很明顯,同樣的事情也發(fā)生在storyboards中,但我并沒有找到任何支持這個結(jié)論的文檔。另外,快速打開app是很重要的,特別是用戶第一次打開它時,對app來講,第一印象太太太重要了。你能做的就是使它盡可能做更多的異步任務(wù),比如加載遠端或者數(shù)據(jù)庫數(shù)據(jù),解析數(shù)據(jù)。
還是那句話,避免過于龐大的XIB,因為他們是在主線程上加載的。所以盡量使用沒有這個問題的Storyboards吧!注意,用Xcodedebug時watchdog并不運行,一定要把設(shè)備從Xcode斷開來測試啟動速度
2.16 學(xué)會手動創(chuàng)建Autorelease Pool
NSAutoreleasePool負責釋放block中的autoreleased objects。一般情況下它會自動被UIKit調(diào)用。但是有些狀況下你也需要手動去創(chuàng)建它。
假如你創(chuàng)建很多臨時對象,你會發(fā)現(xiàn)內(nèi)存一直在減少直到這些對象被release的時候。這是因為只有當UIKit用光了autorelease pool的時候memory才會被釋放。
好消息是你可以在你自己的@autoreleasepool里創(chuàng)建臨時的對象來避免這個行為:
NSArray *urls = [@"url1",@"url2"];
for (NSURL *url in urls){
@autoreleasepool{
NSError *error;
NSString *fileContents = [NSString stringWithContentsOfURL: url encoding: NSUTF8StringEncoding error: &error];
/* Process the string, creating and autoreleasing more objects. */
}
}
這段代碼在每次遍歷后釋放所有autorelease對象