검색 결과를 UICollectionView에 표시할때 크기가 큰 이미지가 들어오는 상황에서
<원본 이미지 크기들>
let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, imageSourceOptions) else {
return UIImage()
}
if let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as Dictionary? {
let pixelWidth = imageProperties[kCGImagePropertyPixelWidth] as! Int
let pixelHeight = imageProperties[kCGImagePropertyPixelHeight] as! Int
print(url.path, "Width: \(pixelWidth), Height: \(pixelHeight)")
}
<메모리 사용량>
5번 검색하면 메모리 사용량이 1기가가 넘었다...
이걸 어째 🤯
큰 이미지를 사용해서 이런 결과가 나왔다면... 원본 이미지를 셀의 크기에 맞게 width 60 height 80으로 줄이면 해결되지 않을까?
단순히 이미지 사이즈만 줄이면 처리 속도도 빨라지지 않을까? 라는 이유로 처음엔 resizing을 생각했다.
하지만, WWDC 영상 'Image and Graphics Best Practices' 를 보고 생각이 바뀌었다.
결론부터 말하자면 메모리 사용량을 줄이려면, resizing이 아니라 downsampling을 해야한다.
지금부터 그 이유에 대해 알아보자 😆
UIImageView는 view를 display하는 역할을 하고,
UIImage는 view를 load하는 역할을 한다.
크게 보면 UIImage가 view(=이미지)를 로딩하면
UIImageView가 그 결과물을 렌더링해서 Frame Buffer라는 곳에서 보여준다.
UIImage가 하는일
Data Buffer(JPEG, PNG...)에서 Image Buffer로 Decoding
UIImageView가 하는일
Image Buffer를 UIImageView의 contentMode에 맞게 rendering 하여 Frame Buffer에 표시
이때 UIImage가 하는 decoding 작업은 굉장히 CPU 집약적이라서 비용이 크다고 한다. 따라서 UIImage는 한번 디코딩된 이미지(Image Buffer)를 거의 영구적으로 가지고 있어버림...
이때 저 디코딩된 이미지는 원본 이미지라서 사이즈가 큰 이미지인 경우에 굉장한 비용이 들어가는 것,,
(렌더링된 작은 UIImageView의 사이즈를 가지고 있으면 참 좋을텐데...)
이렇게 디코딩된 이미지를 저장하려고 메모리를 많이 할당하게 되면 다른 컨텐츠들을 파편화시키게됨... cpu가 개입하고.. 심하면 앱종료로 이어진다.
하여튼 결론은 원본 이미지의 사이즈가 큰 경우에 UIImage가 디코딩된 이미지를 가지고 있으므로 메모리 사용량이 늘어난다는 소리다.
그렇다면, UIImage가 data buffer를 디코딩하기 전에 원본 사이즈를 줄이고 나서 디코딩하고, 이때 디코딩된 이미지(image buffer)를 저장하면 되겠지!
그렇게 하면 앱의 메모리 사용량을 줄일 수 있다.
<원래 방식>
<downsampling 하는 경우>
디코딩 하기전에 data buffer 자체의 사이즈를 줄여버려서 메모리 절약 가능~!
url을 통해 이미지를 불러오고,
downsampling 하고싶은 사이즈를 입력하면 된다.
예를 들어 이미지 크기를 width 60, height 90으로 줄이고 싶다면 CGSize(width: 60, height: 90)
입력하면 된다.
func downSample(at url: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage {
let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, imageSourceOptions) else {
return
}
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
let downsampleOptions = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
] as CFDictionary
guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions) else {
return UIImage()
}
return UIImage(cgImage: downsampledImage)
}
커스텀 셀 configure에서 url을 받아서 셀의 이미지뷰에 이미지를 할당하는 부분에서 적용해주었다.
let serialQueue = DispatchQueue(label: "Decode queue")
serialQueue.async { [weak self] in
let downsampled = downSample(
at: url,
to: CGSize(width: 60, height: 90),
scale: UIScreen.main.scale
)
DispatchQueue.main.async {
self?.posterImageView.image = downsampled
}
}
이미지들이 블러처리한듯이 퀄리티가 낮아졌다.
그 이유는 앞에서 설명하지 않은 마지막 파라미터
scale 때문... scale을 화면에 맞게 설정하지 않으면 이미지 퀄리티가 낮아보인다.
따라서 scale에는 현재 화면의 scale 정보인UIScreen.main.scale
을 넣어줬는데
DownSampling makes image blurry
위 링크에 따르면 디스플레이의 종류에 따라서 1point가 몇개의 pixel로 이루어지는지가 달라진다고 한다.
UIScreen.main.scale
이
그러면 똑같은 CGSize(width: 60, height: 90)을 표현하기 위해서 iPhone 13 Pro는 180, 270 pixel을 사용하고 iPhone SE는 120, 180 pixel을 사용한다는 것이다.
이렇게 downsampling을 적용한 결과 같은 횟수로 검색한 결과 메모리 사용량이 현저하게 줄었다.. 거의 1/5 😆