Fix case where traversal was out of sync with snapshot
Summary: Its was possible for the view tree observer to be observing one root but this can a dead root with no view in it. As a result the snapshot will be empty and the observer will never fire. The layout traversal Applicaiton ref descriptor had logic to handle these dead roots, this logic is now extracted and shared between the descriptor in the traversal and by the decor view tracker so they are in sync Reviewed By: lblasa Differential Revision: D50848155 fbshipit-source-id: ce6da13df40632cbb7a302a59382b4907131d9f5
This commit is contained in:
committed by
Facebook GitHub Bot
parent
d26612d840
commit
5b89331ea2
@@ -11,15 +11,30 @@ import android.util.Log
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewTreeObserver
|
import android.view.ViewTreeObserver
|
||||||
import com.facebook.flipper.plugins.uidebugger.LogTag
|
import com.facebook.flipper.plugins.uidebugger.LogTag
|
||||||
|
import com.facebook.flipper.plugins.uidebugger.descriptors.ApplicationRefDescriptor
|
||||||
import com.facebook.flipper.plugins.uidebugger.descriptors.ViewDescriptor
|
import com.facebook.flipper.plugins.uidebugger.descriptors.ViewDescriptor
|
||||||
import com.facebook.flipper.plugins.uidebugger.util.StopWatch
|
import com.facebook.flipper.plugins.uidebugger.util.StopWatch
|
||||||
import com.facebook.flipper.plugins.uidebugger.util.Throttler
|
import com.facebook.flipper.plugins.uidebugger.util.Throttler
|
||||||
import com.facebook.flipper.plugins.uidebugger.util.objectIdentity
|
import com.facebook.flipper.plugins.uidebugger.util.objectIdentity
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The responsibility of this class is to find the top most decor view and add a pre draw observer
|
* The UIDebugger does 3 things:
|
||||||
* to it This predraw observer triggers a full traversal of the UI. There should only ever be one
|
* 1. Observe changes
|
||||||
* active predraw listener at once
|
* 2. Traverse UI hierarchy, gathering tree
|
||||||
|
* 3. Generate snapshot
|
||||||
|
*
|
||||||
|
* All 3 of these stages need to work on the same view else there will be major inconsistencies
|
||||||
|
*
|
||||||
|
* The first responsibility of this class is to track changes to root views, find the top most decor
|
||||||
|
* view and add a pre draw observer to it. There should only ever be one active predraw listener at
|
||||||
|
* once.
|
||||||
|
*
|
||||||
|
* This pre-draw observer triggers a full traversal of the UI, the traversal of the hierarchy might
|
||||||
|
* skip some branches (active child) so its essential that both the active child decision and top
|
||||||
|
* root decision match.
|
||||||
|
*
|
||||||
|
* The observer also triggers a snapshot, again its essential the same root view as we do for
|
||||||
|
* traversal and observation
|
||||||
*/
|
*/
|
||||||
class DecorViewTracker(private val context: UIDContext, private val snapshotter: Snapshotter) {
|
class DecorViewTracker(private val context: UIDContext, private val snapshotter: Snapshotter) {
|
||||||
|
|
||||||
@@ -42,11 +57,13 @@ class DecorViewTracker(private val context: UIDContext, private val snapshotter:
|
|||||||
// remove predraw listen from current view as its going away or will be covered
|
// remove predraw listen from current view as its going away or will be covered
|
||||||
currentDecorView?.viewTreeObserver?.removeOnPreDrawListener(preDrawListener)
|
currentDecorView?.viewTreeObserver?.removeOnPreDrawListener(preDrawListener)
|
||||||
|
|
||||||
// setup new listener on top most view
|
// setup new listener on top most view, that will be the active child in traversal
|
||||||
val topView = rootViews.lastOrNull()
|
val topView = rootViews.lastOrNull(ApplicationRefDescriptor::isUsefulRoot)
|
||||||
val throttler = Throttler(500) { currentDecorView?.let { traverseSnapshotAndSend(it) } }
|
|
||||||
|
|
||||||
if (topView != null) {
|
if (topView != null) {
|
||||||
|
val throttler =
|
||||||
|
Throttler(500) { currentDecorView?.let { traverseSnapshotAndSend(it) } }
|
||||||
|
|
||||||
preDrawListener =
|
preDrawListener =
|
||||||
ViewTreeObserver.OnPreDrawListener {
|
ViewTreeObserver.OnPreDrawListener {
|
||||||
throttler.trigger()
|
throttler.trigger()
|
||||||
@@ -60,8 +77,6 @@ class DecorViewTracker(private val context: UIDContext, private val snapshotter:
|
|||||||
|
|
||||||
// schedule traversal immediately when we detect a new decor view
|
// schedule traversal immediately when we detect a new decor view
|
||||||
throttler.trigger()
|
throttler.trigger()
|
||||||
} else {
|
|
||||||
Log.i(LogTag, "Stack is empty")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,15 +18,7 @@ object ApplicationRefDescriptor : ChainedDescriptor<ApplicationRef>() {
|
|||||||
|
|
||||||
override fun onGetActiveChild(node: ApplicationRef): Any? {
|
override fun onGetActiveChild(node: ApplicationRef): Any? {
|
||||||
val children = onGetChildren(node)
|
val children = onGetChildren(node)
|
||||||
if (children.isNotEmpty()) {
|
return children.lastOrNull(ApplicationRefDescriptor::isUsefulRoot)
|
||||||
val last = children.last()
|
|
||||||
if (last.javaClass.simpleName.contains("OverlayHandlerView")) {
|
|
||||||
return children.getOrNull(children.size - 2)
|
|
||||||
}
|
|
||||||
return last
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onGetBounds(node: ApplicationRef): Bounds = DisplayMetrics.getDisplayBounds()
|
override fun onGetBounds(node: ApplicationRef): Bounds = DisplayMetrics.getDisplayBounds()
|
||||||
@@ -48,19 +40,30 @@ object ApplicationRefDescriptor : ChainedDescriptor<ApplicationRef>() {
|
|||||||
|
|
||||||
for (root in activeRoots) {
|
for (root in activeRoots) {
|
||||||
// if there is an activity for this root view use that,
|
// if there is an activity for this root view use that,
|
||||||
// if not just return the mystery floating decor view
|
// if not just return the root view that was added directly to the window manager
|
||||||
val activity = decorViewToActivity[root]
|
val activity = decorViewToActivity[root]
|
||||||
if (activity != null) {
|
if (activity != null) {
|
||||||
children.add(activity)
|
children.add(activity)
|
||||||
} else {
|
} else {
|
||||||
if (root is ViewGroup && root.childCount > 0) {
|
|
||||||
// sometimes there is a root view on top that has no children and we dont want to add
|
|
||||||
// these as they will become active
|
|
||||||
children.add(root)
|
children.add(root)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return children
|
return children
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isUsefulRoot(obj: Any): Boolean {
|
||||||
|
if (obj is Activity) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val isFoldableOverlayInfraView = javaClass.simpleName.contains("OverlayHandlerView")
|
||||||
|
return if (isFoldableOverlayInfraView) {
|
||||||
|
false
|
||||||
|
} else if (obj is ViewGroup) {
|
||||||
|
// sometimes there is a root view on top that has no children that isn't useful to inspect
|
||||||
|
obj.childCount > 0
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user