안드로이드 면접대비 _ 2

안드로이드 면접 질문

  1. View가 그려지는 과정
  2. View lifecycle
  3. 대용량 Bitmap 로드시 메모리 문제를 해결하는 방법
  4. 화면 렌더링 속도를 개선하는 방법

주요내용출처1, 주요내용출처2, 주요내용출처3

위 3개 링크에 있는 내용을 그냥 필요한대로 옮겨다 적은 것.


1. View가 그려지는 과정

뷰는 포커스를 얻으면 레이아웃을 그리도록 요청한다. 이때 레이아웃의 계층구조 중 루트 뷰를 제공해야한다. 따라서 그리기는 루트노드에서 시작되어 트리를 따라 전위 순회 방식으로 그려진다. 부모 뷰는 자식 뷰가 그려지기 전에(즉, 자식 뷰 뒤에) 그려지며 형제 뷰는 전위 방식에 따라 순서대로 그려진다. 레이아웃을 그리는 과정은 측정(measure)단계와 레이아웃(layout)단계를 통해 그려지게 된다.

측정단계 - measure(int widthMeasureSpec, int heightMeasureSpec)

부모노드에서 자식노드를 경유하며 실행되며, 뷰의 크기를 알아내기 위해 호출된다. 이것은 뷰의 크기를 측정하는 것은 아니며, 실제 크기 측정은 내부에서 onMeasure(int, int)를 호출하여 뷰의 크기를 알아낸다. 측정 과정에서는 부모 뷰와 자식 뷰간의 크기정보를 전달하기 위해 2가지의 클래스를 사용한다.

ViewGroup.LayoutParams

자식 뷰가 부모 뷰에게 자신이 어떻게 측정되고 위치를 정할지 요청하는데 사용된다. ViewGroup의 sub class에 따라 다른 ViewGroup.LayoutParams의 sub class가 존재할 수 있다. 예를 들어 ViewGroup의 sub class인 RelativeLayout 경우 자신만의 ViewGroup.LayoutParams의 sub class는 자식 뷰를 수평적으로 또는 수직적으로 가운데정렬을 할 수 있는 능력이 있다.

  • 숫자 (ex. android:layout_width=”320dp”)
  • MATCH_PARENT (ex.android:layout_width=”match_parent”)
  • WRAP_CONTENT (ex.android:layout_width=”wrap_content”)
ViewGroup.MeasureSpec

부모 뷰가 자식 뷰에게 요구사항을 전달하는데 사용된다.

  • UNSPECIFIED - 부모 뷰는 자식 뷰가 원하는 치수대로 결정한다.
  • EXACTLY - 부모 뷰가 자식 뷰에게 정확한 크기를 강요한다.
  • AT MOST - 부모 뷰가 자식 뷰에게 최대 크기를 강요한다.

레이아웃단계 - layout(int l, int t, int r, int b)

부모노드에서 자식노드를 경유하며 실행되며, 뷰와 자식뷰들의 크기와 위치를 할당할 때 사용된다. measure(int, int)에 의해 각 뷰에 저장된 크기를 사용하여 위치를 지정한다. 내부적으로 onLayout()를 호출하고 onLayout()에서 실제 뷰의 위치를 할당하는 구조로 되어있다.

measure()layout()메소드는 내부적으로 각각 onMeasure()onLayout()함수를 호출한다. 이것은 final로 선언된 measure()layout() 대신 onMeasure()onLayout()을 구현(override)할 것을 장려하기 위해서이다.

뷰의 measure()메소드가 반환할때, 뷰의 getMeasureWidth()getMeasureHeight()값이 설정된다. 만약 자식 뷰 측정값의 합이 너무 크거나 작을 경우 다시 measure()메소드를 호출하여 크기를 재측정한다.

참고 : 뷰가 그려지는 과정, 안드로이드에서 view가 어떻게 그려지는가, 안드로이드 Docs - draws views

위로


2. View lifecycle

1. Constructor

모든 뷰는 생성자에서 출발한다. 생성자에서 초기화를 하고, default 값을 설정한다. 뷰는 초기설정을 쉽게 세팅하기 위해서 AttributeSet이라는 인터페이스를 지원한다. 먼저 attrs.xml파일을 만들고 이것을 호출함으로써 뷰의 설정값을 쉽게 설정할 수 있다.

