likes
comments
collection
share

iOS 进阶知识总结(三)

作者站长头像
站长
· 阅读数 29

3~5年开发经验的 iOS工程师 应该知道的内容~本文总结以下内容

  • 视图渲染和离屏渲染
  • 事件传递和响应链
  • crash处理和性能优化
  • 编译流程和启动流程

导航

  • 对象
  • 类对象
  • 分类
  • runtime
  • 消息与消息转发
  • KVO
  • KVC
  • 多线程
  • runloop
  • 计时器
  • 视图渲染和离屏渲染
  • 事件传递和响应链
  • crash处理和性能优化
  • 编译流程和启动流程
  • 内存管理
  • 野指针处理
  • autoreleasePool
  • weak
  • 单例、通知、block、继承和集合
  • 网络基础
  • AFNetWorking
  • SDWebImage

渲染

屏幕撕裂的原因?

  • 单一缓存模式下,帧缓冲区只有一个缓存空间
  • 图片需要经过CPU -> 内存 -> GPU -> 展示 的渲染过程
  • CPU和GPU的协作过程中出现了偏差,GPU应该完整的绘制图片,但是工作慢了只绘制出图片上半部分。
  • 此时CPU又把新数据存储到缓冲区,GPU继续绘制的时候下半部分就变成了新数据。
  • 造成了两帧同时出现了一部分在屏幕上,看起来就撕裂了。

怎么解决屏幕撕裂?

  • 解决上一帧和下一帧的覆盖问题,需要使用不同的缓冲区,通过两个图形缓冲区的交替来解决。
  • 出现速度差的时候,就把下一帧存储在后备缓冲区,绘制完成后再切换帧。
  • 当绘制完最后一个像素点就会发出这个垂直同步信号通知展示完成。
  • 所以屏幕撕裂问题需要通过 双缓冲区 + 垂直同步信号 解决。

掉帧是怎么产生的?

  • 屏幕正在展示A帧的时候,CPU和GPU会处理B帧。
  • A帧展示完成该切换展示B帧的时候B帧的数据未准备好。
  • 没办法切换就只能重复展示A帧,感官上就是卡了,这就是掉帧的问题

怎么解决掉帧?

  • 掉帧根本原因是CPU和GPU渲染计算耗时过长
  • 1、降低视图层级
  • 2、提前或减少在渲染期的计算

CPU渲染职能

  • 布局计算:如果视图层级过于复杂,呈现或修改的时候需要消耗大量时间计算
  • 视图懒加载:当显示的时候才会加载视图,这对内存使用和程序启动时间很有好处。但展示前的操作都不会被及时响应,当视图从xib加载或者涉及图片显示,懒加载都会比正常加载慢。
  • 解压图片PNG或者JPEG压缩之后的图片会比位图小得多。在绘制到屏幕之前,必须把它扩展成完整的尺寸(通常等同于图片宽 x 长 x 4个字节)。为了节省内存,真正绘制的时候(第一次赋值UIImageView或者把它绘制到Core Graphics)才会解码图片,大图会占用一定的时间。
  • Core Graphics绘制:实现了drawRect:drawLayer:inContext:CALayerDelegate 的方法,在绘制前都会产生一个巨大的性能开销以开辟绘画上下文。
  • 图层打包:由于GPU不知道图层结构,CPU需要通过OpenGL把渲染树中的每个可见图层转换成纹理三角板。CPU的工作量和图层量成正比,如果层级关系太复杂就会渲染速度。

GPU渲染职能

  • GPU会根据生成的前后帧缓存数据,根据实际情况进行合成
  • 造成GPU渲染负担:离屏渲染,图层混合,延迟加载

一个UIImageView添加到视图上以后,内部如何渲染到手机上的?

  • 图片显示分为三个步骤: 加载、解码、渲染
  • 通常,我们程序员的操作只是加载,至于解码和渲染是由UIKit内部进行的。
  • 例如:UIImageView显示在屏幕上的时候需要赋值iamgeUIImage持有的数据是未解码的压缩数据,赋值的时候图像数据会被解码变成RGB颜色数据,最终渲染到屏幕上。

说说渲染流程

  • 1、确定顶点位置:CPU确定绘制图形的位置,从iOS坐标系换算成屏幕坐标系。
  • 2、图元装配:确定顶点间的连线关系。
  • 3、光栅化:就是把展示用到的像素点摘出来。
  • 4、着色器处理:GPU计算像素点展示的颜色,并存入缓冲区。
  • 5、屏幕展示

