继续没事翻翻书,做做笔记,因为整本书都还在 Early Access 的状态,出来哪章写哪章,稍后再调整顺序吧。
Xcode 8 增加了几项功能,让我们调试起来更加得心应手。Xcode 8 提供的调试工具可以标记竞争危害(Race Conditions)、内存泄露、以及运行时的布局约束问题。
本章主要介绍 Xcode 8 增加的三个调试特性:
本章通过一个简单的 Demo 来学习 Xcode 8 的新特性,这个是一个 Master-Detail 类型的 Demo,左边是一个 tableView,右边是详情页面:
这个 Demo 的结构也很简单:
loadData()
方法从数据源导入 colors 和 emojis,数据源 colojiStore 我们单独放在了 ColojiDataStore.swift 文件中 Demo 存在四个 Bug:
我们来一一解决它们
运行应用,上下滚动列表,打开 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
虽然修复了内存泄露,但还是创建了很多重复的 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) ]) } }
再次运行就一切正常了
现在我们来排查一下线程问题,打开准备好的应用,运行多次,发现每次展示的内容都不完整,且展示的部分也各不相同。这一次只有三行 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() }
看来是并发代码导致了线程之间的竞争,从而产生了随机结果。庆幸的是 Xcode 8 提供了新的 Thread Sanitizer 来帮助我们追踪竞争危害(race conditions)。它也在 Issue navigator 上提供了 runtime 反馈
线程问题恐怕是最令我们头疼的难题之一了吧,感谢 Thread Sanitizer
不过要注意 Thread Sanitizer 只支持模拟器调试。更牛逼的是即使运行过程中没发生异常,只要涉及到线程间的竞争,对资源的互斥访问,它也能检测出来。它一直监视着线程访问数据的情况。
除了监视竞争危害(race conditions),Thread Sanitizer 还可以标记线程泄露(thread leaks),在错误的线程使用互斥量(uninitiated mutexes)和锁。
使用 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 中的错误消失了
还是运行我们预备好的代码,发现一片空白
Xcode 8 现在可以查看运行时的约束警告了,有点类似于在 IB 中给的那些提示。同时在 Debug navigator 中我们可以通过内存地址,类名甚至父类来过滤 View 的层级。在 Object Inspector 栏目可以直接跳转到所选的 View class。Debug snapshots 也比过去更快了,苹果官方宣传提速了 70%。
现在来排查我们的 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
问题解决
但详情页面的偏移还未修正
我们运行到详情页面,选择 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) ])
再次运行,问题解决
Xcode 8 大大增强了调试运行时错误的能力,但也没忘了继续增强 static analyzer 。不过唯一遗憾的就是不支持 Swift,它只支持 C, C++ 和 Objective-C。
使用 static analyzer 在打开的工程项目中选择 Product/Analyze 就好了,如果有错误,Xcode 会通知你
问题详情
它新加入了一些在使用 MRC 时的有关实例清理的警告,以及非空性检查,对使用 Swift 和 OC 混编的程序员来说尤其有用。