本文主要是介绍让你的iOS应用更有生机——六个专业技巧(WWDC 2018 session 233),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
WWDC 2018 session 233: Adding Delight to Your iOS App
概述
这个session主要讲的是六个神奇的专业提示:
- 如何支持外接显示器,让您的应用在更大的屏幕上有更好的表现
- 介绍一种全新的编程模式——布局驱动UI(Layout-Driven UI)
- 更快的启动app,来提升用户体验(Laser-Fast Launches)
- 更流畅的滑动体验,让一切变得美好
- 体验连续互通(Continuity)的神奇,应用使用Handoff是如此的简单
- 一些基本的(Matrix)调试技巧,让您调试更专业
如何支持外接显示器
iOS设备有着令人叹为观止的继承显示器。同时你可以通过添加对外接显示器的支持,来进一步提升应用的体验。我们做了一个demo来帮助说明这一点。外部显示器可以完全镜像复制iOS内部显示器的展示(下图左边是内置,右边是外接,注意看有边框)。
下面是我们的演示程序,很明显这是一个简单的照片查看器。当点击照片缩略图时,页面push动画结束后,照片将会全屏展示。
(竖着全屏的大图没有找到,自行脑补一下,其实跟上图类似,只不过大图沾满了屏幕的中间位置,屏幕两侧依旧是黑色的)
为了充分利用外接显示器的尺寸,我们可以将iPhone旋转到横屏以填充它。
这整个交互体验,我们什么都没有做,看起来效果还不错。但是我们可以让他表现的更好。iOS API允许你单独的为外接显示器上创建完全自定义界面。接下来让我们看几个例子,Keynotes就是一个很好的例子。在外接显示器上,您仍然可以专注于手边的主幻灯片,在iPhone的显示器上,您可以看到演示者笔记,以及下一张幻灯片。
或者您有一块游戏,在iPhone上通常会有虚拟按键,但在外接设备上可以展示的更完整、全面,来得到更真实的体验。
外接显示器设计注意事项
- 除了显而易见的尺寸差别之外,您的iPhone也是个人的。因此在iPhone屏幕上展示的信息类型视为私有。然而,外接显示器通常是其他人也可以看到的,比如电视或者会议室的投影。因此,在外接显示器上展示的信息应该是公开的。
- 虽然iPhone和iPad的内置显示器是交互式的,但外接显示器往往不是。因此,您应该避免在外接显示器上显示可交互的UI元素。
所以把这个想法融入我们的demo,看看会出现什么化学反应。下面这就是我们优化后在外接显示器上的样子。现在我们在外接显示器上全屏展示了选择的照片,而在iPhone屏幕上展示了相册缩略图列表,以及图片的选中状态。
虽然看起来很简单,但确实是非常实用的设计。下面我们将通过三方面向您展示我们是如何做到的。
连接
您怎么知道是否连接了外接显示器?通过UIScreen
的screens
类变量,通过判断该数组包含屏幕的个数,就能判断是否连接了外接显示器。
if UIScreen.screens.count > 1 {
// 连接了外接显示器 ...
}
由于外接显示器可能会随时连接或者断开,所以您要监听UIScreen.didConnectNotification
和UIScreen.didDisconnectNotification
通知,在通知的回调中做出相应的处理即可。
在连接的回调中
// 得到外界屏幕对象
if let externalScreen = UIScreen.screens.last {// 新建一个UIWindowexternalWindow = UIWindow()// 将外线显示器赋值给UIWindow的screen属性externalWindow.screen = externalScreen// 做一些配置,虽然封装了函数,但是这里只是跟平时在iPhone显示器开发一样,新建一个ViewController并赋值给externalWindow的viewController属性configureExternalWindow(externalWindow)externalWindow.isHidden = false
}
在断开连接的回调中,更简单了
// 设置externalWindow为不可见,并且释放内存
externalWindow.isHidden = true
externalWindow = nil
行为
处理连接和断开就是这么的简单。接下来你需要考虑的是修改app在外接显示器上的默认行为。比如当我们在缩略图列表页面点击某一个缩略图时,如果我们没有连接外接显示器,我们创建了PhotoViewController
并将其推送到我们的导航堆栈中。但是当我们连接了外接显示器,我们已经在外接显示器上我们全屏展示了PhotoViewController
,并且我们只让他展示那张照片,只需要做屏幕适配,这很容易。
// 我们在缩略图列表页面点击某一个缩略图时
if inSingleDisplayMode {photoViewController.photo = photonavigationController?.pushViewController(photoViewController, animated: true)
} else {showOnExternalDisplay(photo)
}
过渡
您需要考虑的第三件事是您应该通过优雅的过渡来处理连接的变化。回到demo中,当我们在手机上正全屏展示某一张图片时连接外接显示器,这时的交互应该是,在手机上出现一个Pop
动画返回到缩略图列表页,同时选中刚刚在全屏页展示的图片。在外接显示器上,一个从无到有的动画,来展示刚刚手机上全屏页面展示的图片。下面两图是动画的开始和结束,请自行脑补动画?。
这正是优雅的过渡帮助保留上下文,同时可以帮助用户了解他们在整个app流程中的位置。这就是如何支持外接显示器,非常容易。在设计的时候考虑好不同的上下文环境,并且确保能正常处理连接的变化。更多信息,请查看WWDC 2011 AirPlay以及外接显示器的应用。
Layout-Driven UI
布局驱动UI是一种强大的编程方式,它可以轻松的添加功能,并且易于调试。布局驱动UI帮助我们解决iOS app中的首要问题——管理复杂的UI。我相信包括我在内的所有开发者都遇到过这个问题。当需要从UI控件中得到一些值的时候,你添加了一些代码和手势回调或者添加了更多的更新UI的代码在通知的回调中。版本迭代,突然你发现代码很奇怪,而且很难理解。而且你必须要从这些奇怪、难理解的代码中来复现那些不容易复现的bug。随着功能越来越多,问题就会越来越严重。如果我们换一种方式,将UI刷新添加到布局中,我们可以摆脱这些bug,并且可以方便添加新功能。所以让我们来看看为您的应用添加布局驱动UI的方法。
如何去做?
++我们需要找到并追踪影响UI的状态,然后当状态改变时调用setNeedsLayout
方法来标记需要重新布局,最后在layoutSubviews
中使用此状态并更新UI。++
这个方法非常的简单易用。如果我们将布局驱动UI应用到我们的整个应用中,同时考虑iOS应用中布局、动画、手势三个核心组件,你会发现我们实现了三个组件和谐的工作。
布局
让我们从布局开始,布局是将应用程序的内容放到屏幕显示的过程。但是,我们还是建议您在布局中执行所有其他的UI更新。让我们看一个简单的例子,来说明这一点。这个应用只有一个很酷的emoji表情?显示在屏幕中间,这表示他很酷。如果我觉得他不酷了,那它会隐藏起来(显示黑屏),但如果我们觉得他很酷?,所以他又显示出来了。虽然是个简单的例子,但通过它我们可以理解布局驱动UI是如何工作的。所以然跟我们来看看这个程序的框架,来看看布局驱动UI是什么。
首先,我们通过CoolView
来管理这个很酷的emoji的视图。
- 第一步,我们需要识别和跟踪影响我们UI的状态。我们之前说了,当我们觉得很酷的时候,这个emoji?就展示,如果我们不觉得酷,那它就消失。所以我们需要一个
feelingCool
的变量。 - 第二步,每当这个状态变化时,我们需要通过调用setNeedsLayout来标记需要重新布局。所以我们需要在
feelingCool
变量发生变化的时候,来调用setNeedsLayout
方法(Swift中可以使用didSet
方法,Objective-C中可以重写set
方法或者使用KVO
)。 - 第三步,在
layoutSubviews
中使用此状态并更新UI。这很简单,我们重写layoutSubViews
方法即可。
class MyView : UIView {//...let coolView = CoolView()var feelingCool = true {didSet {setNeedsLayout()} }override func layoutSubviews() {super.layoutSubviews()coolView.isHidden = !feelingCool}
}
就是这样。为您的应用程序添加布局驱动UI只需要做这些事情。虽然这对于简单的例子非常有效,但它更和用于更加复杂的例子。
动画
动画是iOS优秀用户体验的标志。界面上的逼真的动画可以让您的app充满活力。UIKit
拥有一些强大的API来帮你完成不可思议的动画效果。
- UIViewPropertyAnimator
是一个强大的API,去年它通过一系列新功能进行了无论增压。如果要详细了解,请观看WWDC 2017的UIKit高级动画。
- UIView closure API
也是制作好动画的方法。
我们可以将UIViewAnimations
与基于布局驱动UI的应用结合使用。要明确一件事,我们总是要使用beginFromCurrentState
这个动画选项。这告诉UIKit
在进行动画时,即使它是动画中期,也可以获取视图在当前屏幕的位置。因此这让我们可以完成非常精彩、可中断的交互式动画。让我们看一个卡牌游戏的例子。这里我们有一个变量cardsInDeck
来追踪我们牌组中的牌。并且在didSet
函数中去调用setNeedsLayout
。
var cardsInDeck = [CardView]() {didSet {setNeedsLayout()}
}
接下来,我们想要在牌组中放入一张卡片时,我们要做的就是将卡片添加到牌组数组中,这会标记需要重新布局。然后在动画的选项中使用beginFromCurrentState
选项,并且在动画block
中调用layoutIfNeeds
。这样会在合适的时机触发layoutSubviews
函数的调用,播放适当的状态转换动画。
var cardsInDeck = [CardView]() {didSet {setNeedsLayout()}
}
func putCardInDeck(_ card: CardView) {cardsInDeck.append(card)UIView.animate(withDuration: 0.3,delay: 0,options: [.beginFromCurrentState],animations: {self.layoutIfNeeded()}, completion: nil)
}
这里要重点强调一下,注意我们没有为这些动画添加任何的特殊情况。通过这种方式我们得到了自由的动画布局,而不是通过动画block
。所以这就是我们如何添加动画到我们基于布局驱动UI的应用。
手势
提到手势,我们肯定会想到UIGestureRecognizer
,这个优秀的为视图添加手势交互的UIKit API
。UIKit
提供了许多非常具体的UIGestureRecongnizer
的子类。比如pan
、pinches
、swipe
、rotation
等。当然他们也是高度可定制的。如果有什么其他的想法,可以自己写一个子类继承UIGestureRecognizer
。
查看UIGestureRecognizer
及其子类的API,了解离散和连续手势之间的区别是很重要的。
离散手势告诉我们一个事件发生了。从Possible
状态开始,快速的进入到Recognized
状态。所以在交互中不会告诉你各个阶段。
Possible->Recognized
连续手势,为您提供更高的保真度。跟离散手势一样,也是从Possible
状态开始,但随着它们被识别,他们会变成Begin
状态。当他们跟踪时,他们会进入Changed
状态,在这个状态中,你会持续的收到手势的事件流。最后手势完成了,会进入Ended
状态。
Possible->Begin->Changed->Ended
我们最喜欢的连续手势之一UIPanGestureRecognizer
。此外,还有两个很棒的功能可以帮助您充分利用它。当滑动时,
translationInView
告诉您手势相对于视图的位置velocityInView
告诉您手势的移动速度
并且,这对于在手势和后续动画之间切换速度非常有用。如果想了解更多的话,请查看WWDC 2014 构建可中断和响应交互。
通过UIPanGestureRecognizer
可以来构建上个demo说的卡牌拖动的行为。让我们看一下使用布局驱动UI是如何做到这一点的。同样,我们有一个局部变量cardsToOffsets
来追踪滑动过程中每张卡牌的偏移量。当它发生变化时,我们就会调用setNeedsLayout
方法。接下来,我们在UIPanGestureRecognizer
的回调函数中我们得到当前的变化和手势,并将手势赋值给一张卡卡牌。然后我们在字典中,增加这个卡牌的偏移量。最终在layoutSubviews
中,我们将根据字典中的偏移量来更新卡牌的位置。
var cardsToOffsets = [CardView : CGPoint]() {didSet {setNeedsLayout()}
}
@objc func handleCardPan(_ pan: UIPanGestureRecognizer) {if let card = pan.view as? CardView,let currentOffset = cardsToOffsets[card] {let translation = pan.translation(in: self)cardsToOffsets[card] = CGPoint(x: currentOffset.x + translation.x,y: currentOffset.y + translation.y)pan.setTranslation(.zero, in: self)}
}
override func layoutSubviews() {super.layoutSubviews()//...for card in cards {if let offset = cardsToOffsets[card] {card.frame.origin = offset} }
}
需要注意的是,除了传统的布局驱动UI之外,我们实际上并没有做什么特别的事情。我们只是恰好有一个手势驱动的状态,并且在layoutSubviews
中做出了响应。事实上,如果你再整个app开发中都遵守这个模式,你会发现很多交互适配起来会变得更容易。这就是布局驱动UI,查找并追踪所有影响UI变化的状态,并且在此状态发生变化时,调用setNeedsLayout
方法。最后在layoutSubviews
的实现中,确保根据追踪状态更新了视图。
再回忆一下这个模式所需要做的事情:
1.找到并追踪所有影响视图的状态
2.当状态发生变化时,调用setNeedsLayout
方法来编辑需要重新布局
3.在layoutSubviews
函数中根据状态更新UI
更快的app启动
iOS的体验就是响应。并且您需要提供更快的响应,来提升用户体验。当用户点击应用图标后,最直观影响用户体验的就是你应用的启动时长。为了帮助您优化它,我们从五方面来剖析启动时长。
1.进程分叉(Progress Forking)
2.动态链接(Dynamic Linking)
3.构建UI(UI Construction)
4.第一帧(First Frame)
5.扩展启动操作(Extended Launch Actions)
进程分叉
对于流程分叉,它真的很复杂。如果要优化这块,你需要阅读fork
和exec
相关的文档,并且对POSIX
的基础知识非常熟悉。不不不,开玩笑的,iOS系统将为您处理进程分叉(皮一下很开心么…)。
动态链接
在这个阶段,我们将分配内存以开始启动应用程序。然后链接库和框架,初始化Swift
,Objective-C
,Foundation
等,以及静态对象的初始化。通常情况下,这部分的时间会占到整个应用启动时间的40%~50%。要注意的是,在这个阶段,你的代码还没有开始执行,因此了解如何优化这个阶段是至关重要的。优化动态链接阶段请务必小心谨慎。因为它占用了应用程序大部分的启动时间。建议如下:
尽可能避免重复的代码
如果你有冗余的函数、对象或者结构,请把他们删除,不要做多余的任务。
要限制使用第三方库
iOS的官方库都是有缓存策略的,在你使用之前很可能同样被其他应用使用并加载到内存中了。但是第三方库没有缓存,即便其他应用使用了跟你一模一样的第三方库。
避免使用静态初始化器以及使用
+(void)initalize
和+(void)load
之类的方法因为这些都必须在你的应用程序做任何事情之前完成。如果想了解关于这一部分的内容,请查看WWDC 2017 启动时间的过去、现在和将来。
构建UI
启动的下一个阶段是构建UI。此时,您正在准备UI,构建ViewControllers
。系统正在恢复状态,并且加载您的首选项,同时您在加载您所需要的数据。如果你希望优化构建UI阶段,那么
要尽可能快的从
willFinishLaunching
、didFinishLaunching
、didBecomeActive
这几个UI程序的激活方法中返回。因为
UIKit
会一直等待这些函数的执行,当执行结束时才会把应用标记为active
。避免写文件的操作
因为写文件是阻塞的,并且需要调用系统函数
避免读取非常大的资源文件
请只加载当前需要的数据,按需加载
请检查数据库是否干净
保持清洁的数据库是一个不错的主意。如果您使用的是像
Core Data
这样的数据库,请考虑优化您的框架。如果您使用的是SQLite
之类,请考虑定期清空数据库,例如在应用更新的时候。
第一帧
下一阶段是创建你的第一帧。此时核心动画正在进行必要的渲染以使该帧准备就绪。绘制文字、加载和解码要展示在界面上的图片。
在准备第一帧时,你要特别注意只需要准备启动期间的UI。如果用户还没到达其他特定的页面,请不要加载。你可以通过懒加载,在使用的时候进行加载。此外,请注意不要隐藏不该看见的图层或者视图,因为即使隐藏了图册或者视图,仍需要一些花销。所以,只需要加载第一帧要显示的UI即可。
扩展启动操作
扩展启动操作,这些都是可以推迟到启动之后的,以便降低启动时长。虽然您的应用可能会对这些任务做出响应,但它可能不是必要的。这个阶段实际上要有限考虑下一步做什么,立即提供屏幕上需要展示的内容。此外,如果您需要从服务器上加载内容,请务必考虑您可能处在一个弱网环境下。如果需要的话,请提供一个UI占位图。
ABM
以上就是要介绍的五个组件,不过今天还有一个其他的内容,就是Always Be Measuring。您必须了解您app的启动时间花费到哪里了,所以测量启动的时间的工具就是Time Profiler。每当你修改了可能会影响启动时长的代码时,您都需要重新进行测试,并且统计平均值。不要依赖一次的数据来评估启动时长。因此降低启动时长,快速响应,只加载那些必须的,并且测量*3(重要的测量说三遍)。
更流畅的滑动体验
滑动时iOS用户体验中关键并且占比很高的一部分。iPhone和iPad的屏幕可以完美的呈现你app应有的样子。所以,重要的是,你需要努力让你的应用程序内容在屏幕上保持滑动的错觉。在Apple内部,我们有一句话,你的应用应该像黄油一样流畅,像德芙一样丝滑(后半句是我自己加的…)。
但是时不时的存在一些问题,让它吃起来不像黄油,而是更像花生酱。
你会有同感,你的app有时会变得迟缓或者卡顿。那app变得缓慢的原因是什么呢???
这种缓慢的行为,我们称之为丢帧。所以我们需要了解为什么会丢帧。这里有两个关键点
- 大量计算
- 太多复杂的图形组件
计算
如何知道是否做了太多的计算?不错,Instruments
中的Time Profiler
是很好的工具。它可以帮助你了解,你的代码运行占用了多少CPU时间。我们鼓励您去看看WWDC 2016 如何使用时间分析工具。所以,一旦您使用Timer Profiler
工具确定了这些热点,我们就会为您提供一些优化技巧。
使用
UICollectionView
和UITableView
的预加载(Prefetching、iOS 10.0+)这些API将在列表滚动到特定单元格的时候告诉您,并且给您提供预加载数据的机会,详细请见WWDC 2016 UICollectionView的新特性。
尽可能多的任务从主队列中转移到后台队列中,释放主队列来更新UI或者接受用户输入。那么什么任务可以放到后台队列中呢?
首先,网络请求和磁盘IO操作,一定不要放到主线程中。其次,一些你想不到的任务也可以放到子线程,比如图片绘制以及文字大小计算。
UIGraphicsImageRenderer
和字符串计算大小,都可以安全的放到子线程中。
图形组件
虽然我们已经优化了计算,但是我们的图形系统仍然可能存在问题。很庆幸,我们有另外的一个强大的工具Core Animation
。它可以精准的查看FPS
值,同时也可以看到GPU的利用率。
如果想要了解它,请查看WWDC 2014 iOS应用程序中的高级图形和动画。
一旦确定了你的应用是图形绑定的,我们可以给您介绍一些我们的 研究成果。通常,你的应用产生图形绑定是由于两个原因,
视觉效果
视觉效果包括模糊(Blur)效果和活力(Vibrancy)效果,他们的消耗很大,所以您应该在您的app中优雅的使用他们。并且您应该避免模糊效果叠加模糊效果使用,因为这会导致GPU超负荷运转,从而影响app的速度。
遮掩或者裁剪
您应该尽可能避免遮掩或者裁剪。比起使用
View
、CALayer
的clipsToBounds
、masksToBounds
方法,我们更推荐您通过在视图上放置不透明的内容来实现相同的视觉效果。
总结
- 使用
Timer Profiler
和Core Animation
工具来测试你的app - 尽可能少在主线程做与UI无关的事情,同时做一些预处理
- 有节制的使用视觉效果以及遮掩、裁剪
想了解更多,请看WWDC 2015 深入分析。
体验连续互通的神奇
连续互通是Apple平台上最神奇的体验之一。而Handoff则是让用户满意的最佳方式。从一台设备上获取任务,并无缝的切换到其他设备上,这种体验是很棒的,同时支持iOS、Mac OS、watch OS。它不需要互联网连接,因为它使用的是点对点连接。最重要的是,它设置起来非常简单。例如,我们在Mac上打开了一个文档,但我不得不或者希望能从iPad上打开相同的文档,我就可以通过点击Dock上的图标来实现。
或者我正在通过我的Apple watch来浏览照片,并且找到了一张照片,而我想查看该相册下的所有照片,那么我就可以直接在我的iPhone上直接查看这张照片,无需搜索那张照片。
Handoff功能非常强大,它可以为用户节省大量的切换时间。接下来要为您展示,实现该功能是多么的简单。它所有的操作都是简历在NSUserActivity API
之上的。NSUserActivity
表示当前您正在执行的状态或活动。在这里例子里,我们正在iPhone上撰写电子邮件。
当这个活动创建了,所有附近登录同一个iCloud账户的设备都会显示的Handoff状态都变为可用。在Mac上,你会看到Dock的左侧有一个图标。
点击该图标,活动就会转移到Mac上,Mail就会启动并从你离开的地方继续开始。
那么我们来看一下设置的代码。
原始设备设备
在原始的设备上,首先要根据类型创建一个NSUserActivity
对象,这个类型表示着用户正在执行的活动类型。然后设置标题,设置isEligibleForHandoff
为true
。接着按照自身需求,填写userInfo
字典,字典里面填写所有能够恢复到当前状态的信息。这是一个关于视频的例子,我们在userInfo
中包含了视频ID,以及当前播放的时间。最后将这个活动赋值给控制器的userActivity
属性。当控制器展示的时候,该活动就会变成当前的活动。
let activity = NSUserActivity(activityType:"com.apple.developer.video")
activity.title = "Adding Delight to your iOS App"
activity.isEligibleForHandoff = true
activity.userInfo = ["session-id" : "2018-223", "currentTime" : 2340]
userActivity = activity
需要继续执行的设备
在需要继续执行的设备上,首先,您的app需要声明对您创建的活动类型的支持。然后需要实现两个UIApplicationDelegate
的回调。第一个是application(_:willContinueUserActivityWithType:)
,该方法在您点击图标切换的时候,就会调用。虽然这个时候我们还没有准备好NSUserActivity
对象,但是您知道这个活动的类型是什么,所以您可以开始准备UI了。收到该消息不久,您就会收到第二个消息application(_:continue:restorationHandler:)
,这个消息中包含了完整的NSUserActivity
对象,从现在开始,您可以再设备上设置并继续体验了。
延续流(continuation streams)
如果userInfo
这个字典无法满足您的需求,我们在NSUserActivity
中提供了延续流(continuation streams)。您需要做的就是将supportsContinuationStreams
设置为true
。然后再需要继续执行的设备上,调用NSUserActivity
中的getContinuationStreams(completionHandler:)
方法,该方法会为您提供输入流和输出流。回到原始的设备上,NSUserActivity
的代理方法中有一个userActivity(_:didReceive:outputStream:)
回调,来提供需要执行的设备的输入流和输出流。通过这种方式,您可以在原始的设备和需要继续执行的设备上建立双向通信。但您需要尽快的完成这里的操作,因为用户可能会随时将两台设备分开。关于流更多的信息,请查看流编程指南。
基于文档的应用
延续流非常适合那些不适合放在字典里面的内容,例如音乐、图像、视频等。但是对于基于文档的app,切换其实更容易。因为你不需要做那么多的操作。UIDecument
和NSDocument
会自动创建NSUserActivity
对象已表示当前正在编辑的文档,并且这适用于所有存储在iCloud上的文档。您所需要做的就是在Info.plist进行相应的配置。
原生应用到web的切换
除了应用到应用的切换之外,我们还支持应用到Web的切换。如果您针对于原生应用有着相同出色Web体验的话,当要继续执行的设备上没有安装对应的应用,您可以切换到浏览器中,并在浏览器中继续您的活动。您需要做的就是在NSUserActivity
对象中设置webpageURL
属性。
Web到原生应用的切换
Handoff同样支持Web到原生应用的切换。您需要在web服务器上配置已授权的应用ID列表。然后在app中添加associated-domains
权利。然后用户就可以无缝的从您的网页中切换到iOS应用上继续操作了。有关于这方面的内容,请查看WWDC2014中如何在iOS和Mac OS上使用Handoff。
总结
这就是Handoff,在您的app中尝试利用它,让用户使用起来更方便。同时NSUserActivity API
在整个系统体验中都会用到。例如,Spotlight Search
以及最新的Siri Shortcuts
,如果有兴趣请查看对应的WWDC视频,介绍搜索API以及介绍Siri Shortcuts。
一些基本的调试技巧
你写了很棒的应用以及用户体验,但是您还是会时不时的需要去调试一些问题。所以我们为您准备了一些Matrix级别的调试技巧。这些技巧虽然非常的实用,但是提交到App Store会被拒的。
- 要有一个侦探的心态,分析如何去处理程序中发现的问题
- 如何通过视图和控制器来调试问题
- 通过LLDB来调试应用中的状态问题
- 一些技巧来解决难以理解的内存问题
侦探心态
首先,当您在分析程序中的问题时,您要知道您app的预期运行结果是什么,然后验证一下是否正确。这对调试问题来说,是非常关键的一步。一旦确认了哪些地方与预期不符,你就可以从这些地方入手,来寻找线索。您将使用本文中提到的工具来测试对象和结构。然后通过修改代码来测试您的修改是否正确。
通过视图和控制器来调试
接下来,我们来看一个真实的案例。这个是苹果的截屏编辑器。最近,我们在调试一个问题,就是截屏的画笔工具消失了,这非常糟糕。如何调试呢?在Xcode中内置了View Debugger
,用于调试应用的视图问题。该工具入口,位于底部调试区域的工具栏中。
Xcode通过3D向您展示整个视图层级的结构。正如下图所示,画笔工具还在,只不过前面的全屏视图挡住了。
所以我们需要去看看我和如何构建的UI,以及为什么顺序出现了问题。
View Debugger
工具能非常直观的调试界面问题,当然我们还有一些其他的工具同样可以解决这个问题(下面说的是私有API,所以审核会被拒)。
- -[UIView recursiveDescription]
- -[UIView _parentDescription]
- +[UIViewController _printHierarchy]
这些都是Objective-C
方法,如果使用swift开发的话,需要通过
settings set target.language objective-c
命令把调试器设置为Objective-C
模式。
recursiveDescription
方法会打印该视图和其子视图的层次结构以及视图中一些可以帮助你了解布局的属性。我们想要找到
_parentDescription
方法会打印该视图和其父视图的层次结构
_printHierarchy
类方法会打印控制器的层次结构(正在展示的控制器,展示过的控制器,父控制器,子控制器等)
LLDB调试技巧
有时你可能需要解决一些非UI的问题,为此,我们提供了一些很棒的状态调试技巧。LLDB的expr
命令可以让你在调试器中运行任意代码(用法expr myStruct.doSomething()
)。这对调试非常有用。您可以调用结构(类)中的函数(方法),得到对象的属性,以及更好的诊断当前程序运行的是什么。如果想了解更多,请查看WWDC 2012 如何利用LLDB进行调试以及WWDC 2014 利用LLDB调试Swift的高级技巧。接下里介绍一些表达式命令。
dump
dump
可以打印所有的Swift对象和结构属性。例如,我现在有一个视图,视图里面包括了一些文本框及图片视图,但是有一个文本框不见了,我们来通过运行在文本框的父视图上运行dump
命令,来查看一下出了什么问题。
我们找到了没有展示出来的文本框,因为它的frame与图像视图的frame有重叠,所以可能是被挡住了。所以我们去查看下布局代码,进行调整。
如果你使用Objective-C
的话,通过-[NSObject _ivarDescription]
方法,可以打印出Objective-C
对象的所有成员变量。
断点
使用dump
和_ivarDescription
是调试bug的好方法。我们为您提供另外一个调试技巧——断点。断点允许您在任意执行状态暂停程序,并运行命令。而使用LLDB命令行或者Xcode UI,你可以再运行断点之前增加条件,并在每一次遇到断点时运行命令。断点是调试过程中重要的一部分。
调试复杂的内存问题
当我们遇到复杂的内存问题,丝毫没有头绪,该怎么办呢?此时你就可以使用Xcode中的Memory Debugger
。这个工具能帮助您准确的查看应用程序是如何使用内存的。我们之前解决了一个ViewController
内存泄漏的问题。我们看到,block对该ViewController
持有。通过打开Malloc stack logging
选项,我们能够追踪该block什么时候创建的。
放大之后,我们可以看到这个block确实是由该ViewController
创建的,同时,这个block持有了ViewController
,所以产生了循环引用。
Xcode中图形化内存调试器是一个很好的工具,您可以使用该工具来解决相关问题。更多信息,请查看WWDC 2017 Xcode 9调试技巧。
总结
- 侦探的心态
- 使用Xcode中
View Debugger
和Memory Debugger
- 使用LLDB的
expr
、dump
命令 - 上述其他的调试技巧
结尾
以上就是今天要讨论的六大技巧,其实针对于其中某些技巧,可能需要去翻一下之前的WWDC相关内容。总之,还是学以致用,让应用变得更好用。
这篇关于让你的iOS应用更有生机——六个专业技巧(WWDC 2018 session 233)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!