actor를 사용해서 ImageCaching 해보기

Groot·2023년 2월 22일
0

TIL

목록 보기
128/153
post-thumbnail

🌱 난 오늘 무엇을 공부했을까?

📌 actor를 사용해서 ImageCaching 해보기

📍 actor를 사용하는 이유

  • 대량의 셀에서 이미지를 다운로드 받는 경우 같은 url로 여러번 이미지의 요청이 이뤄지기 때문에 그 부분을 막기 위해서 lock을 사용하는 경우가 많이 있었다.
  • lock으로도 해결이 가능하지만, actor를 사용하면 Thread Safe를 보장해주기 때문에 간편하다.
  • 그리고 wwdc에서 actor를 설명하는 예시로 Image downLoder를 사용했기 때문에 해보고 싶었다.

📍 내가 원하는 기능

  • 이미지 캐시.
  • url을 입력받아 UIImage를 보내줘야함.
  • cell 재사용 문제 관련해서 생기는 버려지는 reqeust를 취소할 수 있어야함.
  • 같은 url로 여러개의 reqeust 호출이 없어야함.

📍 고민했던 부분

  • request의 취소를 어떻게 할까
  • async/await과 URLSession의 연동
  • Task의 사용

📍 구현

fileprivate actor ImageCacheManager {
    static let shared = ImageCacheManager()
    private var cacheManager = NSCache<NSString, UIImage>()
    private var tasks: [String: Task<UIImage?, Error>] = [:]
    
    private init() {}
}
  • 먼저 actor도 참조타입이니까 싱글톤으로 만들어주고, 이미지를 캐시할 캐시매니저와 Task를 저장할 타입 구현
private func requestImage(url: URL) async throws -> UIImage? {
        guard !Task.isCancelled else { return nil }
        
        let task: (data: Data, response: URLResponse) = try await URLSession.shared.data(from: url)
        
        guard let httpResponse = task.response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
            throw ImageCacheError.invalidResponse
        }
        
        guard let image = UIImage(data: task.data) else {
            throw ImageCacheError.invalidImage
        }
        
        return image
    }
  • 실제로 이미지를 요청하게 될 reqeust를 구현해보자.
  • 내장되어있는 async/await 메서드인 data(from url: URL, delegate: URLSessionTaskDelegate? = nil) 를 사용해서 튜플타입으로 데이터와 응답을 요청받는다.
    func data(from url: URL, delegate: URLSessionTaskDelegate? = nil) async throws -> (Data, URLResponse)
  • 전달받은 응답을 확인 후 200 이면 데이터를 이미지로 변환 후 리턴!
  • !여기서 중요한 부분은 guard !Task.isCancelled else { return nil }
    • 현재 함수가 실행되는 Task가 isCancelled 상태이면 request를 보내지 않고, 빠져나간다. 라고 생각을 했는데 확실히 모르겠다...
func image(key: String) async throws -> UIImage? {
    // 1.
        if let cachedImage = cacheManager.object(forKey: NSString(string: key)) {
            return cachedImage
        }
        
        guard let url = URL(string: key) else { throw ImageCacheError.invalidURL }
        // 2.
        if tasks[key] != nil {
            return try await tasks[key]?.value
        }
        //3.
        let task = Task {
            try await requestImage(url: url)
        }
        //4.
        tasks[key] = task
        //7
        defer { tasks[key] = nil }
        
        guard let image = try await task.value else { return nil }
        //5.
        if let oldImage = cacheManager.object(forKey: NSString(string: key)) {
            return oldImage
        }
        //6.
        cacheManager.setObject(image, forKey: NSString(string: key))
        
        return image
    }
  1. 이미 다운로드 받아서 캐시에 넣은 이미지가 있는지 확인
  2. 동일한 url로 보낸 요청작업이 이미 있는지 확인
  3. 요청을 task에 담아준다.
  4. task 디셔너리에 넣어줌
  5. 특정 url로 이미지를 다운로드 받은 시점에 미리 들어온 동일한 url의 이미지가 있는지 확인 후 있으면 기존 이미지를 내보냄(Actor의 재진입 문제)
  6. 받은 이미지를 캐싱
  7. task가 끝나면 메모리에서 해제
func cancel(url: String) {
        tasks[url]?.cancel()
        tasks[url] = nil
    }
  • 취소기능 구현

📍 후기

  • 내가 원했던 기능은 어느정도 구현 했다고 생각하는데, 대용량 이미지 다운로드 시 알 수 없는 문제가 많음...
  • 어렵다 actor, image 처리,,

https://developer.apple.com/videos/play/wwdc2021/10133
https://developer.apple.com/documentation/uikit/views_and_controls/table_views/asynchronously_loading_images_into_table_and_collection_views

profile
I Am Groot

0개의 댓글