2. onAttachedToWindow

부모 뷰가 addView(childView)를 호출한 후 자식 뷰는 윈도우에 붙게 된다(attached). 이때부터 뷰의 id 를 통해 접근할 수 있다.

3. onMeasure

뷰의 크기를 측정하는 매우 중요한 단계이며, 대부분의 경우 레이아웃에 맞게 특정 크기를 가져야한다. 여기에는 두단계의 과정이 있다.

  1. 뷰가 원하는 사이즈를 계산한다.

  2. MeasureSpec에 따라 크기와 mode를 가져온다.

    1
    2
    3
    4
    5
    6
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    }
  3. MeasureSpec의 mode를 체크하여 뷰의 크기를 적용한다.

    1
    2
    3
    4
    5
    6
    7
    8
    int width;
    if (widthMode == MeasureSpec.EXACTLY) {
    width = widthSize;
    } else if (widthMode == MeasureSpec.AT_MOST) {
    width = Math.min(desiredWidth, widthSize);
    } else {
    width = desiredWidth;
    }

4. onLayout

이 단계에서 뷰의 크기와 위치를 할당한다.

5. onDraw

뷰를 실제로 그리는 단계이다. CanvasPaint객체를 사용하여 필요한 것을 그리게 된다. Canvas객체는 onDraw함수의 파라미터로 제공되며, 이를 이용하여 뷰의 모양을 그린다. Paint객체는 뷰의 색을 그린다.

여기서 주의할 점은 onDraw 메소드는 빈번하게 호출된다는 점이다. Scroll 또는 Swipe 등을 할 경우 뷰는 다시 onDrawonLayout을 다시 호출하게 된다. 따라서 메소드 내에서 객체할당을 피하고, 한 번 할당한 객체를 재사용할 것을 권장한다.

View Update

View Lifecycle을 보면 뷰를 다시 그리도록 유도하는 invalidate()requestLayout()메소드를 볼 수 있는데, 이것은 런타임에 뷰를 다시 그릴 수 있게 해준다. 각각의 사용 용도는 아래와 같다.

invalidate()

단순히 뷰를 다시 그릴 때 사용된다. 예를 들어 뷰의 text 또는 color가 변경되거나 , touch interactivity가 발생할 때 onDraw()메소드를 재호출하면서 뷰를 업데이트한다.

requestLayout()

onMeasure()부터 다시 뷰를 그린다. 뷰의 사이즈가 변경될때 그것을 다시 측정해야하기에 lifecycleonMeasure()부터 순회하면서 뷰를 그린다.

Animation

뷰의 animationframe단위의 프로세스이다. 예를 들어, 뷰가 점점 커질때 뷰를 한 단계씩 차례대로 커지도록 할 것이다. 그리고 각 단계마다 invalidate()를 호출하여 뷰를 그릴 것이다. 대표적으로 애니메이션에 사용하는 클래스는 ValueAnimator이다.

1
2
3
4
5
6
7
8
9
ValueAnimator animator = ValueAnimator.ofInt(0, 100);
animator.setDuration(1000);
animator.setInterpolator(new DecelerateInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
int newRadius = (int) animation.getAnimatedValue();
}
});
animator.start();

위로


3. 대용량 Bitmap 로드시 메모리 문제를 해결하는 방법

1. createScaledBitmap(Bitmap bitmap, int width, int height)

비트맵을 생성할때 작은 크기로 생성하여 메모리 사용을 줄일 수 있다. 장점은 원하는 크기대로의 비트맵이 나온다는 것이다(비율이 안맞을 수도 있다). 단점은 이미 원본 비트맵이 메모리에 로드되어 있어야 리사이즈된 비트맵을 생성할 수 것이다.

2. BitmapFactory.Options.inSampleSize

이 플래그 값을 1이 아닌 값으로 두면 실제 크기의 이미지를 로드할 필요가 없는 원본 사이즈의 값을 가진 이미지로 나온다. 예를 들어 2라면 1/2 크기의 이미지가 나온다.

