大家在面試一些B輪以上的公司,很多面試大佬都會問怎么優化tableView或者iOS程序如何優化等。本篇博客將講述iOS性能優化,圍繞以下問題講述:
一、內存
- 內存布局
- retain
- weak
二、Runloop
- NSTimer
- 面試-Runloop
三、界面
- 內存泄露
- 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 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高度計算效率,以此來節省時間。
下面是定義高度:
- 新建一個繼承於UITableViewCell的子類
- 重寫initWithStyle:reuseIdentifier:的方法
- 添加所有需要顯示的子控件(不需要設置子控件的數據和frame,其中子控件要添加到contentView中)
- 進行子控件一次性的屬性設置
- 提供兩個模型:數據模型:存放文字數據/圖片數據;frame模型:存放所有子控件的frame/cell的高度/存放數據模型
- 然后cell擁有一個frame模型(不直接擁有數據模型)
- 最后重寫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點啦,哈哈,希望大家看完的同時,可以點贊一下哈!祝大家好夢!!!