2017년 1월 24일 화요일

화면전환시 갑자기 ArrayIndexOutOfBoundsException을 내며 뻗을 때

화면전환시 갑자기 ArrayIndexOutOfBoundsException을 내며 뻗을 때

배경

안드로이드 5.0 롤리팝 이하 디바이스에서 종종 화면전환을 시도할 때, ArrayIndexOutOfBoundsException 을 뿜으며 크래쉬되는 현상을 발견했다. 그런데 로그를 보면... 어디가 원인인지 도저히 알수가 없어서 난감한 상황이 생겼다.
ava.lang.ArrayIndexOutOfBoundsException: length=125; index=-1
at android.text.StaticLayout.calculateEllipsis(StaticLayout.java:756)
at android.text.StaticLayout.out(StaticLayout.java:720)
at android.text.StaticLayout.generate(StaticLayout.java:428)
at android.text.StaticLayout.<init>(StaticLayout.java:140)
at android.widget.TextView.makeSingleLayout(TextView.java:5884)
at android.widget.TextView.makeNewLayout(TextView.java:5741)
at android.widget.TextView.onMeasure(TextView.java:6098)
at android.view.View.measure(View.java:15172)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4814)
at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1390)
at android.widget.LinearLayout.measureVertical(LinearLayout.java:681)
at android.widget.LinearLayout.onMeasure(LinearLayout.java:574)
at android.view.View.measure(View.java:15172)
at android.widget.RelativeLayout.measureChildHorizontal(RelativeLayout.java:617)
at android.widget.RelativeLayout.onMeasure(RelativeLayout.java:399)
at android.view.View.measure(View.java:15172)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4814)
at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1390)
at android.widget.LinearLayout.measureVertical(LinearLayout.java:681)
at android.widget.LinearLayout.onMeasure(LinearLayout.java:574)
at android.view.View.measure(View.java:15172)
at android.widget.ScrollView.measureChildWithMargins(ScrollView.java:1196)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:310)
at android.widget.ScrollView.onMeasure(ScrollView.java:318)
at android.view.View.measure(View.java:15172)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4814)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:310)
at android.view.View.measure(View.java:15172)
at android.widget.RelativeLayout.measureChildHorizontal(RelativeLayout.java:617)
at android.widget.RelativeLayout.onMeasure(RelativeLayout.java:399)
at android.view.View.measure(View.java:15172)
at android.support.v4.widget.DrawerLayout.onMeasure(DrawerLayout.java:1081)
at android.view.View.measure(View.java:15172)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4814)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:310)
at android.support.v7.widget.ContentFrameLayout.onMeasure(ContentFrameLayout.java:139)
at android.view.View.measure(View.java:15172)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4814)
at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1390)
at android.widget.LinearLayout.measureVertical(LinearLayout.java:681)
at android.widget.LinearLayout.onMeasure(LinearLayout.java:574)
at android.view.View.measure(View.java:15172)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4814)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:310)
at android.view.View.measure(View.java:15172)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4814)
at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1390)
at android.widget.LinearLayout.measureVertical(LinearLayout.java:681)
at android.widget.LinearLayout.onMeasure(LinearLayout.java:574)
at android.view.View.measure(View.java:15172)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4814)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:310)
at com.android.internal.policy.impl.PhoneWindow$DecorView.onMeasure(PhoneWindow.java:2148)
at android.view.View.measure(View.java:15172)
at android.view.ViewRootImpl.performMeasure(ViewRootImpl.java:1848)
at android.view.ViewRootImpl.measureHierarchy(ViewRootImpl.java:1100)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1273)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:998)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:4212)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:725)
at android.view.Choreographer.doCallbacks(Choreographer.java:555)
at android.view.Choreographer.doFrame(Choreographer.java:525)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:711)
at android.os.Handler.handleCallback(Handler.java:615)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4745)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
at dalvik.system.NativeStart.main(Native Method)

원인

구글링을 해본 결과, 이것은 젤리빈(4.1~4.3)부터 있던 버그로, TextView에서 maxLines=1 / lines=1속성을 사용할 때, ellipsize속성값을 start로 할 경우에 해당 익셉션이 발생하여 크래쉬되는 증상이었다.
https://code.google.com/p/android/issues/detail?id=33868

해결방법

TextView에 singleLine=true속성을 추가해주면 크래쉬가 나지 않는다.
참고로, 이 버그는 6.0에서는 재현되지 않는 것으로 보아, 6.0에서 고쳐진 것 같다.

2017년 1월 13일 금요일

[안드로이드] Activity Leak 확인 툴 - LeakCanary

