iOS 性能優化


大家在面試一些B輪以上的公司,很多面試大佬都會問怎么優化tableView或者iOS程序如何優化等。本篇博客將講述iOS性能優化,圍繞以下問題講述:

一、內存

  1. 內存布局
  2. retain
  3. weak

二、Runloop

  1. NSTimer
  2. 面試-Runloop

三、界面

  1. 內存泄露
  2. TableView優化

下面我們一一講述上面內容。

一、內存

1.1 內存布局

代碼的文件是可執行的二進制文件,在二進制文件中,我們怎么區分這些文件呢,如下圖:

1.1.1 內核

內核是操作系統最關鍵的組成部分。內核的功能是負責接觸底層,所以大部分會用到C語音進行編寫的,有的甚至使用到匯編語言。iOS的核心是XNU內核。 

XNU內核是混合內核,其核心是叫Mach的微內核,其中Mach中亦是消息傳遞機制,但是使用的是指針形式傳遞。因為大部分的服務都在XNU內核中。Mach沒有昂貴的復制操作,只用指針就可以完成的消息傳遞。

1.1.2 棧(stack)

棧主要存放局部變量和函數參數等相關的變量,如果超出其作用域后也會自動釋放。棧區:是向低字節擴展的數據結構,也是一塊連續的內存區域。

1.1.3 堆(heap)

堆區存放new,alloc等關鍵字創造的對象,我們在之前常說的內存管理管理的也是這部分內存。堆區:是向高地址擴展的數據結構,不連續的內存區域,會造成大量的碎片。

1.1.4 BSS段

BSS段存放未初始化的全局變量以及靜態變量,一旦初始化就會從BSS段去掉,轉到數據段中。

1.1.5 Data段

Data段存儲已經初始化好的靜態變量和全局變量,以及常量數據,直到程序結束之后才會被立即收回。

1.1.6 text段

text段是用來存放程序代碼執行的一塊內存區域。這一塊內存區域的大小在程序運行前就已經確定,通常也是只讀屬性。

拓展:全局變量、成員變量、局部變量、實例屬性和靜態變量以及類屬性區別

變量按作用范圍可以分為全局變量和局部變量,其中全局變量也就是成員變量。成員變量按調用的方式可以分為類屬性和實例屬性。類屬性是用static修飾的成員變量,也就是靜態變量。實例屬性是沒有用static修飾的成員變量,也叫作非靜態變量。如下圖更直觀看出關系:

在這其中,如果局部變量和全局變量的名字是一樣的,局部變量的作用范圍區域內全局變量就會被隱藏;但是如果在局部變量的范圍內想要訪問成員變量,必須要使用關鍵字this來引用全局變量(成員變量)

全局變量(成員變量)和局部變量的區別 

  1. 內存中位置不同:全局變量(成員變量)在堆內存,全局變量(成員變量)屬於對象,對象進入堆內存;局部變量屬於方法,方法進入棧內存
  2. 生命周期不同:全局變量(成員變量)隨着對象的創建而存在的,對象消失也隨之消失;局部變量隨着方法調用而存在,方法調用完畢而消失
  3. 初始化不同:全局變量(成員變量)有默認的初始化值;局部變量是沒有默認初始化的,必須定義,然后才能使用。

全局變量(成員變量)和靜態變量的區別:

  1.  內存位置不同:靜態變量也就是類屬性,存放在靜態區;成員變量存放在堆內存
  2. 調用方式不同:靜態變量可以通過對象調用,也可以通過類名調用;成員變量就只能用對象名調用

 

1.2 retain

對於retain,如果經過taggerPointer修飾過的,就直接return,如果不是的話,就調用當前的retain-rootRetain方法 。需要關注當前引用計數什么時候加1-----通過sideTable方法,加一個偏移量refcntStorage。這就是內部實現的過程。

拓展:retain與copy有什么區別?

copy:建立索引計數為1的對象,然后釋放對象;copy建立一個相同的對象,如果一個NSString對象,假如地址為0x1111,內容為@"hello",通過Copy到另一個對象之后,地址為0x2322,內容也相同,而新的對象retain為1,舊的對象是不會發生變化;

retain:retain到另外一個對象之后,地址是不會變化的,地址也為0x1111,實質上是建立一個指針,也就是指針拷貝,內容也是相同的,retain值會加1。

 

1.3 weak

