Локальная база данных загружает изображения, но память бесконечно растет и переполняется - SwiftUI

//Проблема уже решена, и приведенный ниже код уже изменен, пожалуйста, используйте следующие коды

Запоминание полезно знать, обычно его размер составляет 50 МБ, но не больше 100 МБ.

  • Используйте LazyView для отображения изображений
  • При появлении изображений добавляйте новые изображения, если их количество превышает максимальное количество изображений, удаляйте старые изображения.
  • Используйте @ObjectState и @Published, чтобы действительно удалить изображения из памяти.

В моей программе на SwiftUI я наблюдаю неограниченное увеличение использования памяти, когда изображения загружаются из локальной базы данных (сейчас смоделируйте, оригинал используется SQLite3) и отображается пользователю. Поскольку во время прокрутки загружается больше изображений, память не освобождается, что приводит к постоянному росту потребления памяти. Такое поведение предполагает утечку памяти, приводящую к потенциальному переполнению памяти.

Основной вид:

import SwiftUI

let triggerLoadNumber = 5 // How many items from the edge should trigger loading
let batchSize = 10 // Number of items to load per batch
let maxContentCount = 20 // Maximum number of content items to hold in memory

struct ScrollShowView: View {
  @Binding var bookName: String
  @StateObject var imageLoader = ImageLoader()
  
  // The load threshold, presumably used to determine when to start loading new content
  private let loadThreshold = 100.0
  
  var body: some View {
    if !imageLoader.showImages.isEmpty {
      ScrollView {
        LazyVStack(spacing: 0) {
          ForEach(imageLoader.ids, id: \.self) { id in
            Group {
              // Attempt to find the view content by ID
              if let viewContent = imageLoader.showImages.first(where: { $0.id == id }) {
                Image(uiImage: viewContent.image!)
                  .resizable()
                  .aspectRatio(contentMode: .fit)
                  .onAppear {
                    imageLoader.bookName = bookName
                    // Get the index of the current image
                    if let index: Int = imageLoader.showImages.firstIndex(of: viewContent) {
                      // Check if we need to load more content based on scroll position
                      if index < triggerLoadNumber - 1 {
                        // User has scrolled near the top, load previous content
                        imageLoader.loadMoreViewContent(direction: .previous)
                      } else if index > imageLoader.showImages.count - triggerLoadNumber {
                        // User has scrolled near the bottom, load next content
                        imageLoader.loadMoreViewContent(direction: .next)
                      }
                    }
                  }
              } else {
                // Display a loading placeholder if image is not available
                Image("Loading")
                  .resizable()
                  .aspectRatio(contentMode: .fit)
                  .onAppear {
                    imageLoader.bookName = bookName
                    imageLoader.doubleDirectionsLoadViewContent(for: id) {}
                  }
              }
            }
          }
        }
      }
    } else {
      Image("Loading")
        .resizable()
        .aspectRatio(contentMode: .fit)
        .onAppear {
          imageLoader.bookName = bookName
          imageLoader.loadInitialContent()
        }
    }
  }
}

class ImageLoader: ObservableObject {
  @State var bookName: String = ""
  @Published var ids: [Int] = []
  @Published var showImages: [RPViewContent] = []
  @Published var isLoading = false
  
  // Load the initial set of images
  func loadInitialContent() {
    isLoading = true
    // Fetch all page IDs from the book
    ids = BooksDatabase().getAllIds(from: bookName)
    // Load the initial batch of content based on maxContentCount
    let initialIndexes = Array(ids.prefix(maxContentCount))
    singleDirectionLoadViewContent(for: initialIndexes, direction: .next) {
      self.isLoading = false
    }
  }
  
  // Enum to define the direction of content loading
  enum LoadDirection {
    case previous, next
  }
  
  // Load more content in the given direction
  func loadMoreViewContent(direction: LoadDirection) {
    guard !isLoading else { return }
    isLoading = true
    
    // Determine the ID of the relevant content based on the direction of loading
    let relevantId: Int?
    switch direction {
    case .previous:
      relevantId = showImages.first?.id
    case .next:
      relevantId = showImages.last?.id
    }
    
    // Ensure the ID is valid and determine the new range of IDs to load
    guard let id = relevantId, let index = ids.firstIndex(of: id) else {
      isLoading = false
      return
    }
    
    let newIndexes: [Int]
    switch direction {
    case .previous:
      // Load the previous batch of content
      let start = max(0, index - batchSize)
      newIndexes = Array(ids[start..<index])
    case .next:
      // Load the next batch of content
      let end = min(ids.count, index + batchSize)
      newIndexes = Array(ids[index..<end])
    }
    
    // Load the content for the new range of IDs
    singleDirectionLoadViewContent(for: newIndexes, direction: direction) {
      // After loading new content, adjust the currently held content based on the maxContentCount
      switch direction {
      case .previous:
        if self.showImages.count > maxContentCount {
          // If the maximum number is exceeded, remove the trailing elements
          self.showImages.removeLast(self.showImages.count - maxContentCount)
        }
      case .next:
        if self.showImages.count > maxContentCount {
          // If the maximum number is exceeded, remove the header element
          self.showImages.removeFirst(self.showImages.count - maxContentCount)
        }
      }
      self.isLoading = false
    }
  }
  
