Skip to content
Merged
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
7 changes: 7 additions & 0 deletions Bark/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,18 @@ 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()
notifyMessagesDidChange()
// 图片缓存删完再结束后台任务
MessageImageCleaner.shared.removeUnreferencedImages([imageUrl]) {
completionHandler(.newData)
}
return
}

completionHandler(.newData)
Expand Down
84 changes: 84 additions & 0 deletions Common/MessageImageCleaner.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//
// 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] {
return Array(Set(messages.filter("image != nil").compactMap { $0.image }))
}

/// 删除不再被任何消息引用的图片缓存,需要在消息删除`之后`调用
/// - Parameter completion: 清理流程结束后回调,不代表全部删除成功。
/// 无候选图片时立即调用,其余情况在后台队列调用。
func removeUnreferencedImages(_ imageUrls: [String?], completion: (() -> Void)? = nil) {
let candidates = Set(imageUrls.compactMap { $0 })
guard !candidates.isEmpty else {
completion?()
return
}

cleanQueue.async {
guard let cache = self.imageCache, let realm = try? Realm() else {
completion?()
return
}
// GCD 线程上的 Realm 不自动刷新,refresh 以避免读到旧快照
realm.refresh()

// 按索引查询是否还被其它消息引用
let messages = realm.objects(Message.self)
let group = DispatchGroup()
for imageUrl in candidates {
guard let url = URL(string: imageUrl) else {
continue
}
// 原始地址和百分号编码地址对应同一个缓存文件,二者皆查
let aliases = Set([imageUrl, url.absoluteString, imageUrl.removingPercentEncoding].compactMap { $0 })
guard messages.filter("image IN %@", aliases).isEmpty else {
Comment thread
Finb marked this conversation as resolved.
Resolved
continue
}
// 缓存 key 与 ImageDownloader 存入时保持一致
group.enter()
cache.removeImage(forKey: url.cacheKey, fromMemory: true, fromDisk: true) {
group.leave()
}
}
// 等所有异步删除操作结束后再回调,不阻塞清理队列
group.notify(queue: self.cleanQueue) {
completion?()
}
}
}
}
2 changes: 1 addition & 1 deletion Common/RealmConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ let kRealmDefaultConfiguration = {
let fileUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("bark.realm")
let config = Realm.Configuration(
fileURL: fileUrl,
schemaVersion: 18,
schemaVersion: 19,
migrationBlock: { migration, oldSchemaVersion in
switch oldSchemaVersion {
case 0...13:
Expand Down
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])
Comment thread
Finb marked this conversation as resolved.
Resolved
}
// 删除 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
2 changes: 1 addition & 1 deletion Model/Message.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class Message: Object {
@Persisted var body: String?
@Persisted var bodyType: String?
@Persisted var url: String?
@Persisted var image: String?
@Persisted(indexed: true) var image: String?
@Persisted(indexed: true) var group: String?
@Persisted(indexed: true) var createDate: Date?
@Persisted(indexed: true) var expireDate: Date?
Expand Down