NSCache

キャッシュライブラリiOSmacOSSwiftObjective-CメモリキャッシュApple

キャッシュライブラリ

NSCache

概要

NSCacheは、Apple製プラットフォーム(iOS、macOS、watchOS、tvOS)向けのメモリキャッシュクラスです。システムのメモリ不足時に自動的にエントリを削除し、アプリのメモリ効率を最適化します。Swift・Objective-Cの両方で利用でき、UIImageやカスタムオブジェクトなどの一時的なデータの高速なアクセスを提供します。

詳細

NSCacheは、Foundationフレームワークの一部として提供される「リアクティブキャッシュ」です。通常時は積極的にデータをキャッシュしますが、システムのメモリ圧迫時には自動的に一部のエントリを削除してメモリを解放します。これにより、アプリ自体がメモリ不足で終了されるリスクを軽減します。

主要な技術的特徴:

  • 自動エビクション: システムメモリ不足時の自動削除
  • スレッドセーフ: 複数スレッドからの安全なアクセス
  • 制限設定: エントリ数とコスト総量の上限設定
  • Objective-C基盤: NSObjectベースのキーと値のペア

技術的制約:

  • クラス型のみ: 構造体(struct)は直接格納不可
  • NSStringキー: キーはNSObjectベースである必要
  • 予測不可能性: エビクションタイミングの制御不可

メリット・デメリット

メリット

  • 自動メモリ管理: システム圧迫時の自動削除
  • スレッドセーフ: 同期処理不要
  • 高性能: ネイティブ実装による高速アクセス
  • プラットフォーム統合: Apple全プラットフォーム対応
  • 簡単な導入: 標準ライブラリで追加依存なし
  • メモリ効率: アプリの長時間稼働をサポート
  • 公式サポート: Appleによる保守とサポート

デメリット

  • Apple限定: iOS/macOS/watchOS/tvOSのみ
  • 型制約: クラス型とNSObjectキーのみサポート
  • 予測困難: エビクション動作の制御不可
  • Swift制約: Stringキーの直接使用不可(NSStringが必要)
  • 永続化なし: アプリ終了時にデータ消失
  • 分散対応なし: 単一プロセス内でのキャッシュのみ
  • 機能限定: TTLやカスタムエビクションポリシー未対応

参考ページ

書き方の例

基本的なキャッシュ利用

import Foundation

let cache = NSCache<NSString, UIImage>()
cache.name = "ImageCache"

// 画像をキャッシュに保存
let image = UIImage(named: "sample")
cache.setObject(image!, forKey: "sample_image")

// キャッシュから画像を取得
if let cachedImage = cache.object(forKey: "sample_image") {
    print("キャッシュから画像を取得しました")
}

// キャッシュから特定のオブジェクトを削除
cache.removeObject(forKey: "sample_image")

制限設定とコスト管理

import Foundation

let cache = NSCache<NSString, AnyObject>()

// エントリ数の上限設定(例:5個まで)
cache.countLimit = 5

// 総コストの上限設定(例:10MB)
cache.totalCostLimit = 10 * 1024 * 1024

// コスト付きでオブジェクトを保存
let largeData = Data(count: 1024 * 1024) // 1MB のデータ
cache.setObject(largeData as AnyObject, 
                forKey: "large_data",
                cost: largeData.count)

print("現在のキャッシュ設定:")
print("- エントリ数上限: \(cache.countLimit)")
print("- 総コスト上限: \(cache.totalCostLimit)")

リモート画像キャッシュの実装

import Foundation
import UIKit

class ImageCacheManager {
    private let cache = NSCache<NSString, UIImage>()
    
    init() {
        cache.name = "RemoteImageCache"
        cache.countLimit = 50      // 最大50枚の画像
        cache.totalCostLimit = 50 * 1024 * 1024  // 50MB
    }
    