  // Function to load view content for the specified IDs and handle the completion
  func singleDirectionLoadViewContent(for ids: [Int], direction: LoadDirection, completion: @escaping () -> Void) {
    // Fetch the original images from the database for the specified IDs
    BooksDatabase().getOriginal(at: ids, from: bookName) { newImages in
      // Insert or append new images to the showImages array based on the direction
      if direction == .previous {
        // Loading content to be displayed above the current content
        self.showImages.insert(contentsOf: newImages, at: 0)
      } else {
        // Loading content to be displayed below the current content
        self.showImages.append(contentsOf: newImages)
      }
      // Call the completion handler
      completion()
    }
  }
  
  func doubleDirectionsLoadViewContent(for id: Int, completion: @escaping () -> Void) {
    // Calculate the range of IDs to load
    let halfRange = maxContentCount / 2
    let startIdIndex = max(0, id - halfRange)
    let endIdIndex = min(self.ids.count, id + halfRange)
    let loadImagesIds: [Int] = Array(self.ids[startIdIndex..<endIdIndex])
    // Load images for the calculated range of IDs
    BooksDatabase().getOriginal(at: loadImagesIds, from: bookName) { images in
      self.showImages = images
    }
    
    // Call the completion handler
    completion()
  }
}

Имитированная база данных:

class BooksDatabase {
  func getOriginal(at pages: [Int], from bookName: String, completion: @escaping ([RPViewContent]) -> Void) {
    let showImages = pages.compactMap { i -> RPViewContent? in
      guard let path = Bundle.main.path(forResource: String(i), ofType: "jpg") else { return nil }
      let image = UIImage(contentsOfFile: path)
      return RPViewContent(id: i, imageType: .clear, image: image)
    }
    
    completion(showImages)
  }
  
  func getAllIds() -> [Int] {
    return Array(0...300)
  }
}

Определения:

struct Quadrilateral: Codable {
  var topLeft: CGPoint
  var topRight: CGPoint
  var bottomRight: CGPoint
  var bottomLeft: CGPoint
}

enum POSType: String, Codable {
  case noun
  case verb
  case adjective
  case adverb
  case pronoun
  case preposition
  case conjunction
  case interjection
  case determiner
  case other
  
  var stringValue: String {
    switch self {
    case .noun: return "noun"
    case .verb: return "verb"
    case .adjective: return "adjective"
    case .adverb: return "adverb"
    case .pronoun: return "pronoun"
    case .preposition: return "preposition"
    case .conjunction: return "conjunction"
    case .interjection: return "interjection"
    case .determiner: return "determiner"
    case .other: return "other"
    }
  }
}

struct Word: Codable {
  var texts: String
  var pos: POSType
}

enum ShowingImageType {
  case clear
  case mark
}

//RP = ReadingPage
struct RPInfo: Codable {
  var texts: [Word]?
  var positions: [[Quadrilateral]]?
  var unknowWordsIndex: [Int]?
  var learningWordsIndex: [Int]?
  var definitions: [String:[POSType:[String]]]?
}

struct RPViewContent: Equatable {
  let id: Int
  var imageType: ShowingImageType
  var image: UIImage?
}
  1. autoreleasepool {} //не работает с памятью
  2. LazyVStack -> VStack //не работало отображение представления, также не было известно о памяти
  3. попробуйте использовать способ из https://fatbobman.com/en/posts/memory-usage-optimization/, но не помогло

Похоже, профайлер — ваш друг: developer.apple.com/documentation/xcode/… .

lazarevzubov 11.03.2024 08:54

Не уверен, но вы, кажется, много раз воссоздаете массив во время загрузки, что может перерисовывать представление, что может облегчить изображение… Я бы предложил иметь изображения в модели представления, которая будет обрабатывать загрузку изображений и кэширование изображений в своем массиве.

Ptit Xav 11.03.2024 14:05

Я нашел причину: раньше я сохранял файл в Assets.xcassets, но Assets.xcassets автоматически кэшировался, поэтому возникла проблема. Теперь я изменил метод получения базы данных на UIImage(contentsOfFile: path), что оказалось очень успешным.

J W 12.03.2024 21:44
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
3
250
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

«Assets.xcassets» будет кэшироваться автоматически.

class BooksDatabase {
  func getOriginal(at pages: [Int], completion: @escaping ([RPViewContent]) -> Void) {
    let showImages = pages.compactMap { i -> RPViewContent? in
      guard let path = Bundle.main.path(forResource: String(i), ofType: "jpg") else { return nil }
      let image = UIImage(contentsOfFile: path)
      return RPViewContent(id: i, imageType: .clear, image: image)
    }
    
    completion(showImages)
  }
...
}

Другие вопросы по теме