CPU 占用率 100 ,哪里出错了?
在写爬虫玩具的搜索页面时用了一个可以自动换行的 WrappingHStack, 最终实现的效果如图所示:
关键代码如下:
1 | private var vm = SearchViewModel() |
虽然效果不错却发现点击搜索框后,只要互动就卡的不行,一旦更改了搜索框的文本,比如键入字母等操作,就会再次执行 body,最终导致 cpu 占用狂飙到 100。根本无法正常使用。
与输入框绑定的 searchKey 是 @State 修饰的,在 SwiftUI 中 @State 修饰的数据发生变化,则会引发与之有关 View 树的重新计算。在上面代码中分别在 1, 2, 3处使用了 searchKey。 代码 1 , 用 @State 修饰 searchKey 以便后续使用;代码2,搜索框的数据用 $searchKey 绑定,实时更新 searchKey 数据;代码3, 当点击键盘的 return 按钮后,触发 isShowDetailView,跳转到 ResultView ,并把 searchKey 当前的值传递过去。
所以一旦操作键盘就会导致 searchKey 发生改变, searchKey 改变后发现 body 中有使用 searchKey ,而 body 中的 WrappingHStack 中有又有几百个数据(发生卡顿时的 tags 有五百多个数据)。每操作一个字符,就要重新计算几百个视图的位置。 这就是导致 cpu 占用过高最终卡顿的原因。
TextField 的数据在 body 中使用,body 中又有复杂布局,最终造成 View 树频繁 Re-render。
既然原因找到了,那么就来思考一下如何解决。 最终想要的结果是,既要获取 TextField 的数据,又不想要 WrappingHStack 再次计算。
思考 解决方法
那么第三库 WrappingHStack 是不是存在问题呢?
于是就去看了看 WrappingHStack 的 Issue,发现情况差不多的也是高 cpu 占用 # WrappingHStack causes high CPU usage,但点进去发现不一样,那老哥创建了 300 个 WrappingHStack 滚动测试,而我仅有一个视图,只是数据多一点。顺便又看了看源码,暂时没发现问题(其实是仅能看懂代码优化谈不上)。
既然暂时无法从第三方库找出问题,那代码中数据改动能不触发 body 的重计算就好了。
方式一:SwiftUI View 树
《SwiftUI 编程思想》中有讲过 SwiftUI 只会重新去执行那些使用了 @State 属性的 view 的 body。上面代码的简略 View 图,如图所示:
于是尝试了下把 WrappingHStack 封装到一个子视图里。
虽然操作键盘还是会触发 body,但是 Subview 的 body 不会再被触发。问题解决。
如果不封装视图,不改变 view 树层级,仅通过属性包装器能不能解决呢?
方式二:属性包装器
常用的属性包装器有 @State 、 @StateObject 、@ObservedObject、@Enviroment、@EnviromentObject、 @Binding 等。
- @State 用于修饰值类型
- @StateObject 用于修饰引用类型,生命周期与所在 View 一致
- @ObservedObject 用于修饰引用类型,通常用于传递数据
- @Enviroment 用来修饰从环境中获取的值类型,用于上下级传递数据
- @EnviromentObject 用来修饰从环境中获取的引用类型,用于上下级传递数据
- @Binding 可用于修饰值类型和引用类型,用于双向数据绑定
这里仅简要说明一下基本用法,详细用法和坑点还请参阅其它资料。
由于代码中的 .searchable(text: $searchKey)
搜索框必须给一个绑定值, 这样搜索内容可以被记录到,搜索内容会在 body 的 NavigationLink
的 destination 中使用。
@State 修饰的 searchKey 显然不符合我们的要求,会导致 body 重新计算。
把 searchKey 放到 @StateObject 修饰的 ViewModel 中,如果以 @Published 修饰 searchKey 同样会引起 body 重新计算;如果不加任何修饰, 则传递到下个页面时接收不到 searchKey 的值,简略代码如下:
1 | class SearchViewModel: ObservableObject { |
用 @Enviroment 来传值,暂不考虑,因为用来传递的类型要遵守 EnvironmentKey
协议,要写 extension EnvironmentValues
扩展,要写 extension View
扩展,然后用 @Enviroment 接收。 非系统 key 值这样一套下来太繁琐了。
searchKey 作为一个 @ObservedObject 修饰的引用类型 SearchKey 的属性,用 @EnviromentObject 把 SearchKey 传递给后一个页面,不会引起 body 的重计算,但这样传值略显麻烦,且后一个页面被 @EnviromentObject 修饰的属性访问时没有值会引起崩溃,太不安全。简略代码如下:
1 | class SearchKey: ObservableObject { |
最后再来看看 searchKey 作为一个 @ObservedObject 修饰的引用类型 SearchKey 属性,同时把 SearchKey 传递给后一个页面。
不会触发 body 重新计算,同时后一个页面也可以获取到值
简略代码如下:
1 | class SearchKey: ObservableObject { |
总结
在平时开发中尽量做好视图的封装,不要让多个视图在同一个 body 中。或者考虑使用 @ObservedObject 来传值。
PS: 该文章的具体代码可以在这里找到
参考
书籍
《SwiftUI for Absolute Beginners》