    func loadImage(from urlString: String, completion: @escaping (UIImage?) -> Void) {
        let key = urlString as NSString
        
        // キャッシュから確認
        if let cachedImage = cache.object(forKey: key) {
            completion(cachedImage)
            return
        }
        
        // ネットワークから取得
        guard let url = URL(string: urlString) else {
            completion(nil)
            return
        }
        
        URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
            guard let data = data, let image = UIImage(data: data) else {
                completion(nil)
                return
            }
            
            // キャッシュに保存(コストは画像データサイズ)
            self?.cache.setObject(image, forKey: key, cost: data.count)
            
            DispatchQueue.main.async {
                completion(image)
            }
        }.resume()
    }
}

// 使用例
let imageManager = ImageCacheManager()
imageManager.loadImage(from: "https://example.com/image.jpg") { image in
    if let image = image {
        // 画像を表示
        print("画像読み込み完了")
    }
}

カスタムオブジェクトのキャッシュ

import Foundation

// キャッシュ対象のカスタムクラス
class UserProfile: NSObject {
    let id: String
    let name: String
    let avatar: Data?
    
    init(id: String, name: String, avatar: Data? = nil) {
        self.id = id
        self.name = name
        self.avatar = avatar
    }
}

class UserProfileCache {
    private let cache = NSCache<NSString, UserProfile>()
    
    init() {
        cache.name = "UserProfileCache"
        cache.countLimit = 100
        
        // メモリ警告時のオブザーバー登録
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(clearCache),
            name: UIApplication.didReceiveMemoryWarningNotification,
            object: nil
        )
    }
    
    func setProfile(_ profile: UserProfile) {
        let key = profile.id as NSString
        let cost = profile.avatar?.count ?? 0
        cache.setObject(profile, forKey: key, cost: cost)
    }
    
    func getProfile(for userId: String) -> UserProfile? {
        return cache.object(forKey: userId as NSString)
    }
    
    @objc private func clearCache() {
        cache.removeAllObjects()
        print("メモリ警告によりキャッシュをクリアしました")
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

// 使用例
let userCache = UserProfileCache()

let profile = UserProfile(id: "user123", name: "田中太郎")
userCache.setProfile(profile)

if let cachedProfile = userCache.getProfile(for: "user123") {
    print("ユーザー: \(cachedProfile.name)")
}

Objective-Cでの利用

#import <Foundation/Foundation.h>

@interface ImageCache : NSObject
@property (nonatomic, strong) NSCache *cache;
@end

@implementation ImageCache

- (instancetype)init {
    if (self = [super init]) {
        _cache = [[NSCache alloc] init];
        _cache.name = @"ImageCache";
        _cache.countLimit = 20;
        _cache.totalCostLimit = 20 * 1024 * 1024; // 20MB
    }
    return self;
}

- (void)cacheImage:(UIImage *)image forKey:(NSString *)key {
    if (image && key) {
        NSData *imageData = UIImagePNGRepresentation(image);
        [self.cache setObject:image forKey:key cost:imageData.length];
    }
}

- (UIImage *)imageForKey:(NSString *)key {
    return [self.cache objectForKey:key];
}

- (void)removeImageForKey:(NSString *)key {
    [self.cache removeObjectForKey:key];
}

@end

Thread-safe なアクセスパターン

import Foundation

class ThreadSafeCache {
    private let cache = NSCache<NSString, AnyObject>()
    private let queue = DispatchQueue(label: "cache.queue", attributes: .concurrent)
    
    func setObject(_ object: AnyObject, forKey key: String) {
        queue.async(flags: .barrier) {
            self.cache.setObject(object, forKey: key as NSString)
        }
    }
    
    func object(forKey key: String, completion: @escaping (AnyObject?) -> Void) {
        queue.async {
            let object = self.cache.object(forKey: key as NSString)
            DispatchQueue.main.async {
                completion(object)
            }
        }
    }
    
    func removeObject(forKey key: String) {
        queue.async(flags: .barrier) {
            self.cache.removeObject(forKey: key as NSString)
        }
    }
}

// 使用例
let threadSafeCache = ThreadSafeCache()

// 複数スレッドから安全にアクセス可能
DispatchQueue.global().async {
    threadSafeCache.setObject("データ1" as AnyObject, forKey: "key1")
}

threadSafeCache.object(forKey: "key1") { object in
    if let data = object as? String {
        print("取得: \(data)")
    }
}