Skip to content

jeongju9216/Jeongfisher

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

59 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Jeongfisher

이미지 캐시 라이브러리입니다.
Swift Concurrency를 적극 활용하여 개발하였습니다.

기능

  1. 이미지 캐싱
  2. 이미지 다운로드
  3. 이미지 다운샘플링
  4. 메모리 캐시 클래스
  5. 디스크 캐시 클래스

설치

Swift Package Manager(SPM)을 지원합니다.

https://github.com/jeongju9216/Jeongfisher.git

사용법

1. 이미지 캐싱

url

URL을 이용해 이미지를 다운로드, 캐싱합니다.
표시할 이미지 URL을 전달하세요.
다운샘플링 이미지를 UIImageView에 설정합니다.

imageView.jf.setImage(with: url)

원본 이미지를 설정하고 싶다면 showOriginalImage 옵션 또는 setOriginalImage(with:)를 사용하세요.

posterImageView.jf.setImage(with: url, options: [.showOriginalImage])
posterImageView.jf.setOriginalImage(with: url)

placeholder

네트워크 요청 후 n초 뒤 표시할 placeholder를 설정할 수 있습니다.
기본 값은 nil 입니다.
nil인 경우 placeholder가 표시되지 않습니다.

let placeHolder = UIImage(...)
imageView.jf.setImage(with: url, placeHolder: placeHolder)

waitPlaceHolderTime

placeholder 대기 시간을 설정할 수 있습니다.
waitPlaceHolderTimeTimeInterval(초 단위)를 전달하세요.
기본 값은 1초입니다.

let placeHolder = UIImage(...)
imageView.jf.setImage(with: url,
                      placeHolder: placeHolder,
                      waitPlaceHolderTime: 3.0)

options

이미지 설정에 부가적인 옵선을 설정할 수 있습니다.
e.g.

imageView.jf.setImage(with: url, options: [.cacheMemoryOnly])
옵션 목록
  • cacheMemoryOnly
    • 메모리 캐시만 사용하고, 디스크 캐시를 사용하지 않습니다.
  • onlyFromCache
    • 캐시 데이터만 사용합니다.
    • 캐시에 없어도 네트워킹을 하지 않습니다.
  • forceRefresh
    • 항상 네트워킹을 합니다.
    • 캐시를 사용하지 않습니다.
  • showOriginalImage
    • 다운샘플링을 하지 않습니다.
  • disableETag
    • ETag를 확인하지 않습니다.
  • downsamplingScale(CGFloat)
    • 다운샘플링 비율 설정. e.g. 1.0(ImageView와 같은 Size), 1.5(ImageView의 1.5배 Size)

기술적 고민

⭐️ 다운샘플링 적용

관련 블로그 포스팅 (추천)

https://jeong9216.tistory.com/670

적용 이유

  • Jeongfisher는 썸네일처럼 작은 이미지를 보여주는 용도로 적합함
  • Downsampling을 기본으로 적용하여 메모리 효율 증가 효과를 기대함

적용 방법

  • WWDC18 - Image and Graphics Best Practices에서 소개된 방법을 사용함

성능 비교

  • 다운샘플링 이미지 설정과 원본 이미지 설정을 비교함
  • XCTest에서 XCTClockMetric, XCTMemoryMetric, XCTCPUMetric 옵션으로 성능을 측정함
  • 1000x1000 이미지 설정을 100번 수행함

성능 비교 결과

Clock Monotonic Time

  • 둘 다 0.00으로 동일
  • ClockMonotonicTime

메모리 사용량

  • 다운샘플링8배 낮았음
  • 왼쪽이 다운샘플링, 오른쪽이 원본 이미지
  • 다운샘플링 메모리 원본 메모리

Memory Peak Physical

  • 다운샘플링3MB 더 낮았음
  • Memory Peak Physical

Memory Physical

  • 다운샘플링3.113 kB로 약 4배 더 낮았음
  • Memory Physical

CPU(CPU Cycles, CPU Instructions Retired, CPU Time)

  • 둘이 같았음

성능 비교 결론

  • 메모리 측면에서 다운샘플링이 압도적으로 유리하고, 이외의 측면에서는 큰 차이는 없었음
  • 다운샘플링 이미지는 화질 저하가 있으므로 UIImageView 크기가 커지면 원본 이미지 설정이 필요함
  • 원본 이미지가 필요하면 showOriginalImage 옵션이나 setOriginalImage 메서드를 사용하면 되기 때문에 다운샘플링 적용은 좋은 결정이었다고 생각함
