본문 바로가기

Android(Kotlin) Study A부터 Z까지 등반

About Graphic API in android.graphics (커스텀뷰란?) Canvas, Bitmap, Paint

Bitmap

  • 이미지를 표현하기 위해서 사용되는 녀석
  • 안드로이드 기본 디코더에서 지원하는 이미지 포맷 : BMP, GIF, JPG, PNG, WebP, HEIF - 물론 버전별로 조금씩 상이하긴 하다.
  • bitmap은 OS에서 제대로 관리를 해주지 않기 때문에 메모리 누수가 나지 않기 위해서는 별도의 관리를 해주어야 한다.
  • 안드로이드 3.0이하에서는 Native Heap 영역에 메모리가 할당 → GC 대상이 아님
  • 안드로이드 3.0 이상 Dalvik VM heap에 할당 → GC 대상
  • 따라서 할당 해제를 해주는 것도 좋다고 한다.
  • BitmapFactory에서 decodeXXX의 함수들로 bitmap으로 변환해준다.

💡 따라서 bitmap은 자료구조라고 생각하면 되고, 그것을 만들어주고 관리해주는 것이 BitmapFactory라고 생각하면 된다.

 

따라서 bitmap을 핸들링 하기 위해선 BitmapFactory안에 있는 메소드를 주로 사용한다.

  • 왜냐하면 bitmap이 화면에 표시되는 과정이 이미지 생성 → 화면에 그리기인데, 이미지 생성을 bitmapFactory가 하고 화면에(Ex: Canvas) 연결하는 작업도 BitmapFactory에서 해주기 때문이다.
  • Bitmap은 Canvas에 넣는것보다 더 다양한 곳에서 역할을 수행한다.

해당 decode관련 메소드들

decodeByteArray : byte to Bitmap

decodeFile : image to Bitmap in Local Storage

decodeFileDescriptor : FileDescripter image to bitmap

decodeResource : image to bitmap in project resource

decodeStream : inputStream to bitmap


BitmapFactory.Options라는게 있는데 bitmap에 대해서 옵션을 지정하여 효과적으로 사용도 가능하고 최적화, 효율적인 옵션을 줄 수 있다.

비트맵을 사용하려면 GC걱정을 해야한다. 왜냐하면 이미지가 많은 애플리케이션은 많은 이미지를 디코딩해야 하므로 애플리케이션에서 메모리의 지속적인 할당 및 할당 해제가 발생합니다. 이로 인해 GC가 자주 호출됩니다. 그리고 GC를 너무 많이 호출하면 애플리케이션 UI가 멈추게 된다.

이 문제를 해결하려면 어떻게 해야할까? 단순하다, 비트맵 힙 영역에 있는 데이터를 재활용하면 된다. 지정한 크기에 맞게 비트맵을 렌더링하는 방식이다. 하지만 알아야할 것은 18버전 이하에서는 지정한 크기에 딱 맞지 않으면 에러가 난다. 이후 버전은 지정한 사이즈에 대해서 같거나 더 작기만 하면 된다.

어떻게 사용하는 것일까?

inBitmap 을 사용한다고 한다. 메모리를 재사용함 bitmap의 getAllocationByteCount을 통해서 픽셀을 저장하는 데에 할당한 메모리 크기를 반환한 값으로 지정한다.

BitmapFactory를 이용해서 비트맵을 객체화 하여 다른 비트맵을 디코딩하여 재사용하는 것이라고 한다. BitmapRegionDecoder가 그려준다고 한다.


간단한 예제 inbitmap 사용과정

Bitmap bitmapOne = BitmapFactory.decodeFile(filePathOne);
imageView.setImageBitmap(bitmapOne);
// lets say , we do not need image bitmapOne now and we have to set // another bitmap in imageView
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filePathTwo, options);
if (canUseForInBitmap(bitmapOne, options)) { 
//canUseForInBitmap check if the image can be reuse or not by //checking the image size and other factors
options.inMutable = true;
    options.inBitmap = bitmapOne;
}
options.inJustDecodeBounds = false;
Bitmap bitmapTwo = BitmapFactory.decodeFile(filePathTwo, options);
imageView.setImageBitmap(bitmapTwo);