weak的底層實現也是面試官經常被問到的,本人也在杭州有贊面試中被問到,不過說實話,把我問倒了,回來看了一下weak的實現源碼runtime,然后寫了一篇博客。在這里就不重復了,看一下下面的博客地址就可以了解weak底層是怎么實現的。

 weak實現原理:https://www.cnblogs.com/guohai-stronger/p/10161870.html

 

二、Runloop

 2.1 NSTimer

關於NSTimer造成的潛在循環引用問題,想必大家都知道,很菜的我都不繼續說了,如果想要了解iOS可能存在的循環引用問題,大家可以讀本人寫的專門針對iOS循環引用問題的博客。

循環引用的博客地址:https://www.cnblogs.com/guohai-stronger/p/9011806.html

2.1.1 NSTimer與Block處理方式

首先看下面代碼:

_timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(fire) userInfo:nil repeats:YES];

對於上面的代碼會造成循環引用,那我們加上這句代碼

__weak typeof(self) weakSelf = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:weakSelf selector:@selector(fire) userInfo:nil repeats:YES];

加上這句代碼,為什么就不可以解決循環引用呢?

答案是當然不可以解決循環引用。__weak typeof(self) weakSelf = self;這個weakSelf指向的地址就是當前self指向的地址;如果再使用strong--->weakSelf,也會使self連帶指針retain(加1)操作,沒有辦法避免當前VC引用計數加1。如下圖:

反之,為什么block就可以呢?

self.name = @"logic"
__weak typeof(self) weakSelf = self;
_block = ^(void){
NSLog(
"%@",self.name); }

通過上面代碼,原本是block-->self,self-->block兩者相互持有,導致無法釋放;現在有weakSelf打破這種關系,用weak修飾,在block執行完就會被釋放,不會循環引用。

再看一個viewDidLoad代碼中如下:

self.name = @"logic"
__weak typeof(self) weakSelf = self;
_block = ^(void){
    __strong typeof(weakSelf) strongSelf = weakSelf;
    NSLog("%@",self.name);  
}

在block內使用__strong 同樣會使當前的self引用計數retain(加1),會延遲當前對象的釋放,那為什么不造成引用呢?

原因是__strongSelf是在block代碼塊里面。當我們viewDidLoad執行完之后,初始化的局部變量也會被隨之釋放;類比來說,block也是這個原理,當我們代碼塊的邏輯執行完之后,__strong聲明的__strongSelf也會被回收,__strongSelf會被回收,代表着self的引用計數回歸正常,調用析構函數,完成回收。

 

2.1.2 NSTimer創建方式區別

NSTimer官方的類並不是很多,我們把它粘貼出來

@interface NSTimer : NSObject
//初始化,最好用scheduled方式初始化,不然需要手動addTimer:forMode: 將timer添加到一個runloop中。
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

- (id)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(id)ui repeats:(BOOL)rep;

- (void)fire;  //立即觸發定時器

- (NSDate *)fireDate;//開始時間
- (void)setFireDate:(NSDate *)date;//設置fireData,其實暫停、開始會用到

- (NSTimeInterval)timeInterval;//延遲時間

- (void)invalidate;//停止並刪除
- (BOOL)isValid;//判斷是否valid

- (id)userInfo;//通常用nil

@end

而創建方式有三種

1. scheduledTimerWithTimeInterval:invocation:repeats:或者scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:

2.timerWithTimeInterval:invocation:repeats:或者timerWithTimeInterval:target:selector:userInfo:repeats:

3.initWithFireDate:interval:target:selector:userInfo:repeats:

第一種創建方式:scheduledTimerWithTimeInterval這兩個類方法會創建一個timer,並將timer指定到一個默認的runloop模式,也是NSDefaultRunLoopMode,但是有一個問題,當UI刷新滾動的時候,就不會是NSDefaultRunLoopMode了,這樣timer就會停下來了。scheduledTimerWithTimeInterval不需要添加adddTimer:forMode:方法,方法也就是會自動運行timer。

第二種創建方式:timerWithTimeInterval這兩個類方法創建的timer對象沒有安排到運行循環中,必須通過NSRunloop對象對應的方法adddTimer:forMode:方法,必須手動添加運行循環。

第三種創建方式:initWithFireDate方法創建timer,也是需要NSRunloop下對應的adddTimer:forMode:,然后訂到一個runloop模式中。

我們一般使用

 

2.1.3 使用NSProxy解決NSTimer循環引用詳解

NSTimer循環引用的解決方法,目前有以下幾種

(1)類方法

(2)GCD方法

(3)weakProxy

