让你的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内部显示器的展示(下图左边是内置,右边是外接,注意看有边框)。
image
下面是我们的演示程序,很明显这是一个简单的照片查看器。当点击照片缩略图时,页面push动画结束后,照片将会全屏展示。
image
(竖着全屏的大图没有找到,自行脑补一下,其实跟上图类似,只不过大图沾满了屏幕的中间位置,屏幕两侧依旧是黑色的)
为了充分利用外接显示器的尺寸,我们可以将iPhone旋转到横屏以填充它。
image
这整个交互体验,我们什么都没有做,看起来效果还不错。但是我们可以让他表现的更好。iOS API允许你单独的为外接显示器上创建完全自定义界面。接下来让我们看几个例子,Keynotes就是一个很好的例子。在外接显示器上,您仍然可以专注于手边的主幻灯片,在iPhone的显示器上,您可以看到演示者笔记,以及下一张幻灯片。
image
或者您有一块游戏,在iPhone上通常会有虚拟按键,但在外接设备上可以展示的更完整、全面,来得到更真实的体验。
image

外接显示器设计注意事项

  • 除了显而易见的尺寸差别之外,您的iPhone也是个人的。因此在iPhone屏幕上展示的信息类型视为私有。然而,外接显示器通常是其他人也可以看到的,比如电视或者会议室的投影。因此,在外接显示器上展示的信息应该是公开的。
  • 虽然iPhone和iPad的内置显示器是交互式的,但外接显示器往往不是。因此,您应该避免在外接显示器上显示可交互的UI元素。
    image
    所以把这个想法融入我们的demo,看看会出现什么化学反应。下面这就是我们优化后在外接显示器上的样子。现在我们在外接显示器上全屏展示了选择的照片,而在iPhone屏幕上展示了相册缩略图列表,以及图片的选中状态。
    image
    image
    虽然看起来很简单,但确实是非常实用的设计。下面我们将通过三方面向您展示我们是如何做到的。

连接

您怎么知道是否连接了外接显示器?通过UIScreenscreens类变量,通过判断该数组包含屏幕的个数,就能判断是否连接了外接显示器。

if UIScreen.screens.count > 1 {
// 连接了外接显示器 ...
}

由于外接显示器可能会随时连接或者断开,所以您要监听UIScreen.didConnectNotificationUIScreen.didDisconnectNotification通知,在通知的回调中做出相应的处理即可。

在连接的回调中

//  得到外界屏幕对象
if let externalScreen = UIScreen.screens.last {
    //  新建一个UIWindow
    externalWindow = 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 = photo
    navigationController?.pushViewController(photoViewController, animated: true)
} else {
    showOnExternalDisplay(photo)
}

过渡

您需要考虑的第三件事是您应该通过优雅的过渡来处理连接的变化。回到demo中,当我们在手机上正全屏展示某一张图片时连接外接显示器,这时的交互应该是,在手机上出现一个Pop动画返回到缩略图列表页,同时选中刚刚在全屏页展示的图片。在外接显示器上,一个从无到有的动画,来展示刚刚手机上全屏页面展示的图片。下面两图是动画的开始和结束,请自行脑补动画?。
image
image
这正是优雅的过渡帮助保留上下文,同时可以帮助用户了解他们在整个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 APIUIKit提供了许多非常具体的UIGestureRecongnizer的子类。比如panpinchesswiperotation等。当然他们也是高度可定制的。如果有什么其他的想法,可以自己写一个子类继承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)

进程分叉

对于流程分叉,它真的很复杂。如果要优化这块,你需要阅读forkexec相关的文档,并且对POSIX的基础知识非常熟悉。不不不,开玩笑的,iOS系统将为您处理进程分叉(皮一下很开心么…)。

动态链接

