- 가장 중요한 두 자원: CPU+메모리
- 이 두가지는 긴밀하게 연결되어 있다.
- 배터리와 반응성에도 중요한 영향을 미친다.
- UIImage & UIImageView
- UIImage: 이미지 컨텐츠나 아이콘등을 나타내는 모델 객체
- 모델 -> 렌더링의 간단한 구조로 보이지만, 여기서는 디코딩이라는 중요한 과정이 숨어 있다.
- 디코딩
- 버퍼(buffer): 연속된 메모리 영역, 동일한 구조의 원소들의 나열로 볼 수 있음
- 이미지는 개별픽셀의 색을 나타내는 원소들로 메모리에서 표현된다.(Image buffer)
- 이미지 버퍼의 크기는 이미지의 사이즈에 영향을 받는다.
- 렌더링 된 이미지는 프레임 버퍼에 쓰여져서, 디스플레이로 보여지게 된다. -> 디스플레이로 보여지는 과정은 일정한 주기로 일어난다.
- 이미지 데이터가 바뀌지 않으면 기존에 버퍼에 있던 것을 그대로 내보내고, 버퍼 내용이 바뀌면 바뀐데이터가 가게 된다.
- 이미지 파일은 메타 데이터와 압축된 데이터로 이루어져 있기 떄문에(data buffer) 이미지 버퍼 형태로 바꾸기 위해서는 이를 해석하는 과정이 필요하다.
- 디코딩은 CPU를 집중적으로 쓰는 작업이기 때문에, 반복적인 요청을 대비해 메모리에 유지된다.
- 메모리를 차지하고 있는 양은 뷰의 크기가 아닌 이미지 자체의 크기이기 때문에 이게 계속 메모리를 차지하고 있으면, 메모리 사용량에 부정적인 영향을 미친다.
- 메모리 사용량이 너무 많으면
- 파편화가 늘어난다.
- 지역성이 나빠진다.
- 시스템이 메모리를 압축하기 시작한다.
- 프로세스가 죽을 수도 있다.
- 선제적으로 메모리를 줄이는 법
- Downsampling
- Load(CGImageSource)
- Thumbnail(CGImageRef)
- Decode
- UIImage
- Render(UIImageView)
func downSample(imageAt imageURL: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage {
let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, imageSourceOptions)!
let maxDimensionInPixels = max(pointSize.width, pointSize.heihgt) * scale
let downsampleOptions = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true, // core graphics에게 해당 이미지로 이미지 버퍼를 만들 것임을 알려주는 역할
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimentionInPixels] as CFDictionary
let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions)!
return UIImage(cgImage: downsampledImage)
}
- 스크롤 뷰에서의 디코딩
- 스크롤 할 때 마다 CPU 사용량이 급격하게 올라가면서 , 반응성과 배터리 수명에 악영향을 준다.
- 해결법
- prefecting
- background decoding/downsampling
func collectionView(_ collectionView: UICollectionView,
prefecthItemsAt indexPaths [IndexPath]) {
// concurrent큐로 하면 스레드 폭발의 위험성이 있다.
// serial Queue를 만들어서 해라
for indexPath in indexPaths {
Dispatchqueue.global(qos: .userInitiated).async {
let downsampledImage = downsample(images[indexPath.row])
DispatchQueue.main.async { self.update(at: indexPath, with: downsampledImage) }
}
}
}
- Image source
- asset catalog에 있는 이미지 -> 권장
- 이름과 trait에 기반한 참조가 쉽다.
- 버퍼 캐싱이 스마트하게 된다.
- 디바이스별 씨닝이 가능
- 벡터 이미지도 쉽게 다룰 수 있다.
- 앱/프레임워크 번들에 있는 파일들
- Document나 cache에 있는 파일
- 벡터 이미지 파이프라인: 디코딩 대신 레스터라이징 단계가 들어감
- Xcode가 컴파일 할 때 관련된 스케일에 대한 레스터라이제이션을 미리 해준다.
- 이렇게 미리 레스터라이제이션된 이미지는 이미지가 본연의 사이즈로 그려질 때 사용된다.
- 만약 고정 사이즈라면, 벡터말고 에셋을 여러개 넣어서 대응하는 게 낫다.
- Custom Drawing with UIKit
- 커스텀 드로잉은 권장하지 않는다. -> CALayer가 이미지 버퍼를 직접 가지고 있어야 되고, 그걸 개발자가 직접 채워야 하기 때문이다.
- backing store
- 뷰의 크기만큼 메모리 차지
- 더 넓은 색영역을 쓰면, 크기가 동적으로 넓어진다.(iOS 12부터)
- CALayer.contentFormat을 설정하면(수동으로 색영역 지정), 이러한 최적화가 사라진다.
- layerWillDraw를 오버라이드 했다면, 최적화를 실수로 끄지 않도록 주의하라.
- backing store 사용량 감소
- 큰 뷰를 서브뷰로 나누라
- 커스텀 드로잉은 줄이거나 없애라
- 이미지 데이터를 중복해서 복사하지 말것
- 최적화된 뷰 프로퍼티나 서브뷰들을 적극 활용할 것
- backgroundColor를 지정할 때도 패턴으로 컬러를 지정하면 최적화가 안된다. -> UIImageView를 써라
- UIView.maskView와 CALayer.maskLayer는 임시적인 이미지 버퍼를 필요로 한다.
- CornerRadius는 추가 버퍼를 필요로 하지 않는다.
- CornerRadius로 커버가 안된다면, UIImageView를 고려하라
- UIImageView는 단색 이미지에 컬러를 입히는 작업을 프레임 버퍼에 다이렉트로 할 수 있다. -> renderingMode를 template으로 하고, tintColor를 먹이면 된다.
- UILabel은 단색 문자열에 최적화 되어 있다.
- 약 75%정도 아낄 수 있다.
- 다양한 색깔의 문자열, 이모지 등에 대해서는 좀 더 메모리를 소모한다.
- OffScreen 렌더링이 필요하다면
- UIGraphicsImageRenderer를 쓰면 된다.
- UIGraphicsBeginImageContext()는 넓은 색영역을 쓰지 않기 때문에 권장되지 않는다.
- backing Store는 CALayer와 동일하게 제공되며, 어떤 액션을 취하는 지에 따라 동적으로 조절된다.
- iOS 12 이전에서는 하드웨어에 따라 와이드 컬리 지원의 기본 설정 여부가 갈린다.
- prefersExtendedRange 프로퍼티를 설정해서 직접 세팅할 수는 있다.
- UIImage를 다시 렌더링 해야 한다면, imageRendererFormat 프로퍼티로 렌더링 옵션을 조절할 수 있다.
- Advanced CPU and GPU techniques
- Core Image를 쓰면 GPU를 활용하기 때문에, 실시간 이펙트 처리에 좋다.
- Metal, Vision, Accelerate 등에 데이터를 넘길 때는, CVPixelBuffer를 사용하라
- 생성자를 잘 선택해서, 이미 디코딩 된 이미지를 되돌리지는 말라
- CPU와 GPU간 데이터 이동 중에 경함이 일어나지 않도록 주의하라
- Accelerate에서 사용하는 포맷에 맞는지 확인을 할 것