이미지 캐시 라이브러리입니다.
Swift Concurrency를 적극 활용하여 개발하였습니다.
- 이미지 캐싱
- 이미지 다운로드
- 이미지 다운샘플링
- 메모리 캐시 클래스
- 디스크 캐시 클래스
Swift Package Manager(SPM)
을 지원합니다.
https://github.com/jeongju9216/Jeongfisher.git
URL
을 이용해 이미지를 다운로드, 캐싱합니다.
표시할 이미지 URL
을 전달하세요.
다운샘플링
이미지를 UIImageView
에 설정합니다.
imageView.jf.setImage(with: url)
원본
이미지를 설정하고 싶다면 showOriginalImage
옵션 또는 setOriginalImage(with:)
를 사용하세요.
posterImageView.jf.setImage(with: url, options: [.showOriginalImage])
posterImageView.jf.setOriginalImage(with: url)
네트워크 요청 후 n초 뒤 표시할 placeholder
를 설정할 수 있습니다.
기본 값은 nil
입니다.
nil
인 경우 placeholder
가 표시되지 않습니다.
let placeHolder = UIImage(...)
imageView.jf.setImage(with: url, placeHolder: placeHolder)
placeholder
대기 시간을 설정할 수 있습니다.
waitPlaceHolderTime
로 TimeInterval(초 단위)
를 전달하세요.
기본 값은 1초
입니다.
let placeHolder = UIImage(...)
imageView.jf.setImage(with: url,
placeHolder: placeHolder,
waitPlaceHolderTime: 3.0)
이미지 설정에 부가적인 옵선을 설정할 수 있습니다.
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번 수행함
- 둘이 같았음
- 메모리 측면에서 다운샘플링이 압도적으로 유리하고, 이외의 측면에서는 큰 차이는 없었음
- 다운샘플링 이미지는 화질 저하가 있으므로 UIImageView 크기가 커지면 원본 이미지 설정이 필요함
- 원본 이미지가 필요하면
showOriginalImage
옵션이나setOriginalImage
메서드를 사용하면 되기 때문에 다운샘플링 적용은 좋은 결정이었다고 생각함
메모리 캐시 구현 - 자료구조 선택
https://jeong9216.tistory.com/671#자료구조-선택
배열
은원소 재배치 오버헤드
가 발생함- Hit 데이터를 맨 앞으로 이동시키기 때문에
배열
은 비효율적 (LRU 기준)- Hit 데이터를 맨 뒤로 보내도 동일함
- 뒤에 넣는 경우에는 cost가 부족해졌을 때 앞의 원소를 삭제하므로
원소 재배치 오버헤드
가 발생함
- 이 문제를 해결하기 위해
링크드 리스트
로 구현- 원소 삭제를 효율적으로 하기 위해
양방향 링크드 리스트
로 구현함 - tail을 이용해 맨 뒤 원소에 바로 접근할 수 있어서 효율적임
- 원소 삭제를 효율적으로 하기 위해
링크드 리스트
의느린 탐색
단점을 해소하기 위해 도입함- 메모리 캐시는 데이터를 빠르게 읽어야 하기 때문에 느린 탐색은 치명적인 단점
딕셔너리
를 사용하여 상수 시간복잡도로 데이터를 읽을 수 있음
메모리 캐시 구현 - 동시성 문제
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를 없애면서 코드 가독성을 개선함
Task
와Enum
은중복 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
적용을 결심함
Jeongfisher
와UIImageView
역할이 분리되었음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과 기능 구현만 해주면 되서 확장성이 개선됨 - 다른 메서드로 옵션을 전달할 때 편리해짐