메모리 캐시 구현 - 자료구조 선택

관련 블로그 포스팅

https://jeong9216.tistory.com/671#자료구조-선택

배열과 링크드 리스트

  • 배열원소 재배치 오버헤드가 발생함
  • Hit 데이터를 맨 앞으로 이동시키기 때문에 배열은 비효율적 (LRU 기준)
    • Hit 데이터를 맨 뒤로 보내도 동일함
    • 뒤에 넣는 경우에는 cost가 부족해졌을 때 앞의 원소를 삭제하므로 원소 재배치 오버헤드가 발생함
  • 이 문제를 해결하기 위해 링크드 리스트로 구현
    • 원소 삭제를 효율적으로 하기 위해 양방향 링크드 리스트로 구현함
    • tail을 이용해 맨 뒤 원소에 바로 접근할 수 있어서 효율적임

딕셔너리(Dictionary)

  • 링크드 리스트느린 탐색 단점을 해소하기 위해 도입함
    • 메모리 캐시는 데이터를 빠르게 읽어야 하기 때문에 느린 탐색은 치명적인 단점
  • 딕셔너리를 사용하여 상수 시간복잡도로 데이터를 읽을 수 있음
메모리 캐시 구현 - 동시성 문제

관련 블로그 포스팅

https://jeong9216.tistory.com/671#동시성-문제

딕셔너리의 동시성 문제 해결

  • 딕셔너리는 Thread safe 하지 않음

    • 같은 키에 여러 thread가 동시에 접근하면 런타임 에러가 발생
  • 이를 해결하기 위해 두 가지 방법을 고민함

  • DispatchQueue barrier (기각)

    • 리턴이 있는 메서드에서 completionHandler를 사용해야 함
    • 리턴이 있는 메서드가 많았기 때문에 코드 복잡도가 높아질 것이라 판단하여 기각
  • NSLock (채택)

    • 간단하면서 강력한 Lock을 지원
    • 처음에는 lock 효율을 위해 좁은 범위로 lock과 unlock을 수행함
    • "lock은 안정성이 최우선이다"라는 리뷰를 받고 defer를 활용해 메서드 단위로 lock을 수행함
디스크 캐시 구현 - ETag 적용

관련 포스팅

https://jeong9216.tistory.com/671#디스크-캐시

  • 디스크 캐시의 장기 보관 특징을 극대화할 수 있는 방법을 고민함
  • ETag를 활용하여 장기 보관 개선
  • ETag가 동일하다면 만료일을 갱신해서 캐시 데이터 보관 기간을 늘림
  • ETag를 지원하지 않거나 사용하지 않고 싶다면 옵션으로 비활성할 수도 있음
⭐️ JFImageDownloader 구현

관련 포스팅 (추천)

https://jeong9216.tistory.com/672

발생한 문제

  • 중복 Request를 처리하는 과정에서 문제가 있었음
    • 동일한 URL이 동시에 Request가 되면 첫 번째 Request만 처리됨
    • 예를 들어, 10개의 UIImageView가 동일한 URL을 Request 하면 1번 UIImageView에만 이미지가 설정되고 나머지 UIImageView에는 이미지 설정이 되지 않음
  • 딕셔너리 동시성 문제를 DispatchQueue로 해결해서 코드 복잡성이 증가함

해결 방법

  • actor, Task, Enum, async/await을 활용하여 해결함
  • actor는 동시성 문제르 해결하기 위해 적용
    • DispatchQueue를 없애면서 코드 가독성을 개선함
  • TaskEnum중복 Request를 처리하기 위해 적용
    • Enum 연관값으로 Task를 전달
    • 딕셔너리로 Enum을 관리
    • Enum 케이스를 변경하여 완료 처리
        let jfImageData = try await task.value
        cache[url] = .complete(jfImageData)
    • 중복 Request가 들어왔다면, Task의 value를 대기하고 완료되면 전달
        //이미 같은 URL 요청이 들어온 경우 Task 완료 대기
        if let cached = cache[url] {
            switch cached {
            case .inProgress(let task):
                return try await task.value
            case .complete(let jfImageData):
                return jfImageData
            }
        }