在这个阶段,我们将分配内存以开始启动应用程序。然后链接库和框架,初始化SwiftObjective-CFoundation等,以及静态对象的初始化。通常情况下,这部分的时间会占到整个应用启动时间的40%~50%。要注意的是,在这个阶段,你的代码还没有开始执行,因此了解如何优化这个阶段是至关重要的。优化动态链接阶段请务必小心谨慎。因为它占用了应用程序大部分的启动时间。建议如下:

  • 尽可能避免重复的代码

    如果你有冗余的函数、对象或者结构,请把他们删除,不要做多余的任务。

  • 要限制使用第三方库

    iOS的官方库都是有缓存策略的,在你使用之前很可能同样被其他应用使用并加载到内存中了。但是第三方库没有缓存,即便其他应用使用了跟你一模一样的第三方库。

  • 避免使用静态初始化器以及使用+(void)initalize+(void)load之类的方法

    因为这些都必须在你的应用程序做任何事情之前完成。如果想了解关于这一部分的内容,请查看WWDC 2017 启动时间的过去、现在和将来

构建UI

启动的下一个阶段是构建UI。此时,您正在准备UI,构建ViewControllers。系统正在恢复状态,并且加载您的首选项,同时您在加载您所需要的数据。如果你希望优化构建UI阶段,那么

  • 要尽可能快的从willFinishLaunchingdidFinishLaunchingdidBecomeActive这几个UI程序的激活方法中返回。

    因为UIKit会一直等待这些函数的执行,当执行结束时才会把应用标记为active

  • 避免写文件的操作

    因为写文件是阻塞的,并且需要调用系统函数

  • 避免读取非常大的资源文件

    请只加载当前需要的数据,按需加载

  • 请检查数据库是否干净

    保持清洁的数据库是一个不错的主意。如果您使用的是像Core Data这样的数据库,请考虑优化您的框架。如果您使用的是SQLite之类,请考虑定期清空数据库,例如在应用更新的时候。

第一帧

下一阶段是创建你的第一帧。此时核心动画正在进行必要的渲染以使该帧准备就绪。绘制文字、加载和解码要展示在界面上的图片。

在准备第一帧时,你要特别注意只需要准备启动期间的UI。如果用户还没到达其他特定的页面,请不要加载。你可以通过懒加载,在使用的时候进行加载。此外,请注意不要隐藏不该看见的图层或者视图,因为即使隐藏了图册或者视图,仍需要一些花销。所以,只需要加载第一帧要显示的UI即可。

扩展启动操作

扩展启动操作,这些都是可以推迟到启动之后的,以便降低启动时长。虽然您的应用可能会对这些任务做出响应,但它可能不是必要的。这个阶段实际上要有限考虑下一步做什么,立即提供屏幕上需要展示的内容。此外,如果您需要从服务器上加载内容,请务必考虑您可能处在一个弱网环境下。如果需要的话,请提供一个UI占位图。

ABM

以上就是要介绍的五个组件,不过今天还有一个其他的内容,就是Always Be Measuring。您必须了解您app的启动时间花费到哪里了,所以测量启动的时间的工具就是Time Profiler。每当你修改了可能会影响启动时长的代码时,您都需要重新进行测试,并且统计平均值。不要依赖一次的数据来评估启动时长。因此降低启动时长,快速响应,只加载那些必须的,并且测量*3(重要的测量说三遍)。

更流畅的滑动体验

滑动时iOS用户体验中关键并且占比很高的一部分。iPhone和iPad的屏幕可以完美的呈现你app应有的样子。所以,重要的是,你需要努力让你的应用程序内容在屏幕上保持滑动的错觉。在Apple内部,我们有一句话,你的应用应该像黄油一样流畅,像德芙一样丝滑(后半句是我自己加的…)。
image
但是时不时的存在一些问题,让它吃起来不像黄油,而是更像花生酱。
image
你会有同感,你的app有时会变得迟缓或者卡顿。那app变得缓慢的原因是什么呢???