inSampleSize는 2의 지수 값만 가질 수 있으며, 2의 지수만큼 이미지를 작게만든다. inSampleSize크기만큼 픽셀을 건너뛰어 리사이징하기 때문에 속도가 매우 빠르다. 그러나 2의 지수가 아닌 값으로는 리사이징을 못하는 단점이 있다.

3. BitmapFactory.Options.inScaled / BitmapFactory.Options.inDensity

어떠한 사이즈로든 리사이징이 가능하고, 리사이징 필터가 적용되어 더욱 정교한 리사이징이 가능하다. 하지만 추가적인 필터링 단계는 많은 시간소요가 발생하기에 inSampleSize방법에 비해 느리다.

그래서 이 둘을 섞는게 가장 효과적인 방법이다.

inSampleSize

4. Combine inSampleSize, inScaled & inDensity

원하는 이미지 크기보다 2배 큰 이미지를 inSampleSize를 통해서 리사이징한다. (2의 지수만큼 리사이징이 가능하므로) 원하는 크기까지 inScaledinDensity를 이용하여 정교하게 리사이징하여 원하는 크기의 이미지를 얻는다.

하지만 문제점은 이미지의 원래 크기를 구하는 방법이 복잡하다는 것이다.

5. Bitmap.Options.inJustDecodeBounds

원본 Bitmap 객체를 생성하지 않은 채로 원본 이미지 크기를 구할시 inJustDecodeBounds옵션을 이용한다. 이것의 값이 true일 경우 BitmapFactory.decodeFile(fileName, Options)를 통해 Bitmap을 생성시 Bitmap 객체를 반환하지 않고 Bitmap 정보를 Options 객체에 담는다. 따라서 Options.outWidth, Options.outHeight를 통해 너비와 높이를 알 수 있다. 반대로 Bitmap 객체를 생성하고 싶을 경우 inJustDecodeBounds 값을 false로 설정하여 decode하면 객체를 반환한다.

참고 : [Youtube] Pre-Scaling Bitmaps


추가 질문

Q. 이미지뷰에서 scale 을 조정할 수 있는데 왜 비트맵을 직접 조정했나?

A. 길이가 제각각인 이미지를 서버로 받아서 동일한 크기의 이미지로 자른 후 총 개수를 파악하여 이미지뷰를 인플레이션 해야했고, 또한 각각의 이미지뷰에 정해진 이미지를 보여주려면 비트맵으로 변환하여 가공하는 과정이 필요했다.

위로


4. 화면 렌더링 속도를 개선하는 방법

기본적으로 View 의 움직임이 어색하거나 스크롤이 버벅거리거나 랜더링이 느린 경우는 뷰를 그리는 속도가 16ms 보다 오래걸리는 현상이다. 초당 60프레임의 속도로 화면을 그려주어야 사람의 시각에 어색함이 없이 보이는데, View를 그리는 시간이 이보다 오래 걸릴 경우 버벅이는 문제가 발생할 수 있다. 따라서 랜더링이 느리다면 2가지를 의심해볼 것이다.

첫 번째로 View 계층 구조가 복잡한지 의심해 볼 것이다. View 는 그려기지 전에 Mesure, Layout, Draw 3단계를 계층적으로 실행한다. 만약 계층이 복잡하다면 당연히 View 가 그려지는 시간 또한 오래 걸릴 것이기 때문이다. 따라서 뷰의 깊이를 얕게 하기 위해 ConstraintLayout 사용을 적극 고려할 것이다.

두 번째로 onDraw() 에서 오버드로우 현상이 일어나는지 확인할 것이다.onDraw() 함수 안에서 객체생성을 하였는지, 오래 걸리는 작업을 실행하지 않는지 확인하여 문제가 되는 로직을 수정하거나 제거 할 것이다.

추가적으로 디버깅 옵션에서 프로필 GPU 렌더링을 통해 스마트폰에서 직접 메모리 사용량을 확인할 수 있다. 녹색 가로선은 16ms을 나타내며, 초당 60개의 프레임 속도로 처리하기 위해서는 막대가 이 선 아래에 머물러야한다.

참고 : Android Performance Patterns: Why 60fps?, GPU 렌더링 속도 및 오버드로 검사

위로

Share