在我的 SwiftUI 程序中,当图像从本地数据库加载(现在模拟,原始由 SQLite3 使用)并向用户显示时,我观察到内存使用量无限增加。随着滚动过程中加载更多图像,内存不会被回收,导致内存消耗持续增长。此行为表明内存泄漏,导致潜在的内存溢出。
主视图:
import SwiftUI
import UIKit
struct ScrollShowView: View {
// Binding to a parent's state, allowing this view to access and modify the bookName
@Binding var bookName: String
// State variables to hold the current state of the view
@State var ids: [Int] = [Int]() // Holds the IDs of the pages
@State var showImages: [RPViewContent] = [] // Holds the currently loaded images
@State var showInfo: [RPInfo] = [] // Holds additional information related to the images
@State private var isLoading = false // Tracks whether content is currently loading
// Constants to define when to trigger loading and how much content to load
private let triggerLoadNumber = 5 // How many items from the edge should trigger loading
private let batchSize = 10 // Number of items to load per batch
private let maxContentCount = 20 // Maximum number of content items to hold in memory
// The load threshold, presumably used to determine when to start loading new content
private let loadThreshold = 100.0
var body: some View {
if !showImages.isEmpty {
ScrollView {
LazyVStack(spacing: 0) {
ForEach(ids, id: \.self) { id in
Group {
// Attempt to find the view content by ID
if let viewContent = showImages.first(where: { $0.id == id }) {
Image(uiImage: viewContent.image!)
.resizable()
.aspectRatio(contentMode: .fit)
.onAppear {
// Get the index of the current image
let index: Int = 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
loadMoreViewContent(direction: .previous)
} else if index > showImages.count - triggerLoadNumber {
// User has scrolled near the bottom, load next content
loadMoreViewContent(direction: .next)
}
}
} else {
// Display a loading placeholder if image is not available
Image("Loading")
.resizable()
.aspectRatio(contentMode: .fit)
.onAppear {
// Calculate the range of IDs to load
let halfRange = maxContentCount / 2
let startIdIndex = max(0, id - halfRange)
let endIdIndex = min(ids.count, id + halfRange)
let loadImagesIds: [Int] = Array(ids[startIdIndex..<endIdIndex])
// Load images for the calculated range of IDs
BooksDatabase().getOriginal(at: loadImagesIds, from: bookName) { images in
showImages = images
}
}
}
}
}
}
}
} else {
// If no images are present, display a black background and trigger the initial content load
Color.black.onAppear { loadInitialContent() }
}
}
// Load the initial set of images
private 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))
loadViewContent(for: initialIndexes, direction: .next) {
isLoading = false
}
}
// Enum to define the direction of content loading
private enum LoadDirection {
case previous, next
}
// Load more content in the given direction
private 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
loadViewContent(for: newIndexes, direction: direction) {
// After loading new content, adjust the currently held content based on the maxContentCount
if direction == .previous {
// Remove excess items from the end if necessary
let limitedImages = Array(showImages.prefix(maxContentCount))
showImages = limitedImages
} else {
// Remove excess items from the beginning if necessary
let limitedImages = Array(showImages.suffix(maxContentCount))
showImages = limitedImages
}
isLoading = false
}
}
// Function to load view content for the specified IDs and handle the completion
private func loadViewContent(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
showImages.insert(contentsOf: newImages, at: 0)
} else {
// Loading content to be displayed below the current content
showImages.append(contentsOf: newImages)
}
// Call the completion handler
completion()
}
}
}
模拟数据库:
class BooksDatabase {
func getOriginal(at pages: [Int], from bookName: String, completion: @escaping ([RPViewContent]) -> Void) {
DispatchQueue.main.async {
let showImages = pages.map { i in
return RPViewContent(id: i, imageType: .clear, image: UIImage(named: String(i))!)
}
completion(showImages)
}
}
func getAllIds(from bookName: String) -> [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?
}
“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)
}
...
}