什么是离屏渲染

  • 普通渲染流程:APP - 帧缓冲区 - 展示
  • 离屏渲染流程:APP - 离屏渲染缓冲区 - 帧缓冲区 - 展示
  • 离屏渲染,是无法一次性处理渲染,需要分部处理并存储中间结果引起的。
  • 所以判断是否出现离屏渲染的根本条件就是判断渲染是否需要分部处理~
    • 需要分步处理,会产生离屏渲染
    • 一次性渲染,不产生离屏渲染

离屏渲染的影响

  • 需要分几步就需要开辟出几个离屏渲染缓冲区存储中间结果,造成空间浪费。
  • 最后合并多个离屏渲染缓冲区才能展示结果,会影响性能。

什么操作会触发离屏渲染?

  • 光栅化 layer.shouldRasterize = YES
  • 遮罩layer.mask
  • 圆角layer.maskToBounds = YesLayer.cornerRadis
  • 阴影layer.shadowXXX

视图

AutoLayout的原理,性能如何

  • Auto Layout 只关注视图之间的关系,通过布局引擎和已有的约束计算出各个视图的frame
  • 每当约束改变时会重新计算各个视图的frame
  • 获得frame的过程,就是根据各个视图已有的约束条件解方程式的过程。
  • 性能会随着视图数量的增加呈指数级增加
  • 达到一定数量的视图时,布局所需要的时间就会大于16.67ms,超过屏幕的刷新频率时会出现卡顿。

你认为自动布局怎么实现的

  • 原理是线性公式,使用了系统提供的NSLayoutConstraint
  • Masonry基于它封装

ViewController生命周期

  • initWithCoder:通过nib文件初始化时触发。
  • awakeFromNib:nib文件被加载的时候,会发生一个awakeFromNib的消息到nib文件中的每个对象。
  • loadView:开始加载视图控制器自带的view
  • viewDidLoad:视图控制器的view被加载完成。
  • viewWillAppear:视图控制器的view将要显示在window上。
  • updateViewConstrains:视图控制器的view开始更新AutoLayout约束。
  • viewWillLayoutSubviews:视图控制器的view将要更新内容视图的位置。
  • viewDidLayoutSubviews:视图控制器的view已经更新视图的位置。
  • viewDidAppear:视图控制器的view已经展示到window上。
  • viewWillDisappear:视图控制器的view将要从window上消失。
  • viewDidDisappear:视图控制器的view已经从window上消失。

LayoutSubviews调用时机

  • init 初始化不会调用 LayoutSubviews 方法
  • addsubView 时候会调用
  • 改变一个 viewframe 的时候调用
  • 滚动 UIScrollView 导致 UIView 重新布局的时候会调用
  • 手动调用 setNeedsLayout 或者 layoutIfNeeded

setNeedsLayoutlayoutIfNeeded的区别

  • setNeedsLayout 标记为需要重新布局
    • 异步调用layoutIfNeeded刷新布局,不立即刷新,在下一轮runloop结束前刷新。
    • 对于这一轮runloop之内的所有布局和UI更新只会刷新一次,layoutSubviews一定会被调用。
  • layoutIfNeeded
    • 如果有需要刷新的标记,立即调用layoutSubviews进行布局
    • 如果没有标记,不会调用layoutSubviews
    • 如果想在当前runloop中立即刷新,调用顺序应该是
    [self setNeedsLayout];
    [self layoutIfNeeded];
    

drawRect调用时机

  • drawRectloadViewViewDidLoad之后调用

UIViewCALayer是什么关系?

  • View可以响应并处理用户事件,CALayer 不可以。
  • 每个 UIView 内部都有一个 CALayer 提供尺寸样式(模型树),进行绘制和显示。
  • 两者都有树状层级结构,layer 内部有 subLayersview 内部有 subViews
  • CALayer是支持隐式动画的,View 作为Layer的代理,通过 actionForLayer:forKey:Layer提交相应的动画
  • layer 内部维护着三分 layer tree
    • 动画树presentLayer Tree,修改动画的属性改的是动画树的属性值
    • 模型树modeLayer Tree,最终展示在界面上的其实是提供视图的模型树
    • 渲染树render Tree

