iOS UITableView 优化面试竟然是这样子的
今天师兄想要讲讲前几天腾讯视频的电话面试,其实啥都没问,就是简单的几个问题?
- 自我介绍
- 你的优点缺点
- 你是怎么看待 React-Native、Flutter以及 Native?分别说说他们的优缺点,以及适用于哪些类型的 App?
- Feed流优化,先说说你优化的点,然后详细说说为什么这么优化
- 探讨异步绘制的弊端以及优势
- 算法题 简答的股票题
基本就是这么多了,今天在这里做一个小小的总结
自我介绍
自我介绍,常规的三步走,一是基本信息你叫啥、学校、专业以及哪年毕业;二是工作经历,建议由近及远,哪年到哪年在哪家公司,什么岗位,做过的项目。三是着重介绍一个项目,在里面担当的角色等,有何突出的表现?
你的优点缺点
一句话突出优点,弱化缺点,优点突出写程序逻辑严谨等,缺点尽量不要和代码相关,比如说激进,不务实这些就不要说了
你是怎么看待 React-Native、Flutter以及 Native?分别说说他们的优缺点,以及适用于哪些类型的 App?
开放性的问题,但是大前端是一个趋势,拥抱新技术,保持求知欲,了解内部原理。依次说出他们的优缺点,然后进行比较,为什么会这样,你们的项目中为什么选择这个,原因是什么就可以了,这一块面试官不会为难你的
Feed流优化,先说说你优化的点,然后详细说说为什么这么优化
Feed流优化,是这次面试的重中之重,聊了三十多分钟。
TableViewCell 复用
iOS 以后支持给出预估高度,并且将高度缓存,避免重复计算,减少 cpu 压力
视图层级优化,避免动态创建视图,在内存可控的情况下,缓存 view,善用 hidden 属性,减少视图层级
避免直接在 drawRect中直接对 layer 进行绘制
少用透明色,减少圆角绘制、阴影,尽量用图片代替,避免离屏渲染
图片优化
差不多就是这么多了
然后我们会挨个来详细介绍
重用机制是什么样的,如果让你自己实现你会怎么设计?
简而言之就是创建一个重用管理类,重用池中声明两个属性,一个是已经使用的重用池,一个是等待使用的。所有这一切的目的就是牺牲空间换取时间
@interface ReusePool() @property (nonatomic, strong)NSMutableSet *useSet; @property (nonatomic, strong)NSMutableSet *waitingUseSet; @end @implementation ReusePool -(instancetype)init { self = [super init]; if (self) { self.useSet = [[NSMutableSet alloc] init]; self.waitingUseSet = [[NSMutableSet alloc] init]; } return self; } - (UIView *)dequeueReuseView { UIView *view = [self.waitingUseSet anyObject]; if (!view) { return nil; } [self.waitingUseSet removeObject:view]; [self.useSet addObject:view]; return view; } - (void)addReuseView:(UIView *)view { [self.useSet addObject:view]; } - (void) reset { UIView *view = nil; while ((view = [self.useSet anyObject])) { [self.useSet removeObject:view]; [self.waitingUseSet addObject:view]; } } @end - (void)addViews { [self reloadViews]; self.clickedCount++; int max = 5; if (self.clickedCount / 2) { max = 10; } for (int i=0; i<max; i++) { UILabel *label = (UILabel *)[self.pool dequeueReuseView]; if (!label) { label = [[UILabel alloc] init]; label.font = [UIFont systemFontOfSize:18]; label.textColor = [UIColor blackColor]; label.textAlignment = NSTextAlignmentCenter; [self.pool addReuseView:label]; NSLog(@"创建了"); } else { NSLog(@"重用了"); } label.text = @(i).stringValue; label.frame = CGRectMake(0, i * 44 + 128, self.view.bounds.size.width, 44); [self.view addSubview:label]; } } - (void) reloadViews { for (UIView *view in self.view.subviews) { if ([view isKindOfClass:[UILabel class]]) { [view removeFromSuperview]; } } [self.pool reset]; }
该类有2个Set。useSet存放没有使用但已经放入到重用池中的view。waitingUseSet 存放等待重用的view。看一下使用方法。
每次更新视图的时候都会调用addViews方法,根据按钮的点击次数决定本次渲染的视图个数。在每次刷新之前,我们需要将pool重置reset将useSet中的数据放置到watingUseSet中。第一次刷新的时候创建5个label,重用池中没有数据,刷新结束之后,useSet中有5个视图,第二次刷新的时候先将useSet中的5个视图放到waitingUseSet中,在创建label的时候先从waitingUseSet中取,有数据就返回视图,并将视图重新加入到useSet中,没有数据就返回nil,所以第二次刷新完之后,重用了5个视图,但useSet中增加到了10个可重用视图,之后每次刷新数据都是10个视图重用,不会再次创建。
面试小集:iOS手动实现重用池
zhuanlan.zhihu.com
图标
为什么减少 cpu 压力可以避免卡顿呢?
这里考察的还是很深入的,首先要明确显示原理,其次 cpu 和 gpu 的关系,实际上明白这两者的关系,基本也就清楚了为什么要做上述那些优化了。
显示原理
在VSync信号到来后,系统图形服务会通过CADisplayLink等机制通知App,App主线程开始在CPU中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后CPU会将计算好的位图提交到GPU去,由GPU进行变换、合成、渲染。随后GPU会把渲染结果提交到帧缓冲区去,等待下一次VSync信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个VSync时间内,CPU或者GPU没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。
在开发中,CPU和GPU中任何一个压力过大,都会导致掉帧现象,所以在开发时,也需要分别对CPU和GPU压力进行评估和优化。
图片优化
为什么要对图片进行优化?
不要用JPEG的图片,应当使用PNG图片。
子线程预解码(Decode),主线程直接渲染。因为当image没有Decode,直接赋值给imageView 会进行一个Decode操作。
优化图片大小,尽量不要动态缩放(contentMode)。
尽可能将多张图片合成为一张进行显示。
异步绘制原理
在之前的文章中已经详细的描述了,在这里就不赘述了。主要就是考虑一个问题,异步绘制,将 ui 的绘制工作放到后台线程就一定比主线程快么?
这里肯定是不一定的,详细说一下多线程?多线程的目的就是将大任务拆解成多个子任务并发进行,等任务完成以后提交到主线程进行合并。这样做的目的就是牺牲空间换取时间。追问,如果tableview 滑动过快,你的后台线程此时并没有将 ui 绘制出来,此时会发生什么,界面空白是吧,这个时候要怎么处理? 加锁可以解决,但是会不会产生卡顿呢,因为你会把子线程堵塞。如果任务过大是会堵住的,那么你的异步绘制还有什么意义呢? 提前准备内容,预绘制,所有这一切都是空间换时间。再问,你知道 facebook开源的组建 AsyncDisplayKit,他是怎么实现,原理是什么?
ASTableNode/ASCollectionNode开辟的新航路
首先的好消息是,作为ASDK的一员,ASTableNode以及其cellNode已经具备了异步布局和异步渲染的能力,即使没有做额外优化,仅仅利用ASDK通用的异步机制将耗时操作延后,相对于一般UITableView已经有了显著的提升。虽然性能锯齿仍然存在,但是将其转移到了后台线程以后,用户感受到的卡顿就已经不会那么明显了。
然而这些似乎还不够,在进入屏幕之后才开始渲染,会有短暂的白屏现象(等待渲染完成)再显示内容。既然渲染工作可以在显示之后再进行,那么类似的,也可以在显示之前的一段时间,把布局和渲染的工作预先完成。
要达到这些目的,首先介绍一些相关的类:
ASTableNode/ASCollectionNode,可以认为是UITableView/UICollectionView的异步版本,内部包装了原来的UIKit的对应版本,并扩展了一系列功能使他们能够实现异步布局及渲染。
ASInterfaceState,表示一个node不同的显示状态。其实每个ASDisplayNode都具备interfaceState属性,它主要的用武之地还是在tableNode/collectionNode之中。对于一个UITableViewCell来说,布局和渲染一般都是在cellForRowAtIndexpath同时完成,然而当需要精细处理任务时就需要把每一个不同的状态分开,降低某一瞬间由于CPU负载高导致卡顿的可能性。ASInterfaceState递进地分为5种状态:
None,该node在一段时间内不会进入屏幕
MeasureLayout,可能会在一段时间后进入屏幕,应该准备layout和size计算
Preload,加载所需要的数据,如下载图片,缓存读取等等
Display,马上将要进入屏幕,开始进行渲染操作,显示包含的文字或者图片
Visible,该node(所对应的view)至少有1个像素已经在屏幕内,正在显示
对于每一个cell而言,原本需要在同一时间点进行的所有初始化/加载/布局/渲染等工作,现在被均匀分配到了不同的状态进行预处理。随着用户滚动列表,根据cell离屏幕的距离不同,设置相应的interfaceState并触发不同阶段的工作,达到均匀分配的目的。同时,由于不需要在主线程上进行,多个cell的工作可以通过共享后台线程来大幅提高并行效率。
ASDataController,与ASTableNode一一对应,负责代替ASTableNode管理delegate和dataSource的一系列方法,诸如初始化,插入,删除和一些代理方法等。
ASRangeController,同样与ASTableNode一一对应,并且可以根据设备性能自定义布局、加载、渲染的工作indexPath区间,在滚动时动态高效地调整各cell的interfaceState来层层触发不同显示阶段的工作,对于流畅滚动起到了至关重要的作用。
ASScrollDirection,定义了列表滚动的方向(上下左右)。在ASRangeController调整各阶段的工作区间时,一般在用户滚动的方向上需要多加载一些,而滚出屏幕的cell在一定时间内回到屏幕的概率较低,因此其分配到的资源也就相应少一些。
在ASDK1.x的时代,由于彼时还没有ASRangeController的存在,cell的渲染只会在进入屏幕以后进行,也就是说,虽然性能能够达到60fps,但是滚动较快时,渲染跟不上,『白屏』现象就出现了。到了2.x有了ASRangeController之后,虽然在滚动极快的情况下仍然会因为资源不足而产生白屏现象,但是在一般情况下,因为资源分配更加合理,这个问题得到了显著的改善。
关于 ASDK 的内容来自
即刻技术团队:AsyncDisplayKit介绍(三)深度优化列表性能
zhuanlan.zhihu.com
图标
算法题也放一下吧
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。
注意:你不能在买入股票前卖出股票。
class Solution(object):
def maxProfit(self, prices):
"""
:type prices: List[int]
:rtype: int
"""
if len(prices) < 2:
return 0
MIN = prices[0]
MAX = 0
for num in prices:
MAX = max(MAX, num - MIN)
MIN = min(MIN, num)
return MAX
以上就是今天的内容,共勉