해상도
- 화면 또는 인쇄 등에서 이미지의 정밀도를 나타내는 지표
- 이미지를 표현하는 데 몇 개의 픽셀 또는 도트로 나타냈는지 그 정도를 나타내는 말이다.
- 즉, 해상도가 높다는 말은 1인치 당 찍게 되는 점의 수가 많기 때문에 선명하다는 것이다.
- 점의 개수가 많아진다는 것은 메모리가 더 필요하고 처리 과정이 느려질 수 있다는 것이기 때문에, 적절한 해상도를 사용해야 한다.
문제점
- 이미지는 일반적인 애플리케이션 UI 에 비해 크기가 크다.
- 더 높은 해상도의 이미지는 특별한 이점 없이 메모리를 더 많이 차지하며 즉시 추가로 확장해야 하는 부가적인 성능 오버헤드가 발생한다.
- 예시) 시스템 갤러리 애플리케이션은 Android 기기의 카메라를 사용하여 촬영한 사진을 표시하는데 일반적으로 이러한 사진은 기기의 화면 밀도보다 해상도가 훨씬 높다.
- 예시) 크기가 100*150인 ImageView 에 2000*3000 해상도를 가진 이미지를 보여주는 것은 매우 큰 리소스 낭비가 생긴다.
해결책
- 제한된 메모리로 작업하는 경우 메모리에 저해상도 버전만 로드하는 것이 이상적이다.
- 저해상도 버전은 이미지를 표시하는 UI 구성요소의 크기와 일치해야 한다.
방법
1. 큰 비트맵을 효율적으로 로드
- 작게 서브 샘플링한 버전을 메모리에 로드하여 애플리케이션당 메모리 제한을 초과하지 않고 큰 비트맵을 디코딩하기
비트맵 크기 및 유형 읽기
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeResource(resources, R.id.myimage, options)
val imageHeight: Int = options.outHeight
val imageWidth: Int = options.outWidth
val imageType: String = options.outMimeType
- BitmapFactory 클래스는 다양한 소스에서 Bitmap 을 만들 수 있는 여러 가지 디코딩 메서드(decodeByteArray(), decodeFile(), decodeResource() 등)를 제공한다. 이미지 데이터 소스에 따라 가장 적합한 디코딩 메서드를 선택한다.
- 이러한 메서드는 생성된 비트맵에 메모리를 할당하려고 하므로 쉽게 OutOfMemory 예외가 발생할 수 있다.
- 각 유형의 디코딩 메서드에는 BitmapFactory.Options 클래스를 통해 디코딩 옵션을 지정할 수 있는 추가 서명이 있다.
- 디코딩 시 inJustDecodeBounds 속성을 true 로 설정하면 메모리 할당이 방지된다.
- 즉, 이 기법을 사용하면 비트맵을 생성(메모리 할당 포함)하기 전에 이미지 데이터의 크기와 유형을 읽을 수 있다. 비트맵 객체에 null 이 반환되지만 outWidth, outHeight, outMimeType 은 설정된다.
- 디코딩 시 inJustDecodeBounds 속성을 true 로 설정하면 메모리 할당이 방지된다.
- 가용 메모리에 적합한 예측 가능 크기의 이미지 데이터를 제공하는 소스를 절대적으로 신뢰하지 못한다면, java.lang.OutOfMemory 예외를 방지하기 위해 비트맵 디코딩 전에 비트맵 크기를 확인한다.
축소 버전을 메모리로 로드
fun decodeSampledBitmapFromResource(
res: Resources,
resId: Int,
reqWidth: Int,
reqHeight: Int
): Bitmap {
// 먼저 inJustDecodeBounds=true로 디코드하여 차원을 확인합니다.
return BitmapFactory.Options().run {
inJustDecodeBounds = true
BitmapFactory.decodeResource(res, resId, this)
// inSampleSize 계산
inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
// inSampleSize 세트로 비트맵 디코딩
inJustDecodeBounds = false
BitmapFactory.decodeResource(res, resId, this)
}
}
- inJustDecodeBounds 를 true 로 설정한 상태에서 디코딩한 다음 옵션을 전달하고 새 inSampleSize 값과 false 로 설정한 inJustDecodeBounds 를 사용하여 다시 디코딩한다.
// 타겟 너비와 높이를 기준으로 2의 거듭제곱인 샘플 크기 값을 계산하는 메서드
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
// 이미지의 원시 높이와 너비
val (height: Int, width: Int) = options.run { outHeight to outWidth }
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight: Int = height / 2
val halfWidth: Int = width / 2
// 2의 거듭제곱인 가장 큰 inSampleSize 값을 계산하고
// 높이와 너비를 모두 요청된 높이와 너비보다 크게 유지합니다.
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}
- 이미지 크기를 알면 전체 이미지를 메모리에 로드할지 아니면 서브 샘플링된 버전을 대신 로드할지 결정할 수 있다.
- 이미지를 서브 샘플링하여 더 작은 버전을 메모리에 로드하도록 디코더에 지시하려면 BitmapFactory.Options 객체에서 inSampleSize 를 true 로 설정하면 된다.
- 예시) 해상도가 2048x1536이고 inSampleSize 가 4로 디코딩된 이미지는 약 512x384의 비트맵을 생성한다.
- 이 비트맵을 메모리에 로드하면 전체 이미지에 12 MB 대신 0.75 MB 가 사용된다(비트맵 구성은 ARGB_8888 이라고 가정함).
- 예시) 해상도가 2048x1536이고 inSampleSize 가 4로 디코딩된 이미지는 약 512x384의 비트맵을 생성한다.
imageView.setImageBitmap(
decodeSampledBitmapFromResource(resources, R.id.myimage, 100, 100)
)
- 위 코드 예에서와 같이 100x100픽셀의 썸네일을 표시하는 ImageView 에 임의의 큰 비트맵을 쉽게 로드할 수 있다.
- 필요에 따라 적절한 BitmapFactory.decode* 메서드를 대체하여 비슷한 절차를 따라 다른 소스의 비트맵을 디코딩할 수 있다.
2. 이미지 포맷 최적화
- PNG 를 JPG 로 변환하면 손실 압축 방식으로 파일 크기가 줄어 로딩 시간이 단축되지만, 일부 데이터가 영구적으로 삭제되어 품질이 저하된다.
- WebP 는 무손실과 손실 압축을 모두 지원하며, 손실 방식의 경우 JPEG 보다 약 30% 더 작고, 무손실 방식의 경우 PNG 보다 20~30% 더 작은 파일 크기를 가진다.
Webp 이미지 만들기
3. 라이브러리에 제공되는 캐시 기능
4. 이미지를 위한 성능 최적화
- 가능하면 비트맵 대신 벡터 사용하기
- 화면에 무언가를 시각적으로 표시할 때, 벡터로 표현할 수 있는지를 고려해야 한다. 벡터 이미지는 크기를 조정해도 픽셀화되지 않기 때문에 비트맵보다 더 유리하다. 하지만 모든 것을 벡터로 표현할 수는 없다. 예를 들어, 카메라로 촬영한 이미지는 벡터로 변환할 수 없다.
- 다양한 화면 크기에 맞는 대체 리소스 제공하기
- 앱과 함께 이미지를 제공할 때, 기기 해상도에 따라 다양한 크기의 애셋을 제공하는 것이 좋다. 이렇게 하면 앱의 다운로드 크기가 줄어들고, 해상도가 낮은 기기에서는 더 낮은 해상도의 이미지가 로드되어 성능이 개선된다. (다양한 기기 크기에 맞는 대체 비트맵을 제공하는 방법)
- ImageBitmap 사용 시 그리기 전에 prepareToDraw 호출하기
- ImageBitmap 을 사용할 때 GPU 에 텍스처를 업로드하기 위해 실제로 그리기 전에 ImageBitmap#prepareToDraw()를 호출해야 한다. 이 메서드를 호출하면 GPU 가 텍스처를 준비하여 화면에 시각 요소를 표시하는 성능이 향상됩니다. 대부분의 이미지 로드 라이브러리는 이러한 최적화를 이미 수행하지만, ImageBitmap 클래스를 직접 사용할 경우 이 점을 염두에 두어야 한다.
- Composable 에 Painter 대신 Int DrawableRes 또는 URL 을 매개변수로 전달하기
- 이미지 처리의 복잡성으로 인해(예: 비트맵의 equals 함수를 작성하는 데 드는 높은 계산 비용) Painter API 는 안정적인 클래스로 명시되지 않는다. 불안정한 클래스의 경우 데이터 변경 여부를 컴파일러가 쉽게 추론할 수 없어 불필요한 리컴포지션이 발생할 수 있다. 따라서 Painter 를 매개변수로 전달하기보다는, URL 이나 드로어블 리소스 ID 를 매개변수로 컴포저블에 전달하는 것이 좋다.
- 비트맵을 필요한 기간보다 오래 메모리에 저장하지 않기
- 메모리에 로드하는 비트맵이 많을수록 기기의 메모리가 부족해질 가능성이 높아진다. 예를 들어, 화면에 대량의 이미지 컴포저블 목록을 로드하는 경우, 큰 목록을 스크롤할 때 메모리를 효율적으로 관리하기 위해 LazyColumn 이나 LazyRow 를 사용하는 것이 좋다.
- 대용량 이미지를 AAB/APK 파일로 패키징하지 않기
- 앱 다운로드 크기가 커지는 주요 원인 중 하나는 AAB 또는 APK 파일 내에 포함된 그래픽입니다. APK Analyzer 도구를 사용하여 필요 이상으로 큰 이미지 파일이 패키징되지 않도록 확인해야 한다. 이미지를 줄이거나, 서버에 두고 필요할 때만 다운로드하는 방법도 고려하는 것이 좋다.
'안드로이드 > Android' 카테고리의 다른 글
[Android] 빌드, MultiDex (0) | 2024.11.30 |
---|---|
[Android] Image Loader Library (0) | 2024.11.11 |
[Android] Memory Cache와 Disk Cache (0) | 2024.11.08 |