iOS開發之性能優化全解析

網上有不少優化iOS性能的文章,讀後受到了不少啓發,在這裏進行一下總結並加入一些本身的感悟以備後續回憶,若是恰巧在某個點能幫到後續閱讀的你就最好不過了^O(∩_∩)O哈哈~程序員

一、ARC內存的管理

在咱們進行視圖的優化前咱們首先要保證的就是內存的管理,如今咱們開發基本都是使用的ARC進行內存管理的,大部分狀況下咱們是不必刻意關心某個對象什麼時候釋放,ARC會自動爲你管理retain和release的過程。可是有幾個點仍是須要特別注意下的。web

1>循環引用

當兩個不一樣的對象各有一個強引用指向對方或者多個對象間造成強引用的閉環,那麼循環引用便產生了。
兩個對象循環引用緩存

咱們重點須要關注的幾類循環引用有:block、delegate、NSTime。安全

Block

在 ARC 中,在被拷貝的 block 中不管是直接引用 self 仍是經過引用 self 的成員變量間接引用 self,該 block 都會 retain self。
若是某類將block做爲本身的屬性變量如:微信

@property (nonatomic, copy) TestCircleBlock testCircleBlock;

那爲避免block中使用該類而形成循環引用,咱們能夠這樣:數據結構

__weak typeof(self) weakSelf = self;
 self.testObject.testCircleBlock = ^{
      __strong typeof (weakSelf) strongSelf = weakSelf;
      [strongSelf doSomething];
};
delegate

對於代理的的屬性定義,須要以弱引用來打破循環如:異步

@property (nonatomic, weak) id <TestDelegate> delegate;

假如咱們是寫的strong,那麼 兩個類之間調用代理就是這樣的啦oop

BViewController *bViewController = [[BViewController alloc] init];
bViewController.delegate = self; //假設 self 是AViewController
[self.navigationController pushViewController:bViewController animated:YES];

/**
 假如是 strong 的狀況
    bViewController.delegate ===> AViewController (也就是 A 的引用計數 + 1)
    AViewController 自己又是引用了 <BViewControllerDelegate> ===> delegate 引用計數 + 1
 致使: AViewController <======> Delegate ,也就循環引用啦
 */
NSTime

NSTimer 其實相對來講,咱們實際上是很容易忽略它這種狀況的,畢竟仍是很特殊的。
NSTime循環引用佈局

scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:

方法的最後一個參數爲YES時,NSTimer會保留目標對象,等到自身失效才釋放.執行完任務後,一次性的定時器會自動失效;重複性的定時器,須要主動調用invalidate方法纔會失效.
或許你想在dealloc方法中使定時器失效,那你就太天真了.此時定時器還保留着當前控制器,此方法是不可能調用的,所以會出現內存泄漏.
這裏採用block塊的方法爲NSTimer增長一個分類,具體細節看代碼(程序員最好的語言是代碼)。性能

//.h文件
#import <Foundation/Foundation.h>

@interface NSTimer (SGLUnRetain)
+ (NSTimer *)sgl_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval
                                        repeats:(BOOL)repeats
                                          block:(void(^)(NSTimer *timer))block;
@end

//.m文件
#import "NSTimer+SGLUnRetain.h"

@implementation NSTimer (SGLUnRetain)

+ (NSTimer *)sgl_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block{
    
    return [NSTimer scheduledTimerWithTimeInterval:inerval target:self selector:@selector(sgl_blcokInvoke:) userInfo:[block copy] repeats:repeats];
}

+ (void)sgl_blcokInvoke:(NSTimer *)timer {
    
    void (^block)(NSTimer *timer) = timer.userInfo;
    
    if (block) {
        block(timer);
    }
}
@end

//控制器.m

#import "ViewController.h"
#import "NSTimer+SGLUnRetain.h"

//定義了一個__weak的self_weak_變量
#define weakifySelf  \
__weak __typeof(&*self)weakSelf = self;

//局域定義了一個__strong的self指針指向self_weak
#define strongifySelf \
__strong __typeof(&*weakSelf)self = weakSelf;

@interface ViewController ()

@property(nonatomic, strong) NSTimer *timer;

@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    __block NSInteger i = 0;
    weakifySelf
    self.timer = [NSTimer sgl_scheduledTimerWithTimeInterval:0.1 repeats:YES block:^(NSTimer *timer) {
        strongifySelf
        [self p_doSomething];
        NSLog(@"----------------");
        if (i++ > 10) {
            [timer invalidate];
        }
    }];
}

