안드로이드의 메모리 누수 패턴


Memory Leak Patterns in Android를 번역한 글입니다.

메모리 누수란?

모든 앱은 작업을 수행하는 데 필요한 리소스로 메모리가 필요합니다. Android의 각 앱에 충분한 메모리가 있는지 확인하려면 Android 시스템에서 메모리 할당을 효율적으로 관리해야 합니다. Android 런타임은 메모리가 부족한 경우 가비지수집(GC)을 트리거합니다. GC의 목적은 더 이상 유용하지 않은 객체를 정리하여 메모리를 회수하는 것입니다. 다음 3단계로 진행됩니다.

  1. GC 루트에서 메모리에 있는 모든 객체 참조를 나열하여 GC 루트의 참조가 있는 활성 객체를 표시합니다.
  2. 나열되지 않아 표시된 모든 객체는 메모리에서 지워집니다.
  3. 살아있는 객체를 다시정렬합니다.
GC 루트에서 나열되는 활성화된 객체 표시

간단히 말해서, 사용자에게 서비스를 제공하는 모든 것을 메모리에 기록해야 하며, 리소스를 확보하기 위해 메모리에서 모든 것을 지워야 합니다.
그러나 사용되지 않는 객체가 사용되는 객체에서 어떻게든 참조되는 좋지 않은 코드로 인해 GC는 사용되지 않은 객체를 유용한 객체로 표시하고 객체를 제거할 수 없게 됩니다. 이를 메모리 누수라고합니다.

메모리 누수

 

왜 메모리 누수는 좋지 않은가?

어떤 객체도 오랫동안 메모리에 기록되어야 합니다. 사용자들에게 실질적인 가치를 제공하기 위해 사용될 수 있는 소중한 자원을 차지합니다. 안드로이드의 경우 다음과 같은 문제가 발생합니다.

1] 메모리 누수가 발생하면 사용 가능한 메모리가 부족하게 됩니다. 결과적으로 안드로이드 시스템은 빈번하게 GC이벤트를 호출합니다. GC이벤트는 모든 이벤트를 멈추게 합니다. GC가 발생하면 UI렌더링과 이벤트 처리가 중단됩니다. 안드로이드는 화면을 16ms로 그립니다. GC가 오래 걸리면 안드로이드는 프레임을 잃어버리기 시작합니다. 일반적으로, 100~200ms이상일 때 사용자가 앱이 느리다는 것을 인지하게됩니다.

안드로이드 화면 그리기
빈번한 GC로 인한 프레임 손실

 

안드로이드에서 애플리케이션 응답은 Activity 매니저와 Window 매니저 시스템 서비스로 모니터링됩니다. 안드로이드는 다음 조건중 하나를 감시하면 특정 응용 프래그램에 대한 ANR 다이얼로그를 표시합니다.

  • 5초 내에 입력 이벤트(키 누름 또는 화면 터치 이벤트)에 대한 응답이 없음 경우.
  • BroadcastReceiver가 10초 내에 실행을 완료하지 않을 경우.
ANR

어떤 사용자도 앱이 응답지연 다이얼로그를 보고 싶어 하지 않는 것을 확신합니다.

2] 앱에 메모리 누수 있는 경우, 객체는 메모리에서 반환될 수 없습니다. 결과적으로 안드로이드 시스템은 더 많은 메모리를 요청합니다. 그러나 한계가 있습니다. 결국 시스템은 앱에 더 많은 메모리를 할당하는 것을 거부합니다. 이렇게 되면 앱은 메모리 부족으로 인해 강제 종료됩니다. 물론 강제 종료를 아무도 좋아하지 않습니다. 사용자는 당장 앱을 제거하거나 앱 리뷰를 나쁘게 줄 것입니다.

3] 메모리 누수 문제는 QA 테스트로 찾기 어렵습니다. 재현하기도 어렵습니다. 그리고 안드로이드 시스템이 메모리 할당을 거부할 때 언제 어디서나 발생할 수 있기 때문에 크래쉬 리포트로 추론하기가 어렵습니다.

 

메모리 누수 확인방법?

메모리 누수를 찾기 위해 GC가 어떻게 작동하는지 잘 이해해야 합니다. 코드 작성과 리뷰를 부지런히 노력해야 합니다. 그러나 안드로이드에는 일부 코드가 의심스러울 때 누수를 예측할 수 있는 유용한 도구가 있습니다.

1] Square에서 만든 Leak Canary라는 메모리 누수를 감지하는데 유용한 도구가 있습니다. 앱의 액티비티들에 대해 약한 참조를 만듭니다. (다른 객체에 감시 기능을 추가하여 커스컴 할 수도 있습니다.) 그런 다음 GC후에 참조가 지워졌는지 확인합니다. 그렇지 않으면. hprof파일로 힙을 덤프 하고 분석하여 누수가 발생했는지 확인합니다. 있는 경우 알림이 표시되고 별도의 앱을 통해 누수가 발생된 위치를 트리 형태로 표시됩니다.

개발자/테스트 빌드에만 Leak Canary를 설치하는 것이 좋습니다. 사용자 빌드를 만들기 전에 개발자와 QA가 미리 메모리 누수를 찾기 위해서입니다.

Leak Canary

2] 안드로이드 스튜디오에는 메모리 누수를 감지하기 위한 좋은 툴이 있습니다. 앱의 일부 Activity가 누수되는 것이 의심된다면 이를 수행하면 됩니다.

1단계: 컴퓨터에 기기 또는 에뮬레이터에서 디버그 모드로 실행합니다.
2단계: 의심스러운 Activity로 이동하고 이전으로 돌아간 뒤 다시 실행합니다.
3단계: 안드로이드 모니터 창의 메모리 섹션에서 GC 시작(Initiate GC) 버튼을 누릅니다. 그 후 Java 힙 덤프(Dump Java Heap) 버튼을 누릅니다.

