尝试用AI辅助我写一个类抖音的APP(三)

368 阅读8分钟

记一个SwiftUI中LazyVStack包裹大数量View卡死的问题。

LazyVStack是一个懒加载的组件实现,是不是懒加载就可以展示大量的View呢?给个实践的结论, 在环境xcode14.3.1, iPhone 14 pro真机和模拟器里测试了,LazyVStack在包裹1000个View还好,超过2500个就卡的像老黄牛一样了。

设计思路

尝试用AI辅助我写一个类抖音的APP(一)中探索了AI不能一下把APP的所有功能实现出来。 在尝试用AI辅助我写一个类抖音的APP(二)中探索和实现了核心的滑动翻页部分。

这一期我们探索一下,从后端加载的待滑动列表是什么样的方式。因为已有一些现成的展示列表,我们前期做简单点,不做千人千面。那能不能一下把一个大的展示列表例如1500个给到APP展示呢?自然而然就想到用到SwiftUI的懒加载LazyVStack。那么,开始吧。

首次遇到问题

一开始是用下面的代码在调试的时候,卡住了。还不清楚什么原因,于是去问问ChatGPT。

SwiftUI中,当skins.list为很大的数组时例如10000个元素,会初始化10000个SuitDetailView,有什么优化方案吗

struct ContentView: View {
    @State private var currentPage = 0
    @GestureState private var translation: CGFloat = 0
    @StateObject var skins : SuitListModel = SuitListModel(sex: .boy)
    var body: some View {
            ScrollView(.vertical, showsIndicators: false) {
                LazyVStack(spacing: 0) {
                    ForEach(skins.list, id: \.uuid) { s in
                        SuitDetailView(suit: s)//.environment(\.colorScheme, .dark)
                            .environmentObject(ImageModel(img: s.img, armpx: s.armpx))
                            .frame(height: screenHeight)
                    }
}}}}

ChatGPT4给了我答案,说是LazyVStack没有性能问题,指出是我的ImageModel会导致问题。

SwiftUI中,LazyVStack已经是一个延迟加载的视图,它只会在元素即将出现在屏幕上时创建对应的视图所以即使你的数组有10000个元素,也不会立即创建10000SuitDetailView,只有当用户滚动到对应的位置时,才会创建对应的视图

但是,你的代码中有一个可能的性能问题,即对每个SuitDetailView,你都创建了一个新的ImageModel如果ImageModel的初始化过程很耗资源(例如,它需要从网络加载图片),这可能会导致性能下降

为了解决这个问题,你可以考虑将ImageModel的创建移到SuitDetailView的内部,并使用@StateObject来管理它这样,ImageModel只会在对应的SuitDetailView被创建时创建,而且在SuitDetailView的整个生命周期内,ImageModel会被保留下来

这是一个修改后的示例:

struct SuitDetailView: View {
    var suit: Suit
    @StateObject var imageModel: ImageModel

    init(suit: Suit) {
        self.suit = suit
        _imageModel = StateObject(wrappedValue: ImageModel(img: suit.img, armpx: suit.armpx))
    }

    var body: some View {
        // your view code
    }
}
在这个示例中,ImageModel只会在SuitDetailView被创建时创建,并且在SuitDetailView的生命周期内被保留这样可以避免在滚动过程中不必要地创建和销毁ImageModel,从而提高性能

排除ImageModel影响,直击LazyVStack

为了验证是不是ChatGPT说的那样,我把ImageModel去掉了。 我用这样的代码跑了一下

struct Skin3DView: View {
    init(){
        mtlog.info("Skin3DView init()")
    }
    ...
}



struct ContentView: View {
    @State private var currentPage = 0
    @GestureState private var translation: CGFloat = 0
    @StateObject var skins : SuitListModel = SuitListModel(sex: .boy)