UIView显示原理

  • UIView 可以显示是因为内部有一个layer作为根图层,根图层上可以放其他子图层。
  • UIView 中所有能够看到的内容都包含在layer
  • UIView需要显示到屏幕上会调用drawRect:方法进行绘图,并且会将所有内容绘制在自己的layer
  • 绘图完毕后,系统将图层展示到屏幕上,完成了UIView的显示。

UIView显示过程

  • view.layer创建一个图层类型的上下文(Layer Graphics Contex
  • 触发代理方法drawLayer:inContext:,传入刚才准备好的上下文
  • drawLayer:inContext:内部会让view调用drawRect:方法绘图
  • 开发者在drawRect:方法中实现绘图代码,内容最终绘制到view.layer
  • 系统将view.layer展示到屏幕,完成了view的显示

UITableView的重用机制?

  • UITableView 通过重用单元格来节省内存
  • 为每个单元格指定一个重用标识符,即指定了单元格的种类
  • 当屏幕上的单元格滑出屏幕时,系统会把这个单元格添加到重用队列中,等待被重用。
  • 当有新单元格从屏幕外滑入屏幕内时,从重用队列查找可重用的单元格,如果有就拿来用,如果没有就创建一个使用。

UITableView卡顿的的原因有哪些?

  • 隐式绘制 CGContext
  • 绘制 Core Graphics
  • 文本 CATextLayerUILabel
  • 截图 -renderInContext:
  • 可伸缩图片
  • 混合图层
  • 对象回收
  • 离屏渲染
    • 光栅化 shouldRasterize
    • 阴影效果 shadowPath
    • 圆角裁切 cornerRadius
    • 遮罩

UITableVIew优化

  • 重用机制(缓存池)
  • 少用有透明度的View
  • 尽量避免使用xib
  • 尽量避免过多的层级结构
  • iOS8以后出的预估高度
  • 减少离屏渲染操作(圆角、阴影)
  • 缓存cell的高度(提前计算好cell的高度,缓存进当前的模型里面)
  • 异步绘制
  • 滑动的时候,按需加载
  • 尽量少add、remove 子控件,最好通过hidden控制显示

imageNameimageWithContentsOfFile区别?

  • imageWithContentsOfFile:加载本地目录图片,不能加载image.xcassets里面的图片资源。不缓存占用内存小,相同的图片会被重复加载到内存中。不需要重复读取的时候使用。
  • imageNamed:可以读取image.xcassets的图片资源,加载到内存缓存起来,占用内存较大,相同的图片不会被重复加载。直到收到内存警告的时候才会释放不使用的UIImage。需要重复读取同一个图片的时候用。

IBOutlet连出来的视图属性为什么可以被设置成weak?

  • 因为Xcode内部把链接的控件放进了一个_topLevelObjectsToKeepAliveFromStoryboard的私有数组中,这个数组强引用这所有topLevel的对象,所以用weak也无伤大雅。

UIScrollerView实现原理

  • 滚动其实是在修改原点坐标。当手指触摸后,scrollview拦截触摸事件创建一个计时器。
  • 如果计时器到点后没有发生手指移动事件,scrollview 发送 tracking events 到被点击的 subview
  • 如果计时器到点前发生了移动事件, scrollview 取消 tracking 自己滚动。

如何实现视图的变形?

  • 修改viewtransform

事件响应链和事件传递

什么是响应链

  • 由链接在一起的响应者(UIResponse及其子类)组成的链式关系。
  • 最先的响应者称为第一响应者
  • 最底层的响应者是UIApplication

写出一个响应链

subView -> view -> superView -> viewController -> window -> application

什么是事件传递

  • 触发事件后,事件从第一响应者到application的传递过程

事件传递的过程

  • 当程序中发生触摸事件之后,系统会将事件添加到UIApplication管理的一个队列当中
  • application将任务队列的首个任务向下分发
  • application -> window -> viewController -> view
  • view需要满足条件才可以处理任务,透明度>0.01、触摸在view的区域内、userInteractionEnabled=YEShidden=NO
  • 满足条件的view遍历自身的subViews,判断是否满足上述条件
  • 如果所有subView都无法满足条件,那么最佳响应者就是自己。
  • 如果没有任何一个view能处理事件,事件会被废弃。

找出触摸的View

// 返回的View是本次点击的最佳响应者
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

// 判断点是否落在某区域
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event

重写hitTest:withEvent

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
   // 1.判断当前控件能否接收事件
   if (self.userInteractionEnabled == NO || 
       self.hidden == YES || 
       self.alpha <= 0.01) {
        return nil;
    }
   // 2. 判断点在不在当前控件
   if ([self pointInside:point withEvent:event] == NO) {
        return nil;
   }
   // 3.从后往前遍历自己的子控件
   NSInteger count = self.subviews.count;
   for (NSInteger i = count - 1; i >= 0; i--) {
      UIView *childView = self.subviews[I];
      // 把当前控件上的坐标系转换成子控件上的坐标系
      CGPoint childP = [self convertPoint:point toView:childView];
      UIView *fitView = [childView hitTest:childP withEvent:event];
       if (fitView) { // 寻找到最合适的view
           return fitView;
       }
   }
   // 循环结束 没有比自己更合适的view
   return self;
}

