大家在面试一些 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
而创建方式有三种
scheduledTimerWithTimeInterval:invocation:repeats: 或者 scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
timerWithTimeInterval:invocation:repeats: 或者 timerWithTimeInterval:target:selector:userInfo:repeats:
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 点啦, 哈哈, 希望大家看完的同时, 可以点赞一下哈! 祝大家好梦!!!
来源: https://www.cnblogs.com/guohai-stronger/p/10430106.html