canUseForInBitmap() 재사용할 수 있는지 여부를 판단하는 함수

public static boolean canUseForInBitmap(
        Bitmap candidate, BitmapFactory.Options targetOptions) {

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// From Android 4.4 (KitKat) onward we can re-use if the byte size of
        // the new bitmap is smaller than the reusable bitmap candidate
        // allocation byte count.
int width = targetOptions.outWidth / targetOptions.inSampleSize;
int height = targetOptions.outHeight / targetOptions.inSampleSize;
int byteCount = width * height * getBytesPerPixel(candidate.getConfig());

try {
return byteCount <= candidate.getAllocationByteCount();
        } catch (NullPointerException e) {
return byteCount <= candidate.getHeight() * candidate.getRowBytes();
        }
    }
// On earlier versions, the dimensions must match exactly and the inSampleSize must be 1
return candidate.getWidth() == targetOptions.outWidth
&& candidate.getHeight() == targetOptions.outHeight
&& targetOptions.inSampleSize == 1;  **//여기서 샘플 사이즈를 1로 두는 이유는 
//원본 배율로 하기 위함, 4로 하면 1/4 크기로 된다(미리 보기 이미지 같은걸 쓸 때 사용)**
}

픽셀 포맷에 대해서 분기하는 구분

private static int getBytesPerPixel(Bitmap.Config config) {
 if (config == null) {
     config = Bitmap.Config.ARGB_8888;
 }
int bytesPerPixel;
 switch (config) {
   case ALPHA_8:                      //아마도 흑백
       bytesPerPixel = 1;
       break;
   case RGB_565:                //16비트로 표현된 RGB라고한다.
   case ARGB_4444:            //조금 덜 좋은거 API 29레벨에서 사용되지 않음, 알파, 빨강, 녹색, 파랑 4비트씩
       bytesPerPixel = 2;
       break;
   case ARGB_8888:            //이게 아마 PNG 급인듯 로드하는 데에 48 MB 메모리 가량이 소모된다고 함
   default:
       bytesPerPixel = 4;
       break;
 }
 return bytesPerPixel;
}

💡 Glide 와 Fresco가 다음과 같은 작업을 처리해주어 부드럽게 이미지를 다룰 수 있다.

  • 이러한 작업을 하지 않으면(Bitmap Pool이라고 함) 이미지를 불러오는 리스트뷰 같은 경우에는 UI가 가끔 깜빡이는 모습과 지연이 되는 모습을 볼 수 있습니다.

위에 말했던 처리만 해주면 되는데 앞서 말했던 라이브러리를 쓰는가?

  • 더 위에 언급했듯이 API 레벨마다 처리해줘야하는 방식이 많이 다르다, 그것을 하나하나 신경써주지 못하지만 Glide같은 라이브러리에서는 해당 로직을 처리해줌과 동시에 최적화까지 되어있기 때문에 많이들 쓰는 것이다.
  • 라이브러리에 대한 더 자세한 내용은 나중 주차에 알아보자.

 💡 참고 drawable 폴더에 이미지를 넣으면 무손실 이미지 압축을 자동으로 해준다.

 

이외에 Recycle에 대한 이야기

왜 해줘야할까?

Android Activity 의 lifecycle 과, VM의 Object lifecycle 은 별개로 동작하기 때문에 발생하는 문제, bitmap은 OS에서 제대로 관리를 해주지 않기 때문에 메모리 누수가 나지 않기 위해서는 별도의 관리를 해주어야 한다.

안드로이드 3.0 이하

Bitmap의 메모리가 Dalvik VM(달빅 가상머신)에 할당되는 것이 아니고 Native Heap영역에 할당되기 때문에 Bitmap이 VM의 GC(Garbage Collecting)의 대상이 되지 않는다. 즉, recycle()을 호출해줘야한다.

안드로이드 3.0 이상