Crash

常见Crash的原因有哪些?

  • 1、找不到方法的实现 unrecognized selector sent to instance
  • 2、KVC造成的crash
  • 3、KVO造成的crash
  • 4、EXC_BAD_ACCESS
  • 5、集合类相关崩溃,越界等
  • 6、多线程中的崩溃,使用了被释放的对象
  • 7、后台返回错误的数据结构

BAD_ACCESS在什么情况下出现?

  • 访问已经释放对象的成员变量
  • 给已经释放对象发消息
  • 死循环

不使用第三方,排查闪退问题?

  • 1、使用NSSetUncaughtExceptionHandler统计闪退的信息
  • 2、将统计到的信息发给后台
  • 3、在后台收集信息,进行排查
    static void my_uncaught_exception_handler (NSException *exception) {
        //获取NSException 信息
        NSLog(@"***********************************************");
        NSLog(@"%@",exception);
        NSLog(@"%@",exception.callStackReturnAddresses);
        NSLog(@"%@",exception.callStackSymbols);
        NSLog(@"***********************************************");
    }
        
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        NSSetUncaughtExceptionHandler(&my_uncaught_exception_handler);
        return YES;
    }
    

性能优化

优化启动时间

  • 启动时间是用户点击App图标,到第一个界面展示的时间。
  • 启动时间在小于400ms是最佳的,因为从点击图标到显示Launch Screen,到Launch Screen消失这段时间是400ms。
  • 启动时间不可以大于20s,否则会被系统杀掉。

main函数作为分水岭,启动时间其实包括了两部分:

mian函数之前的启动优化

  • 减少或合并动态库(这是目前为止最耗时的了, 基本上占了95%以上的时间)
  • 确认动态库是optional or required。如果该Framework在支持的所有iOS系统版本都存在,那么就设为required,否则就设为optional,因为optional会有些额外的检查

mian函数之后的启动优化

  • 1、合并和删减不必要的类或者分类
  • 2、将不必需在+load方法中做的事情,延时放到+initialize
  • 3、非必要的 SDK 和配置事件可以放在第一个界面处理
  • 4、减少创建线程,线程创建和运行需要多次的上下文切换所带来的开销,平均消耗大约在 29 毫秒。这是很大的时间开销,应该避免滥用。
  • 5、编译器插桩获取方法符号,生成order file设置到xcode。减少页中断带来的耗时。

网络优化

  • IP直连,将我们原本的请求链接www.baidu.com 修改为 180.149.132.47
  • 运营商在拿到请求后发现是IP地址会直接放行,而不会去走DNS解析
  • 不走他的DNS解析也就不会存在DNS被劫持的问题
  • 实现方法1:接使用HTTPDNS等sdk
  • 实现方法2:服务端下发发域名-IP对应列表,客户端缓存,通过缓存IP来进行业务访问。

