iOS 10 by Tutorials 笔记(二)

Chapter 2: Xcode 8 Debugging Improvements

Xcode 8 增加了几项功能,让我们调试起来更加得心应手。Xcode 8 提供的调试工具可以标记竞态条件(Race Conditions)、内存泄露、以及运行时的布局约束问题。

本章主要介绍 Xcode 8 增加的三个调试特性:

  • View Debugger 提供了一些警告来解决约束冲突
  • Thread Sanitizer 全新的 runtime 工具,提醒你线程方面的问题
  • Memory Graph Debugger 以全新图形化的方式标记某一时刻的内存问题

本章通过一个简单的 Demo 来学习 Xcode 8 的新特性,这个是一个 Master-Detail 类型的 Demo,左边是一个 tableView,右边是详情页面:

这个 Demo 的结构也很简单:

  • ColojiTableViewController.swift 用来管理 table view,通过 loadData() 方法从数据源导入 colors 和 emojis,数据源 colojiStore 我们单独放在了 ColojiDataStore.swift 文件中
  • Coloji.swift Model,可以用于设置 cells,数据源也会用到
  • ColojiViewController.swift 对应着 Detail View

Demo 存在四个 Bug:

  1. Memory leak
  2. View bug
  3. Auto Layout Constraint bug
  4. Race condition

我们来一一解决它们

一、Memory Graph debugging

运行应用,上下滚动列表,打开 Memory Usage,观察内存占用率攀升地很快

放在过去,我们或许会去查看 Allocations or Leaks instruments,但现在有了新式武器,并且它的学习曲线也没有那么陡峭。

在 Debug bar 上点击 Debug Memory Graph 按钮

先看左边的 Debug navigator 会发现堆内存上分配的对象列表

上面创建了 171 个 ColojiCellFormatter 和 181 个 ColojiLabel 实例对象显然是不正常的,一个 ColojiCellFormatter 用来配置一个 cell 的外观,而 ColojiLabel 只存在于可见 cell 中。重复创建了这么多的实例对象,显然是没有重用。

我们可以看见一个紫色的警告

点击后跳转到 Issue 导航栏,选中 Runtime

选择其中一个 ColojiCellFormatter 实例

然后在编辑区可以看到一个图形化的循环引用(两个箭头表示相互强引用对方)

选择 Closure captures,然后观察 Memory Inspector

提示 backtrace 默认是关闭的,因为它会增加开销以及和其他工具冲突,所以我们仅在使用时开启。具体打开方法:点击 Edit Scheme,勾选 Malloc Stack,选择 Live Allocations Only

再次运行,就能看到 Backtrace 了。然后看到一个箭头可以跳转到具体调用代码处 cellFormatter.configureCell(cell)

继续查看闭包的定义

lazy var configureCell: (UITableViewCell) -> () = {  
  cell in
  if let colojiCell = cell as? ColojiTableViewCell {
    colojiCell.coloji = self.coloji
  }
}

显然这里犯了一个经典的错误,在闭包中强引用了 self,修复也很简单

[unowned self] cell in

Improving memory usage

虽然修复了内存泄露,但还是创建了很多重复的 ColojiLabel 实例,我们只希望它存在于可见 cell 上。运行程序,在 Debug navigator 中选择 ColojiLabel 实例,可以看到一张张状态图

在上图中选择 ColojiTableViewCell,观察内存地址

多切换几个 ColojiLabel,观察所对应的 ColojiTableViewCell 地址,会发现一个 ColojiTableViewCell 对应了好几个 ColojiLabel。即 cell 上有重复的 label。

重新选择 ColojiLabel 通过 backtrace 找到调用的代码

在 ColojiTableViewCell.swift 中的 addLabel(coloji:)

let label = ColojiLabel()  
label.coloji = coloji  
label.translatesAutoresizingMaskIntoConstraints = false  
contentView.addSubview(label)  
NSLayoutConstraint.activate(  
  [label.leadingAnchor.constraint(equalTo:
    contentView.leadingAnchor),
   label.bottomAnchor.constraint(equalTo:
    contentView.bottomAnchor),
   label.trailingAnchor.constraint(equalTo:
    contentView.trailingAnchor),
   label.topAnchor.constraint(equalTo:
    contentView.topAnchor)
  ])

以上代码是有问题的,cell 每次在屏幕上出现都会被赋值一个 label,我们先把创建 label 的操纵移出来,然后进行判断,如果 label 已经有父类了,即已经在 cell 的 contentView 里了就不要添加了

private let label = ColojiLabel()

private func addLabel(coloji: Coloji) {  
  label.coloji = coloji
  if label.superview == .none {
    label.translatesAutoresizingMaskIntoConstraints = false
    contentView.addSubview(label)
    NSLayoutConstraint.activate([
      label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
      label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
      label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
      label.topAnchor.constraint(equalTo: contentView.topAnchor)
    ])
  } 
}

再次运行就一切正常了

二、Thread Sanitizer

现在我们来排查一下线程问题,打开准备好的应用,运行多次,发现每次展示的内容都不完整,且展示的部分也各不相同。这一次只有三行 cell 显示出来

排查一下代码中的载入数据部分 loadData()

// 1
let group = DispatchGroup()  
// 2
for color in colors {  
  queue.async(
    group: group,
    qos: .background,
    flags: DispatchWorkItemFlags(),
    execute: {
      let coloji = createColoji(color: color)
      self.colojiStore.append(coloji: coloji)
  })
}

