iOS 性能优化
Posted 国孩
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS 性能优化相关的知识,希望对你有一定的参考价值。
本人已迁移博客至掘进,以后会在掘进平台更新最新的文章也会有更多的干货,欢迎大家关注!!!https://juejin.im/user/588993965333309
大家在面试一些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值,也提供了几个参数值来展示渲染性能。
以上是关于iOS 性能优化的主要内容,如果未能解决你的问题,请参考以下文章