在笔者3月份为Apple Watch开发RebelSheep小游戏进行真机测试的时候,就发现了目前Apple Watch上第三方应用的性能并不理想,而测试部分Watch App Store里推荐的应用甚至都没能成功开启。尽管Watch OS 1.01已经提升了应用启动的速度,但用户普遍感受还是体验较差,因此我们有必要尽全力优化自己的Apple Watch应用。笔者结合自己的体会和其他先驱者的一些心得,对相关技巧做了一些汇总,分设计优化和资源优化两方面来说一下。
优化目标: 缩短WatchApp的启动时间,提升响应速度
代码环境: Swift 1.2、Xcode6.3.2
与iPhone应用可以充分发挥艺术品质的设计不同,Apple Watch应用必须遵循简单、直接、轻量级的原则,因此在整个软件界面及程序架构模型设计上就必须全面考虑。Apple官方文档《 WatchKit Development Tips 》里提到,为了提升性能,Apple Watch应用应实现:最少的通信量、只更新变化的内容、延迟加载内容、快速初始化分页控制器、简化控制器场景、减少表格初始显示行数。下面我们具体看一下:
WatchKit扩展应用开发目前面临的一个很大麻烦就是UI组件的状态都是可写而不可读的,这样每次刷新界面内容时很难判断哪些是变动的数据而不得不把屏幕上所有内容都更新一遍。RobinSenior在 这篇文章 里提出利用视图模型的存储可以减少通信量和实现仅更新变化对象的数据。其实道理很简单,就是实现一个协议,判断原始内容是否和新内容一致。
比如对于标签控件WKInterfaceLabel,利用以下代码可以实现仅当标签文本发生变化时才更新标签内容。
protocol Updatable { typealias T func updateFrom(oldValue : T?, to newValue : T?) } extension WKInterfaceLabel : Updatable { func updateFrom(oldValue:String?, to newValue:String?){ if newValue != oldValue { self.setText(newValue) } } }
对于WKInterfaceImage,可利用此思路实现图像下载的网络地址改变时才去下载缓存图像并更新图片,对于WKInterfaceTable可以实现读取表格后续数据行直接在表格后附加而不用刷新整个表格等等,这里不多赘述。
为了优化Watch App的启动速度和响应能力,我们的程序设计上需要考虑初始化时只加载本屏显示的内容,滚屏显示的额外内容延迟加载。而使用dispatch_async异步方式去处理耗时长的界面图像元素加载等任务将能够更快的提前呈现视图控制器。大致代码结构如下:
override func willActivate() { super.willActivate() dispatch_async(dispatch_get_main_queue(), { //加载界面的图像元素等长时间操作 }) }
顺便一提,利用Swift语言的lazy关键字修饰变量,其懒加载机制也可降低初始化工作的压力。
在使用多页视图模式时一定要特别注意,各页的控制器的init和awakeWithContext会比第一页控制器的willActivate更早执行,因此每页的数据加载等长时间任务有必要放到willActivate函数里运行。另外,每次切换分页都会执行对应控制器的willActivate函数,而在Watch OS 1.01版里,为了提升性能系统甚至会提前运行下一页的willActivate,为了少做无用功,我们可以设计一些缓存和避免重复加载的功能。
我们为了实现一些提示功能,可能会在控制器里放置一些隐藏的标签控件等,但如果数量太多,也会严重影响视图加载速度。而对于表格,前面已经提到,最初应该仅加载第一屏里能看到的行。这些措施都能够大幅提升响应速度。
1.5.1 状态保存
Apple官方文档《 WatchKit Development Tips 》里建议,我们可以在视图控制器的willActivate和didDeactivate两个方法里恢复/保存app的状态和数据。但很多情况下是不必要的(比如在多视图场景切换时也会执行有关代码),一项更好的选择是利用以下系统通知:
我们可以在主视图控制器的init或awakeWithContext里通过NSNotificationCenter注册,比如:
override init() { super.init() NSNotificationCenter.defaultCenter().addObserver( self, selector:"onDeactivate", name:"NSExtensionHostWillResignActiveNotification", object: nil) NSNotificationCenter.defaultCenter().addObserver( self, selector:"onBackground", name:"NSExtensionHostDidEnterBackgroundNotification", object: nil) NSNotificationCenter.defaultCenter().addObserver( self, selector:"onForeground", name:"NSExtensionHostWillEnterForegroundNotification", object: nil) NSNotificationCenter.defaultCenter().addObserver( self, selector:"onActive", name:"NSExtensionHostDidBecomeActiveNotification",object: nil) }
上述代码中NSExtensionHostWillResignActiveNotification对应的应用挂起事件处理方法onDeactivate里就是一个保存应用状态数据的不错选择,相对应的WillEnterForegroundNotification的处理方法里可以读取回复应用状态数据。
1.5.2 视图更新中的风险
虽然按照Apple要求,我们可以只更新界面变化的内容,但值得注意的一点是,如果尝试更新时视图却处于不可见的状态,那么更新操作将会失被系统忽略而失败,你也无法得到操作失败的通知。典型的情况比如:视图控制器A有一个文本标签,其内容是时刻变化的,然而控制器A切换/弹出到了视图控制器B,那么此时更新控制器A的文本标签内容可能将会失败(A已经处于Deactivated状态),关闭控制器B返回控制器A时其内容就并非最新的。有必要的话请通过设立好标识变量等方式辅助解决此问题。