    var body: some View {
        GeometryReader { geometry in
            let screenHeight = geometry.size.height
            let threshold = screenHeight * 0.1  //这里把0.2改成0.1更顺畅了
            
            ScrollView(.vertical, showsIndicators: false) {
                LazyVStack(spacing: 0) {
                    ForEach(skins.list, id: \.uuid) { s in
                        Skin3DView()
                    }
                }
           ...                
    }

skins.list有1815个item 显示如下

。。。
2023-08-17 17:44:52.204534+0800 tikmc[27402:2476013] [tikmc] Skin3DView init()
2023-08-17 17:44:52.204541+0800 tikmc[27402:2476013] [tikmc] Skin3DView init()
2023-08-17 17:44:52.204548+0800 tikmc[27402:2476013] [tikmc] Skin3DView init()
2023-08-17 17:44:52.204555+0800 tikmc[27402:2476013] [tikmc] Skin3DView init()
2023-08-17 17:44:52.204562+0800 tikmc[27402:2476013] [tikmc] Skin3DView init()
2023-08-17 17:44:52.348506+0800 tikmc[27402:2476013] [WindowServer] CADisplayTimingsControl too many requests.
2023-08-17 17:44:52.348529+0800 tikmc[27402:2476013] [WindowServer] CADisplayTimingsControl too many requests.
2023-08-17 17:44:52.348539+0800 tikmc[27402:2476013] [WindowServer] CADisplayTimingsControl too many requests.
2023-08-17 17:44:52.348550+0800 tikmc[27402:2476013] [WindowServer] CADisplayTimingsControl too many requests.
2023-08-17 17:44:52.348558+0800 tikmc[27402:2476013] [WindowServer] CADisplayTimingsControl too many requests.
2023-08-17 17:44:52.348567+0800 tikmc[27402:2476013] [WindowServer] CADisplayTimingsControl too many requests.
2023-08-17 17:44:52.348575+0800 tikmc[27402:2476013] [WindowServer] CADisplayTimingsControl too many requests.
。。。

上面的Skin3DView init() 有5445个, 除了后面reload skins.list占1815个, 前面有3630个init()。 也就是说,LazyVStack不会真的做到只对即将进入可视范围的View再init创建,看这个结果它是对列表里所有的View都调用了init。 运行的时候直接卡死了,滑动不了。后来发现不能滑动的原因是因为 Skin3DView里加载了3d模型, 经过测试同时最大值为32个Skin3DView,可以理解为32个SceneView

通过这部分测试也可以验证了LazyVStack并没有像预期的那样按需创建View里的内容。

真实的LazyVStack测试

LazyVStack中多少个View打印的View init次数滑动一次打印的View init次数14 pro真机性能
100020002000流畅,约30fps
250050005000明显卡顿,3fps
50001000010000明显卡顿,1fps
100002000020000明显卡顿,1fps
100000内存爆了内存爆了内存爆了

上面的结果让我很费解,不是说LazyVStack可以按需加载包裹在里面的View吗?有文章说SwiftUI的结构体View很轻量,创建结构体init了,不一定会获取body的值。试试代码验证一下。

//ContentView中的body
    var body: some View {
        GeometryReader { geometry in
            let screenHeight = geometry.size.height
            let threshold = screenHeight * 0.1  //这里把0.2改成0.1更顺畅了
            
            ScrollView(.vertical, showsIndicators: false) {
                LazyVStack(spacing: 0) {
                    ForEach(1..<1000){ i in
                        Skin3DView(id: i)
                            .frame(height: screenHeight)
                    }

                }
            }
            
//Skin3DView代码
struct Skin3DView: View {
    var id: Int
    init(id: Int){
        self.id = id
        mtlog.info("Skin3DView init() \(id)")
    }
    var body: some View {
        ZStack{
            let _ = mtlog.info("Skin3DView body \(id)")
            //添加背景,颜色随机
            Rectangle()
                .foregroundColor(Color.randomColor())
                .edgesIgnoringSafeArea(.all)

        }
    }
}

结果让我大跌眼镜,完完整整地构造了一个View的全部。以下是打印的log。

2023-08-19 11:40:00.557758+0800 tikmc[83042:6996519] [tikmc] Skin3DView init() 1
2023-08-19 11:40:00.559627+0800 tikmc[83042:6996519] [tikmc] Skin3DView init() 1
2023-08-19 11:40:00.559707+0800 tikmc[83042:6996519] [tikmc] Skin3DView body 1
2023-08-19 11:40:00.578620+0800 tikmc[83042:6996519] [tikmc] Skin3DView init() 2
2023-08-19 11:40:00.578934+0800 tikmc[83042:6996519] [tikmc] Skin3DView init() 2
2023-08-19 11:40:00.579005+0800 tikmc[83042:6996519] [tikmc] Skin3DView body 2

。。。省略约3000行

2023-08-19 11:40:00.920815+0800 tikmc[83042:6996519] [tikmc] Skin3DView init() 998
2023-08-19 11:40:00.920917+0800 tikmc[83042:6996519] [tikmc] Skin3DView init() 998
2023-08-19 11:40:00.920956+0800 tikmc[83042:6996519] [tikmc] Skin3DView body 998
2023-08-19 11:40:00.921077+0800 tikmc[83042:6996519] [tikmc] Skin3DView init() 999
2023-08-19 11:40:00.921191+0800 tikmc[83042:6996519] [tikmc] Skin3DView init() 999
2023-08-19 11:40:00.921229+0800 tikmc[83042:6996519] [tikmc] Skin3DView body 999

这么看来LazyVStack和VStack在初始化包裹的View方面没什么区别啊。代码里把LazyVStack换成VStack试了一下。 VStack是1000个从头到尾init了一遍,再init第二遍, 然后再调佣body。而LazyVStack是1000个从头到尾一次两个init接着一个body。 VStack和LazyVStack滑动页面的时候都不会调用body, 但init调用跟上面一样,init两次,方式也是有区分。

结论

所以双向绑定的用法只是让我们更好地操作和展示数据,但并不能处理特别大的数据列表,会有性能问题。 那么处理这种大的数据列表,还是得编码实现这里的取数展示逻辑。

例如有10000个数据要展示,代码逻辑里不能将10000个数据全塞到填充LazyVStack的list里,而是先塞30个,等用户滑动到第25个,则再加载下一批30个,添加到list里。这样list会慢慢增加,不会一下增加便不会再初始化时耗时太久。另外要解决list达到一定数量时,滑动卡顿问题,这个放到下一期探索吧。