今天我們着重講解weakProxy方法解決NSTimer造成的循環引用,其他方法自行百度哈。

使用weakProxy解決循環引用的原因是:

weakProxy是利用runtime消息轉發機制來斷開NSTimer對象與視圖的強引用關系。初始NSTimer時把觸發事件的target對象替換成了另一個單獨的對象,緊接着對象中NSTimer的SEL方法觸發時讓這個方法在當前視圖中實現。

下面是代碼實現。

新建一個繼承NSProxy類的子類WeakProxy類

#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN

@interface WeakProxy : NSProxy
@property(nonatomic , weak)id target;

@end

NS_ASSUME_NONNULL_END


#import "WeakProxy.h"
#import <objc/runtime.h>

@implementation WeakProxy

- (void)forwardInvocation:(NSInvocation *)invocation{
    [self.target forwardInvocation:invocation];
}

- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    return [self.target methodSignatureForSelector:sel];
}

@end

然后在需要用到的類中引入WeakProxy,並聲明屬性

#import "ViewController.h"
#import "WeakProxy.h"

@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@property(nonatomic,strong)WeakProxy *weakProxy;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _weakProxy = [WeakProxy alloc];
    _weakProxy.target = self;
    _timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:_weakProxy selector:@selector(fire) userInfo:nil repeats:YES];

}

- (void)fire{
    NSLog(@"fire");
}

- (void)dealloc{
    [self.timer invalidate];
    self.timer = nil;
}

@end

利用上面的代碼,可以解決NSTimer的循環引用問題,原理就是上面的一幅圖,在這不必多言。

 

2.2 Runloop

對於Runloop的詳解,本人也已經在以前寫過Runloop底層原理的講解,對於Runloop的面試題,那篇博客可以為大家解答。

Runloop底層原理:https://www.cnblogs.com/guohai-stronger/p/9190220.html

 

三、界面

3.1 內存泄露

3.1.1 內存泄露的檢測

舉例,如何檢測VC(ViewController)是不是內存泄露?

平常的思路可以使用dealloc方法,打印一下是否造成了內存泄露,但為了以后不想在dealloc方法寫太多,可以寫一個工具類實現。

如果想要檢測ViewController內存有沒有泄露,就要Hook生命周期函數。在離開VC時調用ViewDidDisAppear,可以向ViewDidDisAppear延遲發送一個消息,如果當前消息的處理者為nil,則什么都不會發生,反之有對象,發送消息時就會響應。而我們並不想改變原ViewDidDisAppear的內部邏輯,所以我們想到了分類Category。下面就是思路圖:

這個代碼邏輯也是騰訊使用memoryLeak第三方內存泄露的原理。

首先我們需要創建一個分類,基於UIViewController,新建分類UIViewController+LGLeaks

下面是分類實現代碼:

#import "UIViewController+LGLeaks.h"
#import <objc/runtime.h>

const char *LGVCPOPFLAG = "LGVCPOPFLAG";

@implementation UIViewController (LGLeaks)

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self lg_methodExchangeWithOriginSEL:@selector(viewWillAppear:) currentSEL:@selector(lg_viewWillAppear:)];
        [self lg_methodExchangeWithOriginSEL:@selector(viewDidDisappear:) currentSEL:@selector(lg_viewDidDisAppear:)];
    });
}

+ (void)lg_methodExchangeWithOriginSEL:(SEL)originSEL currentSEL:(SEL)currentSEL{
    Method originMethod = class_getInstanceMethod([self class], originSEL);
    Method currentMethod = class_getInstanceMethod([self class], currentSEL);
    method_exchangeImplementations(originMethod, currentMethod);
}

- (void)lg_viewWillAppear:(BOOL)animate{
    [self lg_viewWillAppear:animate];
    //
    objc_setAssociatedObject(self, LGVCPOPFLAG, @(NO), OBJC_ASSOCIATION_ASSIGN);
}

- (void)lg_viewDidDisAppear:(BOOL)animate{
    [self lg_viewDidDisAppear:animate];
    if ([objc_getAssociatedObject(self, LGVCPOPFLAG) boolValue]) {
        [self lg_WillDelloc];
    }
}

- (void)lg_WillDelloc{
    //給 nil 對象發消息 ---
    //pop -- 回收內存
    __weak typeof(self) weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        __strong typeof(self) strongSelf = weakSelf;
        NSLog(@"lg_NotDelloc : %@",NSStringFromClass([strongSelf class]));
    });
}