for emoji in emoji {  
  queue.async(
    group: group,
    qos: .background,
    flags: DispatchWorkItemFlags(),
    execute: {
      let coloji = createColoji(emoji: emoji)
      self.colojiStore.append(coloji: coloji)
  })
}
// 3
group.notify(queue: DispatchQueue.main) {  
  self.tableView.reloadData()
}
  1. group 用来管理任务的执行顺序
  2. 添加每一个 color 和 emoji 到数组中,代码都是在后台异步执行的,这个操作创建了 coloji,然后添加到 store 中。这些任务都在一个 group 中,所以可以一起完成。
  3. 当 group 中的所有异步任务完成后,刷新 tableView

看来是并发代码导致了线程之间的竞争,从而产生了随机结果。庆幸的是 Xcode 8 提供了新的 Thread Sanitizer 来帮助我们追踪竞态条件(race conditions)。它也在 Issue navigator 上提供了 runtime 反馈

线程问题恐怕是最令我们头疼的难题之一了吧,感谢 Thread Sanitizer

不过要注意 Thread Sanitizer 只支持模拟器调试。更牛逼的是即使运行过程中没发生异常,只要涉及到线程间的竞争,对资源的互斥访问,它也能检测出来。它一直监视着线程访问数据的情况。

除了监视竞态条件(race conditions),Thread Sanitizer 还可以标记线程泄露(thread leaks),在错误的线程使用互斥量(uninitiated mutexes)和锁。

Detecting thread conflicts

使用 Thread Sanitizer 只需要开启它就好了,同样是在 Scheme 中 勾选 Thread Sanitizer

运行,在 Runtime 中可以看到具体的线程问题

我们可以直接定位到问题所在的代码:

data = data + [coloji]  

这行代码不是线程安全的,两个线程可能会在同一时刻进行读写操作。解决方法也很简单,创建一个DispatchQueue,然后同步地去执行相关操作。在 ColojiDataStore.swift 中创建一个串行队列,然后使用它来控制对 data store array 的读写:

let dataAccessQueue = DispatchQueue(label: "com.raywenderlich.coloji.datastore")

func colojiAt(index: Int) -> Coloji {  
  return dataAccessQueue.sync {
    return data[index]
  }
}
func append(coloji: Coloji) {  
  dataAccessQueue.async {
    self.data = self.data + [coloji]
  }
}
var count: Int {  
  return dataAccessQueue.sync {
    return data.count
  }
}

读是同步、写是异步

再次运行,Runtime 中的错误消失了

三、View debugging

还是运行我们预备好的代码,发现一片空白

Xcode 8 现在可以查看运行时的约束警告了,有点类似于在 IB 中给的那些提示。同时在 Debug navigator 中我们可以通过内存地址,类名甚至父类来过滤 View 的层级。在 Object Inspector 栏目可以直接跳转到所选的 View class。Debug snapshots 也比过去更快了,苹果官方宣传提速了 70%。

Debugging the cell

现在来排查我们的 bug,因为是 cell 不显示内容,所以先来看看 ColojiLabel 是否在 cell 的 content view 中。运行,点击 Debug View Hierarchy 按钮

在 Debug navigator 的底部输入 ColojiLabel,过滤出所有的 ColojiLabel

随便选择一个 labels,看看它的 Size Inspector

原来长高都为 0,怪不得看不见,我们切换到面板上的 Object Inspector,因为 ColojiLabel 是私有的,所以这里看不到它的信息。可以在编辑区选择它的父视图,找到 ColojiTableViewCell

点击图中的按钮跳转到代码中找到添加 label 的方法:addLabel(coloji:)

label.coloji = coloji  
if label.superview == .none {  
  contentView.addSubview(label)
  NSLayoutConstraint.activate([
    label.leadingAnchor.constraint(equalTo:
      contentView.leadingAnchor),
    label.bottomAnchor.constraint(equalTo:
      contentView.bottomAnchor),
    label.trailingAnchor.constraint(equalTo:
      contentView.trailingAnchor),
    label.topAnchor.constraint(equalTo:
      contentView.topAnchor)
  ]) 
}

我们的问题是在运行时 label 没有设置 height 和 width,通过这段代码可以看出,虽然激活了 Auto Layout,但并没有禁用 autoresizing masks,而后者会阻止生成正确的约束。我们来修正一下,在 if label.superview == .none 的后面添加:

label.translatesAutoresizingMaskIntoConstraints = false  

问题解决

但详情页面的偏移还未修正

Runtime constraint debugging

我们运行到详情页面,选择 Debug View Hierarchy,在 Runtime 一栏看到一个警告

选中后在 Size Inspector 中查看更多细节

问题很明显:Y 坐标未确定,我们找到布局代码,让它垂直居中即可:

NSLayoutConstraint.activate([  
  emojiLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
  emojiLabel.widthAnchor.constraint(equalTo: view.widthAnchor),
  emojiLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])

再次运行,问题解决

四、Static analyzer enhancements

Xcode 8 大大增强了调试运行时错误的能力,但也没忘了继续增强 static analyzer。不过唯一遗憾的就是不支持 Swift,它只支持 C, C++ 和 Objective-C。

使用 static analyzer 在打开的工程项目中选择 Product\Analyze 就好了,如果有错误,Xcode 会通知你

问题详情

它新加入了一些在使用 MRC 时的有关实例清理的警告,以及非空性检查,对使用 Swift 和 OC 混编的程序员来说尤其有用。


-EOF-

chengway

认清生活真相之后依然热爱它!

Subscribe to Talk is cheap, Show me the world!

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!