fatbobman / blogComments

1 stars 0 forks source link

SwiftUI + Core Data App 的内存占用优化之旅 | 肘子的Swift记事本 #181

Open fatbobman opened 1 year ago

fatbobman commented 1 year ago

https://www.fatbobman.com/posts/memory-usage-optimization/?

尽管 SwiftUI 的惰性容器以及 Core Data 都有各自的内存占用优化机制,但随着应用视图内容的复杂( 图文混排 ),越来越多的开发者遇到了内存占用巨大甚至由此导致 App 崩溃的情况。本文将通过对一个演示 App 进行逐步内存优化的方式( 由原先显示 100 条数据要占用 1.6 GB 内存,优化至显示数百条数据仅需 200 多 MB 内存 ),让读者对 SwiftUI 视图的存续期、惰性视图中子视图的生命周期、托管对象的惰值特性以及持久化存储协调器的行缓存等内容有更多的了解。

LiYanan2004 commented 1 year ago

好文章!

  1. Core Data 的 Allow External Storage
  2. SwiftUI 中的惰性容器(Lazy Container)不会自动销毁离开屏幕的元素
  3. @State 不会自动释放内存,可以使用 @StateObject 以引用的方式来释放内存占用

最终方案是:仅在视图元素进入可视区域再从 Core Data 中拉取对应的 Binary Data,在离开可视区域后快速销毁视图和 Object Data 释放内存,确保最高的内存利用效率

LiYanan2004 commented 1 year ago
截屏2023-03-08 15 58 13

小建议:这样改一下在视觉上会显得更加流畅(虽然这篇文章讨论的是内存占用)

fatbobman commented 1 year ago

小建议:这样改一下在视觉上会显得更加流畅(虽然这篇文章讨论的是内存占用)

好建议,我调整了一下代码

image
yanlicheng commented 1 year ago
image

新手上路,这里有点不太懂,可以稍微展开讲解一下不 🥺

fatbobman commented 1 year ago

新手上路,这里有点不太懂,可以稍微展开讲解一下不 🥺

if let objectID = item.picture?.objectID {  // 获取 item 对应的 picture 实例的托管对象ID( 这个 ID 是唯一的)
                    let imageData: Data? = await PersistenceController.shared.container.performBackgroundTask { context in // 创建一个新的私有上下文,并在其中执行如下操作
                        if let picture = try? context.existingObject(with: objectID) as? Picture, let data = picture.data {  // existingObject 可以通过一个 NSManagedObjectID 获取它对应的托管对象实例,它的查询范围是 上下文 -> 行缓存 -> SQLite 数据库 ,
                            return data // 如果找到对应的实例,则返回实例中的 data 属性数据,赋值给  imageData
                        } else { return nil } // 如果无法获取实例,则返回 nil
                    }
                    if let imageData {
                       // 在 imageData 有值的情况下,用数据创建 Image,
                        image = Image(uiImage: UIImage(data: imageData)!)
                    }
                }
yanlicheng commented 1 year ago

“如果滚动过快,可能会导致内存占用增大。估计与系统无暇进行清理操作有关。” 肘子兄,这个问题有什么解决办法吗?

fatbobman commented 1 year ago

“如果滚动过快,可能会导致内存占用增大。估计与系统无暇进行清理操作有关。” 肘子兄,这个问题有什么解决办法吗?

目前还没有仔细考虑过。

idhun90 commented 1 year ago

I'm developing an app that records clothes data in coredata and I'm having issues with a lot of memory allocation. Like this article, I am storing images as a binary data type. Could it be the images that are causing the high memory allocation?

Should I create a class object like in this post and handle it the way I need to be passed a coredata object in onAppear? My view hierarchy is main -> detail -> edit.

fatbobman commented 1 year ago

I'm developing an app that records clothes data in coredata and I'm having issues with a lot of memory allocation. Like this article, I am storing images as a binary data type. Could it be the images that are causing the high memory allocation?

Should I create a class object like in this post and handle it the way I need to be passed a coredata object in onAppear? My view hierarchy is main -> detail -> edit.

