iOS-性能優化深刻探究

iOS-性能優化深刻探究

deeply-ios-performance-optimization02

上圖是幾種時間複雜度的關係,性能優化必定程度上是爲了下降程序執行效率減低時間複雜度。 以下是幾種時間複雜度的實例:node

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 時,會將重複元素合併爲一個,而且失去排序。ios

NSDictionary / NSMutableDictionary

和 Set 同樣均可以使用 hash table ,多了鍵值對應。添加和刪除元素都是 O(1)。objective-c

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(&map, (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

舉個會形成線程爆炸和死鎖的例子:swift

for (int i = 0, i < 999; i++) {
    dispatch_async(q,^{...});
}
dispatch_barrier_sync(q,^{...});
複製代碼

deeply-ios-performance-optimization05

如何避免上述的的線程爆炸和死鎖呢? 首先使用 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

deeply-ios-performance-optimization06

爲什麼使用 NSCache 而不適應 NSMutableDictionary 呢?相交字典 NSCache 有如下優勢:性能優化

  • 自動清理系統所佔內存(在接收到內存警告⚠️時)
  • 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. 複用機制

UICollectionViewUITableView 會使用到 代碼複用的機制,在所展現的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, 能夠提升繪圖效率。 在靜態視圖做用可能不明顯,但在 UITableVeiwUICollectionView 這種滾動 的 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. 合理選擇 imageNamedimageWithContentsOfFile

  • 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)
}

做者:知識小集
連接:https://juejin.im/post/5b396fece51d4558a3055131
來源:掘金
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
複製代碼

詳細關於二者的分析可參照筆者的另一篇博客:iOS-UIImage imageWithContentsOfFile 和 imageName 對比

21. 合理進行線程分配

GCD 很輕易的能夠開闢一個異步線程(不會100%開闢新線程),若不加以控制,會致使開闢的子線程愈來愈多浪費內存。而且在多線程狀況下由於網絡時序會形成數據處理錯亂,因此能夠:

  • UI 操做和 DataSource 操做在主線程
  • DB 操做,日誌記錄,網絡回調在各自固定線程
  • 不一樣業務,經過使用隊列保持數據一致性。

22. 預處理和延時加載

預處理:初次展現須要消耗大量內存的數據需提早在後臺線程處理完畢,須要時將處理好的數據進行展示 延時加載:提早加載下級界面的數據內容。舉個栗子:相似抖音視頻滑動,在播放當前視頻的時候就提早將下個視頻的數據加載好,等滑到下個視頻時直接進行展現!

23. 在合適的時機使用 CALayer 替代 UIView

若視圖無需和用戶交互,相似繪製線條,單純展現一張圖片,能夠將圖片對象賦值給 layer 的 content 屬性,以提升性能。 可是不能濫用,不然會形成代碼難以維護的惡果。

以上。

參考:

深刻剖析 iOS 性能優化

相關文章
相關標籤/搜索