@end

我們在導航欄控制器下進行傳值LGVCPOPFLAG

#import "UINavigationController+LGLeaks.h"
#import <objc/runtime.h>

@implementation UINavigationController (LGLeaks)

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method originMethod = class_getInstanceMethod([self class], @selector(popViewControllerAnimated:));
        Method currentMethod = class_getInstanceMethod([self class], @selector(lg_popViewControllerAnimated:));
        method_exchangeImplementations(originMethod, currentMethod);
    });
}

- (UIViewController *)lg_popViewControllerAnimated:(BOOL)animated{
    UIViewController *popViewController = [self lg_popViewControllerAnimated:animated];
    extern const char *LGVCPOPFLAG;
    objc_setAssociatedObject(popViewController, LGVCPOPFLAG, @(YES), OBJC_ASSOCIATION_ASSIGN);
    return popViewController;
}

@end

以上檢測VC內存是否泄漏demo已上傳到github。

Demo:https://github.com/zxy1829760/iOS-CustomLeakTest

 

3.2 TableView優化

3.2.1 TableView為什么會出現卡頓?

1.cellForRowAtIndexPath:方法中處理了很多的業務

2.tableViewCell的subView層級太復雜,其中做了很多的透明處理

cell的高度動態變化的時候計算方式不對

3.2.2 TableView優化

1.提前計算好並緩存好高度(布局),因為heightForRowAtIndexPath是最頻繁調用的方法。

對於cell高度的計算,我們分為兩種cell,一種是動態高度的cell,還有一種是定高的cell。

1)對於定高的cell,我們應該采用這種方式:

self.tableView.rowHeight = 66;

此方法指定了tableView所有的cell高度都是66,對於tableView默認的cell高度是rowHeight=44,所以經常看到一個空的tableView會顯示成那樣。我們不要去是實現tableView:heightForRowAtIndexPath:,因為這樣會多次調用,不用這種方式以節省不必要的開銷和計算。

2)動態高度的cell

需要實現-(CGFloat)tableView:(UITableView *)tableViewheightForRowAtIndexPath:(NSIndexPath *)indexPath

這個代理實現之后,上面rowHeight的設置就會變為無效,我們就需要提高cell高度計算效率,以此來節省時間。

下面是定義高度:

  1. 新建一個繼承於UITableViewCell的子類
  2. 重寫initWithStyle:reuseIdentifier:的方法
  3. 添加所有需要顯示的子控件(不需要設置子控件的數據和frame,其中子控件要添加到contentView中)
  4. 進行子控件一次性的屬性設置
  5. 提供兩個模型:數據模型:存放文字數據/圖片數據;frame模型:存放所有子控件的frame/cell的高度/存放數據模型
  6. 然后cell擁有一個frame模型(不直接擁有數據模型)
  7. 最后重寫frame模型屬性的setter方法,然后在方法中直接賦值和frame

如果是自定義高度,下面是自定義高度的原理:

  • 因為heightForRow比cellForRow方法是先調用,創建frame模型保存的高度,實現自定義高度的cell。
  • 設置最大尺寸,為了更好地展示尺寸。

2.UITableViewCell的重用機制。

UITableView只會創建一個屏幕或者一個屏幕多點的大的UITableViewCell,其它的都是從中取出來重用的。每當UITableViewCell的cell滑出屏幕的時候,就會放到一個緩沖池中,當要准備顯示某一個Cell時,會先去緩沖池中取(根據reuseIndetifier)。如果有,就直接從緩沖池取出來;反之,就會創建,將創建好的再次放入緩沖池以便下次再取,這樣做就會極大的減少了內存開銷。

3.TableView渲染

為保證TableView滾動的流暢,當我們快速滾動時,cell必須被快速的渲染出來,這就要求cell的渲染速度必須要快。如何提高cell渲染速度?

  • 有圖像的時候,預渲染圖片,在bitmao context應該先畫一遍,導出為UIImage對象,然后再繪制到屏幕中去,就會大大的提高渲染的速度。
  • 我們不要使用透明的背景,將opaque值設置為Yes,背景色盡量不要使用clearColor,也不要使用陰影漸變效果。
  • 可以使用CPU渲染,也可以在drawRect方法中自定義繪制。

4.減少視圖數目

盡量在TableViewCell中少添加過多的視圖,這樣會導致渲染速度變慢,消耗過大的資源。

5.減少多余的繪制

在drawRect方法中,rect是繪制的區域,在rect之外的區域不需要繪制,否則會消耗資源。

