iOS-效能優化深入探究

極客學偉發表於2018-07-03

iOS-效能優化深入探究

deeply-ios-performance-optimization02

上圖是幾種時間複雜度的關係,效能優化一定程度上是為了降低程式執行效率減低時間複雜度。 如下是幾種時間複雜度的例項:

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

舉個會造成執行緒爆炸和死鎖的例子:

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 屬性,以提高效能。 但是不能濫用,否則會造成程式碼難以維護的惡果。

以上。

相關文章