- (void)p_doSomething {
    
}

- (void)dealloc {
      // 務必在當前線程調用invalidate方法,使得Runloop釋放對timer的強引用(具體請參閱官方文檔)
     [self.timer invalidate];
}
@end

在使用中,最須要注意的就是下面這段代碼:

weakifySelf
self.timer = [NSTimer sgl_scheduledTimerWithTimeInterval:0.1 repeats:YES block:^(NSTimer *timer) {
       strongifySelf
       [self p_doSomething];
       NSLog(@"----------------");
       if (i++ > 10) {
           [timer invalidate];
       }
   }];
2>修飾屬性變量類型

strong,weak,retain,assign,copy咱們常用到的類型的區別是什麼呢?如何正確使用減小程序出錯也是很關鍵的。

  • assign: 簡單賦值,不更改索引計數(Reference Counting)對基礎數據類
  • copy:內容拷貝,實際上是創建了一個相同的對象
  • retain:釋放舊的對象,將舊對象的值賦予輸入對象,再提升輸入對象的索引計數爲1
  • assign:簡單賦值,不更改索引計數(Reference Counting)對基礎數據類

weak 和 strong 屬性只有在你打開ARC時纔會被要求使用,這時你是不能使用retain release autorelease 操做的,由於ARC會自動爲你作好這些操做,可是你須要在對象屬性上使用weak 和strong,其中strong就至關於retain屬性,而weak至關於assign。

不過在聲明Block時,使用strong和retain會有大相徑庭的效果。strong會等於copy,而retain居然等於assign!固然定義Block最好仍是應該用copy。

以上是基本使用介紹,幾個須要特別注意的點大體以下:

weak和assign的區別
  • weak只能夠修飾對象。若是修飾基本數據類型,編譯器會報錯
  • weak 不會產生野指針問題。由於weak修飾的對象釋放後(引用計數器值爲0),指針會自動被置nil,以後再向該對象發消息也不會崩潰。 weak是安全的。
  • assign 可修飾對象,和基本數據類型
  • assign 若是修飾對象,會產生野指針問題;若是修飾基本數據類型則是安全的。修飾的對象釋放後,指針不會自動被置空,此時向對象發消息會崩潰。
assign 適用於基本數據類型如int,float,struct等值類型,不適用於引用類型。由於值類型會被放入棧中,遵循先進後出原則,由系統負責管理棧內存。而引用類型會被放入堆中,須要咱們本身手動管理內存或經過ARC管理。
weak 適用於delegate和block等引用類型,不會致使野指針問題,也不會循環引用,很是安全。
copy 關鍵字
  • NSString、NSArray、NSDictionary 等等常用copy關鍵字,是由於他們有對應的可變類型:NSMutableString、NSMutableArray、NSMutableDictionary;
  • block 也常用 copy 關鍵字
非ARC下不copy的Block會在棧中,ARC中的Block都會在堆上的
@property (copy) NSMutableArray *array;

特別注意如上寫法會出現問題,可變類型不能使用copy,由於 copy 就是複製一個不可變 NSArray 的對象。

二、界面的優化

首先分析一下界面卡頓出現的主要緣由:
原理圖
在 iOS 系統中,圖像內容展現到屏幕的過程須要 CPU 和 GPU 共同參與。CPU 負責計算顯示內容,好比視圖的建立、佈局計算、圖片解碼、文本繪製等。隨後 CPU 會將計算好的內容提交到 GPU 去,由 GPU 進行變換、合成、渲染。以後 GPU 會把渲染結果提交到幀緩衝區去,等待下一次 VSync 信號到來時顯示到屏幕上。因爲垂直同步的機制,若是在一個 VSync 時間內,CPU 或者 GPU 沒有完成內容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時顯示屏會保留以前的內容不變。這就是界面卡頓的緣由。

所以,咱們須要平衡 CPU 和 GPU 的負荷避免一方超負荷運算。爲了作到這一點,咱們首先得了解 CPU 和 GPU 各自負責哪些內容。
模塊層級
那明白這些原理後,優化程序要遵循的兩個基本原則:

  • 一、減小CPU和GPU負載
  • 二、均衡使用CPU和GPU
1>CPU 消耗型任務
佈局計算:

佈局計算是 iOS 中最爲常見的消耗 CPU 資源的地方,若是視圖層級關係比較複雜,計算出全部圖層的佈局信息就會消耗一部分時間。所以咱們應該儘可能提早計算好佈局信息,而後在合適的時機調整對應的屬性。還要避免沒必要要的更新,只在真正發生了佈局改變時再更新。

對象建立:

對象建立過程伴隨着內存分配、屬性設置、甚至還有讀取文件等操做,比較消耗 CPU 資源。儘可能用輕量的對象代替重量的對象,能夠對性能有所優化。好比 CALayer 比 UIView 要輕量許多,若是視圖元素不須要響應觸摸事件,用 CALayer 會更加合適。

一些objects的初始化很慢,好比NSDateFormatter和NSCalendar。然而,你又不可避免地須要使用它們,好比從JSON或者XML中解析數據。
想要避免使用這個對象的瓶頸你就須要重用他們,能夠經過添加屬性到你的class裏或者建立靜態變量來實現。
Autolayout:

對於複雜視圖來講經常會產生嚴重的性能問題

文本計算:

若是一個界面中包含大量文本(好比微博、微信朋友圈等),文本的寬高計算會佔用很大一部分資源,而且不可避免。

一個比較常見的場景是在 UITableView 中,heightForRowAtIndexPath這個方法會被頻繁調用,即便不是耗時的計算在調用次數多了以後也會帶來性能損耗。這裏的優化就是儘可能避免每次都從新進行文本的行高計算,能夠在獲取到 Model 數據後就根據文本內容計算好佈局信息,而後將這份佈局信息做爲一個屬性保存到對應的 Model 中,這樣在 UITableView 的回調中就能夠直接使用 Model 中的屬性,減小了文本的計算。

文本渲染:

屏幕上能看到的全部文本內容控件,包括 UIWebView,在底層都是經過 CoreText 排版、繪製爲 Bitmap 顯示的。常見的文本控件 (UILabel、UITextView 等),其排版和繪製都是在主線程進行的,當顯示大量文本時,CPU 的壓力會很是大。

圖像的繪製:

圖像的繪製一般是指用那些以 CG 開頭的方法把圖像繪製到畫布中,而後從畫布建立圖片並顯示的過程。前面的模塊圖裏介紹了 CoreGraphic 是做用在 CPU 之上的,所以調用 CG 開頭的方法消耗的是 CPU 資源。

2>GPU 消耗型任務
大量幾何結構:

全部的 Bitmap,包括圖片、文本、柵格化的內容,最終都要由內存提交到顯存,綁定爲 GPU Texture。不管是提交到顯存的過程,仍是 GPU 調整和渲染 Texture 的過程,都要消耗很多 GPU 資源。當在較短期顯示大量圖片時(好比 TableView 存在很是多的圖片而且快速滑動時),CPU 佔用率很低,GPU 佔用很是高,界面仍然會掉幀。避免這種狀況的方法只能是儘可能減小在短期內大量圖片的顯示,儘量將多張圖片合成爲一張進行顯示。

另外當圖片過大,超過 GPU 的最大紋理尺寸時,圖片須要先由 CPU 進行預處理,這對 CPU 和 GPU 都會帶來額外的資源消耗。

若是要在UIImageView中顯示一個來自bundle的圖片,你應保證圖片的大小和UIImageView的大小相同。在運行中縮放圖片是很耗費資源的,特別是UIImageView嵌套在UIScrollView中的狀況下。
若是圖片是從遠端服務加載的你不能控制圖片大小,好比在下載前調整到合適大小的話,你能夠在下載完成後,最好是用background thread,縮放一次,而後在UIImageView中使用縮放後的圖片。
視圖的混合:

當多個視圖(或者說 CALayer)重疊在一塊兒顯示時,GPU 會首先把他們混合到一塊兒。若是視圖結構過於複雜,混合的過程也會消耗不少 GPU 資源。爲了減輕這種狀況的 GPU 消耗,應用應當儘可能減小視圖數量和層次,而且減小沒必要要的透明視圖。

離屏渲染:

由以上能夠看出離屏渲染須要從新開闢新的緩存空間,一定要更加消耗資源。
經過查資料目前知道了設置瞭如下屬性時,都會觸發離屏繪製:
shouldRasterize(光柵化)
masks(遮罩)
shadows(陰影)
edge antialiasing(抗鋸齒)
group opacity(不透明)
複雜形狀設置圓角等
漸變