6. 不要給cell動態的添加subView

我們在初始化Cell的時候可以將所有需要展示的子控件添加完之后,然后根據需要來設置hide屬性設置顯示和隱藏起來。

7.異步UI,堅決不要阻塞主線程

8.滑動時可以按需加載對應的內容

舉例:目標行和當前行相差超過了指定行數,只需要在目標滾動范圍的前后指定3行加載。

-(void)scrollViewWillEndDragging:(UIScrollView *)scrollViewwithVelocity:(CGPoint)v
elocitytargetContentOffset:(inoutCGPoint *)targetContentOffset{
    NSIndexPath *ip=[selfindexPathForRowAtPoint:CGPointMake(0,targetContentOffset-
>y)];
    NSIndexPath *cip=[[selfindexPathsForVisibleRows]firstObject];
    NSIntegerskipCount=8;
    if(labs(cip.row-ip.row)>skipCount){
        NSArray *temp=[selfindexPathsForRowsInRect:CGRectMake(0,targetContentOffse
t->y,self.width,self.height)];
        NSMutableArray *arr=[NSMutableArrayarrayWithArray:temp];
        if(velocity.y<0){
:0]];
:0]];
:0]];
}
}
NSIndexPath *indexPath=[templastObject];
if(indexPath.row+33){
    [arraddObject:[NSIndexPathindexPathForRow:indexPath.row-3inSection
    [arraddObject:[NSIndexPathindexPathForRow:indexPath.row-2inSection
    [arraddObject:[NSIndexPathindexPathForRow:indexPath.row-1inSection
        [needLoadArraddObjectsFromArray:arr];
    }
}

if(needLoadArr.count>0&&[needLoadArrindexOfObject:indexPath]==NSNotFound){
    [cellclear];

return;

}

 

滾動很快時,只加載目標范圍內的cell,這就是按需加載。

9.離屏渲染的問題

  • 圖層的設置遮罩(layer.mask)
  • 圖層的masksToBounds,以及ClipsToBounds屬性設為ture
  • 圖層設置陰影
  • 圖層設置邊角問題,cornerRadius
  • 使用CGContext在drawRect:方法也會導致離屛渲染。

針對這個問題,下面是有一些優化:

  • 圓角優化:可以使用貝塞爾曲線
  • 對於shadow優化:我們可以設置shadowPath來優化,大幅度提高性能。
mageView.layer.shadowColor=[UIColorgrayColor].CGColor;
imageView.layer.shadowOpacity=1.0;
imageView.layer.shadowRadius=2.0;
UIBezierPath *path=[UIBezierPathbezierPathWithRect:imageView.frame];
imageView.layer.shadowPath=path.CGPath;
  • 需要圓角效果,可以使用中間透明圖片蒙上去
  • 可以使用Core Animation工具來檢測離屏渲染。可以通過Xcode->Open Develeper Tools->Instruments找到

Core Animation工具用來檢測性能,提供了FPS值,也提供了幾個參數值來展示渲染性能。

􏱤􏶫􏱀下面我們說一下每個選項的功能:

  • Color Blended Layers:這個選項勾選,出現效果中如果顯示紅色就是代表透明的,綠色代表不透明的。

 

  • Color Hits Green and Misses Red:紅色是代表沒有復用離屏渲染的緩存,綠色是表示復用了緩存,作為開發,我們是需要復用的。
  • Color Copied Images:當圖片顏色格式GPU不支持時,Core Animation會拷貝數據讓CPU進行轉化。
  • Color Immediately:如果勾選上,默認是每毫秒10次的頻率更新圖層調試的顏色。
  • Color Misaligned Images:如果勾選此項,圖片需要縮放時就會標記為黃色,沒有像素對齊時會標記為紫色。

 

  • Color Offscreen-Rendered Yellow:勾選上是用來檢測離屏渲染。如果顯示有黃色,代表有離屛渲染,還是要結合

   Color Hits Green and Misses Red來看,看是否已經開復用了緩存。

  • Color OpenGL Fast Path Blue:此選項對使用OpenGL的圖層才有用。

上面就是UITableView的優化方案,可能大家還有其它的方法,歡迎分享,可以博客下面留言。

 

以上就是本人關於iOS性能優化方面的方案,總結出來了一篇博客,希望對大家有所幫助,現在已經晚上1點啦,哈哈,希望大家看完的同時,可以點贊一下哈!祝大家好夢!!!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM