본문 바로가기

Android

안드로이드 성능개선 관련 내용 정리

[UI 반응성 개선]

(http://android-developers.blogspot.com/2010/10/improving-app-quality.html)

=시간이 오래 걸리는 작업은 메인 스레드가 아닌 별도의 스레드 사용

관련 영상 : http://www.youtube.com/watch?v=c4znvD-7VDA (구글 I/O 세션 영상)

 

=레이아웃의 복잡성을 최소화

- hierarchyviewer 를 통해 살펴봤을 때, 레이아웃이 5단계 이상으로 복잡하게 구성되어 있다면, 레이아웃 구성을 단순하게 할 것을 고려

- 복잡하게 꼬인 LinearLayout 대신 RelativeLayout 을 사용

- View 오브젝트는 1 에서 2 KB 정도의 메모리를 차지

- View 계층 구조는 빈번한 GC 작업을 일으키며 어플리케이션 메인 스레드가 느려지게 만듬

관련영상 : http://www.youtube.com/watch?v=wDBM6wVEO70 (구글 I/O 세션 영상)

관련글 : http://huewu.blog.me/110083521070

 

[성능을 위한 설계]

(http://developer.android.com/guide/practices/design/performance.html)

=기본 규칙

- 필요 없는 일 하지 말 것

- 메모리 할당을 되도록이면 피할 것

 

=불필요한 객체 생성 피할 것

- 사용자 인터페이스 루프에 오브젝트 생성하면 불필요한 GC 작업 발생

- 객체 생성 피하는 것과 관련한 예제

  입력 데이터에서 문자열 추출 시 복사 생성하지 말고 원본 데이터의 부분 문자열 받을 것

  문자열 반환 메소드가 있고 그 결과가 스트링 버퍼를 사용하는 경우가 있다면 임시 객체 생성보다는 직접적으로 값이 더해지도록 할 것

- 다차원 배열 보다는 병렬의 단일 일 차원 배열 사용이 성능 면에서 우수함

  (tec, ace)와 같은 단일 배열 보다는 tec[], ace[] 처럼 두 개의 병렬 배열로 사용하는 것이 성면에서 우수

 

=네이티브 메소드 사용 권장

- 문자열 처리시 String.indexOf(), String.lastIndexOf() 와 그 밖의 특별한 메소드를 사용하는 것을 권장

  이런 메소드들은 자바 루프로 된 것 보다 대략 10~100배 빠른 C/C++ 코드로 구현 되어 있음

 

=인터페이스 보다는 가상 연결 선호

- HashMap 객체를 가지고 있을 경우 HashMap이나 Map을 이용해 아래와 같이 선언 가능

  Map tecMap1 = new HashMap();

  HashMap tecMap2 = new HashMap();

 

  Map이 범용 성 면에선 우수하지만 성능 면에선 HashMap이 우수함

  (공용 API는 예외, 작은 성능 고려보다 좋은 API가 언제나 으뜸)

 

=가상 연결보다 정적 연결을 선호

- 객체의 필드에 접근할 필요가 없다면, 메소드를 정적(static)으로 만들 것

  가상 메소드 테이블을 필요로 하지 않기 때문에 그게 더 빠르게 불려짐

 

=내부에서 Getter/Setter 사용을 피할 것

- 가상 메소드 호출(e.g. i = getCount())은 인스턴스 필드 참조(i = mCount)보다 더 비용이 높음

  클래스 내부에서는 언제나 직접적으로 필드 접근 할 것

  (일반적인 객체 지향 프로그래밍 관습에 따르거나, 공용 인터페이스에서 getter, setter을 가지는 것은 이치에 맞음)

 

=필드 참조들을 캐시 할 것

- 객체의 필드에 접근하는 것은 지역 변수에 접근하는 것 보다 더 느림

 

이것보다는

for (int i = 0; i < this.mCount; i++)

      dumpItem(this.mItems[i]);

 

이렇게 할 것

  int count = this.mCount;

  Item[] items = this.mItems;

 

  for (int i = 0; i < count; i++)

      dumpItems(items[i]);

 

(멤버 변수라는 것을 명확히 하기 위해 명시적인 "this"를 사용하고 있음)

 

유사한 가이드라인으로, 결코 "for"문의 두 번째 조건에서 메소드를 호출하지 말 것

예를 들어, 다음 코드는 간단하게 int 값으로 캐쉬 할 수 있는 경우임에도 큰 낭비가 되는 getCount()메소드를 매번 반복 마다 실행하게 됨

for (int i = 0; i < this.getCount(); i++)

    dumpItems(this.getItem(i));

 

인스턴스 필드를 한번 이상 접근해야 한다면, 아래와 같이 지역 변수를 만드는 것 또한 좋은 생각임

    protected void drawHorizontalScrollBar(Canvas canvas, int width, int height) {

        if (isHorizontalScrollBarEnabled()) {

            int size = mScrollBar.getSize(false);

            if (size <= 0) {

                size = mScrollBarSize;

            }

            mScrollBar.setBounds(0, height - size, width, height);

            mScrollBar.setParams(

                    computeHorizontalScrollRange(),

                    computeHorizontalScrollOffset(),

                    computeHorizontalScrollExtent(), false);

            mScrollBar.draw(canvas);

        }

    }

mScrollBar 멤버 필드에 네 개의 분리된 참조가 있음

지역 스택 변수로 mScrollBar를 캐싱 함으로써, 네 개의 멤버 필드 참조가 더욱 효율적인 네 개의 스택 변수 참조로 바뀜

(메소드 인자들은 지역 변수와 같은 성능 특성을 가짐)

 

=상수를 Final로 선언

아래와 같은 선언을 하면,

 

static int intVal = 42;

static String strVal = "Hello, world!";

 

컴파일러는 클래스가 처음 사용될 때 실행하게 되는 <clinit>라 불리는 '클래스 초기화 메소드'를 생성

이 메소드가 intVal 42 값을 저장하고, strVal에는 클래스파일 문자 상수 테이블로부터 참조를 추출하여 저장

나중에 참조될 때 이 값들은 필드 참조로 접근

 

"final" 키워드로 향상시킬 수 있음

static final int intVal = 42;

static final String strVal = "Hello, world!";

 

클래스는 더이상 <clinit> 메소드를 필요로 하지 않음

상수들은 VM에 의해 직접적으로 다루어 지는 '클래스파일 정적 필드 초기자'에 들어가기 때문

intVal의 코드 접근은 직접적으로 정수 값 42를 사용할 것이고, strVal로의 접근은 필드 참조보다 상대적으로 저렴한 "문자열 상수" 명령을 사용

 

"final"으로 메소드나 클래스의 선언을 하는 것은 즉각적인 성능 이득을 주지는 못하지만, 특정한 최적화를 가능하게 함

예를 들어, 컴파일러가 서브클래스에 의해 오버라이드 될 수 없는 "getter"메소드를 알고 있다면, 메소드 호출을 inline화 할 수 있음

 

지역 변수를 final로 선언할 수 있습니다. 하지만 이것은 결정적인 성능 이득은 없음

지역 변수에는 오직 코드를 명확히 하기 위해서 "final"을 사용

 

=주의 깊게 향상된 반복문(Enhanced For Loop)을 사용

- 향상된 반복문(때로 "for-each"로 알려진 반복문) Iterable 인터페이스를 구현한 컬렉션들을 위해 사용될 수 있음

- 반복자는 hasNext() next()을 호출하는 인터페이스를 만들기 위해 할당됨

- ArrayList의 경우 직접 탐색하는 것이 좋을 수 있지만, 다른 컬렉션들에서는 향상된 반복문 구문이 명시적인 반복자의 사용과 동등한 성능을 보여줌

 

다음 코드로 향상된 반복문의 만족스러운 사용법을 볼 수 있음

 

public class Foo {

    int mSplat;

    static Foo mArray[] = new Foo[27];

 

    public static void zero() {

        int sum = 0;

        for (int i = 0; i < mArray.length; i++) {

            sum += mArray[i].mSplat;

        }

    }

 

    public static void one() {

        int sum = 0;

        Foo[] localArray = mArray;

        int len = localArray.length;

 

        for (int i = 0; i < len; i++) {

            sum += localArray[i].mSplat;

        }

    }

 

    public static void two() {

        int sum = 0;

        for (Foo a: mArray) {

            sum += a.mSplat;

        }

    }

}

 

zero() 는 반복되는 매 주기마다 정적 필드를 두 번 부르고 배열의 길이를 한번 얻음

one() 은 참조를 피하기 위해 지역 변수로 모든 것을 끌어냄

two() 는 자바 언어의 1.5버전에서 소개된 향상된 반복문 구문을 사용

컴파일러에 의해 생성된 코드는 배열 참조와 배열의 길이를 지역 변수로 복사해주어, 배열의 모든 원소를 탐색하는데 좋은 선택이 될 수 있음

주 루프 내에 추가적인 지역 읽기/저장이 만들어지고(명백하게 "a"에 저장), one()보다 조금 느리고 4 바이트 길어지긴 함

향상된 반복문 구문은 배열과 잘 동작하지만, 추가적인 객체 생성이 있게 되는 Iterable 객체와 함께 사용할 때엔 조심해야 함

 

=열거형(Enum)을 피할 것

- 열거형은 매우 편리하나, 크기와 속도 측면에서 안 좋음

예를들면 아래의 코드는,

 

public class Foo {

   public enum Shrubbery { GROUND, CRAWLING, HANGING }

}

 

900 바이트의 클래스 파일 (Foo$Shrubbery.class) 로 변환됨

처음 사용할 때, 클래스 초기자는 각각의 열거화된 값들을 표기화 하는 객체상의 <init>메소드를 호출

각 객체는 정적 필드를 가지게 되고 총 셋은 배열("$VALUES"라 불리는 정적 필드)에 저장

단지 세 개의 정수를 위해 많은 코드와 데이터를 필요로 하게 됨

 

다음 코드:

Shrubbery shrub = Shrubbery.GROUND;

는 정적 필드 참조를 야기

"GROUND"가 정적 final int 였더라면, 컴파일러는 알려진 상수로서 다루고, inline화 했을 수도 있음

 

물론, 반대적 측면에서 열거형으로 더 좋은 API를 만들 수 있고 어떤 경우엔 컴파일-타임 값 검사를 할 수 있음

trade-off가 적용됨

반드시 공용 API에만 열거형을 사용하고, 성능문제가 중요할 때에는 사용을 피할 것

 

어떤 환경에서는 ordinal() 메소드를 통해 정수 값 열거를 갖는 것이 도움이 될 수 있음

예를 들어, 다음 코드를:

for (int n = 0; n < list.size(); n++) {

    if (list.items[n].e == MyEnum.VAL_X)

       // do stuff 1

    else if (list.items[n].e == MyEnum.VAL_Y)

       // do stuff 2

}

다음 코드로 대신:

   int valX = MyEnum.VAL_X.ordinal();

   int valY = MyEnum.VAL_Y.ordinal();

   int count = list.size();

   MyItem items = list.items();

 

   for (int  n = 0; n < count; n++)

   {

        int  valItem = items[n].e.ordinal();

 

        if (valItem == valX)

          // do stuff 1

        else if (valItem == valY)

          // do stuff 2

   }

항상 보장할 수 없지만, 이것이 더 빠를 수 있음

 

=내부 클래스와 함께 패키지 범위를 사용

다음 클래스 정의를 고려:

public class Foo {

    private int mValue;

 

    public void run() {

        Inner in = new Inner();

        mValue = 27;

        in.stuff();

    }

 

    private void doStuff(int value) {

        System.out.println("Value is " + value);

    }

 

    private class Inner {

        void stuff() {

            Foo.this.doStuff(Foo.this.mValue);

        }

    }

}

여기서 주목해야 할 중요한 것은, 외부 클래스의 private 메소드와 private 인스턴스 필드에 직접 접근하고 있는 내부 클래스(Foo$Inner)를 정의했다는 것

이것은 유효하고, 코드는 기대했던 대로 "Value is 27"을 출력

 

문제는 Foo$Inner는 기술적으로는 완전히 분리된, Foo private 멤버로 직접적인 접근을 하는 적법하지 못한 클래스라는 것

이 차이를 연결 짓기 위해, 컴파일러는 두 개의 합성 메소드를 만듬:

/*package*/ static int Foo.access$100(Foo foo) {

    return foo.mValue;

}

/*package*/ static void Foo.access$200(Foo foo, int value) {

    foo.doStuff(value);

}

내부 클래스 코드는 외부 클래스에 있는 "mValue" 필드에 접근하거나 "doStuff" 메소드를 부르기 위해 이 정적 메소드를 부름

이것은 이 코드가 결국은 직접적인 방법 대신 접근자 메소드를 통해 멤버 필드에 접근하고 있다는 것을 뜻함

"보이지 않는" 성능 타격 측면에서 특정 언어의 어법이 야기하게 되는 문제에 대한 예제임

 

이 문제는 내부 클래스가 접근하는 필드와 메소드 선언에 private 범위가 아닌 package 범위를 가지도록 함으로써 피할 수 있음

이로써 더욱 빠르게 동작하게 되고 자동 생성되는 메소드에 의한 오버헤드를 제거할 수 있음

(불운하게도 이 또한 직접적으로 같은 패키지 내의 다른 클래스들이 필드들에 접근할 수 있다는 것을 뜻함,

모든 필드들은 private로 해야 한다는 표준적인 관습을 거스르게 됨

다시 말해서, 공용 API를 설계하게 된다면 이 최적화를 사용하는 것을 조심스럽게 고민해야만 할 것)

 

=Float를 피하라

펜티엄 CPU가 출시되기 전, 게임 제작자들에겐 정수 계산에 최선을 다하는 것이 일반적이었음

펜티엄과 함께 부동소수점 계산 보조 프로세서는 일체형이 되었고, 정수와 부동소수점 연산을 넣음에 따라

순수하게 정수 계산만을 사용하는 것 보다 게임은 더 빠르게 되었음

자유롭게 부동소수점을 사용하는 것은 데스크탑 시스템에서는 일반적

 

하지만, 임베디드 프로세서에게는 빈번하게 하드웨어적으로 부동소수점 계산이 제공되지 않고 있어,

"float" "double"의 모든 계산이 소프트웨어적으로 처리됨

어떤 기초적인 부동소수점 계산은 완료까지 대략 일 밀리 초 정도 걸릴 수 있음

 

또한, 정수에서도 어떤 칩들은 하드웨어 곱셈을 가지고 있지만 하드웨어 나눗셈이 없기도 함

이러한 경우, 정수 나눗셈과 나머지 연산은 소프트웨어적으로 처리됨

만약 해시 테이블을 설계하거나 많은 계산이 필요하다면 생각해 보아야 할 것

 

=성능 예시 숫자 몇 개

약간의 기초적인 행동들에 대해 대략적인 실행시간을 나열한 테이블

이 값들은 절대적인 숫자가 아니라는 것을 주목

CPU시간과 실제 구동 시간의 조합이고, 시스템의 성능 향상에 따라 변화할 수 있음

멤버 변수를 더하는 것은 지역 변수를 더하는 것보다 대략 네 배가 걸림

이 값들 사이에 관계를 적용해 보는 것은 주목할 만한 가치가 있음

행동

시간

지역 변수 더하기

1

멤버 변수 더하기

4

String.length() 호출

5

빈 정적 네이티브 메소드 호출

5

빈 정적 메소드 호출

12

빈 가상 메소드 호출

12.5

빈 인터페이스 메소드 호출

15

HashMap Iterator:next() 호출

165

HashMap put() 호출

600

XML로부터 1 View 객체화(Inflate)

22,000

1 TextView를 담은 1 LinearLayout 객체화(Inflate)

25,000

6개의 View 객체를 담은 1 LinearLayout 객체화(Inflate)

100,000

6개의 TextView 객체를 담은 1 LinearLayout 객체화(Inflate)

135,000

activity 시작

3,000,000

 

=맺음 말

임베디드 시스템을 위해 좋고 효율적인 코드를 작성하는 최선의 방법은 작성하는 코드가 실제로 무엇을 하는지 이해하는 것

 

[응답성을 위한 설계]

(http://developer.android.com/guide/practices/design/responsiveness.html)

무엇이 ANR을 유발하는가?

안드로이드에서, 애플리케이션 응답성은 액티비티 매니저와 윈도우 매니저 시스템 서비스에 의해 감시됨. 안드로이드는 다음의 조건 중 하나를 감지할 때 특정한 애플리케이션에 대해 ANR 다이얼로그를 보여줄 것임.

- 입력 이벤트 (예를 들어 키 입력, 스크린 터치) 5초 이내에 반응이 없음.

- 브로드캐스트 리시버가 10초 이내에 실행을 끝내지 않음.

 

어떻게 ANR을 피할 것인가?

- 안드로이드 애플리케이션은 하나의 쓰레드(예를 들어 메인(main))상에서 보통 전적으로 실행됨. 이것이 의미하는 바는 메인 쓰레드에서 애플리케이션이 수행하는 어떤 것이 완료되는데 긴 시간을 가진다면 ANR을 유발할 수 있다는 것. 왜냐하면 애플리케이션이 그 자신에게 입력 이벤트 또는 인텐트 브로드캐스트를 처리할 기회를 주지 않기 때문.

- 메인 쓰레드에서 실행하는 메소드는 최소한의 일을 해야 함

- 특히 onCreate(), onResume()과 같은 핵심 생명주기 메소드 최소한 작은 일을 수행해야 함

- 네트워크, 데이터베이스 오퍼레이션 같은 잠재적으로 긴 것들은 자식 쓰레드에서 수행

- Thread.wait(), Thread.sleep()는 호출 하지 말 것

- 자식 쓰레드가 완료될 때 알려주기 위한 핸들러를 제공해야 함

- 노티피케이션 매니저를 사용할 것

 

응답성 강화하기

- 일반적으로 100ms에서 200ms가 사용자가 애플리케이션에서 지연을 느낄 수 있는 경계 지점threshold

- 벡그라운드에서 뭔가 하고 있으면 ProgressBar ProgressDialog를 쓸 것

- 특히 게임에서는, 자식 쓰레드에서 움직임에 대한 계산을 할 것

- 멈춘 것 처럼 인식 하지 않도록 할 것

 

[관련 내용 참고 및 번역 사이트]

안드로이드 어플리케이션 품질을 향상시키는 방법

http://android-developers.blogspot.com/2010/10/improving-app-quality.html

 

안드로이드 Layout Tricks: Efficiency

http://huewu.blog.me/110083521070

 

안드로이드 - 성능을 위한 설계

http://idoun.kr/80068057201

 

응답성을 위한 설계

http://cod.springnote.com/pages/5749771(현재 사이트가 폐쇄된 것으로 보입니다.)