Bitmap의 메모리가 VM에 할당되기 때문에 다른 객체들 처럼 참조를 끊는 것이 가능하며 참조를 끊으면 GC의 대상이된다. 즉, recycle() 을 호출하지 않아도 bitmap = null; 로 메모리를 환원할 수 있다. (그래도 혹시 모르니 recycle()를 쓰는 것을 좀 더 추천한다.)

Drawable.setCallback( null );

Activity 나 Fragment 의 종료시에는 View 에 bind 된 drawable 을 끊어주는 것도 중요하다.즉, Drawable이 View 에 bind 되어 있고, View 는 다시 Context 를 통해 Activity 에게 bind 되어 있다는 의미이다. 이를 통해 메모리 릭의 가능성을 배제할 수 있다. 물론 멤버로 선언하지 않은 경우

destory시에 Bitmap.recycle() 같은 모호한 표현 말고, bitmap = null 과같은 코드만으로도 자원을 반환할 수 있다.

해주지 않는 경우 어디서 문제가 생기는가? 테스트하지 못한 내용

onDestroy()시에 이미지에 넣어놨던 bitmap을 해제해주는 것을 다음과 같은 메소드로 했다고 하자 imageView.setImageBitmap(**null**);

겉보기엔 문제가 안 생길수도 있지만 화면 전환 시에 OOM Error가 뜬다고 한다.

bitmap 은 화면전환 이후 더 이상 필요없으므로, 첫 화면의 onDestroy() 호출 이후에 참조가 제거되어야 함에도 제거되지 않아 VM 이 GC를 제대로 해 주지 못해 발생하는 현상이다. 프레임웍은 지금같은 상황은 화면(Activity)이 바뀌지 않았으므로이미지를 버릴 필요가 없다고 판단한 모양이다. 허나 첫 화면이 onDestroy 로 사라졌으면 그 화면 내에서 선언한 내용들은 GC 대상이되어야 함에도 그렇지 못하다.


Paint

  • 그리는 방법에 대한 기술 어떤 모양으로 그릴지, 어떤 색, 스타일, 글꼴을 정해주는 것
  • inline으로 초기화 가능 캡슐화(?)가 된다.

Constants

ANTI_ALIAS_FLAG : 안티앨리어싱 활성화(이건 보통 해놓으면 좋을듯하다 초기화 때에도 isAntiAlias로 설정가능**) EMBEDDED_BITMAP_TEXT_FLAG : 텍스트를 그릴 때 비트맵 글꼴 사용 FAKE_BOLD_TEXT_FLAG : Bold HINTING_ON / HINTING_OFF : 글꼴 힌트 활성화 여부, 더 뚜렷하게 보이게하는 것 STRIKE_THRU_TEXT_FLAG : 취소선 적용 UNDERLINE_TEXT_FLAG : 밑줄**

 💡 아마 거의 안티앨리어싱만 쓰지 싶다. 폰트 관련해서는 TypeFace라는 것을 주로 쓰는듯하다.

 


private val textPaint = Paint(ANTI_ALIAS_FLAG).apply {
    color = textColor
    if (textHeight == 0f) {
        textHeight = textSize
    } else {
        textSize = textHeight
    }
}
private val piePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    style = Paint.Style.FILL
    textSize = textHeight
} //왜 얘는 Flag 옵션을 Paint. 으로 준건지?

위의 예는 좋은 예는 아니다. 더블 버퍼링을 통해서 미리 객체를 생성하는 것이 최적화에 중요하다.

Double Buffering

  • 도화지에 바로 그림을 그리는 것이 아니라 비트맵이라는 투명한 도화지에 먼저 그리고 이후에 화면에 생성해주는 방법
  • canvas에 한번에 출력을 많이 하다보면 화면이 깜빡거리나 정상적으로 출력되지 않는 경우가 있다. 더블 버퍼링은 새로운 비트맵을 만들어 그 비트맵에 출력을 다 해놓고(화면상에 나타나지 않음) 완성된 비트맵을 canvas에 그리는 기법이다.
  • 메모리에 별도의 버퍼를 마련해 놓고 그리기 작업을 하는 기법
  • 컴퓨터는 일단 스크린에 뿌릴 그림을 그리고 이 버퍼(Buffer)이라고 부르는 그림을 모니터로 전송해야 함.
  • 옛날에는 오직 하나의 버퍼를 사용했으며, 이 버퍼가 그려지고 전송되는 것이 연속적으로 수행된다.