엑티비티 Leak

엑티비티의 참조가 남아있어서 Destroy 후에도 GC되지 않는 증상으로, 안드로이드에서 메모리 누수의 주 원인으로 꼽힌다. 엑티비티 인스턴스는 다른 인스턴스에 비해서 메모리를 많이 차지하고, 일반적으로 뷰 등 여러가지 멤버필드를 가지고있기 때문에 엑티비티 Leak이 발생하면 자칫 치명적인 성능저하를 유발할 수도 있다.

LeakCanary

Square 사(BetterKnife 만든 회사)에서 만든 무료 라이브러리로, 그나마 쉽게 엑티비티 Leak을 감지할 수 있도록 도와준다.

적용방법

  1. app의 build.gradle에 다음의 dependancy를 추가한다.
dependencies {
    //Debug용 컴파일
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
   //Release용 컴파일 - Release시에는 아무 동작도 안하도록
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
}
2.Application의 onCreate() 메서드에서 초기화한다.
public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    LeakCanary.install(this);
    // 앱 초기화 로직
  }
}

이용방법

앱을 테스트하다보면 다음 이미지와 같이, 추가한적 없는 아이콘이 보이는 경우가 있는데, 저 아이콘이 보이고 Notification이 존재한다면 현재 앱은 엑티비티 Leak이 존재하는 것이다.
Notification을 클릭하면, 왜 Leak이 발생했는지를 보여주는 화면이 나타난다.

장/단점

  • 장점
    • 적용이 매우 쉽다.
    • Leak이 발생하면 시각적으로 알 수 있어서 직관적이다.
  • 단점
    • Activity Leak를 확인하는 것만 가능하다. - 다른 메모리 Leak은 불가능
    • Activity의 Destroy 시점에서 검사를 진행하므로 화면 전환속도가 조금 느리다. - release 버전에서는 LeakCanary가 동작하지 않도록 빈 라이브러리를 컴파일하거나, 라이브러리를 제거해야 한다.

참고자료

2017년 1월 4일 수요일

[안드로이드] 무선 ADB 연결

선 없이 ADB 연결하기

안드로이드 개발을 하다보면, 여러 디바이스를 동시에 테스트 해야할 일이 많은데... USB포트의 개수는 한정되어 있어서 컴퓨터에 연결되는 디바이스의 개수가 제한되기 마련이다. ADB는 TCP 기반의 무선연결을 제공하므로, 이 기능을 통해서 USB포트보다 많은 디바이스를 연결할 수 있다.

준비

연결할 디바이스와 ADB가 있는 컴퓨터는 같은 네트워크에 속해있어야 한다. (즉, 3G/LTE일 때는 무선연결을 이용할 수 없다.)

ADB 연결

  1. 연결할 스마트폰을 컴퓨터에 USB로 연결한다.
  2. Terminal / cmd 를 열고, 다음과 같이 입력한다.
    • 이 때, 입력할 명령어는 adb가 설치되어 있는 디렉터리에서 수행되거나, 그 디렉터리가 환경변수로 등록되어 있어야 한다.
    • 아래 예제에서 5555는 포트번호이다. 포트번호는 임의로 설정해준다.
    • 성공시 restarting in TCP mode port: 5555 라는 메시지가 나타난다.
    • 이미 여러개의 장치가 컴퓨터에 연결되어 있을 경우, -s 옵션을 이용하여 device id를 추가로 입력해준다. device id는 adb devices 명령을 이용해 확인할 수 있다.
// Windows
adb tcpip 5555
adb -s <device id> tcpip 5555       // 여러개의 장치가 연결되어 위의 명령어가 안될 경우
// Mac or Linux
./adb tcpip 5555
./adb -s <device id> tcpip 5555      // 여러개의 장치가 연결되어 위의 명령어가 안될 경우
  1. USB를 연결해제한 후, 스마트폰의 ip를 확인한다.
  2. 확인한 ip를 이용해 다음과 같이 adb 명령을 수행한다.
    • 아래 예제에서 192.168.0.100은 스마트폰의 ip, 5555는 위에서 설정한 포트번호이다. 각자의 상황에 맞게 바꿔적어야 한다.
    • 성공시 connect to 192.168.0.100:5555라는 메시지가 나타난다.
// Windows
adb connect 192.168.0.100:5555
// Mac or Linux
./adb connect 192.168.0.100:5555
  1. 안드로이드 스튜디오에서 로그가 제대로 보이는지 확인한다. 혹은 다음의 명령어를 이용한다.
// Windows
adb devices
// Mac or Linux
./adb devices