摘要:
尽管Watch OS 1.01已经提升了应用启动的速度,但用户普遍感受还是体验较差,因此我们有必要尽全力优化自己的Apple Watch应用。本文作者结合自己的体会和其他先驱者的一些心得,对相关技巧做了一些汇总。
在笔者3月份为Apple Watch开发RebelSheep小游戏进行真机测试的时候,就发现了目前Apple Watch上第三方应用的性能并不理想,而测试部分Watch App Store里推荐的应用甚至都没能成功开启。尽管Watch OS 1.01已经提升了应用启动的速度,但用户普遍感受还是体验较差,因此我们有必要尽全力优化自己的Apple Watch应用。笔者结合自己的体会和其他先驱者的一些心得,对相关技巧做了一些汇总,分设计优化和资源优化两方面来说一下。
优化目标:缩短WatchApp的启动时间,提升响应速度
代码环境:Swift 1.2、Xcode6.3.2
1 程序设计优化
与iPhone应用可以充分发挥艺术品质的设计不同,Apple Watch应用必须遵循简单、直接、轻量级的原则,因此在整个软件界面及程序架构模型设计上就必须全面考虑。Apple官方文档《WatchKit Development Tips》里提到,为了提升性能,Apple Watch应用应实现:最少的通信量、只更新变化的内容、延迟加载内容、快速初始化分页控制器、简化控制器场景、减少表格初始显示行数。下面我们具体看一下:
1.1 最少的通信量&只更新变化内容
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可以实现读取表格后续数据行直接在表格后附加而不用刷新整个表格等等,这里不多赘述。
1.2 内容延迟加载
为了优化Watch App的启动速度和响应能力,我们的程序设计上需要考虑初始化时只加载本屏显示的内容,滚屏显示的额外内容延迟加载。而使用dispatch_async异步方式去处理耗时长的界面图像元素加载等任务将能够更快的提前呈现视图控制器。大致代码结构如下:
- override func willActivate() {
- super.willActivate()
- dispatch_async(dispatch_get_main_queue(), {
- //加载界面的图像元素等长时间操作
- })
- }
顺便一提,利用Swift语言的lazy关键字修饰变量,其懒加载机制也可降低初始化工作的压力。
1.3 快速初始化分页控制器
在使用多页视图模式时一定要特别注意,各页的控制器的init和awakeWithContext会比第一页控制器的willActivate更早执行,因此每页的数据加载等长时间任务有必要放到willActivate函数里运行。另外,每次切换分页都会执行对应控制器的willActivate函数,而在Watch OS 1.01版里,为了提升性能系统甚至会提前运行下一页的willActivate,为了少做无用功,我们可以设计一些缓存和避免重复加载的功能。
1.4 简化控制器场景、减少表格初始行数
我们为了实现一些提示功能,可能会在控制器里放置一些隐藏的标签控件等,但如果数量太多,也会严重影响视图加载速度。而对于表格,前面已经提到,最初应该仅加载第一屏里能看到的行。这些措施都能够大幅提升响应速度。
1.5 其它补充
1.5.1 状态保存
Apple官方文档《WatchKit Development Tips》里建议,我们可以在视图控制器的willActivate和didDeactivate两个方法里恢复/保存app的状态和数据。但很多情况下是不必要的(比如在多视图场景切换时也会执行有关代码),一项更好的选择是利用以下系统通知:
NSExtensionHostWillEnterForegroundNotification
NSExtensionHostDidBecomeActiveNotification
NSExtensionHostWillResignActiveNotification
NSExtensionHostDidEnterBackgroundNotification
我们可以在主视图控制器的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有一个文本标签,二手QQ拍卖其内容是时刻变化的,然而控制器A切换/弹出到了视图控制器B,那么此时更新控制器A的文本标签内容可能将会失败(A已经处于Deactivated状态),关闭控制器B返回控制器A时其内容就并非最新的。有必要的话请通过设立好标识变量等方式辅助解决此问题。
2 资源优化
曾经在Xcode6.2beta1测试版里我们是可以使用自定义字体的,然而不久后就被Apple禁止了,而音频目前也不能直接在Apple Watch上播放,因此这里我们主要讨论图像资源的优化。
2.1 资源文件存储路径规划
在不考虑Framework的情况下,通常WatchKit应用的工程包括(iPhone)App、WatchKit Extension、WatchKit App三个target,三个target默认使用其对应Bundle中的资源文件。我们现在只关心后两个:
2.1.1 WatchKit App Bundle
WatchKit App Bundle里的图像资源可以直接用于Storyboard里设置WKInterfaceImage控件的image属性、WKInterfaceGroup和WKInterfaceController的background属性。
图像放这里还有个好处就是资源在应用安装的时候就拷贝到了AppleWatch上,不需要再无线传输一遍。
因此,我们在WatchKit App Bundle里最好是存储应用UI控件的背景底图等静态的资源,50MB的存储上限绝大多数情况都是够用的。然后,尽量使用Assets Catalog,可以避免真机运行时一些莫名其妙的找不到图像的问题。
2.1.2 WatchKit Extension Bundle
WatchKit Extension是一种扩展,用于实现AppleWatch应用的代码逻辑,但存储于此Bundle的资源需要借助WKInterfaceImage的setImage:/setImageData:或WKInterfaceGroupd的setBackgroundImage:/setBackgroundImageData:/setBackgroundImageNamed:自动传输并显示到AppleWatch上。这一过程若时间过长你会在手表界面右上角看到加载动画,造成的卡顿将影响使用体验。因此,我们在WatchKitExtension里处理得最多的一般是动态生成、网络下载的图像。但实际情况中我们也常将一些静态图像存储于该Bundle,一方面是能够方便的通过代码进行显示控制,另一方面对于某些显示次数不确定的图像(比如显示频次较低的游戏结束画面),在图片容量较小的情况下我们完全可以实时加载,或是开启后台线程实现预加载并将其放入缓存,这样也能够减少手表应用安装初始化的时间。
这里有必要再补充讲一下缓存。我们知道AppleWatch为每个应用设置了5MB的缓存空间,这对许多应用来说是足够的。但经过几个beta版的更新,Apple已经去掉了FIFO(先入先出)模式的自动支持,也就是说你必须手动管理这个缓存空间,添加图像到缓存时判断若添加失败则证明缓存已满,可删除不需要的缓存图像或者是放弃进行缓存。
比如下面这段代码可以为WKInterfaceImage设置检查是否有名为cacheName的缓存图,若有就直接取缓存,否则去Extension的Bundle取名为name的图像,并将其添加到缓存。
- extension WKInterfaceImage{
- func setImageWithCacheNamed(name:String!,cacheName:String!){
- if let cImg: AnyObject=WKInterfaceDevice().cachedImages[cacheName]{
- self.setImageNamed(cacheName)
- }
- else {
- let img=UIImage(named: name)!
- if WKInterfaceDevice().addCachedImage(img, name: cacheName){
- self.setImageNamed(cacheName)
- }
- else {
- self.setImage(img)
- }
- }
- }
- }
以上代码未实现FIFO,有兴趣的可以参考一下Github上的WKImageCache,或者直接用KFSwiftImageLoader这个很完善的图像加载器。
再啰嗦一下WatchKit动画的图像处理,我们应该将所有动画帧放到一个UIImage里然后再传这个UIImage,而不是逐帧处理。有必要的话也记得将其添加至缓存空间。
- extension WKInterfaceImage{
- func setAnimationImageNamed(name:String!,range:NSRange){
- let imgs=NSMutableArray()
- for i in range.location..<(range.location+range.length){
- let img=UIImage(named: "\(name)\(i)")!
- imgs.addObject(img)
- }
- let animImage=UIImage.animatedImageWithImages(imgs as [AnyObject], duration: 1.0)
- self.setImage(animImage)
- }
- }
- }
2.2 缩减图像存储空间
使用Mac平台的ImageOptim工具或TinyPNG服务能够大幅压缩图像所占存储空间,这对于AppleWatch应用几乎是必须开展的一项工作。但值得注意的是我曾经碰到过压缩过的动画图像帧在模拟器上显示正常但在AppleWatch真机上出现alpha通道显示异常的问题。
来自Mike Swanson的一些相关经验值得分享给大家:
为WKInterfaceDevice添加图像缓存时使用addCachedImageWithData:name: 能直接使用NSData数据无需转换,那么利用UIImageJPEGRepresentation()编码能够压缩图像减少传输开销。
为WKInterfaceDevice添加图像缓存时若使用addCachedImage:name:方法似乎使用的是PNG编码格式,但这个方法的实际空间占用是UIImagePNGRepresentation() 加额外的753字节,因此若使用addCachedImageWithData:name:可以为每个缓存图像节省753字节存储空间。
cachedImages属性是一个键为名称、值为NSNumber型表示存储开销的字典,因此你也能够计算具体空间占用及可以资源。
最后,文章难免会产生的一些疏漏和不足之处,希望各位读者能够指出,谢谢!