4단계: Java 덤프 버튼을 누르면 안드로이드 스튜디오에서 덤프 된. hprof 파일을 엽니다. hprof파일 뷰어에는 메모리 누수를 확인할 수 있는 몇 가지 방법이 있습니다. 오른쪽 상단에 있는 Analyzer Tasks 도구를 사용하여 누수되는 Activity를 자동으로 탐지할 수 있습니다. 또는 왼쪽 상단 Class List View를 Package Tree View로 변경하여 Destory 해야 하는 Activity를 찾을 수 있습니다. Activity 객체의 총개수를 확인하세요. 인스턴스가 하나 이상 있으면 누수가 있음을 의미합니다.

5단계: 누수되는 Activity를 찾았다면 하단의 참조 트리(reference tree) 창에서 Activity를 참조하고 있는 객체를 찾으세요.

더 많은 정보는 ‘HPROF Viewer and Analyzer‘에서 확인하실 수 있습니다.

 

일반적인 누수의 패턴은?

안드로이드에서 메모리 누수가 발생하는 이유는 여러 가지가 있습니다. 요약하면 3가지 카테고리로 나눌 수 있습니다.

  1. 정적 참조에 대한 Activity 누수
  2. 작업 스레드에 대한 Activity 누수
  3. 스레드 자체 누수

Github의 SinsOfMemoryLeaks에서 다양한 방식으로 메모리 누수를 시키는 간단한 앱을 만들었습니다.

LEAK 브랜치에서는 메모리 누수가 되는 코드를 볼 수 있습니다. 앞에서 언급한 안드로이드 스튜디오에서 모니터링 툴을 통해 누수를 추적할 수도 있습니다. FIXED 브랜치에서는 누수가 어떻게 수정되었는지 확인할 수 있습니다. 확신이 들지 않는다면 앞서 언급한 도구를 사용하여 실제로 수정되었는지 확인할 수 있습니다. 두 가지 브랜치는 서로 다른 앱 ID이기 때문에 동일한 기기에 설치하여 나란히 사용할 수 있습니다.

다양한 원인을 3가지 카테고리로 나누었는데 하나씩 알아보겠습니다.

정적 참조에 대한 Activity 누수

정적 참조는 앱이 메모리에 있는 한 계속 유지됩니다. Activity는 일반적으로 여러 번 파괴되고 다시 생성되는 생명주기를 가지고 있습니다. 정적 참조에서 Activity를 참조하는 경우 Activity는 생명주기에 의해 Destory 된 후에 GC 되지 않습니다. Activity는 콘텐츠에 따라 수 킬로 바이트에서 많은 경우 메가바이트까지 다양합니다. 복잡한 뷰 계층 구조나 고해상도의 이미지의 경우 많은 양의 메모리가 누수될 수 있습니다.

이 카테고리에서는 다음과 같은 항목이 있습니다.

정적 뷰에 Activity를 참조

정적 변수에 Activity를 참조

싱글톤 객체에 Activity를 참조

Activity의 내부 클래스를 정적으로 참조

작업 스레드에 대한 Activity 누수

Activity는 작업 스레드보다 오래 지속될 수 있습니다. Activity보다 더 오래 작업하는 스레드에서 Activity를 참조하면 누수가 발생합니다. 이 카테고리에도 다음과 같은 몇 가지 항목이 있습니다.

스레드에서 Activity 참조

Handler에서 Activity 참조

AsyncTask에서 Activity참조
스레드에서 Activity 참조와 동일하게 AsyncTask의 기술인 스레드풀, ExecutorService에도 동일한 원칙이 적용됩니다.

스레드 자체 누수

Activity에서 스레드를 시작할 때마다 스레드를 직접 관리해야 합니다. 스레드는 Activity보다 오래 작업할 수 있기 때문에 Activity가 소멸되면 스레드를 중지시켜야 합니다. 이렇게 하지 않으면 스레드가 누수될 위험이 있습니다.

 

까다로운 메모리 누수?

이상적으로 메모리 누수를 일으키는 코드를 작성하지 말아야 하며 존재하는 메모리 누수 문제를 수정해야 합니다. 그러나 실제로 다른 작업으로 인해 메모리 누수 수정의 우선순위를 판단하기 힘든데, 다음 3가지 목록을 통해 심각도를 평가할 수 있습니다.

1. 누수된 메모리는 얼마나 큰가?
모든 메모리 누수는 동일하지 않습니다. 일부는 몇 킬로바이트만 누설합니다. 일부는 많은 양의 메가바이트까지 유출할 수 있습니다. 앞서 언급한 도구를 사용하여 누설되는 메모리 크기를 측정하여 사용자의 기기에서 얼마나 중요한 용량인지 여부를 통해 결정할 수 있습니다.

2. 누수된 객체는 얼마나 오랫동안 메모리에 상주하는가?
스레드를 통한 누수는 스레드의 작업이 완료될 때까지 지속됩니다. 스레드가 최악의 시나리오에서 얼마나 오랫동안 지속되는지 검사를 해야 합니다. 예제에서 스레드는 무한루프이기 때문에 누수되는 객체는 영원히 메모리에 상주합니다. 실제로 대부분의 스레드가 파일 시스템에 액세스 하거나 네트워크 호출을 하는 등의 작업을 수행하기 때문에 일반적으로 시간이 제한되어 있는 짧은 시간일 것입니다. 발생할 수 있는 최대 시간은 메모리 누수를 수정의 우선순위를 결정할 때 고려해야 할 사항입니다.

3. 얼마나 많은 객체가 누수되는가?
예제의 정적 참조와 같이 메모리 누수는 하나의 객체에서만 나타납니다. 새로운 Activity가 생성되자 말자, 레퍼런스는 새로운 Activity를 참조되기 시작됩니다. 이전 Activity는 GC수집 대상이 됩니다. 따라서 최대 누출은 개수는 Activity 인스턴스 한 개입니다. 그러나 누수가 있는 경우 새 객체가 생성될 때마다 Activity 인스턴스 개수가 늘어납니다. 예제에서도 Activity가 생성될 때마다 스레드에 Activity에 참조가 걸려 누수가 발생됩니다. 따라서 기기를 20번 회전시키는 동안 20개의 스레드가 누수됩니다. 새로운 인스턴스가 GC에 의해 정리되지 않는 다면 사용 가능한 모든 메모리가 점점 줄어들기 때문에 정말로 나쁜 상황이 생길 수 있습니다.

 