If you run into memory issues, you can try the methods described in this article. This is a blog written by a good independent developer in China this week. He also encountered a similar problem, and finally used the method of this article to improve the memory usage

https://mp.weixin.qq.com/s/lGR6Md-WZdTszThDWiyAUg

idhun90 commented 1 year ago

Thank you. Thanks to you, I was able to reduce my memory usage by 1/3.

I have one more question. When I scroll in a List or ScrollView, the CPU usage is approaching 30-40%, is this normal?

If I build my app when I have 100 items, it will use the

ScrollView + LazyVStack

List

All using FetchRequest. I have an issue where just scrolling 3 Text Views Row in CoreData causes high CPU usage…

      1. 오후 8:22, 东坡肘子 @.***> 작성:

I'm developing an app that records clothes data in coredata and I'm having issues with a lot of memory allocation. Like this article, I am storing images as a binary data type. Could it be the images that are causing the high memory allocation?

Should I create a class object like in this post and handle it the way I need to be passed a coredata object in onAppear? My view hierarchy is main -> detail -> edit.

If you run into memory issues, you can try the methods described in this article. This is a blog written by a good independent developer in China this week. He also encountered a similar problem, and finally used the method of this article to improve the memory usage

https://mp.weixin.qq.com/s/lGR6Md-WZdTszThDWiyAUg

— Reply to this email directly, view it on GitHub https://github.com/fatbobman/blogComments/issues/181#issuecomment-1578526429, or unsubscribe https://github.com/notifications/unsubscribe-auth/AMFQGYSGSKA7XZOGRGVDF3TXJ4HH7ANCNFSM6AAAAAAVTCRZ3I. You are receiving this because you commented.

fatbobman commented 1 year ago

Thank you. Thanks to you, I was able to reduce my memory usage by 1/3. I have one more question. When I scroll in a List or ScrollView, the CPU usage is approaching 30-40%, is this…

It's great to hear that the article was helpful for you.

I believe the CPU usage you are experiencing is normal. Currently, SwiftUI's optimization for scrolling is not very good, and regardless of the amount of data, there will be a higher CPU usage. We can only hope that future versions of SwiftUI will improve on this.

idhun90 commented 1 year ago

Wow, thanks for the quick reply.

I'm glad to see these numbers are within the normal range. I was feeling a tepid heat on the back of my device after scrolling through the list several times...🥲

Which do you recommend more, ScrollView + LazyVStack or List when storing data in the app for a long time?

Also, may I ask which do you prefer, storing images as binary data type in CoreData or storing only the URL in CoreData and the actual image on the device?

Thank you for your time!!

      1. 오후 3:55, 东坡肘子 @.***> 작성:

Thank you. Thanks to you, I was able to reduce my memory usage by 1/3. I have one more question. When I scroll in a List or ScrollView, the CPU usage is approaching 30-40%, is this…

It's great to hear that the article was helpful for you.

I believe the CPU usage you are experiencing is normal. Currently, SwiftUI's optimization for scrolling is not very good, and regardless of the amount of data, there will be a higher CPU usage. We can only hope that future versions of SwiftUI will improve on this.

— Reply to this email directly, view it on GitHub https://github.com/fatbobman/blogComments/issues/181#issuecomment-1592468805, or unsubscribe https://github.com/notifications/unsubscribe-auth/AMFQGYUNCNY4BNIPMAOAYG3XLKWVNANCNFSM6AAAAAAVTCRZ3I. You are receiving this because you commented.

fatbobman commented 1 year ago

Wow, thanks for the quick reply. I'm glad to see these numbers are within the normal range. I was feeling a tepid heat on the back of my device after scrolling through the list several times...🥲 Which do you recommend more, ScrollView + LazyVStack or List when storing data in the app for a long time? Also, may I ask which do you prefer, storing images as binary data type in CoreData or storing only the URL in CoreData and the actual image on the device? Thank you for your time!!

ScrollView + LazyVStack allows developers to design layouts more freely, especially in the latest version of SwiftUI where ScrollView has been greatly enhanced with new features that cannot be used with List.

