Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Bark.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@
06B1158F247BB1FB006D91FB /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06B1158E247BB1FB006D91FB /* Message.swift */; };
06B11591247BC132006D91FB /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06B1158E247BB1FB006D91FB /* Message.swift */; };
06BBB88725650C6C0076F63E /* ArchiveSettingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06C5953224811505006B98F3 /* ArchiveSettingManager.swift */; };
062218542F741C91003E2DA0 /* MessageImageCleaner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062218512F741C91003E2DA0 /* MessageImageCleaner.swift */; };
06BBB89125650CCF0076F63E /* ArchiveSettingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06C5953224811505006B98F3 /* ArchiveSettingManager.swift */; };
06BBB896256518760076F63E /* NewServerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06BBB895256518760076F63E /* NewServerViewModel.swift */; };
06BBB8B72567AC140076F63E /* MessageSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06BBB8B62567AC140076F63E /* MessageSettingsViewModel.swift */; };
Expand Down Expand Up @@ -419,6 +420,7 @@
06C5952C2480E3F8006B98F3 /* LabelCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelCell.swift; sourceTree = "<group>"; };
06C5953024811392006B98F3 /* ArchiveSettingCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveSettingCell.swift; sourceTree = "<group>"; };
06C5953224811505006B98F3 /* ArchiveSettingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveSettingManager.swift; sourceTree = "<group>"; };
062218512F741C91003E2DA0 /* MessageImageCleaner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageImageCleaner.swift; sourceTree = "<group>"; };
06C595352481160F006B98F3 /* BKLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BKLabel.swift; sourceTree = "<group>"; };
06C777CF2ECEFF210032A044 /* MarkdownParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownParser.swift; sourceTree = "<group>"; };
06CF784021C7A50300A052D7 /* NotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -686,6 +688,7 @@
0653677729B727A60038BDB8 /* CryptoSettingRelay.swift */,
06F08EA629B1DDFE006AB9CA /* Error+Extension.swift */,
06E944742C07012E00AC86AB /* RealmConfiguration.swift */,
062218512F741C91003E2DA0 /* MessageImageCleaner.swift */,
0687F2A72CCB791A00B2A52F /* UIFont+Extension.swift */,
06BCAE552CDB19260092867A /* GroupMuteSettingManager.swift */,
06C777CF2ECEFF210032A044 /* MarkdownParser.swift */,
Expand Down Expand Up @@ -1349,6 +1352,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
062218542F741C91003E2DA0 /* MessageImageCleaner.swift in Sources */,
0635A8072CE4883A0027E00F /* DonateCell.swift in Sources */,
06840DBB272298FB001B3193 /* BKColor.swift in Sources */,
06C2CF232685B88D0034B127 /* TextCell.swift in Sources */,
Expand Down
2 changes: 2 additions & 0 deletions Bark/AppDelegate+Realm.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ extension AppDelegate {
let expiredMessages = realm.objects(Message.self)
.filter("expireDate != nil AND expireDate <= %@", now)
let expiredMessageIds = Array(expiredMessages.map(\.id))
let expiredImageUrls = MessageImageCleaner.shared.imageUrls(in: expiredMessages)

if !messagesToAdd.isEmpty || !expiredMessages.isEmpty {
do {
Expand Down Expand Up @@ -161,6 +162,7 @@ extension AppDelegate {

if !expiredMessageIds.isEmpty {
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: expiredMessageIds)
MessageImageCleaner.shared.removeUnreferencedImages(expiredImageUrls)
}

for plistUrl in plistFiles {
Expand Down
3 changes: 3 additions & 0 deletions Bark/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
if let realm = try? Realm(),
let message = realm.objects(Message.self).filter("id == %@", id).first
{
// 删除前收集图片地址
let imageUrl = message.image
try? realm.write {
realm.delete(message)
}
WidgetHistorySnapshotStore.shared.refreshFromRealmAsync()
MessageImageCleaner.shared.removeUnreferencedImages([imageUrl])
notifyMessagesDidChange()
}

Expand Down
72 changes: 72 additions & 0 deletions Common/MessageImageCleaner.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// MessageImageCleaner.swift
// Bark
//
// Created by Cnitor on 2026/8/26.
// Copyright © 2026 Cnitor. All rights reserved.
//

import Foundation
import Kingfisher
import RealmSwift

/*
消息记录里的 image 字段只保存图片地址,图片本体由 NotificationServiceExtension 下载后,
存放在 App Groups 共享的 Kingfisher 图片缓存中(详见 ImageDownloader)。
存入时用的是 StorageExpiration.never,不会自动过期,所以消息删除后必须主动清理,否则图片文件会一直残留。

通知中心里推送显示的图不受影响,那是 NSE 复制出来的附件副本。
但下拉查看大图读的是同一份缓存,缓存删掉后会重新联网下载,离线时那条推送的大图就显示不出来了,
这是删图片必然要付出的代价。
*/
class MessageImageCleaner {
static let shared = MessageImageCleaner()

private let cleanQueue = DispatchQueue(label: "me.fin.bark.message-image-clean", qos: .utility)

/// 与 ImageDownloader 使用同一个图片缓存
private lazy var imageCache: ImageCache? = {
guard let groupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.bark") else {
return nil
}
return try? ImageCache(name: "shared", cacheDirectoryURL: groupUrl)
}()

private init() {}

/// 收集这些消息引用的图片地址,需要在消息删除`之前`调用
func imageUrls(in messages: Results<Message>) -> [String] {
// 不用 distinct(by:),image 字段没有加索引,交给 Set 去重更稳妥
return Array(Set(messages.filter("image != nil").compactMap { $0.image }))
}

/// 删除不再被任何消息引用的图片缓存,需要在消息删除`之后`调用
func removeUnreferencedImages(_ imageUrls: [String?]) {
let candidates = Set(imageUrls.compactMap { $0 })
guard !candidates.isEmpty else {
return
}

cleanQueue.async {
guard let cache = self.imageCache, let realm = try? Realm() else {
return
}

// 同一个图片地址可能被多条消息引用,所以先取出所有仍被引用的地址
// image 字段没有加索引,逐个地址去查会退化成多次全表扫描,所以只扫一遍
let referenced = Set(
realm.objects(Message.self)
.filter("image != nil")
.compactMap { $0.image }
)

for imageUrl in candidates.subtracting(referenced) {
guard let url = URL(string: imageUrl) else {
continue
}
// 缓存 key 与 ImageDownloader 存入时保持一致
cache.removeImage(forKey: url.cacheKey, fromMemory: true, fromDisk: true)
}
}
}
}
8 changes: 8 additions & 0 deletions Controller/MessageListViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ class MessageListViewModel: ViewModel, ViewModelType {
realm.delete(message)
}
WidgetHistorySnapshotStore.shared.refreshFromRealmAsync()
MessageImageCleaner.shared.removeUnreferencedImages([model.image])
}
// 删除 cell item
section.messages.removeAll { cellItem in
Expand All @@ -354,10 +355,13 @@ class MessageListViewModel: ViewModel, ViewModelType {
messageResult = self.results?.filter("group == nil")
}
if let messageResult {
// 删除前收集图片地址
let imageUrls = MessageImageCleaner.shared.imageUrls(in: messageResult)
try? realm.write {
realm.delete(messageResult)
}
WidgetHistorySnapshotStore.shared.refreshFromRealmAsync()
MessageImageCleaner.shared.removeUnreferencedImages(imageUrls)
}
}
// 删除 cell item
Expand Down Expand Up @@ -390,6 +394,7 @@ class MessageListViewModel: ViewModel, ViewModelType {
realm.delete(message)
}
WidgetHistorySnapshotStore.shared.refreshFromRealmAsync()
MessageImageCleaner.shared.removeUnreferencedImages([model.image])
}

if let index = section.messages.firstIndex(where: { item in
Expand Down Expand Up @@ -425,10 +430,13 @@ class MessageListViewModel: ViewModel, ViewModelType {
return
}

// 删除前收集图片地址
let imageUrls = MessageImageCleaner.shared.imageUrls(in: messages)
try? realm.write {
realm.delete(messages)
}
WidgetHistorySnapshotStore.shared.refreshFromRealmAsync()
MessageImageCleaner.shared.removeUnreferencedImages(imageUrls)
}

self.page = 0
Expand Down