고치거나 피하는 방법은?

Activity 클래스에서 정적 변수를 사용할 때 매우 주의해야 합니다. 정적 변수가 Activity를 직접 또는 간접적으로 참조할 가능성이 있는 경우 onDestory에서 참조를 끊어야 합니다. Manager 인스턴스 또는 싱글톤 객체에 리스너로 Activity를 전달할 때, 전달한 Activity 인스턴스로 다른 객체가 무엇을 하는지 알고 있어야 합니다. 필요한 경우 onDestory에서 리스너를 null로 설정하세요.

Activity 클래스에 내부 클래스를 만들 때 가능한 정적으로 만듭니다. 내부 클래스와 익명 클래스에는 암시적 참조가 있습니다. 따라서 내부/익명 클래스의 인스턴스가 포함된 클래스보다 오랫동안 유지되면 문제가 발생합니다. 누수의 위험을 피하기 위해 내부/익명 클래스가 아닌 정적 클래스를 사용하면 됩니다.

싱글톤 또는 Manager클래스를 만들 경우 Listener 인스턴스의 참조를 저장하여 관리할 수 있게 해야 하며, 사용자에 의해 참조를 관리할 수 없는 경우 Listener를 WeakReference로 관리하게 합니다. WeakReference는 GC에서 해당 대상을 지우지 않고 다시 회수하지 못하게 합니다. 이 기능은 메모리 누수를 막는데 큰 도움이 되지만 참조된 객체가 필요할 때 사용하지 못하는 부작용도 있을 수 있습니다. 따라서 메모리 누수를 수정하기 위해 가장 마지막 수단으로 사용해야 합니다. Activity에서 시작한 스레드 작업은 onDestory에서 항상 종료하세요.

 

마치며

우리는 메모리 누수가 무엇인지, 어떻게 발생하는지, 안드로이드 시스템에서 어떤 결과가 발생하는지에 대해 알아보았습니다. 메모리 누수를 탐지하고 식별하는 도구와 안드로이드의 일반적인 메모리 누수 패턴을 검사하는 방법, 심각도를 평가하는 방법과 피하거나 수정하는 방법을 소개하였습니다. Github저장소에서 일반적인 메모리 누수 패턴 및 수정에 대한 코드 예제를 한 번씩 확인해보세요. 모두 행복한 안드로이드 앱을 만드세요:)

 

참고:
https://developer.android.com/training/articles/perf-anr.html
https://www.dynatrace.com/resources/ebooks/javabook/how-garbage-collection-works/
https://developer.android.com/studio/profile/am-hprof.html
https://developer.android.com/reference/java/lang/ref/WeakReference.html
https://medium.com/google-developer-experts/finally-understanding-how-references-work-in-android-and-java-26a0d9c92f83#.h9w7hp13h




FlexboxLayout 뜯어보기

부모 레이아웃의 넓이에 따라 자식 뷰의 행을 동적으로 맞출 수 있는 레이아웃을 소개하겠습니다. 태그 목록을 구현할 때 가장 많이 쓰이지 않을까 싶습니다. 크기가 서로 다른 태그를 정해진 넓이에 따라 자동으로 행을 바꿔서 배치를 원하는 경우 안드로이드에서 기본으로 제공되는 레이아웃으로는 불가능합니다.

ConstraintLayout? 불가능.
자식 뷰들 간의 밀접한 관계 구성으로 이미 정해진 자식 뷰의 개수와 넓이인 경우에는 배치가 쉽지만, 새로운 항목이 추가되거나 제거되는 동적인 환경에서는 사용하기가 어렵습니다. 목록이 많아지는 경우 스크롤되지 않으며 자식 뷰의 크기가 줄어들어 적합하지 않습니다.

RelatveLayout? 불가능.
ConstraintLayout와 동일한 구조적인 문제로 적합하지 않습니다.

GridLayout? 확실 구현불가능.
각 행에 표시될 항목의 기본 개수를 지정해야 하기 때문에 자식 뷰의 넓이에 따라 동적으로 행의 개수를 가져야 하는 것과 너무 동떨어 지기 때문에 GridLayout도 적합하지 않습니다.

LinearLayout? 최선의 선택은 아니지만 구현은 가능.
ScrollView와 같이 조합하여 수동으로 구현할 수 있습니다. 부모 레이아웃의 넓이를 가져와 자식 뷰를 하나씩 추가하면서 행을 언제 바꿔야 할지 수동으로 구현하는 방법을 생각할 수 있습니다. 이미 Github에 TagView 등이 이런 방식을 채택하고 있습니다.

FlexboxLayout?

위의 LinearLayout으로 구현하기 위해 사용한 중첩 구조를 완벽하게 피 할 수 있으면서 원하는 구조를 짤 수 있었습니다.

dependencies {
   compile 'com.google.android:flexbox:0.3.0-alpha2'
}

FlexboxLayout을 사용하기 위해 build.gradle에 추가합니다. FlexLayout의 wrap속성을 사용하면 ViewGroup내의 자식 뷰들이 자동으로 다음 줄로 이동합니다. GridLayout의 행에 고정된 항목이 아닌 행 별로 다른 넓이를 가질 수 있도록 항목이 가변적일 수 있음을 의미합니다. FlexboxLayout은 RecyclerView와 함께 사용하여 스크롤을 자동으로 처리할 수 있습니다.

<com.google.android.flexbox.FlexboxLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:flexwrap="wrap”/>

화면에 모두 표시된다면 위의 코드로는 문제가 없습니다. 하지만 아이템이 너무 많아서 스크롤되어야 한다면 ScrollView에서 FlexboxLayout을 구현하는 것보다 RecyclerView를 이용하는 것이 성능이나 메모리면에서 훨씬 더 많은 이점이 있습니다.

RecyclerView에서 FlexboxLayout을 사용하기 위해서는 FlexboxLayoutManager를 사용하면 됩니다.

<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"
app:layoutManager="com.google.android.flexbox.FlexboxLayout”/>

레이아웃에서 layoutManager속성을 FlexboxLayout을 주면 됩니다. 하지만 코드에서 FlexboxLayout의 flexWrap속성을 wrap값로 변경해야 합니다.

