Android Vitals. - 1차 추첨 시간.👩‍🎨

Header image: Light Field by Romain Guy.


이 블로그 시리즈는 Android 응용 프로그램이 생산에서의 안정성과 성능 모니터링을 주목한다.지난주에 나는 썼다.
오늘 우리의 중점은 냉각 가동이 끝난 시간을 확정하는 것이다.
기준 Play Console documentation:

Startup times are tracked when the app's first frame completely loads.


우리는 App startup cold time documentation 에서 더 많은 것을 배웠다.

Once the app process has completed the first draw, the system process swaps out the currently displayed background window, replacing it with the main activity. At this point, the user can start using the app.


년, 우리는 다음과 같은 사실을 알게 되었다.

  • ActivityThread.handleResumeActivity() 첫 번째 프레임을 조정합니다.
  • 은 첫 번째 프레임Choreographer.doFrame()에서 호출ViewRootImpl.doTraversal()하고 이 호출은 보기 차원에서 측정 과정, 레이아웃 과정을 실행하며 마지막으로 첫 번째 그리기 과정을 실행한다.

  • 첫 번째 프레임


    API 레벨이 16이기 때문에 Android는 다음 프레임이 발생할 때 리셋을 설정하는 간단한 API를 제공합니다: Choreographer.postFrameCallback().
    class MyApp : Application() {
    
      var firstFrameDoneMs: Long = 0
    
      override fun onCreate() {
        super.onCreate()
        Choreographer.getInstance().postFrameCallback {
          firstFrameDoneMs = SystemClock.uptimeMillis()
        }
      }
    }
    
    불행하게도 호출Choreographer.postFrameCallback()의 부작용은 스케줄링이 처음 반복되기 전에 실행되는 프레임입니다.그래서 여기에 보고된 시간은 처음 그려진 프레임을 실행하기 전이다.나는 API 25에서 이 오류를 재현할 수 있지만, API 30에서 발생하지 않았기 때문에 이 오류는 이미 복구되었을 수도 있다.

    첫 번째 추첨.


    ViewTreeObserver


    Android에서는 각 뷰 계층 구조ViewTreeObserver가 배치나 드로잉 등 전역 이벤트를 리셋할 수 있습니다.

    ViewTreeObserver。addOnDrawListener()


    우리는 ViewTreeObserver.addOnDrawListener() 를 호출하여 제도 탐지기를 등록할 수 있습니다.
    view.viewTreeObserver.addOnDrawListener { 
      // report first draw
    }
    

    ViewTreeObserver。removeOnDrawListener()


    우리는 첫 번째 무승부에만 관심을 가지기 때문에 리턴을 받은 후 즉시 삭제해야 한다OnDrawListener.유감스럽게도 onDraw() 에서 리셋할 수 없습니다 ViewTreeObserver.removeOnDrawListener():
    public final class ViewTreeObserver {
      public void removeOnDrawListener(OnDrawListener victim) {
        checkIsAlive();
        if (mInDispatchOnDraw) {
          throw new IllegalStateException(
              "Cannot call removeOnDrawListener inside of onDraw");
        }
        mOnDrawListeners.remove(victim);
      }
    }
    
    따라서 게시물에서 삭제해야 합니다.
    class NextDrawListener(
      val view: View,
      val onDrawCallback: () -> Unit
    ) : OnDrawListener {
    
      val handler = Handler(Looper.getMainLooper())
      var invoked = false
    
      override fun onDraw() {
        if (invoked) return
        invoked = true
        onDrawCallback()
        handler.post {
          if (view.viewTreeObserver.isAlive) {
            viewTreeObserver.removeOnDrawListener(this)
          }
        }
      }
    
      companion object {
        fun View.onNextDraw(onDrawCallback: () -> Unit) {
          viewTreeObserver.addOnDrawListener(
            NextDrawListener(this, onDrawCallback)
          )
        }
      }
    }
    
    멋진 확장 함수 주의:
    view.onNextDraw { 
      // report first draw
    }
    

    부동 TreeObserver


    뷰 계층 구조를 추가하기 전에 View.getViewTreeObserver() 를 호출하면 진정한 ViewTreeObserver를 사용할 수 없으므로 뷰가 리셋을 저장하기 위해 a fake one 생성됩니다.
    public class View {
      public ViewTreeObserver getViewTreeObserver() {
        if (mAttachInfo != null) {
          return mAttachInfo.mTreeObserver;
        }
        if (mFloatingTreeObserver == null) {
          mFloatingTreeObserver = new ViewTreeObserver(mContext);
        }
        return mFloatingTreeObserver;
      }
    }
    
    그런 다음 뷰를 첨부하면 merged back into the real ViewTreeObserver 로 콜백합니다.
    이것은 매우 좋다. 버그 fixed in API 26 를 제외하고는 탐지기를 실제 보기 트리 관찰자에게 통합하지 않았다.
    드로잉 탐지기를 등록하기 전에 뷰가 추가될 때까지 기다림으로써 이 문제를 해결합니다.
    class NextDrawListener(
      val view: View,
      val onDrawCallback: () -> Unit
    ) : OnDrawListener {
    
      val handler = Handler(Looper.getMainLooper())
      var invoked = false
    
      override fun onDraw() {
        if (invoked) return
        invoked = true
        onDrawCallback()
        handler.post {
          if (view.viewTreeObserver.isAlive) {
            viewTreeObserver.removeOnDrawListener(this)
          }
        }
      }
    
      companion object {
        fun View.onNextDraw(onDrawCallback: () -> Unit) {
          if (viewTreeObserver.isAlive && isAttachedToWindow) {
            addNextDrawListener(onDrawCallback)
          } else {
            // Wait until attached
            addOnAttachStateChangeListener(
                object : OnAttachStateChangeListener {
              override fun onViewAttachedToWindow(v: View) {
                addNextDrawListener(onDrawCallback)
                removeOnAttachStateChangeListener(this)
              }
    
              override fun onViewDetachedFromWindow(v: View) = Unit
            })
          }
        }
    
        private fun View.addNextDrawListener(callback: () -> Unit) {
          viewTreeObserver.addOnDrawListener(
            NextDrawListener(this, callback)
          )
        }
      }
    }
    

    DecorView 회사


    이제 다음 그림을 들을 수 있는 좋은 유틸리티가 생겼습니다. 이벤트를 만들 때 사용할 수 있습니다.첫 번째로 만든 활동은 그려지지 않을 수 있습니다. 응용 프로그램은 보통 트램펄린 활동을 이니시에이터 활동으로 하고 다른 활동을 즉시 시작하여 자체적으로 수행합니다.활성 창 DecorView 에 드로잉 탐지기를 등록합니다.
    class MyApp : Application() {
    
      override fun onCreate() {
        super.onCreate()
    
        var firstDraw = false
    
        registerActivityLifecycleCallbacks(
          object : ActivityLifecycleCallbacks {
          override fun onActivityCreated(
            activity: Activity,
            savedInstanceState: Bundle?
          ) {
            if (firstDraw) return
            activity.window.decorView.onNextDraw {
              if (firstDraw) return
              firstDraw = true
              // report first draw
            }
          }
        })
      }
    }
    

    잠금 창 특성


    Window.getDecorView() 파일에 따라 다음을 수행합니다.

    Note that calling this function for the first time "locks in" various window characteristics as described in setContentView().


    불행하게도 우리는 ActivityLifecycleCallbacks.onActivityCreated() 에서 Window.getDecorView(), 즉 called by Activity.onCreate() 으로 전화를 걸었다.전형적인 활동에서setContentView()super.onCreate() 이후에 호출되기 때문에 우리는 호출Window.getDecorView() 전에 호출setContentView()하면 예상치 못한 부작용을 일으킬 수 있다.
    장식 보기를 검색하기 전에 setContentView() 호출을 기다려야 합니다.

    창문.답조onContentChanged()


    우리는 Window.peekDecorView() 를 사용하여 장식 보기가 있는지 확인할 수 있다.만약 없다면, 우리는 창에 리셋을 등록할 수 있습니다. 이것은 우리가 필요로 하는 갈고리를 제공합니다Window.Callback.onContentChanged().

    This hook is called whenever the content view of the screen changes (due to a call to Window#setContentView() or Window#addContentView()).


    그러나 한 창에는 리셋만 있을 수 있고 활동은 이미 sets itself 창으로 리셋되었습니다.그래서 우리는 리셋을 교체해서 그것을 의뢰해야 한다.
    다음은 유틸리티 클래스입니다. 이를 실현하고 Window.onDecorViewReady() 확장 함수를 추가했습니다.
    class WindowDelegateCallback constructor(
      private val delegate: Window.Callback
    ) : Window.Callback by delegate {
    
      val onContentChangedCallbacks = mutableListOf<() -> Boolean>()
    
      override fun onContentChanged() {
        onContentChangedCallbacks.removeAll { callback ->
          !callback()
        }
        delegate.onContentChanged()
      }
    
      companion object {
        fun Window.onDecorViewReady(callback: () -> Unit) {
          if (peekDecorView() == null) {
            onContentChanged {
              callback()
              return@onContentChanged false
            }
          } else {
            callback()
          }
        }
    
        fun Window.onContentChanged(block: () -> Boolean) {
          val callback = wrapCallback()
          callback.onContentChangedCallbacks += block
        }
    
        private fun Window.wrapCallback(): WindowDelegateCallback {
          val currentCallback = callback
          return if (currentCallback is WindowDelegateCallback) {
            currentCallback
          } else {
            val newCallback = WindowDelegateCallback(currentCallback)
            callback = newCallback
            newCallback
          }
        }
      }
    }
    

    창을 이용하다.onDecorViewReady()


    우리는 이 모든 것을 함께 두자.
    class MyApp : Application() {
    
      override fun onCreate() {
        super.onCreate()
    
        var firstDraw = false
    
        registerActivityLifecycleCallbacks(
          object : ActivityLifecycleCallbacks {
          override fun onActivityCreated(
            activity: Activity,
            savedInstanceState: Bundle?
          ) {
            if (firstDraw) return
            val window = activity.window
            window.onDecorViewReady {
              window.decorView.onNextDraw {
                if (firstDraw) return
                firstDraw = true
                // report first draw
              }
            }
          }
        })
      }
    }
    

    아직 잘 모르겠어요.


    OnDrawListener.onDraw() 문서를 살펴보겠습니다.

    Callback method to be invoked when the view tree is about to be drawn.


    드로잉에는 여전히 시간이 필요합니다.우리는 그림이 언제 완성되는지 알고 싶다. 언제 시작하는 것이 아니라.불행히도 API가 없습니다.
    에서 우리는 첫 번째 프레임과 반복이 모두 하나의 ViewTreeObserver.OnPostDrawListener 메시지에서만 발생한다는 것을 알게 되었다.만약 우리가 소식이 언제 끝날지 확정할 수 있다면, 우리는 언제 그림을 다 그렸는지 알게 될 것이다.

    처리 절차.PostAfrontofQueue()

    MSG_DO_FRAME 메시지가 언제 끝날지 확인하는 것과 달리, 우리는 Handler.postAtFrontOfQueue() 메시지를 메시지 대기열의 전단으로 보내서 다음 메시지가 언제 시작되는지 검사할 수 있다.
    class MyApp : Application() {
    
      var firstDrawMs: Long = 0
    
      override fun onCreate() {
        super.onCreate()
    
        var firstDraw = false
        val handler = Handler()
    
        registerActivityLifecycleCallbacks(
          object : ActivityLifecycleCallbacks {
          override fun onActivityCreated(
            activity: Activity,
            savedInstanceState: Bundle?
          ) {
            if (firstDraw) return
            val window = activity.window
            window.onDecorViewReady {
              window.decorView.onNextDraw {
                if (firstDraw) return
                firstDraw = true
                handler.postAtFrontOfQueue {
                  firstDrawMs = SystemClock.uptimeMillis()
                }
              }
            }
          }
        })
      }
    }
    
    편집: 대량 설비에서 생산 중 첫 번째MSG_DO_FRAME와 아래onNextDraw() 사이의 시간 차이를 측정한 결과 다음과 같습니다.
  • 10백분위: 25ms
  • 25백분위: 37ms
  • 50 백분위: 61ms
  • 75백분위: 109ms
  • 90백분위: 194ms
  • 이 간격은 충분히 중요하기 때문에 소홀히 해서는 안 된다.

    결론


    우리는 현재 생산 중의 냉각 가동 시간을 감시하는 데 필요한 모든 것을 가지고 있다.
  • 년에 우리는 냉각 가동을 측정하는 방법을 배웠다.
  • 에서 우리는 응용 프로그램의 시작 시간을 어떻게 정하는지 배웠다.
  • 이 블로그에서 우리는 첫 번째 추첨 시간을 어떻게 정하는지 배웠다.
  • 나는 네가 이런 깊이 다이빙을 좋아하길 바란다. 계속 관심을 가져라!

    회사 명⚔
    @ 피와이

    Android Vitals, 하나의 단서!나는 안드로이드 응용 프로그램의 생산 안정성과 성능 모니터링에 관심을 가진 블로그 시리즈를 쓰고 있다.나는 새 글을 발표할 때 링크로 이 게시물을 업데이트할 것이다.매주 새로운 내용 보기!🧵⬇️⬇️⬇️⬇️⬇️⬇️🧵
    2020년 8월 5일 오후 21:40
    0
    이.

    좋은 웹페이지 즐겨찾기