Canvas

  • 앞서 말했던 bitmap, paint를 그려넣을 View 같은거 라고 생각하면 된다.
  • SKIA라는 2D 그래픽 라이브러리에 포함되어있는 SKCanvas를 랩핑해둔 클래스
  • paint로 모양을 정해줄 수 있지만, Canvas에서도 모양 등을 정해줄 수 있다.
  • 도형같은건 paint로 하지만 복잡한 그림은 bitmap을 주로 사용한다.
  • 이전 스터디에서 뷰가 렌더링 될 때 호출되는 메소드에 대해서 말했었다.
    • onMeasure() → onLayout() → onDraw()
    • 다시 간단하게 설명하자면 순서대로 뷰와 내부 자식의 크기 설정, 위치 설정, 그리기의 순서대로 된다.
  • onDraw에서 canvas 설정, measure에서도 크기 커스텀 가능
  • View클래스를 확장해서 onDraw를 재정의 하는 것.
  • Path라는 클래스를 사용하면 모양을 커스텀 할 수 있다.
    • canvas.drawline대신 canvas.drawPath 사용
  • Canvas는 좌표 값으로 화면에 그림을 그린다. 자동적으로 화면의 중앙은 (x/2, y/2)이다.

  • Path로 사용해서 원래 이미지의 모양을 커스텀 하는 방식은 마스킹 기법을 사용한다고 생각하면 된다. 기존의 있는 이미지에서 일부분만 잘라서 사용하는 것, lineTo()
  • 지금껏 말했던 것은 앱의 UI에 대한 것으로 UI에 대한 것은 메인스레드에서 처리를 해준다. 따라서 오래 걸리는 작업을 하면 안 된다.
  • 기본적으로 시스템은 16ms 마다 UI를 reload 하는데 16ms 가 넘는 작업을 하려면 frame drop이 생길 수도 있다.
  • 또한 GC 실행 시간 단축도 해야겠쥬

따라서 다 처리해주긴 어렵지만 성능 향상을 위해서 할 수 있는 규칙 및 방법은 다음과 같다.

  • 필요하지 않은 경우 개체를 할당하지 마십시오.
  • 객체를 일찍 할당하지 말고 필요할 때만 객체를 할당하십시오. 지연 초기화를 사용합니다.
  • Integer, Boolean 등은 Integer와 같은 클래스가 더 많은 메모리를 사용하므로 Auto-Boxing을 피하십시오.
  • ArrayMap 및 SparseArray를 사용합니다. 이 기사를 참조 하면 Android 애플리케이션을 최적화하기 위해 ArrayMap 및 SparseArray를 사용해야 하는 이유와 시기를 보여줍니다.
  • 메모리 변동을 방지하려면 개체 풀의 개념을 사용하십시오. 여기에서 비트맵 풀에 대해 알아보세요 .
  • 무거운 작업은 메인 스레드에서 멀리하십시오. 백그라운드 스레드에서 전송합니다.
  • 상수(또는 Kotlin에서는 const val )에 정적 final을 사용하세요 .
  • 필요하지 않은 곳에서는 내부 Getter/Setter를 사용하지 마십시오(직접 필드 액세스가 3배 빠름).
  • 내부 클래스에서 컨텍스트를 누출하지 마십시오.
  • 정적이 아닌 것보다 정적 내부 클래스를 사용하십시오.
  • 비트맵의 중복 디코딩을 피하기 위해 비트맵에 LRU 캐시를 사용하면 GC 호출이 계속해서 줄어듭니다.
  • StrictMode를 사용 하여 Android 개발에서 애플리케이션의 기본 스레드에서 우발적인 디스크 또는 네트워크 액세스 또는 데이터베이스 쿼리와 같은 실수로 수행한 작업을 찾습니다.
  • 프로필 GPU 렌더링 사용: 16ms 벤치마크를 기준으로 UI 창의 프레임을 렌더링하는 데 걸리는 시간을 시각적으로 빠르게 보여줍니다. 설정-> 개발자 옵션-> 모니터링 섹션-> 프로필 GPU 렌더링 선택에서 활성화할 수 있습니다.
  • 마지막으로 불필요한 개체를 많이 할당하지 마십시오.