그렇기 때문에 레이아웃에서 layoutManager를 바로 주는 것보다 아래와 같이 코드를 통해 RecyclerView의 layoutManager를 설정하는 게 좋습니다.

FlexboxLayoutManager layoutManager = new FlexboxLayoutManager();
layoutManager.setFlexWrap(FlexWrap.WRAP);
recyclerView.setLayoutManager(layoutManager);

 

flexGrow 속성은 LinearLayout의 weight와 같은 속성으로 작동합니다. 이렇게 하면 각 행의 나머지 공간을 균등한 공간으로 채울 수 있습니다.

FlexboxLayoutManager.LayoutParams flexboxLayoutParams = 
        (FlexboxLayoutManager.LayoutParams) mImageView.getLayoutParams();
flexboxLayoutParams.setFlexGrow(1.0f);

 

이 외에도 alignItem 속성은 아이템의 정렬되는 기준선을 설정 할 수 있으며, justifyContent를 이용하면 컨텐츠 배치를 설정 할 수 있습니다.

layoutManager.setAlignItems(AlignItems.BASELINE);
layoutManager.setJustifyContent(JustifyContent.CENTER);

 

물론 layout 속성에서도 추가 할 수 있습니다. FlexboxLayout을 활용 하면 다음과 같은 멋진 이미지 뷰어도 만들 수 있습니다.

 

참고:
https://blog.devcenter.co/unboxing-the-flexboxlayout-a7cfd125f023#.wyye4dtld

RecyclerView Prefetch – 스크롤을 부 드럽게 하기위한 시도

RecyclerView Prefetch를 번역한 글입니다.

내가 어렸을 때, 어머니께서 지금 방청소를 하면 나중에 할 필요가 없다며 버릇을 고치려고 하였습니다. 하지만 나는 그것에 속지 않았습니다. 나는 가능한 오랫동안 하지 않는 것이 항상 최선이라는 것을 알았습니다. 한 가지 생각해보면, 만약 청소를 지금 당장 하게 된다면 다시 더러워질 것이고 청소를 두 번 해야 됩니다. 만약에 더 오랫동안 미룬다면 어머니는 잊어버릴 수도 있습니다.

미루는 버릇은 항상 나의 일이었습니다. 하지만 내 친구 인 RecyclerView는 일관된 프레임 속도에 대한 문제가 없습니다.

 

문제점

스크롤이나 플링 연산중 RecyclerView는 화면에 새로운 아이템 정보를 표시해야 합니다. 이러한 아이템은 데이터와 바인딩합니다. (캐시에 항목이 없는 경우 새롭게 생성될 수 있습니다.) 그런 다음 배치되며 화면에 그려지게 됩니다. 이 모든 작업이 완료되면, 프레임에서 처리할 작업이 완료될 때까지 UI스레드는 멈춰있게 됩니다. 그러면 렌더링이 진행되고 스크롤은 스무스하게 움직일 수 있습니다… 다음 새로운 아이템이 나타날 때까지

일반적으로 RecyclerView에서 콘텐츠를 스크롤하는 동안 렌더링 하는 단계입니다 (롤리팝 이상에서). UI스레드에서 입력 이벤트, 이벤트 핸들링, 레이아웃을 수행하고 드로잉 연산 작업을 기록합니다. 그런 다음 렌더스레드가 명령을 GPU로 보냅니다.

스크롤하는 동안의 프레임은 처리할 새로운 내용이 없으므로 RecyclerView는 필요한 작업을 수행하는데 아무런 문제가 없습니다. 이 프레임 동안, UI스레드 입력을 처리하고, 애니메이션을 처리하며 레이아웃을 수행하고 드로잉 명령을 기록합니다. 그런 다음 드로잉 정보를 렌더스레드와 동기화합니다. (롤리팝 이상, 이전 버전은 UI 스레드에서 모든 작업을 수행함) 이러한 명령을 GPU로 보냅니다.

새로운 아이템 생성 시 뷰 생성, 바인딩 및 배치될 때 입력 단계가 가장 오래 걸리게 합니다. 결과적으로 프레임 경계 이후에 끝나기 때문에 렌더링 단계가 늦어져 프레임 누락의 원인이 됩니다.

새로운 아이템이 스크린에 나타나면 입력 단계에 더 많은 작업이 필요하며, 적합한 뷰를 생성할 수 있습니다. 이것은 렌더스레드의 후속 작업뿐만 아니라 나머지 UI 스레드를 늦추어 프레임 범위를 벗어나기 때문에 무의미한 프레임이 됩니다.

입력을 받는 동안 새로운 아이템을 보여주기 위해 생성하고 바인딩 하는데 사용할 수 있는 많은 시간을 할당 할 수 있습니다.

새로운 아이템이 나타날 때 입력 단계에서 호출 스택을 검사하면 많은 시간이 뷰 생성 및 바인딩에 소비된다는 것을 알 수 있습니다. 새로운 아이템을 준비 하는 동안 다른 작업을 지연하지 않는 방법이 있으면 좋지 않을까요?

뷰 생성 및 바인딩은 렌더링 되기 전에 완료되어야 하며 필요한 경우 처리하는 동안 UI스레드에서는 많은 시간을 소비합니다. 이전 프레임에서 많은 시간을 보내고 있으면 지연이 발생됩니다.

Chris Craik(안드로이드 UI 툴킷팀, 그래픽 엔지니어)가 RecyclerView의 스크롤을 관측하기 위해 Systraces를 만들었습니다. 특히, 그는 아이템이 필요할 때 준비하는 시간이 오래 걸린다는 것을 알았습니다. 그리고 프레임을 처리는 동안에 UI스레드가 잠자는데 많은 시간을 보내고 있었습니다. 왜냐하면 UI스레드는 작업을 일찍 끝마치기 때문입니다.

 

해결책

생성과 바인드 연산을 이전 프레임에서 허용하면 UI스레드와 렌더스레드가 병렬로 작업을 수행할 수 있게 되며, 렌더스레드의 작업이 완료되기 전 동기적으로 수행하여 나중에 처리할 필요가 없습니다.