这种缓慢的行为,我们称之为丢帧。所以我们需要了解为什么会丢帧。这里有两个关键点

  • 大量计算
  • 太多复杂的图形组件

计算

如何知道是否做了太多的计算?不错,Instruments中的Time Profiler是很好的工具。它可以帮助你了解,你的代码运行占用了多少CPU时间。我们鼓励您去看看WWDC 2016 如何使用时间分析工具。所以,一旦您使用Timer Profiler工具确定了这些热点,我们就会为您提供一些优化技巧。
image

  • 使用UICollectionViewUITableView的预加载(Prefetching、iOS 10.0+)

    这些API将在列表滚动到特定单元格的时候告诉您,并且给您提供预加载数据的机会,详细请见WWDC 2016 UICollectionView的新特性

  • 尽可能多的任务从主队列中转移到后台队列中,释放主队列来更新UI或者接受用户输入。那么什么任务可以放到后台队列中呢?

    首先,网络请求和磁盘IO操作,一定不要放到主线程中。其次,一些你想不到的任务也可以放到子线程,比如图片绘制以及文字大小计算。UIGraphicsImageRenderer和字符串计算大小,都可以安全的放到子线程中。

图形组件

虽然我们已经优化了计算,但是我们的图形系统仍然可能存在问题。很庆幸,我们有另外的一个强大的工具Core Animation。它可以精准的查看FPS值,同时也可以看到GPU的利用率。
image
如果想要了解它,请查看WWDC 2014 iOS应用程序中的高级图形和动画

一旦确定了你的应用是图形绑定的,我们可以给您介绍一些我们的 研究成果。通常,你的应用产生图形绑定是由于两个原因,

  • 视觉效果

    视觉效果包括模糊(Blur)效果和活力(Vibrancy)效果,他们的消耗很大,所以您应该在您的app中优雅的使用他们。并且您应该避免模糊效果叠加模糊效果使用,因为这会导致GPU超负荷运转,从而影响app的速度。

  • 遮掩或者裁剪

    您应该尽可能避免遮掩或者裁剪。比起使用ViewCALayerclipsToBoundsmasksToBounds方法,我们更推荐您通过在视图上放置不透明的内容来实现相同的视觉效果。

总结

  • 使用Timer ProfilerCore Animation工具来测试你的app
  • 尽可能少在主线程做与UI无关的事情,同时做一些预处理
  • 有节制的使用视觉效果以及遮掩、裁剪

想了解更多,请看WWDC 2015 深入分析

体验连续互通的神奇

连续互通是Apple平台上最神奇的体验之一。而Handoff则是让用户满意的最佳方式。从一台设备上获取任务,并无缝的切换到其他设备上,这种体验是很棒的,同时支持iOS、Mac OS、watch OS。它不需要互联网连接,因为它使用的是点对点连接。最重要的是,它设置起来非常简单。例如,我们在Mac上打开了一个文档,但我不得不或者希望能从iPad上打开相同的文档,我就可以通过点击Dock上的图标来实现。
image
或者我正在通过我的Apple watch来浏览照片,并且找到了一张照片,而我想查看该相册下的所有照片,那么我就可以直接在我的iPhone上直接查看这张照片,无需搜索那张照片。
image
Handoff功能非常强大,它可以为用户节省大量的切换时间。接下来要为您展示,实现该功能是多么的简单。它所有的操作都是简历在NSUserActivity API之上的。NSUserActivity表示当前您正在执行的状态或活动。在这里例子里,我们正在iPhone上撰写电子邮件。
image
当这个活动创建了,所有附近登录同一个iCloud账户的设备都会显示的Handoff状态都变为可用。在Mac上,你会看到Dock的左侧有一个图标。
image
点击该图标,活动就会转移到Mac上,Mail就会启动并从你离开的地方继续开始。
image
image
那么我们来看一下设置的代码。

原始设备设备