包体积优化

  • 1、删除陈旧代码、删除陈旧xib/sb ,删除无用的资源文件(检测未使用图片的工具LSUnusedResources
  • 2、图片、音视频资源压缩后使用。
  • 3、动图可以使用webP格式,加载速度比较慢,但体积小
  • 4、能用动态库的尽量用动态库,一般情况静态库的体积会比动态库大
  • 5、主题类的资源文件提供按需下载功能,不直接打包在应用包里面
  • 6、App Slicing,应用程序切片,只打包下载机型所需的资源文件,不需要开发者处理
  • 7、Bitcode,中间代码, 苹果会对可执行文件进行优化,不需要开发者处理
  • 8、ODR,On Demand Resources,按需加载资源,需要开发者处理

电量优化

  • 1.定位,尽量不要实时更新,可以适当降低精度
  • 2.网络请求,能用缓存的时候尽量使用缓存,降低请求的频率,减少请求的次数,优化传输结构
  • 3.CPU处理,需要复用的数据能缓存的数据尽量缓存,优化算法和数据结构
  • 4.GPU处理,减少离屏渲染

短视频/直播优化

  • 多播放器,像tableview那样维护缓存池。多播放器,才能做预加载。
  • 边下边播,要实现下一个视频或者几个视频能快速的播放起来,首先应该保证正在播放的短视频能顺畅播放,边下边播任务优先级应该高于预加载任务。没有边下边播时才能执行预加载,边下边播任务进行时应当停止预加载。
  • 预加载,预加载的下载应该和边下边播区分开,手势滑动到差不多出现的时候,开始播放预加载的那1m数据。滑动下一个的时候,就取消preload,播放那1m下载的数据,再继续load。同时调起下一个preload准备下载。

短视频缓存管理

  • 缓存主要创建了三个目录管理,分别为temp、media、trash目录
  • 缓存分为临时缓存和最终缓存,当短视频资源未下载完时是放在temp、当视频资源缓存完时移动到media,这样分别存放便能方便读取和管理两种状态的缓存,
  • 所有需要删除的缓存文件都移入trash,随后再删除以此来保证较高的删除效率。所有文件命名使用视频url的md5值保证唯一性。
  • 缓存应该具有自动管理功能,默认配置下ShortMediaCache允许临时缓存最多保存1天,最大100Mb,而最终缓存则允许最多保存2天最大200Mb,如果业务需要可以自定义ShortMediaCacheConfig配置实现。

一般是怎么用Instruments的?

  • Instruments里面工具很多,常用:
  • Time Profiler: 性能分析
  • Zombies:检查是否访问了僵尸对象,但是这个工具只能从上往下检查,不智能。
  • Allocations:用来检查内存。
  • Leaks:检查内存,看是否有内存泄露。

编译 & 启动

编译链接流程

  • 0、输入文件:找到源文件
  • 1、预处理:包括替换宏和导入头文件
  • 2、编译阶段:词法分析、语法分析、语义分析,最终生成IR
    • 2-1、预处理后会进行词法分析,词法分析会把代码切片成一个个token
    • 2-2、语法分析会验证语法是否正确,在词法分析的基础上把单词组合成各种语法短语,然后把节点组成抽象的语法树
    • 2-3、代码生成器根据语法树自顶向下生成LLVM IROC会在这里进行runtime的桥接:property的合成、ARC处理等
  • 3、编译器后端:通过一个个Pass去优化,每个Pass完成一部分功能,最终生成汇编代码
    • 3-1、苹果对代码做了进一步优化,并且通过.ll文件生成.bc文件。
    • 3-2、可以通过.bc.ll文件生成汇编代码
  • 4、生成,.o格式的目标文件
  • 5、链接动态库和静态库
  • 6、通过不同架构,生成对应的可执行文件MachO

APP启动过程

  • 1、加载可执行文件(读取Mach-O
  • 2、加载动态库(Dylib
  • 3、Rebase & Bind
    • 3-1、Rebase的作用是修正ASLR的偏移,把当前MachO的指针指向正确的内存
    • 3-2、Bind的作用是重新修复外部方法指针的指向,fishhook原理
  • 4、objc_init,加载类和分类
  • 5、Initializers,调用load方法,初始化C & C++的对象等
  • 6、main()函数
  • 7、执行AppDelegate的代理方法(如:didFinishLaunchingWithOptions)。
  • 8、初始化Windows,初始化ViewController

dyld做了什么

  • 1、dyld读取Mach-OHeaderLoad Commands
  • 2、找可执行文件的依赖动态库,将依赖的动态库加载到内存中。这是一个递归的过程,依赖的动态库可能还会依赖别的动态库,所以dyld会递归每个动态库,直至所有的依赖库都被加载完毕。
转载自:https://juejin.cn/post/7076026684238987300
评论
请登录