분명히 시간을 가지고 놀 시간이 없습니다. 특히 Chris는 기본 RecyclerView 레이아웃에서 일어나는 일을 재배열하여 필요한 View의 아이템을 미리 가져옵니다. 유휴 상태에서 이 작업을 수행하여 결과를 기다리는 것을 피할 수 있습니다.

이제 이 작업은 기본적으로 무료로 가능합니다. 유휴시간을 사용하여 나중에 해야 될 작업을 수행하기 때문에 UI스레드는 프레임들 사이의 갭에서 어떤 일도 하지 않습니다. 어려운 일이 이미 완료되었기 때문에 훤씬 빠른 속도로 프레임을 만들어 갈 수 있습니다.

 

세부 설명

이 시스템은 RecyclerView가 스크롤링 작업을 시작할 때마다 Runnable을 스케쥴링함으로써 작동합니다. 이 Runnable은 레이아웃 매니저와 뷰가 스크롤되는 방향에 따라 즉시 나타나며 아이템의 프리페치를 수행합니다.

프리페치는 단일 아이템에 국한되지 않습니다. GridLayoutManager 아이템의 행이 화면에 오는 등 여러 아이템을 한 번에 검색할 수 있습니다. 버전 25.1에서 프리페치 연산은 개별 생성/바인드 연산으로 분리되어 아이템별 전체 그룹의 연산보다 UI스레드 갭이 더 쉽게 구분됩니다.

프리 페치 방식에 대한 흥미로운 점 중 하나는 시스템에서 작업에 소요되는 시간과 사용 가능한 갭 내에 들어갈 수 있는지 여부를 예측해야 한다는 것입니다. 결국, 프리 페치 작업이 타이밍을 찾지 못하고 놓쳐 프레임을 지연시키게 되면 프리 페치가 없는 것과 동일하게 무의미한 프레임이 될 수 있습니다. 시스템이 이 세부 사항을 처리하는 방식은 View 타입별 평균 생성 및 바인드 지속 기간을 추적하여 향후 생성 및 바인드 연산에 대해 합리적인 예측을 가능하게 합니다.

내부 RecyclerView를 바인딩해도 자식을 할당하지 않기 때문에 충첩된 RecyclerViews(RecyclerView 내부 컨테이너 아이템)에 대해 작업을 수행하는 것이 더 까다롭습니다. RecyclerView는 첨부 및 레이아웃 될 때 자식을 가져옵니다. 프리 페치 시스템은 여전히 RecyclerView 내부에서 자식들을 준비할 수 있지만 얼마나 많은지 알고 있어야 합니다. LinerLayoutMananger의 버전 25.1에 있는 새로운 API인 setInitialItemPrefetchCount()입니다. 이 API는 화면에서 스크롤할 때 RecyclerView를 채우기 위해 미리 가져올 항목의 수를 시스템에 알려줍니다.

 

주의사항

알고 있어야 할 몇 가지 중요한 주의사항:

  • 프리페칭은 필요로 하지 않는 작업을 할 수도 있습니다. View를 프리페칭하고 있기 때문에 적극적으로 일을 할 수 있으며, RecyclerView는 문제 되는 아이템에 도달하지 않을 수도 있습니다. 이것은 프리페치 작업이 낭비될 가능성이 있다는 것을 의미합니다. 동시에 발생했기 때문에 큰 문제는 없습니다. 필요하기 직전에 인출하고 있기 때문에 이것은 매우 드뭅니다. 스크롤이 두 프레임 간 정지나 역전하는 일은 거의 없습니다.
  • 렌더스레드: 렌더스레드는 롤리팝에서 성능 향상을 위해 도입된 기능으로 렌더링을 UI스레드에 영향을 받지 않도록 다른 스레드로 분리하여 불변의 애니메이션(ex. 잔물결 및 원형틀)을 실행하는 등 일부 개선을 허용합니다. 롤리팝 이전 버전은 병렬로 작업을 처리할 수 없기 때문에 최적화의 이점은 얻지 못합니다.

 

무엇을 원하며  — 어디서 얻을 수 있는가?

프리페치 최적화는 서포트 라이브러리 25 버전에 도입되었으며, 25.1.0 버전에서 더욱 향상된 기능이 추가되었습니다. 따라서 첫 번째 단계는 최신의 서포트 라이브러리 버전을 사용하는 것입니다.

RecyclerView와 함께 기본으로 제공되는 레이아웃 매니저를 사용하면 최적화가 자동으로 수행됩니다. 그러나 중첩된 RecyclerView를 사용하거나 직접 만든 레이아웃 매니저의 경우 기능을 활용하려면 코드 변경이 필요합니다.

중첩된 RecyclerView의 경우, 최상의 성능을 얻으려면 내부 레이아웃 매니저에서 LinearLayoutManager의 새로운 메소드인 setInitialItemPrefetchCount()를 호출하면 됩니다. 예를 들어 세로 목록의 행에 최소 3개 이상이 표시되면 setInitialItemPrefetchCount(4)를 호출하면 됩니다.

직접 LayoutManager를 구현한 경우, 프리페치를 활성화하기 위해 LayoutManager.collectAdjacentPrefetchPositions()를 구현해야합니다.
언제나 그렇듯이 가능한 작은 작업으로 생성과 바인드 단계를 최적화하는 것이 좋습니다. 가장 빠른 코드는 프레임워크가 프리 페치를 통해 수행된 작업을 병렬 처리할 수 있을 때 실행될 필요가 없으며, 여전히 시간이 걸리는 비싼 아이템 생성은 비용이 들 수 있습니다. 예를 들어, 뷰를 최소한으로 써가며 복잡한 구조를 만드는 것보다 항상 생성하고 바인딩하는 것이 더욱 저렴합니다. 바인딩은 기본적으로 setter를 호출하는 것처럼 간단하고 빠릅니다. 현재 코드로 프레임 제한 시간까지 잘 맞출 수 있어도 이를 최적화하면 저사양 기기에서 더 잘 작동할 가능성이 커지며 고사양 기기에서는 배터리 소모에 도움이 됩니다.

