다운샘플링을 통해 이미지를 효율적으로 처리하는 방법 알아보자
쇼핑몰 컬렉션뷰를 구현하는 과정에서 크기가 큰 이미지가 들어올 때 아래같은 현상이 발생했다.
1. 이미지 로딩 속도가 느리고, 메모리 사용량이 급격하게 늘어나는 문제
2. 스크롤시 이미지가 깜빡거리면서 바뀌기도 하는 현상
이 문제를 다운샘플링으로 해결할 수 있었는데, 오늘 그것에 대해 알아보고자 한다.
들어가기 전에 알아둘 것!
메모리 많이 사용 → CPU 활용률 증가 → 많은 CPU 사용 → 배터리 수명 / 앱의 반응성에 부정적인 영향을 미침
UIImage는 이미지를 로드한다.
UIImageView는 렌더링을 담당한다.
디코딩 작업이 비싸다고 하니까 디코딩 하기 전에 사진의 사이즈를 줄이고 → 디코딩하자고 하는것.
그리고 그 디코딩된 이미지 버퍼를 저장하자! 라는 아이디어가 다운샘플링이다.
이렇게 했을 때 앱의 메모리 사용량을 줄일 수 있다.
사진에서 Thumbnail()
부분이 다운샘플링을 진행하는 곳이다. 순서를 봤을 때 디코딩 작업 전에 위치하는것을 확인할 수 있다.
네트워크를 통해 다운받았거나 디스크에서 읽어온 이미지 파일이 data buffer에 담겨있다.
우리는 각 픽셀에 대한 데이터를 frame buffer에 제공해줘야 한다. 하지만 data buffer에는 각 픽셀에 대한 정보가 담겨져 있지 않다. 여기서 decoding 개념이 나오게 되는 것이다.
이때, UIImage는 data buffer에 담겨져 있는 이미지의 크기와 같은 image buffer를 할당해준다. 이게 문제임.
UIImage가 하는 decoding 작업은 굉장히 비싸다. 이유는
decoding 단계는 특히나 사이즈가 큰 이미지에서 CPU-intensive process이다.
디코딩은 이미지가 클 경우 CPU를 많이 소모할 가능성이 있기 때문에 UIImageView가 랜더링할 때마다 디코딩을 수행하지 않는다. 대신, 디코딩된 이미지를 이미지 버퍼에 보관된다.
때문에 디코딩되는 모든 이미지에 대해 영구적이고 큰 메모리 할당이 필요해질 수 있다. (한번 디코딩 해놓으면 렌더링 하기 전에는 그 이미지를 다시 디코딩 하지 않기 때문에 → 영구적이라고 하는것 같다. 특히 사진이 큰 경우에 메모리 할당이 더 많아지는 측면에서 그 단점이 더 부각되는 듯..)
정리하자면
UIKit이 계속 image view에게 rendering하도록 요청하는 것이 아니라 UIImage가 해당 image buffer를 계속 가지고 있어 한 번만 해당 작업이 일어나도록 한다. 또, decoding된 이미지 데이터가 image buffer에 보관되기 때문에 decode되는 모든 이미지에 대해 영구적이고 큰 메모리 할당이 필요해질 수 있다.
따라서 크기가 작은 데이터 버퍼를 디코딩 하는것이 유리하다. 그 이유는
→ 디코딩 이라는 작업이 이미지 크기가 큰 경우 CPU를 많이 소모한다
→ 따라서 이미지뷰가 렌더링 할때마다 디코딩을 하지 않는다 (비용이 많이 들기 때문에)
→ 디코딩 된 이미지는 이미지 버퍼에 보관된다
→ 따라서 디코딩 되는 모든 이미지에 대해 영구적이로 큰 메모리 할당이 필요해질 수 있다
실습을 통해서 실제로 메모리 사용량을 줄어드는지 알아보았다.
간단한 예시 프로젝트를 만들어서 화면에 이미지 뷰
와 일반버튼
, 그리고 다운샘플버튼
을 추가해주었다.
두 버튼 모두 이미지 뷰에 이미지를 로드하는 액션을 연결해 주었다.
단, 일반버튼은 원본 이미지를 그대로 띄우는 방식이고, 다운샘플버튼은 다운샘플링한 이미지를 로드하는 방식이다.
(이미지는 6048 x 4024 사이즈를 사용했다. 일부러 해상도가 엄청 높은 사진을 가져와봤다.)
일반 방식
파라미터로 url을 받아서 이미지 뷰에 넣어주는 코드이다
func setImage(url: URL) {
guard let data = try? Data(contentsOf: url),
let image = UIImage(data: data) else {
return
}
DispatchQueue.main.async {
self.mainImageView.image = image
}
}
다운샘플
다운샘플링을 구현한 코드 이다
func downsample(url: URL) {
let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
guard let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, imageSourceOptions) else {
return
}
let maxDimensionInPixels = 200 * 8
let downsampleOptions = [kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels] as CFDictionary
guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions) else {
return
}
let image = UIImage(cgImage: downsampledImage)
DispatchQueue.main.async {
self.mainImageView.image = image
}
}
두 버튼을 각각 눌러봤을 때 메모리 사용량에 어떤 변화가 있는지 Instrument
의 Allocation
을 통해 메모리 사용량을 확인해보았다.
결과는 이렇게 나왔는데, 더 자세히 알아보면
URL을 직접 이미지를 할당했을때는 메모리 사용량이 304MB 인것을 확인할 수 있다
반면 다운샘플링을 사용했을땐 메모리 사용량이 199MB 이다. (메모리 105MB 덜 사용)
해석해보자면 보통은 150MB정도를 유지하고 있다가, 일반 버튼을 누으면 304MB로 메모리가 치솟고, 다운샘플 버튼을 누으면 199MB의 메모리를 사용하게 된다.
확실히 다운샘플링을 구현한 쪽이 메모리 사용량이 적은 것을 확인할 수 있었다.
왼쪽이 일반적이 방법, 오른쪽이 다운샘플링 방법일 때를 나타낸다. 확인해보면 생각보다 차이가 안나는것을 알 수 있다.
이렇게 몇 번의 처리만으로 메모리를 상당히 아낄 수 있다는 것을 깨달았다. 나의 경우는 사진이 많이 크진 않아서 앱이 깜빡거리는 정도였지만, 썸네일을 많이 사용하는 쇼핑몰같은 경우에는 앱이 종료되는 일이 발생할 수도 있으므로 다운샘플링을 통한 이미지 최적화가 필수일 것 같다.
킹피셔 라는 라이브러리에서도 다운샘플링 기능통해서 이미지 최적화를 한다고 한다. 아직 사용해보진 않았지만 추후에 라이브러리를 사용하게 될 일이 있다면 꽤 유용할 것으로 예상된다.
https://ahyeonlog.tistory.com/87
https://developer.apple.com/videos/play/wwdc2018/416/