The biggest advantage of List is its complete set of functions for moving, deleting, and sliding buttons.

In terms of performance, there is not a big difference between the two, but some developers have reported that ScrollView + LazyVStack may have layout errors when dealing with large amounts of data and varying subview heights (which I have personally not encountered).

Therefore, if your subview heights are relatively fixed and you don't need the default moving and sliding button functions, I would recommend using ScrollView + LazyVStack.

In scenarios that require network synchronization, saving images directly in CoreData (set as external storage) is the most convenient choice for developers because we don't need to separately consider synchronization. If only the URL is saved in CoreData, developers must solve synchronization issues themselves and determine whether the data has been synchronized to the local device.

However, saving data directly in CoreData will also lose the ability to selectively download data on the local device (such as the iOS Photos app, which allows only low-resolution versions to be saved locally).

The specific choice of which method to use still depends on the scenario that your application needs to handle.

idhun90 commented 1 year ago

Thank you for your response. You are correct. I saw a lot of new features added to ScrollView at WWDC this year.

The app I'm developing doesn't have any network operations (except for iCloud), it just allows the user to select and save photos from the user's photo library, show and edit saved items.

Right now I'm storing the original image and thumbnail directly in the storage coredata, showing the thumbnail in the MainView List, and in the DetailView I'm showing the original image downsampled through Image I/O as the List header.

In the memory test article, I unchecked the EXTERNAL STORAGE item for testing, but if I end up storing the image in the core data, do I really need to enable the EXTERNAL STORAGE item?

Thanks.

      1. 오후 4:19, 东坡肘子 @.***> 작성:

Wow, thanks for the quick reply. I'm glad to see these numbers are within the normal range. I was feeling a tepid heat on the back of my device after scrolling through the list several times...🥲 Which do you recommend more, ScrollView + LazyVStack or List when storing data in the app for a long time? Also, may I ask which do you prefer, storing images as binary data type in CoreData or storing only the URL in CoreData and the actual image on the device? Thank you for your time!! … <x-msg://4/#> ScrollView + LazyVStack allows developers to design layouts more freely, especially in the latest version of SwiftUI where ScrollView has been greatly enhanced with new features that cannot be used with List.

The biggest advantage of List is its complete set of functions for moving, deleting, and sliding buttons.

In terms of performance, there is not a big difference between the two, but some developers have reported that ScrollView + LazyVStack may have layout errors when dealing with large amounts of data and varying subview heights (which I have personally not encountered).

Therefore, if your subview heights are relatively fixed and you don't need the default moving and sliding button functions, I would recommend using ScrollView + LazyVStack.

In scenarios that require network synchronization, saving images directly in CoreData (set as external storage) is the most convenient choice for developers because we don't need to separately consider synchronization. If only the URL is saved in CoreData, developers must solve synchronization issues themselves and determine whether the data has been synchronized to the local device.

However, saving data directly in CoreData will also lose the ability to selectively download data on the local device (such as the iOS Photos app, which allows only low-resolution versions to be saved locally).

The specific choice of which method to use still depends on the scenario that your application needs to handle.

— Reply to this email directly, view it on GitHub https://github.com/fatbobman/blogComments/issues/181#issuecomment-1592497117, or unsubscribe https://github.com/notifications/unsubscribe-auth/AMFQGYUMSBR6BPF2IIKMLG3XLKZQZANCNFSM6AAAAAAVTCRZ3I. You are receiving this because you commented.

fatbobman commented 1 year ago

Thank you for your response. You are correct. I saw a lot of new features added to ScrollView at WWDC this year. The app I'm developing doesn't have any network operations (except for iCloud), it just allows the user to select and save photos from the user's photo library, show and edit saved items. Right now I'm storing the original image and thumbnail directly in the storage coredata, showing the thumbnail in the MainView List, and in the DetailView I'm showing the original image downsampled through Image I/O as the List header. In the memory test article, I unchecked the EXTERNAL STORAGE item for testing, but if I end up storing the image in the core data, do I really need to enable the EXTERNAL STORAGE item? Thanks.