이미 생성과 바인딩을 빨리 할 수 있더라도 프리페칭은 프레임들 사이의 틈새에서 남은 시간을 당기는 것에 대해 도와주어야 합니다. LayoutManager.setItemPrefetchEnabled()를 토글 하여 결과를 바탕으로 최적화를 비교해볼 수 있습니다. 결과를 시각적으로 볼 수도 있습니다. 정말 중요하며, 특히 생성하고 바인딩하는데 상당한 시간이 걸리는 아이템 있습니다. 프리페치 사용 여부와 상관없이 실시간으로 모니터링하고 싶다면 Systrace를 실행하거나 GPU 프로파일링을 활성화하면 됩니다.

Systrace는 UI스레드의 유휴 시간동안 발생하는 프리페치를 보여줍니다.

 

마치며..

최신 서포트 라이브러리를 확인하고 RecyclerView의 새로운 프리페치를 사용해보세요. 한편, 나는 방을 청소하지 않고 돌아가겠습니다.

 

안드로이드 RecyclerView 성능 개선팁

RecyclerView는 제한된 화면에서 큰 데이터 세트를 제공하기 위한 유연한 View입니다. RecyclerView는 안드로이드 앱 개발에 있어서 가장 중요한 위젯 중 하나인 ListView를 좀 더 발전시킨 버전입니다. 뉴스 피드나 연락처 목록을 구현 시 사용자가 빠르게 스크롤할 때 성능 문제 또는 불필요한 지연을 방지하기 위해 ListView를 사용했습니다. RecyclerView는 ListView의 성능과 지연을 100% 방지 못하는 문제점을 해결한 버전입니다.

이 글은 RecyclerView 사용방법에 관한 글이 아닙니다. RecyclerView를 사용하면서 유용한 정보와 중요한 규칙, 절대 하지 말아야 할 것을 하나씩 살펴보겠습니다.

피할 수 있는 문제

1. 제대로된 View 재사용
: ViewHolder 내부에서 View 애니메이션을 절대 사용하면 안됩니다. (ex. itemView.animate()호출)
ItemAnimator는 View 애니메이션을 처리할 수 있는 유일한 구성 요소입니다. (ex. RecyclerView.setItemAnimator() )

ListView에서는 아이템 View 애니메이션 처리를 위해 getView()에서 작업을 하였습니다. 이는 View의 재사용 문제가 있으며 RecyclerView에서 동일한 패턴으로는 사용해서는 절대 안됩니다.

