打造一个更流畅的 iOS App (上)

这篇文章应该是欠了很久了,本来应该是……半年前写的文章,不过还好我最终还是找时间写下来了。

在开始聊技术之前,我们可以先探讨一个问题,就是为什么要打造流畅的 App 体验?

流畅的体验意味着优秀的用户体验,技术的本质是为人类服务,脱离了为人类服务,再高超的技术,算法,工程都不具备价值。

因此,我所理解的技术的觉悟有以下三个

  • 技术即服务
  • 将优秀的产品交付给用户,让生活变得更美好
  • 深入技术的细节,获得条理清晰的美感

总之就是让世界充满爱。

下面这张图,列出了最容易遇到性能问题的两部分,第一部分是用户直接打交道的界面层,另外一部分是数据层。

Layout

Layout 有一些常见的 Case 和盲点,这个在 Apple 的 Layout Guide 中有提及,分别为 External Change (外部变化)和 Internal Change (内部变化)。

外部变化主要有以下几点

  • 多设备,例如 iPad
  • 屏幕旋转
  • 来电,录音状态等
  • 屏幕尺寸变化

内部变化有以下几点

  • 内容变化
  • 国际化(例如阿拉伯的从右往左的书写)
  • 动态字号

Layout 解决方案

没有 AutoLayout 之前,代码布局的方案就是 Programming Frame 和 Autoresizing Masks

随着 Autolayout 的发展,这两种方式从主流退居到了辅助,一般是对性能有特别追求的时候,我们才会采用这两种。

AutoLayout 的明显优势有以下几个

  • 自动高度计算
  • 高可读性的布局代码
  • 自动应对各种尺寸和设备变化

为了更好理解这两种布局方式的搭配,我们下面用 Yep 的一个例子来讨论下

Layout Case

在聊天列表这种会发生复杂变动的界面里,如果使用 AutoLayout 进行 Cell 内部的元素布局,并且进行高度的自动推断,那么当用户滑动的时候,needLayout 信号会被反复触发,这时候就会严重的掉帧了。

这时候就需要用 Programming Frame 的方式对 Cell 内每个元素进行布局设定,完全抛弃 AutoLayout,为了避免设定属性的时候发生动画,可以用 UIView.performWithoutAnimation(_:) 的 API 来进行设定,同时缓存每一个 Cell 的高度和文本大小。

这里需要注意的还有两点,一个是如果用 AutoLayout,切忌不要再用 Programming Frame 的方式改变元素的几何属性,如果用了 Programming Frame,那么记得要把元素的 translatesAutoresizingMaskIntoConstraints 设置为 NO,避免 AutoResizingMask 给你添麻烦。

Custom Render 自定义渲染

当你开始自定义布局或者渲染的时候,你的应用就已经比较深入了

或者你有需要高度自定义的元素,复杂的数据展示,或者是有诸如实时滤镜等多媒体渲染的需求。

就此来说,自定义渲染一般有三个思路

UIKit 好用,可以直接实现交互,这个主要应用在一些控件的实现。

Core Graphics 功能强大,线程安全,灵活,一般用在动画和多媒体上。

Core Text,对于图文排版来说,这个是一个全功能的实现,不过除非你有 TextKit 不能实现的,推荐优先使用 TextKit。

Custom Elements 自定义元素

自定义元素里讲两个例子,一个是静态的特定图形头像,一个是动态的富文本文本框。

图像缓存与预处理

这个需求有两个关键点

后台解码 从数据文件转换成 Bitmap 这一步默认是在主线程进行的,通过后台解码避免卡顿

let imageRef = self.CGImage
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.PremultipliedLast.rawValue).rawValue
let contextHolder = UnsafeMutablePointer<Void>()
let context = CGBitmapContextCreate(contextHolder, 
CGImageGetWidth(imageRef), 
CGImageGetHeight(imageRef), 8, 0, colorSpace, bitmapInfo)
if let context = context {
    let rect = CGRectMake(0, 0, CGFloat(CGImageGetWidth(imageRef)), 
  CGFloat(CGImageGetHeight(imageRef)))
    CGContextDrawImage(context, rect, imageRef)
    let decompressedImageRef = CGBitmapContextCreateImage(context)
    return UIImage(CGImage: decompressedImageRef!, scale: scale, orientation: self.imageOrientation)
} else {
    return nil
}

后台裁图 这个场景主要是将方形的原图,转换成圆形的头像,你可以在后台处理,建立缓存等,避免在主线程进行。

let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: self.size)
    UIGraphicsBeginImageContextWithOptions(self.size, false, 1)
UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).addClip()
drawInRect(rect)
return UIGraphicsGetImageFromCurrentImageContext()

这两个操作都需要用 GCD 创建后台的线程来执行,另外你也会面对一个内存超载的问题,例如这个图片分辨率很高,你在裁剪的时候希望不爆内存,那么可以参考这篇文章

Rich TextView 富文本文本框

这部分可以参考我以前的一篇文章

Complex Content (复杂内容)

复杂的界面元素内容,但是会被反复重用。

以这个界面为例子,下面的技能 Bubble 是一个比较复杂的组件,元素样式需要自定义,并且形状不规则。

直接用 Label 或者 Collection View 都会出现性能问题,或者在滑动的时候发生闪烁。

也许,最好的方式,就是做成图片缓存起来。

UIGraphicsBeginImageContextWithOptions(CGSize(), false, UIScreen.mainScreen().scale)
 //// Text Drawing
let textRect = CGRectMake(0, 0, 0, 14)
let textTextContent = NSString(string: skillLocal)
let textStyle = NSParagraphStyle.defaultParagraphStyle().mutableCopy() as! NSMutableParagraphStyle
textStyle.alignment = .Center

let rectanglePath = UIBezierPath(roundedRect: CGRectMake(), cornerRadius:)
fillColor.setFill()
rectanglePath.fill()
skillLabels.append(rect)
textTextContent.drawInRect(rect, withAttributes: textFontAttributes)
let backgroundImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

MultiMedia (多媒体)

多媒体的处理主要已两个领域为例子

分别是实时滤镜和视频后台回放

实时滤镜

实时滤镜的原理分为三步

  • Capture 分线程捕获内容
  • Processing 使用 Core Image 进行图像处理(这一步也可以用 OpenGL 手写图像处理)
  • Preview 在 GLKView 中进行预览

这部分你可以参考 Apple 的 CIFunhouse

后台视频回放(小视频)

和实时滤镜几乎是同一个原理

只是视频源换成了 AVPlayerItem

打造一个更流畅的 iOS App (上)
Share this