Enabling external storage can improve SQLite performance.

SQLite automatically saves data larger than 100KB in separate files outside the database. This approach keeps the size of the SQLite database file small, which is beneficial for data read and write speed and efficiency.

idhun90 commented 1 year ago

Thank you. We'll activate it right away.

Thank you very, very much. I will study more after reading your post!

      1. 오후 4:36, 东坡肘子 @.***> 작성:

Enabling external storage can improve SQLite performance.

SQLite automatically saves data larger than 100KB in separate files outside the database. This approach keeps the size of the SQLite database file small, which is beneficial for data read and write speed and efficiency.

Ilsommo97 commented 5 months ago

Hey, great article! I stumbled upon your blog and each article is teaching me something, thank you! I'm currently trying to figure out why LazyVStacks ( lazyHStacks as well) have a sluggish/bouncy behaviour when scrolling up through items with a variable height ( or width for lazyHStacks). I think you can reproduce the issue with this simple piece of code :

struct variableHeight : Identifiable {
    var id: Int
    var height : CGFloat
}

struct StutteringSample: View {
    let randomHeights : [variableHeight] = (0...100).map{
        i in variableHeight(id: i, height: CGFloat(  Int.random(in: 100...300)))
    }

    var body: some View {
        ScrollView(.vertical){
            LazyVStack(content: {
                ForEach(randomHeights) { vHeight in
                    Image(uiImage:  UIImage(systemName: "camera")!)
                        .resizable()
                        .frame(width: 300,height: vHeight.height)

                }
            })
        }
    }
}

I think that this particular issue have been reported almost 3 years ago but never been solved, so what im trying to do is using a regular VStack ( that instead do not present this problem) in a scroll view implementing a lazy loading mechanism myself. However, when embedding a Vstack inside a scroll view, the cell displayed are not triggering the OnDisappear closure, and I'm not able to free up memory similarly to how you're doing in this article. Any idea? I dont want to use Lists Thanks again for the article and the effort you are putting into this :)

fatbobman commented 5 months ago

@Ilsommo97 This is a long-standing known issue that still lacks a good solution to date. Essentially, the likelihood is that LazyVStack estimates its total height based on the average height of the child views within the current visible area (combined with the number of child views in the LazyVStack). As such, when there is significant variation in the heights of the child views, it continuously adjusts its internal height during scrolling (which also causes continuous changes in the corresponding internal position of the scrollbar). Therefore, when scrolling or dragging the LazyVStack, it might result in the situation you described. Moreover, if the scrolling or dragging speed is fast (with a larger number of child views), there is a high probability of encountering a blank screen scenario.

List focuses only on the current visible area (not concerned with the overall height), so even with significant differences in child view heights, its performance tends to be better than LazyVStack.

VStack does not have lazy loading characteristics; all child views are loaded at once, without triggering onAppear and onDisappear a second time.

It can only be said that, for now, LazyVStack is not quite suitable for scenarios with large differences in child view heights.

ldasein commented 1 month ago

就你这个案例,其实如果真的在乎性能(cpu占用)和内存,还能进一步优化。 优化方案也不复杂,就是类似各种图片库的原理,手动实现一个图片内存池,在异步队列中手动对图片原始data信息进行解码,并按照指定的frame大小,重新编码为尺寸为frame@2x或者@3x的位图。

每次UI界面需要Image的时候,使用图片池中的位图直接生成Image。 既不会产生新的图片位图实例,也不需要coreAnimation重新隐式解码/编码图片。 如果100张图片用的原始图片一模一样,图片本身所占用的内存甚至可以优化为几十kb。就算图片不同,100张图片几MB其实也就够了。 十几年前iPhone 4那个时代就有这种优化了,滑动场景跑满60帧无压力。

不过代价就是每一张图片需要设置的参数太多了,需要手动调整的策略也很多,弄不好就是负优化/内存泄漏。 OC时代支持极致优化的图片库就因为太复杂没多少人用,现在估计更没什么人这么干了