iOS-性能優化深入探究
上圖是幾種時間復雜度的關系,性能優化一定程度上是為了降低程序執行效率減低時間復雜度。
如下是幾種時間復雜度的實例:
O(1)
return array[index] == value;
O(n)
for (int i = 0, i < n, i++) {
if (array[i] == value)
return YES;
}
O(n2)
/// 找數組中重復的值
for (int i = 0, i < n, i++) {
for (int j = 0, j < n, j++) {
if (i != j && array[i] == array[j]) {
return YES;
}
}
}
1. OC 中幾種常見集合對象接口方法的時間復雜度
NSArray / NSMutableArray
-
containsObject; indexOfObject; removeObject 均會遍歷元素查看是否匹配,復雜度等于或小于 O(n)
-
objectAtIndex;firstObject;lastObject; addObject; removeLastObject 這些只針對棧頂,棧底的操作時間復雜度都是 O(1)
-
indexOfObject:inSortedRange:options:usingComparator: 使用的是二分查找,時間復雜度是O(log n)
NSSet / NSMutableSet / NSCountedSet
集合類型是無序并且沒有重復元素的。這樣可以使用hash table 進行快速的操作。比如,addObject; removeObject; containsObject 都是按照 O(1) 來的。需要注意的是將數組轉成Set 時,會將重復元素合并為一個,并且失去排序。
NSDictionary / NSMutableDictionary
和 Set 一樣都可以使用 hash table ,多了鍵值對應。添加和刪除元素都是 O(1)。
containsObject 方法在數組和Set里的不同的實現
containsObject 在數組中的實現
///GUNSTEP NSArray indexOfObject: 方法的實現
- (BOOL)containsObject:(id)anObject {
return [self indexOfObject:anObject] != NSNotFound;
}
- (NSUInteger) indexOfObject: (id)anObject
{
unsigned c = [self count];
if (c > 0 && anObject != nil)
{
unsigned i;
IMP get = [self methodForSelector: oaiSel];
BOOL (*eq)(id, SEL, id)
= (BOOL (*)(id, SEL, id))[anObject methodForSelector: eqSel];
for (i = 0; i < c; i++)
if ((*eq)(anObject, eqSel, (*get)(self, oaiSel, i)) == YES)
return i;
}
return NSNotFound;
}
containsObject 在 Set 里的實現:
- (BOOL) containsObject: (id)anObject
{
return (([self member: anObject]) ? YES : NO);
}
//在 GSSet,m 里有對 member 的實現
- (id) member: (id)anObject
{
if (anObject != nil)
{
GSIMapNode node = GSIMapNodeForKey(?, (GSIMapKey)anObject);
if (node != 0)
{
return node->key.obj;
}
}
return nil;
}
復制代碼在數組中會遍歷所有元素查找到結果后返回,在Set中查找元素是通過鍵值的方式從map映射表中取出,因為S兒童里的元素是唯一的,所以可以hash元素對象作為key達到快速查找的目的。
2. 使用GCD進行性能優化
可以通過GCD提供的方法將一些耗時操作放到非主線程進行,使得App 能夠運行的更加流暢,響應更快,但是使用GCD 時需要注意避免可能引起的線程爆炸和死鎖的情況。在非主線程處理任務也不是萬能的,如果一個處理需要消耗大量內存或者大量CPU操作,GCD也不合適,需要將大任務拆分成不同的階段任務分時間進行處理。
避免線程爆炸的方法:
-
使用串行隊列
-
控制 NSOperationQueue 的并發數 - NSOperationQueue.maxConcurrentOperationCount
舉個會造成線程爆炸和死鎖的例子:
for (int i = 0, i < 999; i++) {
dispatch_async(q,^{...});
}
dispatch_barrier_sync(q,^{...});
如何避免上述的的線程爆炸和死鎖呢?
首先使用 dispatch_apply
dispatch_apply(999,q,^(size_t i){...});
復制代碼或者使用 dispatch_semaphore
#define CONCURRENT_TASKS 4
dispatch_queue_t q = dispatch_queue_create("com.qiuxuewei.gcd", nil);
dispatch_semaphore_t sema = dispatch_semaphore_create(CONCURRENT_TASKS);
for (int i = 0; i < 999; i++) {
dispatch_async(q, ^{
dispatch_semaphore_signal(sema);
});
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}
3. I/O 性能優化
I/O 操作是性能消耗大戶,任何的I/O操作都會使低功耗狀態被打破。所以減少 I/O 操作次數是性能優化關鍵。如下是優化的一些方法:
-
將零碎的內容作為一個整體進行寫入
-
使用合適的 I/O 操作 API
-
使用合適的線程
-
使用 NSCache 做緩存減少 I/O 次數
NSCache
為何使用 NSCache 而不適應 NSMutableDictionary 呢?相交字典 NSCache 有以下優點:
-
自動清理系統所占內存(在接收到內存警告:warning:時)
-
NSCache 是線程安全的
-
- (void)cache:(NSCache *)cache willEvictObject:(id)obj; 緩存對象在即將被清理時回調。
-
evictsObjectWithDiscardedContent 可以控制是否可被清理。
SDWebImage 在設置圖片時就使用 NSCache 進行了性能優化:
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
return [self.memCache objectForKey:key];
}
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key {
// 檢查 NSCache 里是否有
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
return image;
}
// 從磁盤里讀
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
return diskImage;
}
利用 NSCache 自動釋放內存的特點將圖片放到 NSCache 里,這樣在內存警告時會自動清理掉不常用的圖片,在讀取 Cache 里內容時,如果沒有被清理直接返回圖片數據,清理了會執行 I/O 從磁盤中讀取圖片,通過這種方式減少磁盤操作,空間也會更加有效的控制釋放。
4. 控制 App 的喚醒次數
通知,Voip, 定位,藍牙 等都會使設備從 Standby 狀態喚起。喚起這個過程會有比較大的消耗。應該避免頻繁發生。
以 定位 API 舉例:
連續的位置更新
[locationManager startUpdatingLocation]
這個方法會使設備一直處于活躍狀態。
延時有效定位
[locationManager allowDeferredLocationUpdatesUntilTraveled:<#(cllocationdistance)#> timeout:<#(nstimeinterval)#>]
高效節能的定位方式,數據會緩存在位置硬件上。適合跑步應用。
重大位置變化
[locationManager startMonitoringSignificantLocationChanges]
會更節能,對于那些只有在位置有很大變化的時候才需要回調的應用需要采用這種方式,比如天氣應用。
區域監測
[locationManager startMonitoringForRegion:<#(nonnull clregion="">]
也是一種節能的定位方式,比如在博物館內按照不同區域監測展示不同信息之類的應用。
頻繁定位
// start monitoring location
[locationManager startUpdatingLocation]
// Stop monitoring when no longer needed
[locationManager stopUpdatingLocation]
不要輕易使用 startUpdatingLocation() 除非萬不得已,盡快的使用 stopUpdatingLocation() 來結束定位還用戶一個節能設備。
5. 預防性能問題
堅持幾個編碼原則:
-
優化計算的復雜度從而減少CPU的使用
-
在應用響應交互的時候停止沒有必要的任務處理
-
設置合適的 Qos
-
將定時器任務合并,讓CPU更多時候處于 idle 狀態
6. 性能優化技巧篇
1. 復用機制
在 UICollectionView 和 UITableView 會使用到 代碼復用的機制,在所展示的item數量超過屏幕所容納的范圍時,只創建少量的條目(通常是屏幕最大容納量 + 1),通過復用來展示所有數據。這種機制不會為每一條數據都創建 Cell .增強效率和交互流暢性。
在iOS6以后,不僅可以復用cell,也可以復用每個section 的 header 和 footer。
在復用UITableView 會用到的 API:
// 復用 Cell:
- [UITableView dequeueReusableCellWithIdentifier:];
- [UITableView registerNib:forCellReuseIdentifier:];
- [UITableView registerClass:forCellReuseIdentifier:];
- [UITableView dequeueReusableCellWithIdentifier:forIndexPath:];
// 復用 Section 的 Header/Footer:
- [UITableView registerNib:forHeaderFooterViewReuseIdentifier:];
- [UITableView registerClass:forHeaderFooterViewReuseIdentifier:];
- [UITableView dequeueReusableHeaderFooterViewWithIdentifier:];
在使用代碼復用需要注意在設置Cell 屬性是,條件判斷需要覆蓋所有可能,避免因為復用導致數據錯誤的問題。例如在 cellForRowAtIndexPath: 方法內部:
if (indexPath %2 == 0) {
cell.backgroundColor = [UIColor redColor];
}else{
cell.backgroundColor = [UIColor clearColor];
}
2. 設置View為不透明
UIView 又一個 opaque 屬性, 在不需要透明效果的時候,應該盡量設置它為 YES, 可以提高繪圖效率。
在靜態視圖作用可能不明顯,但在 UITableVeiw 或 UICollectionView 這種滾動 的 Scroll View 或是一個復雜動畫中,透明效果對程序性能有較大的影響!
3. 避免使用臃腫的 Xib 文件
當加載一個 Xib 時,它所有的內容都會被加載,如歌這個 Xib 中有的View 你不會馬上用到,加載就是浪費資源。而加載 StoryBoard 時,并不會把所有的ViewController 都加載,只會按需加載。
4. 不要阻塞主線程
UIKit 會把它所有的工作放在主線程執行,比如:繪制界面,管理手勢,響應輸入等。當把所有代碼邏輯都放在主線程時,有可能因為耗時太長而卡住主線程造成程序無法響應,流暢性差等問題。所以一些 I/O 操作,網絡數據解析都需要異步在非主線程處理。
5. 使用尺寸匹配的UIImage
當從 App bundle 中加載圖片到 UIImageView 時,最好確保圖片的尺寸和 UIImageView 相對應。否則會使UIImageView 對圖片進行拉伸,這樣會影響性能。如果圖片時從網絡加載,需要手動進行 scale。在UIImageView 中使用resize 后的圖片
6. 選擇合適的容器
在使用 NSArray / NSDictionary / NSSet 時,了解他們的特點便于在合適的時機選擇他們。
-
Array:數組。有序的,通過 index 查找很快,通過 value 查找很慢,插入和刪除較慢。
-
Dictionary:字典。存儲鍵值對,通過鍵查找很快。
-
Set:集合。無序的,通過 value 查找很快,插入和刪除較快。
7. 啟用 GZIP 數據壓縮
在網絡請求的數據量較大時,可以將數據進行壓縮再進行傳輸。可以降低延遲,縮短網絡交互時間。
8. 懶加載視圖 / 視圖隱藏
展現視圖的兩種形式一種是懶加載,當用到的時候去創建并展現給用戶,另外一種提前分配內存創建出視圖,不用的時候將其隱藏,等用到的時候將其透明度變為1,兩種方案各有利弊。懶加載更合理的使用內存,視圖隱藏讓視圖的展現更迅速。在選擇時需要權衡兩者利弊做出最優選擇。
9. 緩存
開發需要秉承一個原則,對于一些更新頻率低,訪問頻率高的內容進行緩存,例如:
-
服務器響應數據
-
圖片
-
計算值 (UITableView 的 row height)
10. 處理 Memory Warning
處理 Memory Warning 的幾種方式:
-
在 AppDelegate 中實現 - [AppDelegate applicationDidReceiveMemoryWarning:] 代理方法。
-
在 UIViewController 中重載 didReceiveMemoryWarning 方法。
-
監聽 UIApplicationDidReceiveMemoryWarningNotification 通知。
當通過這些方式監聽到內存警告時,你需要馬上釋放掉不需要的內存從而避免程序被系統殺掉。
比如,在一個 UIViewController 中,你可以清除那些當前不顯示的 View,同時可以清除這些 View 對應的內存中的數據,而有圖片緩存機制的話也可以在這時候釋放掉不顯示在屏幕上的圖片資源。
但是需要注意的是,你這時清除的數據,必須是可以在重新獲取到的,否則可能因為必要數據為空,造成程序出錯。在開發的時候,可以使用 iOS Simulator 的 Simulate memory warning 的功能來測試你處理內存警告的代碼。
11. 復用高開銷對象
高開銷對象,顧名思義就是初始化很耗性能的對象。比如:NSDateFormatter , NSCalendar .為了避免頻繁創建,我們可以使用一個全局單例強引用著這個對象,保證整個App 的生命周期只被初始化一次。
// no property is required anymore. The following code goes inside the implementation (.m)
- (NSDateFormatter *)dateFormatter {
static NSDateFormatter *dateFormatter;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy-MM-dd a HH:mm:ss EEEE"];
});
return dateFormatter;
}
設置 NSDateFormatter 的 date format 跟創建一個新的 NSDateFormatter 對象一樣慢,因此當你的程序中要用到多種格式的 date format,而每種又會用到多次的時候,你可以嘗試為每種 date format 創建一個可復用的 NSDateFormatter 對象來提供程序的性能。
12. 選擇正確的網絡返回數據格式
通常用到的有兩種: JSON 和 XML。
JSON 優點:
-
能夠更快的被解析
-
在承載相同數據時,體積比XML更小,傳輸的數據量更小。
缺點:
-
需要整個JSON數據全部加載完成后才能開始解析
而XML的優缺點恰好相反。解析數據不需要全部讀取完才解析,可以變加載邊解析,這樣在處理大數據集時可以有效提高性能。 選擇哪種格式取決于應用場景。
13. 合理設置背景圖片
為一個View 設置背景圖,我們想到的方案有兩種
-
為視圖加一個 UIImageView 設置 UIImage 作為背景
-
通過 [UIColor colorWithPatternImage:<#(nonnull uiimage="">] 將一張圖轉化為 UIColor, 直接為 View 設置 backgroundColor。
兩種方案各有優缺點:若使用一個全尺寸圖片作為背景圖使用 UIImageView 會節省內存。
當你計劃采用一個小塊的模板樣式圖片,就像貼瓷磚那樣來重復填充整個背景時,你應該用 [UIColor colorWithPatternImage:<#(nonnull uiimage="">] 這個方法,因為這時它能夠繪制的更快,并且不會用到太多的內存。
14. 減少離屏渲染
離屏渲染:GPU在當前屏幕緩沖區以外新開辟一個緩沖區進行渲染操作。
離屏渲染需要多次切換上下文環境:先是從當前屏幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結束以后,將離屏緩沖區的渲染結果顯示到屏幕上又需要將上下文環境從離屏切換到當前屏幕,而上下文環境的切換是一項高開銷的動作。
設置如下屬性均會造成離屏渲染:
-
shouldRasterize(光柵化)
-
masks(遮罩)
-
shadows(陰影)
-
edge antialiasing(抗鋸齒)
-
group opacity(不透明)
-
復雜形狀設置圓角等
-
漸變
例如給一個View設置陰影,通常我們會使用這種方式:
imageView.layer.shadowOffset = CGSizeMake(5.0f, 5.0f);
imageView.layer.shadowRadius = 5.0f;
imageView.layer.shadowOpacity = 0.6;
復制代碼這種方式會觸發離屏渲染,造成不必要的內存開銷,我們完全可以使用如下方式代替:
imageView.layer.shadowPath = [[UIBezierPath bezierPathWithRect:CGRectMake(imageView.bounds.origin.x+5, imageView.bounds.origin.y+5, imageView.bounds.size.width, imageView.bounds.size.height)] CGPath];
imageView.layer.shadowOpacity = 0.6;
復制代碼不會造成離屏渲染。
15. 光柵化
CALayer 有一個屬性是 shouldRasterize 通過設置這個屬性為 YES 可以將圖層繪制到一個屏幕外的圖像,然后這個圖像將會被緩存起來并繪制到實際圖層的 contents 和子圖層,如果很很多的子圖層或者有復雜的效果應用,這樣做就會比重繪所有事務的所有幀來更加高效。但是光柵化原始圖像需要時間,而且會消耗額外的內存。
cell.layer.shouldRasterize = YES;
cell.layer.rasterizationScale = [[UIScreen mainScreen] scale];
復制代碼使用光柵化的一個前提是視圖不會頻繁變化,若一個頻繁變化的視圖,例如 排版多變,高度不同的 Cell, 光柵化的意義就不大了,反而造成必要的內存損耗。
16. 優化 UITableView
-
通過正確的設置 reuseIdentifier 來重用 Cell。
-
盡量減少不必要的透明 View。
-
盡量避免漸變效果、圖片拉伸和離屏渲染。
-
當不同的行的高度不一樣時,盡量緩存它們的高度值。
-
如果 Cell 展示的內容來自網絡,確保用異步加載的方式來獲取數據,并且緩存服務器的 response。
-
使用 shadowPath 來設置陰影效果。
-
盡量減少 subview 的數量,對于 subview 較多并且樣式多變的 Cell,可以考慮用異步繪制或重寫 drawRect。
-
盡量優化 - [UITableView tableView:cellForRowAtIndexPath:] 方法中的處理邏輯,如果確實要做一些處理,可以考慮做一次,緩存結果。
-
選擇合適的數據結構來承載數據,不同的數據結構對不同操作的開銷是存在差異的。
-
對于 rowHeight、sectionFooterHeight、sectionHeaderHeight 盡量使用常量。
17.選擇合適數據存儲方式
iOS 中數據存儲方案有以下幾種:
-
NSUserDefaults。只適合用來存小數據。
-
XML、JSON、Plist 等文件。JSON 和 XML 文件的差異在「選擇正確的數據格式」已經說過了。
-
使用 NSCoding 來存檔。NSCoding 同樣是對文件進行讀寫,所以它也會面臨必須加載整個文件才能繼續的問題。
-
使用 SQLite 數據庫。可以配合 FMDB 使用。數據的相對文件來說還是好處很多的,比如可以按需取數據、不用暴力查找等等。
-
使用 CoreData。 Apple 提供的對于SQLite 的封裝,性能不如使用原生 SQLite, 不推薦使用。
18. 減少應用啟動時間
在啟動時的一些網絡配置,數據庫配置,數據解析的工作放在異步線程進行。
19. 使用 Autorelease Pool
當需要在代碼中創建許多臨時對象時,你會發現內存消耗激增直到這些對象被釋放,一個問題是這些內存只會到 UIKit 銷毀了它對應的 Autorelease Pool 后才會被釋放,這就意味著這些內存不必要地會空占一些時間。這時候就是我們顯式的使用 Autorelease Pool 的時候了,一個示例如下:
//一個很大數組
NSArray *urls = <# an="" array="" of="" file="" urls="">;
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 Pool 會在每一次循環中釋放掉臨時對象,提高性能。
20. 合理選擇 imageNamed 和 imageWithContentsOfFile
-
imageNamed 會對圖片進行緩存,適合多次使用某張圖片
-
imageWithContentsOfFile 從bundle中加載圖片文件,不會進行緩存,適用于加載一張較大的并且只使用一次的圖片,例如引導圖等
今年的 WWDC 2018 Apple 向我們推薦了一種性能比較高的大圖加載方案:
func downsample(imageAt imageURL: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage {
let sourceOpt = [kCGImageSourceShouldCache : false] as CFDictionary
// 其他場景可以用createwithdata (data并未decode,所占內存沒那么大),
let source = CGImageSourceCreateWithURL(imageURL as CFURL, sourceOpt)!
let maxDimension = max(pointSize.width, pointSize.height) * scale
let downsampleOpt = [kCGImageSourceCreateThumbnailFromImageAlways : true,
kCGImageSourceShouldCacheImmediately : true ,
kCGImageSourceCreateThumbnailWithTransform : true,
kCGImageSourceThumbnailMaxPixelSize : maxDimension] as CFDictionary
let downsampleImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOpt)!
return UIImage(cgImage: downsampleImage)
}
詳細關于兩者的分析可參照筆者的另外一篇博客: iOS-UIImage imageWithContentsOfFile 和 imageName 對比
21. 合理進行線程分配
GCD 很輕易的可以開辟一個異步線程(不會100%開辟新線程),若不加以控制,會導致開辟的子線程越來越多浪費內存。并且在多線程情況下因為網絡時序會造成數據處理錯亂,所以可以:
-
UI 操作和 DataSource 操作在主線程
-
DB 操作,日志記錄,網絡回調在各自固定線程
-
不同業務,通過使用隊列保持數據一致性。
22. 預處理和延時加載
預處理:初次展示需要消耗大量內存的數據需提前在后臺線程處理完畢,需要時將處理好的數據進行展現
延時加載:提前加載下級界面的數據內容。舉個栗子:類似抖音視頻滑動,在播放當前視頻的時候就提前將下個視頻的數據加載好,等滑到下個視頻時直接進行展示!
23. 在合適的時機使用 CALayer 替代 UIView
若視圖無需和用戶交互,類似繪制線條,單純展示一張圖片,可以將圖片對象賦值給 layer 的 content 屬性,以提高性能。
但是不能濫用,否則會造成代碼難以維護的惡果。
來自:https://juejin.im/post/5b3b41385188251abe49f6f9