2. 세분화된 Adapter 업데이트
: 변경이 된 데이터에 대해서만 Adapter 업데이트를 하세요. (ex. notifyItemChanged(4))
NotifyItemRangeChanged()는 필요로 하지 않는 View를 업데이트하기 때문에 불필요하게 사용하지 마세요. (ex. notifyItemRangeChanged(0, getItemsCount() )

DiffUtil을 사용하여 Adapter의 성능을 측정 해 볼 수 있습니다.

3. onBindViewHolder position != final
: 절대 onBindViewHolder 내부에서 View.OnClickListener를 셋하지 마세요. onBindViewHolder는 데이터를 View에 바인딩하기 위해서만 사용해야 합니다.
아래와 같이 사용합니다.

이러한 간단한 규칙 3가지만으로도 RecyclerView의 성능은 보장됩니다.
데이터 변경에 따른 쉬운 애니메이션 처리방법.

  • 위에서 언급한 RecyclerView.setItemAnimator()를 직접 구현해도 되지만 notifyItemChanged(), notifyItemRangeChanged(), notifyItemInserted(), notifyItemMoved(), notifyItemRemoved()를 사용하기만 해도 기본적인 애니메이션이 적용됩니다.
  • getItemId(int position)와 함께 setHasStableIds(true)를 사용하면 RecyclerView.notifyDataSetChanged()로 모든 애니메이션을 자동으로 처리할 수 있습니다. (참고: http://wp.me/p4L8WU-MF)

 

 

성능 팁!

부드러운 스크롤을 원한다면 아래의 간단한 규칙을 지키면 됩니다.

  • 프레임 당 모든 작업을 수행하는데 16ms내로 작업하도록 해야 합니다. 개발자 옵션에서 프로필 GPU 렌더링 옵션을 사용하여 성능을 모니터링하세요.
  • 레이아웃 구조를 최적화 및 간단한 구조를 유지하세요.
  • 깊은 레이아웃 계층을 피하기 위해 HierarchyViewer를 사용하세요.
  • 오버 드로우 문제를 피하고 시스템 도구로 모니터링 하세요.
  • TextView에 긴 텍스트를 설정하지 마세요. 텍스트 줄을 계산하기위해 많은 연산이 필요하여 성능을 떨어집니다. 텍스트 끝 말줄임표나 최대 줄수를 설정 해두는 것도 하나의 방법입니다.
  • 렌더링 퍼포먼스를 향상하기위해 LayoutManager.setItemPrefetchEnabled()를 사용하세요.

RecyclerView는 안드로이드의 강력한 위젯이며 모든 결과를 얻을 수 있습니다. 하지만 잘못된 작은것 하나가 앱의 품질을 좌우 합니다.

참고: https://medium.com/master-of-code-global/recyclerview-tips-and-recipes-476410fa12fd#.enkpoi5ir

Android에서 TensorFlow 실행하기

Google은 기계 학습을 구현하기 위해 Android에서 사용할 수있는 TensorFlow라는 라이브러리를 오픈 소스로 제공합니다. TensorFlow는 Google에서 제공하는 Machine Intelligence 용 오픈 소스 소프트웨어 라이브러리입니다.

인터넷을 많이 검색했지만 Android 용 TensorFlow를 만드는 간단한 방법이나 간단한 예제를 찾지 못했습니다. 알려진 정보를 토대로 잘 조합하여 빌드 할 수 있게 되었습니다. 이런 과정들을 다른사람들도 쉽게 이해 할 수 있도록 공유하기위해 글을 쓰게 되었습니다.

이 글은 이미 기계 학습에 익숙하고 기계 학습을위한 모델 구축 방법을 알고있는 사람을 위해 작성된 글입니다. (예제에서는 사전 훈련 된 모델을 사용합니다). 기계 학습을 하기위한 벙법은 이미 많이 널려있으니 참고하시길 바랍니다.

Android용 TensorFlow 빌드하기

몇가지 알아두어야할 사항:

  • TensorFlow의 핵심은 C++로 작성되었습니다.
  • 안드로이드 용으로 빌드하려면 JNI(Java Native Interface)를 사용하여 loadModel, getPredictions 등과 같은 C++ 함수를 호출해야합니다.
  • JAVA API를 호출하여 작업을 쉽게사용 하기위해 C++ 컴파일 된 파일 인 .so (공유 객체) 파일과 네이티브 C++을 호출 할 JAVA API로 구성된 jar 파일을 갖습니다.
  • jar (자바 API)와 .so (C++ 컴파일 된) 파일이 필요합니다.
  • 사전 훈련 된 모델 파일과 분류를 위한 라벨 파일이 필요합니다.

개체를 탐지 할 수 있는 예제를 만들어 보겠습니다.

먼저 jar와 .so 파일을 만들어 봅시다.
TensorFlow를 JAVA기반의 안드로이드에서 작동 하기위해 so파일로 빌드하고 interface역할을 하기위해 jar이 필요합니다.

 

필요한 툴:

  • Android SDK 설치(Android Studio에 포함되어 있습니다.)
  • NDK를 설치합니다.
  • Bazel을 설치합니다. (Bazel은 TensorFlow의 기본 구축 시스템입니다.)

git clone –recurse-submodules https://github.com/tensorflow/tensorflow.git
참고 : –recurse-submodules명령을 통해 서브 모듈도 함께 내려받아야 합니다.

복제한 TensorFlow의 루트 디렉토리에서 WORKSPACE 파일을 찾아 편집합니다.

# Uncomment and update the paths in these entries to build the Android demo.
#android_sdk_repository(
# name = "androidsdk",
# api_level = 23,
# build_tools_version = "25.0.1",
# # Replace with path to Android SDK on your system
# path = "<PATH_TO_SDK>",
#)
#
#android_ndk_repository(
# name="androidndk",
# path="<PATH_TO_NDK>",
# api_level=14)

위에서 설치한 SDK, NDK경로를 지정합니다.

android_sdk_repository(
name = "androidsdk",
api_level = 23,
build_tools_version = "25.0.1",
# Replace with path to Android SDK on your system
path = "/Android/sdk/",
)
android_ndk_repository(
name="androidndk",
path="/android-ndk-r13/",
api_level=14)

.so 파일 빌드:

bazel build -c opt //tensorflow/contrib/android:libtensorflow_inference.so \
--crosstool_top=//external:android/crosstool \
--host_crosstool_top=@bazel_tools//tools/cpp:toolchain \
--cpu=armeabi-v7a

armeabi-v7a를 타겟으로 빌드를 합니다.

빌드 후 아래 위치로 so파일이 생성됩니다.
bazel-bin/tensorflow/contrib/android/libtensorflow_inference.so

JAVA 빌드:

bazel build //tensorflow/contrib/android:android_tensorflow_inference_java

빌드후 아래 위치에 jar파일이 생성됩니다.
bazel-bin/tensorflow/contrib/android/libandroid_tensorflow_inference_java.jar

 

이제 jar와 .so 파일을 모두 가지고 있습니다. 이미 .so 파일과 jar 파일을 모두 빌드 했으므로 아래 프로젝트에서 직접 사용할 수 있습니다.

그러나 미리 훈련 된 모델과 레이블 파일이 필요합니다.

여기에서는 주어진 이미지에서 객체 감지를 수행하는 Google의 사전 훈련 된 모델을 사용합니다. (모델 다운로드)

다운받은 zip파일의 압축을 풀면 imagenet_comp_graph_label_strings.txt (객체의 레이블) 및 tensorflow_inception_graph.pb (사전 학습된 모델)가 생성됩니다.

 

Android Studio에서 Android 샘플 프로젝트를 만듭니다.

imagenet_comp_graph_label_strings.txt 및 tensorflow_inception_graph.pb를 assets 폴더에 넣습니다.
libandroid_tensorflow_inference_java.jar를 libs 폴더에 넣고 마우스 오른쪽 버튼을 클릭하여 라이브러리로 추가하십시오.

위에서 빌드한 jar파일과 so파일을 추가 합니다.

compile files('libs/libandroid_tensorflow_inference_java.jar’)

기본 디렉토리에 jniLibs 폴더를 만들고 libtensorflow_inference.so를 jniLibs/armeabi-v7a/ 폴더에 넣습니다.

 

이제 TensorFlow Java API를 호출 할 수 있습니다.

TensorFlow Java API는 TensorFlowInferenceface 클래스를 통해 필요한 모든 메소드를 제공합니다. TensorFlow Java API를 호출하여 모델경로를 지정하고, 예측을 얻기위해 이미지를 입력해보세요!

아래 주소를 통해 설명된 완벽한 예제 소스를 볼 수 있습니다.
https://github.com/MindorksOpenSource/AndroidTensorFlowMachineLearningExample

 

원문: https://blog.mindorks.com/android-tensorflow-machine-learning-example-ff0e9b2654cc#.thihnty8c

이와 별개로 Caffe를 안드로이드에서 작동하는 방법도 있습니다.
https://github.com/kmshack/TrafficLightsDetector-Android

안드로이드 개발력 향상하기

안드로이드 앱개발에 필요한 팁과 학습방법에 관한 글입니다. 아래 정보를 잘 활용하여 안드로이드 개발 학습에 도움이 되거나 실무에 적용하여 업무에 도움이 되기를 바랍니다.

안드로이드 스튜디오의 “라이브 템플릿”을 사용하여 개발 향상

아래 링크는 안드로이드 스튜디오 팁을 모아둔 곳입니다.
https://plus.google.com/u/0/collection/wtO0PB
여기서 가장 쉽고 빠르게 쓸 수 있는 기능 중 하나는 라이브 템플릿 기능으로, 반복되는 메서드를 임의로 지정한 약자를 입력하면 풀네임으로 자동변경되는 기능입니다.
ex) fbc + Enter -> findViewById로 변환

라이브 템플릿 기능에 대해서 아래 링크를 참고하면 좀 더 많은 정보를 확인할 수 있습니다.
https://www.bignerdranch.com/blog/android-studio-live-templates/

안드로이드에서 기본적으로 지원해주는 라이브 템플릿외에도 사용자가 커스텀하게 만들 수도 있습니다. 아래 링크는 많이 사용되는 메서드들에 대한 라이브 템플릿을 커스텀하여 공개하고 있습니다.
https://github.com/keyboardsurfer/idea-live-templates

라이브 템플릿은 반복되는 메서드 입력을 최대한 줄여 개발 시간을 단축시킬 수 있는 가장 좋은 방법입니다.
https://medium.com/@aditlal/must-have-tools-for-android-development-d76ae66f409f#.qhhck9bvk

앱을 디버깅하는 동안 안드로이드 스튜디오와 함께 사용할 수 있는 도구

Library methods count – 안드로이드의 DEX파일 포맷 구조상 65만 개의 메서드로 제한되어 있습니다. 해당 툴을 이용하면 안드로이드 라이브러리의 메서드 개수를 확인 할 수 있습니다.

Stetho – 페이스북에서 만든 안드로이드 앱을 쉽게 감시할 수 있는 라이브러리입니다. 네트워크 트래픽을 디버깅하는데 가장 좋습니다. 이뿐만 아니라 SQLite 데이터베이스, 쉐어드 프리퍼런스도 쉽게 감시할 수 있습니다. 크롬 브라우저의 인스팩트 기능을 이용하기 때문에 웹 개발경험이 있다면 훨씬 쉽게 사용할 수 있습니다.

LeakCanary – 메모리 릭을 감지해주는 라이브러리로 코드 한 줄만으로 쉽게 사용 가능합니다. 릭 탐지 시 별도의 UI화면으로 발생되는 위치와 경로를 알려줍니다.

Gradle, Please – Gradle기반의 라이브러리를 쉽게 찾아줍니다. 라이브러리 이름만 입력하면 최신 버전으로 라이브러리를 컴파일할 수 있는 구문을 찾아줍니다.

Android Arsenal – 안드로이드와 관련된 라이브러리를 한 번에 볼 수 있습니다. 항상 최신의 버전을 유지하며 카테고리 기반으로 빠르게 업데이트됩니다.

Android UI OpenSource – 안드로이드 UI와 관련된 오픈소스를 한 번에 볼 수 있습니다.

AndroidTool Mac – 맥 개발자들을 위한 안드로이드 툴입니다. UI 환경에서 빠르게 화면 캡처, 비디오 캡처, APK 끌어서 설치를 할 수 있습니다.

ButterKnifeZelezny – 안드로이드 스튜디오 플러그인으로 선택한 레이아웃 XML에서 버터나이프 인젝션으로 변환해줍니다.

Adb-idea – ADB명령을 쉘이 아닌 안드로이드 스튜디오의 액션 창에서 작동할 수 있는 플러그인입니다.

AndroidWeekly – 매주 최신의 안드로이드 관련 소식을 메일링으로 받아 볼 수 있습니다.

Android Developers Youtube Channel – 구글 안드로이드 공식 유튜브 채널이며, 가장 최신의 안드로이드 기술을 접할 수 있습니다.

완성된 앱으로 학습하기 

Plaid – 디자인 뉴스와 영감을 제공하기 위한 안드로이드 앱입니다. 안드로이드 UI처리에 대해 전반적인 학습을 할 수 있습니다.

Kickstarter – 구글의 킥스타터 앱으로 예술, 디자인, 영화, 게임 음악 등으로 구성된 수천 개의 프로젝트를 탐색할 수 있습니다. 디자인 가이드라인부터 최신 기술을 한 번에 공부할 수 있습니다.

참고

Ripple Animation

안드로이드 5.0(API 21)의 머트리얼 디자인에서 물결 터치 효과가 처음 소개 되었습니다. 터치 피드백에 대해 UI요소와 사용자간의 상호작용을 비주얼 하게 머트리얼 디자인에서는 제공합니다. 예를 들어 버튼을 터치하면 즉각 물결 효과가 표시 됩니다. 이것은 안드로이드 5.0에서 기본으로 제공됩니다.

Ripple 애니메이션은 새롭게 생긴 RippleDrawable을 이용합니다. 물결 효과는 뷰 경계에서 끝나거나 경제를 넘어 확장 할 수 있도록 구성 할 수 있습니다. 아래 스크린 샷은 터치한 위치에서 점점 퍼지면서 잔물결 효과가 버튼의 가장자리로 퍼지는 것을 보여 주고 있습니다. 터치를 끝마치게 되면 뷰는 다시 원래 모양으로 돌아갑니다. 이 모든것이 1초 내로 이루어 지지만 애니메이션 시간은 더 길거나 짧게 변경 할 수 있습니다.

 

View 클릭처리

이런 물결 효과는 안드로이드 5.0에서 기본적으로 제공하며, 아래와 같이 정의된 attr를 사용하면 된다.

 

버튼 처리

대부분의 버튼은 Drawable를 사용합니다. 일반적으로 selector에서 몇가지 상태에 맞는 리소스를 설정합니다.

안드로이드 5.0부터는 더 이상 selector를 사용하지 않아도 됩니다. ripple를 사용하면됩니다.

물결을 적용할 색을 선택하기만 하면됩니다. 안드로이드에 정의된 attr/colorControlHighlight의 기본 값이 마음에 들지 않는 경우 직접 다른색으로 변경하는것 보다 테마 스타일을 통해서 색상을 변경하면 됩니다. 테마 스타일을 변경하게 되면 colorControlHighlight가 사용되는 모든 색상들을 한번에 변경 할 수 있습니다.

처음 설명에서 가장자리로 퍼질때 경계 끝이 아닌 경계를 넘어서 확장 할 수 있다고 설명했다. 경계를 넘도록 물결 효과를 적용 하고 싶다면 attr/selectableItemBackgroundBorderless를 사용하면 된다. 큰뷰 내에 일부분의 View에서 더 잘작동 합니다.