앱 성능 측정항목

https://blog.mindorks.com/android-app-performance-metrics-a1176334186e

dp, dip, px, sp, dpi 등의 관계

dpi: 1 inch 당 픽셀 수 1 inch (=2.54cm)에 몇 픽셀이 들어가는가를 나타내는 단위다.

px: 스크린의 실제 픽셀 단위를 사용하며, 실제 크기나 밀도와 상관이 없다. mdpi(160 dip)에서 1dp = 1px이다.

  • ldpi : 1dp = 0.75px
  • mdpi : 1dp = 1px
  • hdpi : 1dp = 1.5px
  • xdpi : 1dp = 2px;

dp, dip: 말 그대로 실제 픽셀에 독립적인 단위로 안드로이드 폰의 다양한 해상도를 지원하기 위해 만든 단위이다. 큰 화면이든 작은 화면이든 같은 크기로 나타나게 되어있다. (그러나 적용해보면 미세하게 다르다..) 즉, 화면이 작은 폰에서 10원짜리 만하게 나타난다면 화면이 큰 폰에서도 10원짜리 만하게 나타나도록 되어있다.

  • dp(dip)와 px간의 변환
    • px = dp * (160 / dpi) = dp * density
    • dp = px / (160 / dpi) = px / density
    • 여기서 density는 density = dpi / 160 계산 한다.
    • ldpi : density = 0.75
    • mdpi : density = 1
    • hdpi : density = 1.5
    • xdpi : density = 2

sp: dp와 비슷하지만 사용자가 선택한 글꼴 크기에 의해 크기가 조절된다.

  • SP(Scale-Independent Pixels) : 직역하면 스케일 독립 픽셀 단위로, 시스템이 지정한 font size에 영향을 받음.
  • DP(Density-Independent Pixels) : 직역하면 밀도 독립 픽셀 단위로, 장치의 밀도에 상관없이 물리적으로 동일한 크기를 갖음.
  • SP는 글자 크기가 시스템에 따라 유연하게 변하지만 DP는 시스템에 영향을 받지 않고 늘 고정적인 크기를 가지는 것.

Pixel densities

픽셀 밀도는 화면의 물리적 영역 내의 픽셀 수이며 dpi(인치당 도트 수)라고 합니다. 이것은 화면의 총 픽셀 수인 해상도와 다릅니다.

화면 호환성 관련

https://developer.android.com/guide/practices/screens_support

결론

  • 레아이웃 등의 UI적 요소는 dp(dip) 사용을 권장. (dp를 쓴다고 모두 해결되는 것은 아님)
  • 글자 크기에는 sp를 사용을 권장.
  • 되도록이면px는 지양(상대적이지 못하고 절대적)

참고

Canvas API 이해하기

https://blog.mindorks.com/understanding-canvas-api-in-android

Canvas와 Paint에 대한 간단한 설명

https://developer.android.com/training/custom-views/custom-drawing

https://developer.android.com/guide/topics/graphics/drawables

왜 Glide, Fresco가 GC및 응답률이 빠른지,

https://blog.mindorks.com/how-the-android-image-loading-library-glide-and-fresco-works-962bc9d1cc40

앱 GC 최적화 방법

https://blog.mindorks.com/android-app-performance-tips-smooth-running-android-app

'Android(Kotlin) Study A부터 Z까지 등반' 카테고리의 다른 글

안드로이드 Coil 라이브러리란?!  (0) 2022.04.02
안드로이드 Glide란?  (0) 2022.04.02
RecyclerView 이론  (0) 2022.02.25
Android UI이론  (0) 2022.02.25
안드로이드 Intent의 모든것  (0) 2022.02.12