개선 후 느낀 점

  • 동일한 Request가 들어오면 첫 번째 Request 결과를 대기했다가 반환할 수 있게 됨
  • Swift Concurrency가 코드 가독성에 큰 기여를 한다는 것을 다시 한 번 느낌
  • actor가 처음에는 너무 어려웠지만, 직접 사용해보니 편하게 동시성 문제를 해결할 수 있다는 것을 배움
⭐️ Extension Wrapper

관련 포스팅

https://jeong9216.tistory.com/667
https://jeong9216.tistory.com/673#적용-이유

적용 이유

  • Extension Wrapper 적용 전에는 UIImageView extension에 메서드를 추가함
  • 메서드가 늘어나면서 문제가 발생
    • UIImageView의 역할이 커짐
    • Jeongfisher 기능이 필요 없는 UIImageView에서도 Jeongfisher 메서드가 노출됨
  • 킹피셔는 kf로 킹피셔 메서드를 사용하는 것을 보고 Extension Wrapper 적용을 결심함

적용 결과

  • JeongfisherUIImageView 역할이 분리되었음
  • Jeongfisher의 확장, 수정이 UIImageView에 영향을 주지 않게 됨
  • Jeongfisher 기능만 테스트하기 수월해짐
  • 대형 라이브러리 오픈소스를 읽으면서 배울 점이 많다는 것을 느낌
cancelDownloadImage 구현

관련 포스팅

https://jeong9216.tistory.com/673#canceldownloadimage-구현

발생한 문제

  • cancelDownloadImage는 Task를 중간에 취소하는 메서드
  • 다운로드 중에 CollectionViewCell이 안 보이게 됐을 때 Task를 취소해서 메모리 효율을 높이는 용도로 사용됨
  • 처음에는 URL을 전달하여 구현했지만, URL 데이터 의존성 때문에 cancel 메서드 호출이 자유롭지 못함
  • 킹피셔도 따로 URL을 전달하지 않으니 개선하기로 결정

해결 방법

  • Extension에는 저장 프로퍼티를 저장할 수 없기 때문에 고전함
  • Objective-C 기능인 objc_getAssociatedObject과 objc_setAssociatedObject로 해결함
    /// UIImageView가 사용한 URL
    private var downloadUrl: String? {
        get { getAssociatedObject(base, &JFAssociatedKeys.downloadUrl) }
        set { setRetainedAssociatedObject(base, &JFAssociatedKeys.downloadUrl, newValue) }
    }
    
    ...
    
    func getAssociatedObject<T>(_ object: Any, _ key: UnsafeRawPointer) -> T? {
        return objc_getAssociatedObject(object, key) as? T
    }
    
    func setRetainedAssociatedObject<T>(_ object: Any, _ key: UnsafeRawPointer, _ value: T) {
        objc_setAssociatedObject(object, key, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    }
  • Swift로만 해결 가능한 방법이 생기면 꼭 개선할 것이라 다짐함
JFOption 적용

관련 포스팅

https://jeong9216.tistory.com/673#jfoption%C2%A0구현

발생한 문제

  • setImage를 할 때 필요한 옵션을 전달할 수 있음
  • 기존에는 옵션 하나하나를 메서드 파라미터로 전달함
    public func setImage(url: String,
                          placeHolder: UIImage? = nil,
                          waitPlaceHolderTime: TimeInterval = 1,
                          useCache: Bool = true)
  • 옵션이 추가될 때마다 수정 범위가 커서 확장성이 낮은 구조라는 걸 꺠달음

해결 방법

  • 옵션을 JFOption Enum으로 묶어서 관리함
  • setImage 메서드 파라미터로 Set<JFOption>을 전달해서 확장성을 개선함
    • 옵션의 중복을 없애기 위해 Set을 선택
    public func setImage(
            with url: URL,
            placeHolder: UIImage? = nil,
            waitPlaceHolderTime: TimeInterval = 1.0,
            options: Set<JFOption> = [])

개선 후 느낀 점

  • 기존에는 옵션이 추가될 때마다 메서드가 사용되는 모든 곳을 바꿔야 했음
  • 개선 후에는 JFOption Enum과 기능 구현만 해주면 되서 확장성이 개선됨
  • 다른 메서드로 옵션을 전달할 때 편리해짐

About

유정주의 이미지 캐시 라이브러리

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages