[번역] [Android Performance] Launch-Time Performance

Launch-Time Performance

사용자는 앱이 반응과 로드가 빠른것을 기대한다. 시작 시간이 느린 앱은 이 기대를 충족시켜주지 못하며, 사용자를 실망시킬 수 있다. 이것류의 경험으로 인해 사용자는 Play Store에서 앱을 나쁘게 평가하거나, 앱을 완전히 버리게 될 수 있다.
이 문서는 앱의 실행시간을 최적화 하는데 도움을 주는 정보를 제공한다. 우선 Launch process의 내부구조부터 설명한다.
다음은 어떻게 시작성능을 프로파일 하는지에 대해 이야기 하고, 마지막으로 보편적인 startup-time issue들에 대해 설명하고, 해결방법에 대한 몇가지 힌트를 제공한다.

참고 : https://www.youtube.com/watch?v=Vw1G1s73DsY

Launch Internals


앱 실행은 3가지 상태중 하나에서 시작될 수 있다. 각 상태(콜드 start, 웜 start, 미지근한 start[lukewarm start])는 사용자가 앱을 볼 수 있는데 걸리는 시간에 영향을 준다. 콜드 Start에서는 앱은 처음부터 시작한다. 다른 상태에서는, 시스템은 앱을 백그라운드에서 foreground로 가져와야 한다. 추천하는 바는 항상 콜드 스타트라는 가정에서 최적화를 해야한다는 것이다. 이렇게 하면 웜 Start/Lukewarm Start의 성능도 향상시킬 수 있다. 
앱의 빠른 시작을 위한 최적화를 위해 시스템레벨과 앱 레벨에서 무슨일이 일어나고 있는지와 각 State에서 어떤 상호작용이 일어나고 있는지 이해하면 유용하다.  

Cold start

Cold Start란 앱이 처음부터 시작되는 것을 말한다. 시스템 프로세스는 시작될때 까지 앱의 프로세스를 만들지 않는다. 앱이 디바이스 Booting 이후 처음 실행되는 경우 혹은 시스템이 앱을 kill한 후 처음 실행되는 경우와 같은 상황에 Cold Start가 일어난다. 이런류의 Start는 다른 State에서 보다 시스템 및 응용프로그램이 수행해야 할 작업이 더 많기 때문에, Startup Time을 최소화 하는데 가장 큰 문제가 된다. 
Cold Start의 시작에서, 시스템은 아래 3가지 Task를 수행한다. 
  1. 앱을 로딩하고 실행한다. 
  2. 실행 직후 빈 화면(blank starting window)를 표시한다. 
  3. 앱 프로세스를 생성한다. 

시스템이 앱 프로세스를 생성하자 마자, 앱 프로세스가 다음 단계를 담당한다. 
  1. 앱 객체를 생성한다. 
  2. 메인 Thread를 생성한다. 
  3. 메인 Activity를 생성한다.
  4. View들을 Inflate하고
  5. Screen 레이아웃을 잡고
  6. 초기 draw를 수행한다.
한번 앱 프로세스가 최초 Draw를 완료하면, 시스템 프로세스가 현재 display되고 있는 Background Window를 main Activity와 함께 교체한다. 이때부터 사용자는 앱을 사용할 수 있다.
아래의 그림은 시스템과 앱 프로세스가 서로간에 작업을 처리하는 방법을 보여준다. 


Figure 1. Cold 앱 런칭의 중요한 부분의 시각화
성능 이슈는 앱과 Activity 생성동안 일어날 수 있다. 

Application creation

앱이 런칭될 때, blank starting window는 앱의 첫번째 draw를 완료할 때 까지 스크린에 남아있다. 이 시점에서, 시스템 프로세스는 앱의 Starting window를 교체해서, 사용자가 앱과 상호작용 할 수 있게 한다. 
만약 앱이 Application.oncreate()를 overloaded 했다면, 시스템은 앱의 onCreate() method를 호출한다. 그 후에 앱은 UI thread로도 알려져있는 Main Thread를 생성하고, Main Activity를 생성하는 작업을 수행한다. 
이 때부터, 시스템레벨 그리고 앱 레벨의 프로세스는 app lifecycle stages에 의해 진행된다. 

Activity creation

앱 프로세스가 액티비티를 생성한 이후, 액티비티는 아래 작업을 수행한다.
  1. Value 초기화
  2. 생성자 호출 
  3. 현재 Activity의 Lifecycle에 적합한 Activity.onCreate()와 같은 Callback 호출
일반적으로, onCreate() method는 오버헤드가 가장 큰 작업을 수행하기 때문에, load Time에 큰 영향을 줄 수 있다. : View를 loading/Inflate하고, 액티비티의 실행에 필요한 객체들을 초기화 한다. 

Warm start

앱의 Warm Start는 Cold Start에 비해 훨씬 간단하고 오버헤드도 적다. Warm start에서, 모든 시스템은 액티비티를 foreground로 가져온다. 만약 앱의 모든 액티비티가 memory에 있다면, 앱은 객체의 생성, 레이아웃 Inflation, 랜더링의 반복을 피할 수 있다. 
그러나, onTrimMemory()와 같은 메모리리 trimming 이벤트에 대한 응답으로 일부 메모리가 제거된 경우, 그런 객체는 Warm start event에 대한 응답으로 재 생성해야 한다. 
Warm Start는 cold start 시나리오와 같은 On-Screen Behavior를 디스플레이 한다.(앱이 Activity의 랜더링을 종료할 때 까지  시스템 프로세스가 blank sceen을 디스플레이 한다)

Lukewarm start

Lukewarm start는 Cold Start동안 일어나는 동작의 일부를 포함한다 (동시에, warm start의 오버헤드보다 낮다). Lukewarm start로 간주 될 수 있는 많은 잠재적 State가 있다. 예를 들면:
  • 사용자는 앱을 종료(back out)했다가 다시 실행시킨다. 프로스세스가 계속 실행되었을 수도 있지만, 앱에서 onCreate() 호출을 통해 처음부터 다시 Activity를 만들어야 한다. 
  • 시스템이 메모리에서 앱을 삭제한 후 사용자가 다시 실행한다. 프로셋와 Activity는 재시작 되지만, Tack는 onCreate()를 통해 전달된 Saved Instance state bundle을 통해 약간의 이득을 취할 수 있다.

Profiling Launch Performance


시작시간 성능을 정확하게 측정하려면, 앱을 시작하는데 걸리는 시간을 타나내는 metric을 추적할 수 있다.  

Time to initial display

From Android 4.4 (API level 19), logcat includes an output line containing a value called Displayed.
Android 4.4부터 로그캣의 출력라인에 Displayed값이 포함되었다. 이 값은 프로세스의 시작과 화면에 해당하는 Activity의 Drawing을 마친 시점의 경과시간을 나타낸다. 이 시간은 아래 일련의 이벤트를 포함한다:
  1. 프로세스의 시작
  2. 객체 초기화
  3. 액티비티 생성 및 초기화
  4. 레이아웃 Inflate
  5. 최초 앱 Draw
해당 로그는 아래와 유사한 형태로 보여진다.
ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms
만약 네가 Command line이나 terminal에서 로그캣을 tracking하고 있다면, 경과시간을 찾는것이 간단하다. Android Studio에서 경과시간을 찾기위해, 로그캣 뷰에서 반드시 필터를 disable 시켜야 한다. 필터를 disable 하는 것은 필수이다. 왜냐하면 앱이 아닌 시스템 서버가 이 로그를 제공하기 때문이다.
한번만 적절한 설정을 한 후에는, 쉽게 검색하여 시간을 확인 할 수 있다. 아래 그림은 어떻게 필터를 disable하는지 보여준다. 그리고 로그캣 출력의 Displayed 에 대한 예시도 보여주는데, 아래서 두번째 줄을 참고하라.

Figure 2. 필터를 disable하고, Disabled 값을 로그캣에서 찾는다. 
로그캣 아웃풋의 Disabled 값은 모든 resource들이 로드되고 디스플레이 될때까지의 시간을 나타내지는 않는다. 레이아웃 파일에서 참조되지 않거나, 앱이 객체 초기화의 일부로 만드는 리소스는 제외된다. 이러한 리소스들은 inline process이기 때문에 이러한 리소스는 제외하고, 앱의 초기 Display를 차단하지 않는다.
또한 ADB Shell Activity Manager Command를 통해 실행중인 앱을 실행하여 초기 표시시간을 측정할 수도 있다. 아래 예시를 참고하라.
adb [-d|-e|-s <serialNumber>] shell am start -S -W
com.example.app/.MainActivity
-c android.intent.category.LAUNCHER-a android.intent.action.MAIN
앞에서와 같이 Displayed값은 로그캣 아웃풋으로 나온다. 터미널 Window는 아래와 같이 표시할 것이다.
Starting: Intent
Activity: com.example.app/.MainActivity
ThisTime: 2044
TotalTime: 2044
WaitTime: 2054
Complete
-c와 -a 인자는 optional이며, Intent에 대해 <category>와 <action> 지정할 수 있도록 한다.  

Time to full display

reportFullyDrawn() 를 사용하면 앱의 런칭부터 모든 리소스와 View 계층의 display까지의 경과 시간을 측정할 수 있다. 앱이 Lazy loading 하는 경우 유용할 수 있다. Lazy loading 할 때, 앱은 window의 초기 drawing을 block하지 않는다. 하지만 대신에 비동기적으로 리소스를 로드하고 view 계층구조를 업데이트 한다. 
만약 lazy loading으로 인해 앱의 초기 display가 모든 리소스를 포함하지 않는다면, 별도의 값들로 로딩의 완료와 모든 리소스와 view의 display를 고려해야 한다. 예를들어, UI가 모두 로드되어 일부 Test가 그려졌을지라도, 아직 앱이 네트워크로부터 가져와야 하는 이미지들은 display 되지 않았을 수 있다.
이 문제를 해결하기 위해, 우리는 수동으로 reportFullyDrawn()를 호출해 시스템에게 액티비티가 lazy loading을 끝냈다고 알려줄 수 있다. 이 method를 사용하면, 로그캣이 표시하는 경과시간은 앱 객체를 생성후 reportFullyDrawn()를 호출할때 까지이다. 아래는 로그캣 출력의 에시이다. 
system_process I/ActivityManager: Fully drawn {package}/.MainActivity: +1s54ms
만약 생각한것보다 display time이 늦다면, 이 시작 process에서 병목구간이 어디인지 찾아야 한다.  

Identifying bottlenecks

병목구간을 확인하기 위해 Android Studio의 Method Tracer 툴과 Inline tracing이라는 두가지 좋은 방법이 있다.  Method Tracer에 대해 익히기 위해 툴의 documentation를 참고하라.
만약 Method Tracer Tool에 접근할 수 없거나 로그정보를 얻기위해 tool을 적절한 시간에 시작할 수 없다면, 앱 activity의 onCreate() Method안의 inline tracing을 통해 비슷한 정보를 얻을 수 있다.  inline tracing에 대해 더 익히기 위해 Systrace tool과 Trace 기능에 대한 문서를 참고하라 

Common Issues


이 섹션에서는 종종 앱의 시작성능에 영향을 미치는 여러가지 문제에 대해 설명한다. 이 이슈들은 주로 앱/Activity 객체의 초기화 및 화면 로딩과 관련이 있다.

Heavy app initialization

실행 성능은 코드가 Application 객체를 override하고, 객체를 초기화 할 때 무거운 작업이나 복잡한 로직을 수행하면 문제가 될 수 있다. 만약 Application subclass가 아직 할 필요 없는 초기화 작업을 진행한다면 앱은 Startup 동안 시간을 낭비할 수 있다. 몇몇의 초기화들은 불필요하다. 에를 들면, 앱이 Intent로 시작된 경우에 Main Activity를 위한 상태정보 초기화하는 것이다. Intent를 사용하면 앱은 사용한다 이전에 초기화된 state 데이터의 일부분만 사용하기 때문이다. 
앱 초기화중 다른 문제는 영향력이 있거나 많은 GC이벤트나 초기화와 동시에 진행되는 disk I/O와 같이 초기화 프로세스를 더 blocking하는 것들을 포함한다. GC는 특히 달빅 runtime의 고려사항이다. Art runtime은 GC를 동시에 수행하여, 해당 작업의 영향을 최소화 한다. 

Diagnosing the problem (문제 진단)

method tracing 또는 inline tracing를 사용해서 문제를 진단 할 수 있다. 
Method tracing
Method Tracer 도구를 실행하면 Application.OnCreate() Method가 com.example.customApplication.onCreate Method를 호출하게 된다. 만약 툴이 보여준다 이 method들이 실행을 끝내는데 오랜 시간이 걸리는 것을 보여준다면, 어떤 작업이 이것을 발생시키는지 확인해야 한다. 
Inline tracing
inline tracing를 사용해서 아래 부분들을 조사하라.
  • 앱의 초기 onCreate() 함수 
  • 앱이 초기화 하는 singleton 객체들
  • Bottleneck이 될 수 있는 Disk I/O, deserialization, 타이트한 loop

Solutions to the problem

불필요한 초기화나 Disk I/O에 문제가 있는지 여부와 관계없이 이 솔루션은 lazy initializing을 요구한다. 즉각적으로 필요한 객체만 초기화한다. 예를 들어 Global static 객체를 생성하는 것 보다는 앱이 객체에 처음 액세스 할때만 객체를 초기화 하는 Singleton pattern으로 변경해라. 또한 Dagger같은  DI(dependency injection) framework를 사용하여 객체 및 종속성을 처음으로 주입할 때 생성하는 것을 고려하라. 

Heavy activity initialization

Activity 생성은 종종 높은 오버헤드 작업을 많이 수반한다. 성능개선을 위해 이 작업을 최적화할 방법들이 있다. 아래는 보편적인 이슈상황들이다.
  • 크고 복잡한 레이아웃의 Inflate
  • 디스크 혹은 네트워크 I/O로 인한 Screen Drawing의 Blocking
  • 비트맵의 Loading / Decoding
  • VectorDrawable 객체의 Rasterizing 
  • 액티비티의 다른 서브시스템의 초기화 

Diagnosing the problem (문제 진단)

이 경우에도 Method tracing과 Inline tracing이 유용할 수 있다.
Method tracing
Method Tracer tool을 실행할 때, 앱의 Application 서브클래스 생성자 및 com.example.customApplication.onCreate() 메소드에 초점을 맞출 특정영역을 지정한다. 
만약 툴이 이러한 method들의 실행을 완료하는데 오랜 시간이 걸림을 보여준다면, 거기서 어떤 작업이 일어나는지 더 자세히 알아봐야 한다.
Inline tracing
inline tracing 을 사용해서 아래 부분들을 조사해라: 
  • 앱의 초기 onCreate() 함수 
  • 글로벌 Singleton 객체의 초기화 
  • Bottleneck이 될 수 있는 Disk I/O, deserialization, 타이트한 loop

Solutions to the problem (문제 해결)

많은 잠재적 병목구간이 있지만, 보편적인 두가지 문제점과 해결책은 아래와 같다. 
  • View 계층구조가 클 수록 Inflate 하는데 오랜 시간이 걸린다. 이 이슈를 해결하기 위해 두가지 조치는 아래와 같다.  
    • 중복, 중첩 레이아웃을 줄여서 view 계층을 Flat하게 만들어 준다. 
    • Launch동안 보여질 필요가 없는 UI 부분들은 Inflate하지 않는다. 대신에 마치 하위 계층들을 위한 Placeholder와 같은 ViewStub 객체를 사용해서 앱이 적절한 시간에 inflate 할 수 있게 하자.
  • Having all of your resource initialization on the main thread can also slow down startup. 모든 리소스를 Main Thread에서 초기화 하면, Startup이 느려질 수 있다. 아래 방법들을 통해 이 문제를 해결할 수 있다. :
    • 모든 리소스의 초기화를 다른 Thread에서 lazy하게 처리할 수 있다. 
    • Allow the app to load and display your views, and then later update visual properties that are dependent on bitmaps and other resources. 앱의 view들을 로드하고 display 하고 나서, 비트맵이나 다른 리소스들에 의존하는 시각적 속성들을 나중에 업데이트 하라 

Themed launch screens

앱의 로딩을 특정 테마로 하고 싶을 때, 앱의 시작 화면을 기본 시스템 테마 대신 전용 테마로 할 수 있다. 이렇게 하면 액티비티가 느리게 시작되는 것을 숨길 수 있다. 
테마가 입혀진 시작화면을 구현하는 보편적인 방법은 windowDisablePreview 테마속성을 이용하여 시스템의 Black Screen을 끄는 것이다. 그러나 이러한 방법은 preview window를 숨기지 않는 앱들보다 startup이 느려질 수도 있다. 또한 사용자가 액티비티 런칭되는 동안 아무 피드백 없이 기다려야 하기 때문에, 사용자에게 이 앱이 정상적으로 동작하는 것인지 의문을 품게 만들 수 있다.

Diagnosing the problem (문제 진단)

사용자가 앱을 실행할 때 느린 응답을 확인하여 이 문제를 파악할 수 있다. 이러한 경우에 스크린은 멈추게 되거나 인풋에 대해 응답이 멈추게 된다.

Solutions to the problem

Preview window를 Disable 하는 것 보다 일반적인 Material Design 패턴을 따를 것을 권장한다. 액티비티의 windowBackground 테마 속성을 이용하여 Starting Activity에 간단한 커스텀 Drawable을 설정할 수 있다. 
예를들면, 아래와 같이 새로운 drawable 파일을 생성해 layout XML과 앱의 Manifest에서 참조하게 할 수 있다. 
Layout XML file:
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
  <!-- The background color, preferably the same as your normal theme -->
  <item android:drawable="@android:color/white"/>
  <!-- Your product logo - 144dp color version of your app icon -->
  <item>
    <bitmap
      android:src="@drawable/product_logo_144dp"
      android:gravity="center"/>
  </item>
</layer-list>
Manifest file:
<activity ...android:theme="@style/AppTheme.Launcher" />
기본테마로 전환하기 가장 쉬운 방법은 super.onCreate() 와 setContentView() 의 호출 전에 setTheme(R.style.AppTheme)를 호출해 주는 것이다. 
public class MyMainActivity extends AppCompatActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // Make sure this is before calling super.onCreate
    setTheme(R.style.Theme_MyApp);
    super.onCreate(savedInstanceState);
    // ...
  }
}

댓글

이 블로그의 인기 게시물

[Android] DataBinding의 동작방식 - 4. include Tag 혹은 ViewStub 사용시의 Binding

[Android] DataBinding의 동작방식 - 5. Listener, Callback (CustomView의 Callback을 람다식으로 Binding하기)

[Android] DataBinding의 동작방식 - 2. BindingAdapter의 기본 및 사용 시점