在原始的设备上,首先要根据类型创建一个NSUserActivity对象,这个类型表示着用户正在执行的活动类型。然后设置标题,设置isEligibleForHandofftrue。接着按照自身需求,填写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,切换其实更容易。因为你不需要做那么多的操作。UIDecumentNSDocument会自动创建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,用于调试应用的视图问题。该工具入口,位于底部调试区域的工具栏中。
image
Xcode通过3D向您展示整个视图层级的结构。正如下图所示,画笔工具还在,只不过前面的全屏视图挡住了。
image
所以我们需要去看看我和如何构建的UI,以及为什么顺序出现了问题。

View Debugger工具能非常直观的调试界面问题,当然我们还有一些其他的工具同样可以解决这个问题(下面说的是私有API,所以审核会被拒)。

  • -[UIView recursiveDescription]
  • -[UIView _parentDescription]
  • +[UIViewController _printHierarchy]

这些都是Objective-C方法,如果使用swift开发的话,需要通过
settings set target.language objective-c命令把调试器设置为Objective-C模式。

recursiveDescription方法会打印该视图和其子视图的层次结构以及视图中一些可以帮助你了解布局的属性。我们想要找到
image
_parentDescription方法会打印该视图和其父视图的层次结构
image
_printHierarchy类方法会打印控制器的层次结构(正在展示的控制器,展示过的控制器,父控制器,子控制器等)
image

LLDB调试技巧

有时你可能需要解决一些非UI的问题,为此,我们提供了一些很棒的状态调试技巧。LLDB的expr命令可以让你在调试器中运行任意代码(用法expr myStruct.doSomething())。这对调试非常有用。您可以调用结构(类)中的函数(方法),得到对象的属性,以及更好的诊断当前程序运行的是什么。如果想了解更多,请查看WWDC 2012 如何利用LLDB进行调试以及WWDC 2014 利用LLDB调试Swift的高级技巧。接下里介绍一些表达式命令。

dump

dump可以打印所有的Swift对象和结构属性。例如,我现在有一个视图,视图里面包括了一些文本框及图片视图,但是有一个文本框不见了,我们来通过运行在文本框的父视图上运行dump命令,来查看一下出了什么问题。
image
我们找到了没有展示出来的文本框,因为它的frame与图像视图的frame有重叠,所以可能是被挡住了。所以我们去查看下布局代码,进行调整。

如果你使用Objective-C的话,通过-[NSObject _ivarDescription]方法,可以打印出Objective-C对象的所有成员变量。

image

断点

使用dump_ivarDescription是调试bug的好方法。我们为您提供另外一个调试技巧——断点。断点允许您在任意执行状态暂停程序,并运行命令。而使用LLDB命令行或者Xcode UI,你可以再运行断点之前增加条件,并在每一次遇到断点时运行命令。断点是调试过程中重要的一部分。
image

调试复杂的内存问题

当我们遇到复杂的内存问题,丝毫没有头绪,该怎么办呢?此时你就可以使用Xcode中的Memory Debugger。这个工具能帮助您准确的查看应用程序是如何使用内存的。我们之前解决了一个ViewController内存泄漏的问题。我们看到,block对该ViewController持有。通过打开Malloc stack logging选项,我们能够追踪该block什么时候创建的。
image
放大之后,我们可以看到这个block确实是由该ViewController创建的,同时,这个block持有了ViewController,所以产生了循环引用。
image
Xcode中图形化内存调试器是一个很好的工具,您可以使用该工具来解决相关问题。更多信息,请查看WWDC 2017 Xcode 9调试技巧

总结

  • 侦探的心态
  • 使用Xcode中View DebuggerMemory Debugger
  • 使用LLDB的exprdump命令
  • 上述其他的调试技巧

结尾

以上就是今天要讨论的六大技巧,其实针对于其中某些技巧,可能需要去翻一下之前的WWDC相关内容。总之,还是学以致用,让应用变得更好用。

猜你喜欢

转载自blog.csdn.net/TuGeLe/article/details/81121033
233