Apple Watch 应用优化的一些心得技巧总结
摘要:
尽管Watch OS 已经提升了应用启动的速度,但用户普遍感受还是体验
较差,因此我们有必要尽全力优化自己的 Apple Watch应用。本文作者结合自己
的体会和其他先驱者的一些心得,对相关技巧做了一些汇总。
在笔者3月份为Apple Watch开发RebelSheep小游戏进行真机测试的时候,
就发现了目前 Apple Watch上第三方应用的性能并不理想,而测试部分Watch
App Store里推荐的应用甚至都没能成功开启。尽管Watch OS 已经提升了
应用启动的速度,但用户普遍感受还是体验较差,因此我们有必要尽全力优化自
己的 Apple Watch应用。笔者结合自己的体会和其他先驱者的一些心得,对相关
技巧做了一些汇总,分设计优化和资源优化两方面来说一下。
优化目标:缩短WatchApp的启动时间,提升响应速度
代码环境:Swift 、
1 程序设计优化
与 iPhone应用可以充分发挥艺术品质的设计不同,Apple Watch应用必须
遵循简单、直接、轻量级的原则,因此在整个软件界面及程序架构模型设计上就
必须全面考虑。Apple官方文档《WatchKit Development Tips》里提到,为了提
升性能,Apple Watch应用应实现:最少的通信量、只更新变化的内容、延迟加
载内容、快速初始化分页控制器、简化控制器场景、减少表格初始显示行数。下
面我们具体看一下:
最少的通信量&只更新变化内容
WatchKit扩展应用开发目前面临的一个很大麻烦就是 UI组件的状态都是可
写而不可读的,这样每次刷新界面内容时很难判断哪些是变动的数据而不得不把
屏幕上所有内容都更新一遍。RobinSenior在这篇文章里提出利用视图模型的存
储可以减少通信量和实现仅更新变化对象的数据。其实道理很简单,就是实现一
个协议,判断原始内容是否和新内容一致。
比如对于标签控件WKInterfaceLabel,利用以下代码可以实现仅当标签文本
发生变化时才更新标签内容。
Updatable {
2. typealias T
3. func updateFrom(oldValue : T?, to newValue : T?)
4.}
WKInterfaceLabel : Updatable {
6. func updateFrom(oldValue:String?, to newValue:String?){
7. if newValue != oldValue {
8. (newValue)
9. }
10. }
11. }
复制代码
对于WKInterfaceImage,可利用此思路实现图像下载的网络地址改变时才
去下载缓存图像并更新图片,对于WKInterfaceTable可以实现读取表格后续数
据行直接在表格后附加而不用刷新整个表格等等,这里不多赘述。
内容延迟加载
为了优化Watch App的启动速度和响应能力,我们的程序设计上需要考虑
初始化时只加载本屏显示的内容,滚屏显示的额外内容延迟加载。而使用
dispatch_async异步方式去处理耗时长的界面图像元素加载等任务将能够更快
的提前呈现视图控制器。大致代码结构如下:
func willActivate() {
2. ()
3. dispatch_async(dispatch_get_main_queue(), {
4. //加载界面的图像元素等长时间操作
5. })
6.}
复制代码
顺便一提,利用 Swift语言的 lazy关键字修饰变量,其懒加载机制也可降低
初始化工作的压力。
快速初始化分页控制器
在使用多页视图模式时一定要特别注意,各页的控制器的 init和
awakeWithContext会比第一页控制器的 willActivate更早执行,因此每页的数据
加载等长时间任务有必要放到 willActivate函数里运行。另外,每次切换分页都
会执行对应控制器的 willActivate函数,而在Watch OS 版里,为了提升性
能系统甚至会提前运行下一页的 willActivate,为了少做无用功,我们可以设计
一些缓存和避免重复加载的功能。
简化控制器场景、减少表格初始行数
我们为了实现一些提示功能,可能会在控制器里放置一些隐藏的标签控件
等,但如果数量太多,也会严重影响视图加载速度。而对于表格,前面已经提到,
最初应该仅加载第一屏里能看到的行。这些措施都能够大幅提升响应速度。
其它补充
状态保存
Apple官方文档《WatchKit Development Tips》里建议,我们可以在视图控
制器的 willActivate和 didDeactivate两个方法里恢复/保存 app的状态和数据。
但很多情况下是不必要的(比如在多视图场景切换时也会执行有关代码),一项更
好的选择是利用以下系统通知:
NSExtensionHostWillEnterForegroundNotification
NSExtensionHostDidBecomeActiveNotification
NSExtensionHostWillResignActiveNotification
NSExtensionHostDidEnterBackgroundNotification
我们可以在主视图控制器的 init或 awakeWithContext里通过
NSNotificationCenter注册,比如:
init() {
()
3.
().addObserver(
5. self, selector:"onDeactivate",
name:"NSExtensionHostWillResignActiveNotification", object: nil)
6.
().addObserver(
8. self, selector:"onBackground",
name:"NSExtensionHostDidEnterBackgroundNotification", object: nil)
9.
10. ().addObserver(
11. self, selector:"onForeground",
name:"NSExtensionHostWillEnterForegroundNotification", object: nil)
12.
13. ().addObserver(
14. self, selector:"onActive",
name:"NSExtensionHostDidBecomeActiveNotification",object: nil)
15. }
复制代码
上述代码中 NSExtensionHostWillResignActiveNotification对应的应用挂起
事件处理方法 onDeactivate里就是一个保存应用状态数据的不错选择,相对应
的WillEnterForegroundNotification的处理方法里可以读取回复应用状态数据。
视图更新中的风险
虽然按照 Apple要求,我们可以只更新界面变化的内容,但值得注意的一点
是,如果尝试更新时视图却处于不可见的状态,那么更新操作将会失被系统忽略
而失败,你也无法得到操作失败的通知。典型的情况比如:视图控制器 A有一个
文本标签,其内容是时刻变化的,然而控制器 A切换/弹出到了视图控制器 B,
那么此时更新控制器 A的文本标签内容可能将会失败(A已经处于 Deactivated
状态),关闭控制器 B返回控制器 A时其内容就并非最新的。有必要的话请通过
设立好标识变量等方式辅助解决此问题。
2 资源优化
曾经在 测试版里我们是可以使用自定义字体的,然而不久后
就被 Apple禁止了,而音频目前也不能直接在 Apple Watch上播放,因此这里我
们主要讨论图像资源的优化。
资源文件存储路径规划
在不考虑 Framework的情况下,通常WatchKit应用的工程包Ƒ
(iPhone)App、WatchKit Extension、WatchKit App三个 target,三个 targetƒ
Ɠ使用其对应 Bundle中的资源文件。我们现在只关心后两个:
WatchKit App Bundle
WatchKit App Bundle里的图像资源可以直接用于 Storyboard里设置
WKInterfaceImage控件的 imageƔ性、WKInterfaceGroup和
WKInterfaceController的 backgroundƔ性。
图像放这里还有个好处就是资源在应用ƕƖ的时候就ƗƘ到了 AppleWatch
上,不需要ƙ无ƚƛƜ一遍。
因此,我们在WatchKit App Bundle里最好是存储应用 UI控件的Ɲ景ƞ图
等Ɵ态的资源,50MB的存储上Ơơ大多数情况都是够用的。然后,尽量使用
Assets Catalog,可以避免真机运行时一些Ƣƣ其Ƥ的ƥ不到图像的问题。
WatchKit Extension Bundle
WatchKit Extension是一Ʀ扩展,用于实现 AppleWatch应用的代码Ƨƨ,
但存储于此 Bundle的资源需要Ʃ助WKInterfaceImage的
setImage:/setImageData:或WKInterfaceGroupd的
setBackgroundImage:/setBackgroundImageData:/setBackgroundImageName
d:自动ƛƜ并显示到 AppleWatch上。这一过程ƪ时间过长你会在ƫ表界面Ƭ上
ƭ看到加载动Ʈ,Ư成的ưƱ将影响使用体验。因此,我们在WatchKitExtension
里处理得最多的一Ʋ是动态生成、网络下载的图像。但实Ƴ情况中我们也常将一
些Ɵ态图像存储于该 Bundle,一方面是能够方便的通过代码进行显示控制,另
一方面对于ƴ些显示次数不Ƶ定的图像(比如显示频次较低的游戏结ƶƮ面),在
图片容量较小的情况下我们Ʒ全可以实时加载,或是开启后Ƹƚ程实现ƹ加载并
将其放ƺ缓存,这样也能够减少ƫ表应用ƕƖ初始化的时间。
这里有必要ƙƻ充Ƽ一下缓存。我们知道 AppleWatch为每个应用设置了
5MB的缓存ƽ间,这对ƾ多应用来说是ƿ够的。但经过ǀ个 beta版的更新,Apple
已经去ǁ了 FIFO(先ƺ先出)模式的自动ǂǃ,也就是说你必须ƫ动管理这个缓
存ƽ间,DŽ加图像到缓存时判断ƪDŽ加失败则Dždž缓存已LJ,可Ljlj不需要的缓
存图像或者是放NJ进行缓存。
比如下面这Nj代码可以为WKInterfaceImage设置njǍ是否有ƣ为
cacheName的缓存图,ƪ有就直接取缓存,否则去 Extension的 Bundle取ƣ为
name的图像,并将其DŽ加到缓存。
WKInterfaceImage{
2. func setImageWithCacheNamed(name:String!,cacheName:String!){
3. if let cImg: AnyObject=WKInterfaceDevice().cachedImages[cacheName]{
4. (cacheName)
5. }
6. else {
7. let img=UIImage(named: name)!
8. if WKInterfaceDevice().addCachedImage(img, name: cacheName){
9. (cacheName)
10. }
11. else {
12. (img)
13. }
14. }
15. }
16. }
复制代码
以上代码ǎ实现FIFO,有Ǐǐ的可以Ǒ考一下Github上的WKImageCache,
或者直接用 KFSwiftImageLoader这个很Ʒǒ的图像加载器。
ƙǓǔ一下WatchKit动Ʈ的图像处理,我们应该将所有动ƮǕ放到一个
UIImage里然后ƙƛ这个 UIImage,而不是ǖǕ处理。有必要的话也Ǘ得将其
DŽ加至缓存ƽ间。
WKInterfaceImage{
2. func setAnimationImageNamed(name:String!,range:NSRange){
3. let imgs=NSMutableArray()
4. for i in ..<(+){
5. let img=UIImage(named: "\(name)\(i)")!
6. (img)
7. }
8. let animImage=(imgs as [AnyObject],
duration: )
9. (animImage)
10. }
11. }
12. }
复制代码
缩减图像存储空间
使用 MacǘƸ的 ImageOptim工具或 TinyPNGǙ务能够大幅压ǚ图像所Ǜ
存储ƽ间,这对于 AppleWatch应用ǀǜ是必须开展的一项工作。但值得注意的
是我曾经ǝ到过压ǚ过的动Ʈ图像Ǖ在模Ǟ器上显示ǟ常但在 AppleWatch真机
上出现 alpha通道显示异常的问题。
来自 Mike Swanson的一些相关经验值得分Ǡǡ大Ǣ:
为WKInterfaceDeviceDŽ加图像缓存时使用
addCachedImageWithData:name: 能直接使用 NSData数据无需ǣ换,那么利
用 UIImageJPEGRepresentation()Ǥ码能够压ǚ图像减少ƛƜ开ǥ。
为WKInterfaceDeviceDŽ加图像缓存时ƪ使用addCachedImage:name:方法
Ǧǜ使用的是 PNGǤ码格式,但这个方法的实Ƴƽ间Ǜ用是
UIImagePNGRepresentation() 加额外的 753字ǧ,因此ƪ使用
addCachedImageWithData:name:可以为每个缓存图像ǧǨ 753字ǧ存储ƽ间。
cachedImagesƔ性是一个键为ƣǩ、值为 NSNumber型表示存储开ǥ的字
典,因此你也能够计Ǫ具体ƽ间Ǜ用及可以资源。
最后,文章难免会ǫ生的一些Ǭǭ和不ƿǮ处,ǯǰ各DZ读者能够Dz出,dz
dzǴ
Apple Watch应用优化的一些心得技巧总结