這裏會以圓角爲例講述下如何避免離屏渲染,具體以下:

單獨設置layer.cornerRadius = 10;是不會觸發離屏渲染的,若是再結合layer.masksToBounds = YES,便會觸發離屏渲染。避免離屏渲染我目前能想到的有兩種思路

  • 對控件關聯的主圖層進行裁剪
  • 爲控件添加一個遮罩(非layer.mask)

首先看第一種思路的代碼

- (void)bp_setRaidus:(CGFloat)radius view:(UIView *)view {
    UIImage *image = nil;
    UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, [UIScreen mainScreen].scale);
    CGContextRef currnetContext = UIGraphicsGetCurrentContext();
    [[UIBezierPath bezierPathWithRoundedRect:view.bounds cornerRadius:radius] addClip];
    [view.layer renderInContext:currnetContext];
    image = UIGraphicsGetImageFromCurrentImageContext();
    view.layer.contents = (__bridge id _Nullable)(image.CGImage);
    UIGraphicsEndImageContext();
}

代碼的核心在於UIBezierPath 的 addClip方法,該方法的做用是在當前上下文環境中讓閉合路徑區域可視化,外部區域不可視。而後再把layer在上下文環境中渲染成一張圖片,最後設置到layer.contents中。

第二種思路就是爲控件添加一個圓角矩形的遮罩,這個遮罩能夠是UIView、也能夠是CALayer,前者是控件,後者圖層。這種思路是這樣的,使用抑或運算,先畫一個大的矩形在上下文中,再在裏面繪製一個圓角矩形,對上下文路徑進行抑或就能夠了,代碼以下:

-(void)bp_setRaidus:(CGFloat)radius view:(UIView *)view backgroundColor:(UIColor *)color {
    CALayer *layer = [CALayer layer];
    layer.frame = view.bounds;
    [view.layer addSublayer:layer];
    
    UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, [UIScreen mainScreen].scale);
    UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:layer.bounds cornerRadius:radius];
    UIBezierPath *path1 = [UIBezierPath bezierPathWithRect: layer.bounds];
    [color setFill];
    [path fill];
    [path1 fillWithBlendMode:kCGBlendModeXOR alpha:1];
    layer.contents = (__bridge id)(UIGraphicsGetImageFromCurrentImageContext().CGImage);
    UIGraphicsEndImageContext();
}
3>Table View的重點優化

上述是一些通用的優化,在iOS不少控件裏最有可能被用來處理展現大量數據的就是Table View,下面重點列舉下一些優化注意事項:

  • 正確使用reuseIdentifier來重用cells
  • 儘可能使全部的view opaque,包括cell自身
  • 避免漸變,圖片縮放,後臺選人
  • 緩存行高
  • 若是cell內現實的內容來自web,使用異步加載,緩存請求結果

使用shadowPath來畫陰影

  • 減小subviews的數量
  • 儘可能不使用cellForRowAtIndexPath:,若是你須要用到它,只用一次而後緩存結果
  • 使用正確的數據結構來存儲數據
  • 使用rowHeight, sectionFooterHeight和 sectionHeaderHeight來設定固定的高,不要請求delegate
4>選擇是否緩存圖片

常見的從bundle中加載圖片的方式有兩種,一個是用imageNamed,二是用imageWithContentsOfFile,第一種比較常見一點。

既然有兩種相似的方法來實現相同的目的,那麼他們之間的差異是什麼呢?
imageNamed的優勢是當加載時會緩存圖片。imageNamed的文檔中這麼說:這個方法用一個指定的名字在系統緩存中查找並返回一個圖片對象若是它存在的話。若是緩存中沒有找到相應的圖片,這個方法從指定的文檔中加載而後緩存並返回這個對象。

相反的,imageWithContentsOfFile僅加載圖片。

下面的代碼說明了這兩種方法的用法:

UIImage *img = [UIImage imageNamed:@"myImage"];// caching
// or
UIImage *img = [UIImage imageWithContentsOfFile:@"myImage"];// no caching

那麼咱們應該如何選擇呢?
若是你要加載一個大圖片並且是一次性使用,那麼就不必緩存這個圖片,用imageWithContentsOfFile足矣,這樣不會浪費內存來緩存它。
然而,在圖片反覆重用的狀況下imageNamed是一個好得多的選擇。

以上是從內存和頁面上優化的一些總結,後續有新的感悟會再來更新O(∩_∩)O。

相關文章
相關標籤/搜索