iOS 性能优化

Posted 国孩

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS 性能优化相关的知识,希望对你有一定的参考价值。

本人已迁移博客至掘进,以后会在掘进平台更新最新的文章也会有更多的干货,欢迎大家关注!!!https://juejin.im/user/588993965333309

 

大家在面试一些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值,也提供了几个参数值来展示渲染性能。

以上是关于iOS 性能优化的主要内容,如果未能解决你的问题,请参考以下文章

iOS 应用程序中的性能优化关键代码

iOS 性能优化

iOS 性能优化 - TimeProfiler分析代码耗时

iOS 性能优化 - TimeProfiler分析代码耗时

iOS性能优化之静态分析器

优化片段着色器