/** * Copyright (c) 2017-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.litho; import javax.annotation.CheckReturnValue; import javax.annotation.concurrent.GuardedBy; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import android.content.Context; import android.content.ContextWrapper; import android.graphics.Rect; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.Process; import android.support.annotation.IntDef; import android.support.annotation.Keep; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.view.View; import android.view.ViewParent; import com.facebook.infer.annotation.ReturnsOwnership; import com.facebook.infer.annotation.ThreadConfined; import com.facebook.infer.annotation.ThreadSafe; import static com.facebook.litho.ComponentLifecycle.StateUpdate; import static com.facebook.litho.FrameworkLogEvents.EVENT_LAYOUT_CALCULATE; import static com.facebook.litho.FrameworkLogEvents.EVENT_PRE_ALLOCATE_MOUNT_CONTENT; import static com.facebook.litho.FrameworkLogEvents.PARAM_IS_BACKGROUND_LAYOUT; import static com.facebook.litho.FrameworkLogEvents.PARAM_LOG_TAG; import static com.facebook.litho.FrameworkLogEvents.PARAM_TREE_DIFF_ENABLED; import static com.facebook.litho.ThreadUtils.assertHoldsLock; import static com.facebook.litho.ThreadUtils.assertMainThread; import static com.facebook.litho.ThreadUtils.isMainThread; /** * Represents a tree of components and controls their life cycle. ComponentTree takes in a single * root component and recursively invokes its OnCreateLayout to create a tree of components. * ComponentTree is responsible for refreshing the mounted state of a component with new props. * * The usual use case for {@link ComponentTree} is: * <code> * ComponentTree component = ComponentTree.create(context, MyComponent.create()); * myHostView.setRoot(component); * <code/> */ @ThreadSafe public class ComponentTree { private static final String TAG = ComponentTree.class.getSimpleName(); private static final int SIZE_UNINITIALIZED = -1; // MainThread Looper messages: private static final int MESSAGE_WHAT_BACKGROUND_LAYOUT_STATE_UPDATED = 1; private static final String DEFAULT_LAYOUT_THREAD_NAME = "ComponentLayoutThread"; private static final int DEFAULT_LAYOUT_THREAD_PRIORITY = Process.THREAD_PRIORITY_BACKGROUND; private static final int SCHEDULE_NONE = 0; private static final int SCHEDULE_LAYOUT_ASYNC = 1; private static final int SCHEDULE_LAYOUT_SYNC = 2; private LithoDebugInfo mLithoDebugInfo; @IntDef({SCHEDULE_NONE, SCHEDULE_LAYOUT_ASYNC, SCHEDULE_LAYOUT_SYNC}) @Retention(RetentionPolicy.SOURCE) private @interface PendingLayoutCalculation {} private static final AtomicInteger sIdGenerator = new AtomicInteger(0); private static final Handler sMainThreadHandler = new ComponentMainThreadHandler(); // Do not access sDefaultLayoutThreadLooper directly, use getDefaultLayoutThreadLooper(). @GuardedBy("ComponentTree.class") private static volatile Looper sDefaultLayoutThreadLooper; // Helpers to track view visibility when we are incrementally // mounting and partially invalidating private static final int[] sCurrentLocation = new int[2]; private static final int[] sParentLocation = new int[2]; private static final Rect sParentBounds = new Rect(); private final Runnable mCalculateLayoutRunnable = new Runnable() { @Override public void run() { calculateLayout(null, false); } }; private final Runnable mAnimatedCalculateLayoutRunnable = new Runnable() { @Override public void run() { calculateLayout(null, true); } }; private final ComponentContext mContext; private final boolean mCanPrefetchDisplayLists; // These variables are only accessed from the main thread. @ThreadConfined(ThreadConfined.UI) private boolean mIsMounting; @ThreadConfined(ThreadConfined.UI) private boolean mIncrementalMountEnabled; @ThreadConfined(ThreadConfined.UI) private boolean mIsLayoutDiffingEnabled; @ThreadConfined(ThreadConfined.UI) private boolean mIsAttached; @ThreadConfined(ThreadConfined.UI) private boolean mIsAsyncUpdateStateEnabled; @ThreadConfined(ThreadConfined.UI) private LithoView mLithoView; @ThreadConfined(ThreadConfined.UI) private LayoutHandler mLayoutThreadHandler; @GuardedBy("this") private boolean mHasViewMeasureSpec; // TODO(6606683): Enable recycling of mComponent. // We will need to ensure there are no background threads referencing mComponent. We'll need // to keep a reference count or something. :-/ @GuardedBy("this") private Component<?> mRoot; @GuardedBy("this") private int mWidthSpec = SIZE_UNINITIALIZED; @GuardedBy("this") private int mHeightSpec = SIZE_UNINITIALIZED; // This is written to only by the main thread with the lock held, read from the main thread with // no lock held, or read from any other thread with the lock held. private LayoutState mMainThreadLayoutState; // The semantics here are tricky. Whenever you transfer mBackgroundLayoutState to a local that // will be accessed outside of the lock, you must set mBackgroundLayoutState to null to ensure // that the current thread alone has access to the LayoutState, which is single-threaded. @GuardedBy("this") private LayoutState mBackgroundLayoutState; @GuardedBy("this") private StateHandler mStateHandler; private Object mLayoutLock; protected final int mId; @GuardedBy("this") private boolean mIsMeasuring; @GuardedBy("this") private @PendingLayoutCalculation int mScheduleLayoutAfterMeasure; // This flag is so we use the correct shouldAnimateTransitions flag when calculating // the LayoutState in measure -- we should respect the most recent setRoot* call. private volatile boolean mLastShouldAnimateTransitions; public static Builder create(ComponentContext context, Component.Builder<?> root) { return create(context, root.build()); } public static Builder create(ComponentContext context, Component<?> root) { return ComponentsPools.acquireComponentTreeBuilder(context, root); } protected ComponentTree(Builder builder) { mContext = ComponentContext.withComponentTree(builder.context, this); mRoot = builder.root; mIncrementalMountEnabled = builder.incrementalMountEnabled; mIsLayoutDiffingEnabled = builder.isLayoutDiffingEnabled; mLayoutThreadHandler = builder.layoutThreadHandler; mLayoutLock = builder.layoutLock; mIsAsyncUpdateStateEnabled = builder.asyncStateUpdates; mCanPrefetchDisplayLists = builder.canPrefetchDisplayLists; if (mLayoutThreadHandler == null) { mLayoutThreadHandler = new DefaultLayoutHandler(getDefaultLayoutThreadLooper()); } final StateHandler builderStateHandler = builder.stateHandler; mStateHandler = builderStateHandler == null ? StateHandler.acquireNewInstance(null) : builderStateHandler; if (builder.overrideComponentTreeId != -1) { mId = builder.overrideComponentTreeId; } else { mId = generateComponentTreeId(); } } @ThreadConfined(ThreadConfined.UI) LayoutState getMainThreadLayoutState() { return mMainThreadLayoutState; } @VisibleForTesting protected LayoutState getBackgroundLayoutState() { return mBackgroundLayoutState; } /** * Picks the best LayoutState and sets it in mMainThreadLayoutState. The return value * is a LayoutState that must be released (after the lock is released). This * awkward contract is necessary to ensure thread-safety. */ @CheckReturnValue @ReturnsOwnership @ThreadConfined(ThreadConfined.UI) private LayoutState setBestMainThreadLayoutAndReturnOldLayout() { assertHoldsLock(this); // If everything matches perfectly then we prefer mMainThreadLayoutState // because that means we don't need to remount. boolean isMainThreadLayoutBest; if (isCompatibleComponentAndSpec(mMainThreadLayoutState)) { isMainThreadLayoutBest = true; } else if (isCompatibleSpec(mBackgroundLayoutState, mWidthSpec, mHeightSpec) || !isCompatibleSpec(mMainThreadLayoutState, mWidthSpec, mHeightSpec)) { // If mMainThreadLayoutState isn't a perfect match, we'll prefer // mBackgroundLayoutState since it will have the more recent create. isMainThreadLayoutBest = false; } else { // If the main thread layout is still compatible size-wise, and the // background one is not, then we'll do nothing. We want to keep the same // main thread layout so that we don't force main thread re-layout. isMainThreadLayoutBest = true; } if (isMainThreadLayoutBest) { // We don't want to hold onto mBackgroundLayoutState since it's unlikely // to ever be used again. We return mBackgroundLayoutState to indicate it // should be released after exiting the lock. LayoutState toRelease = mBackgroundLayoutState; mBackgroundLayoutState = null; return toRelease; } else { // Since we are changing layout states we'll need to remount. if (mLithoView != null) { mLithoView.setMountStateDirty(); } LayoutState toRelease = mMainThreadLayoutState; mMainThreadLayoutState = mBackgroundLayoutState; mBackgroundLayoutState = null; return toRelease; } } private void backgroundLayoutStateUpdated() { assertMainThread(); // If we aren't attached, then we have nothing to do. We'll handle // everything in onAttach. if (!mIsAttached) { return; } LayoutState toRelease; boolean layoutStateUpdated; int componentRootId; synchronized (this) { if (mRoot == null) { // We have been released. Abort. return; } LayoutState oldMainThreadLayoutState = mMainThreadLayoutState; toRelease = setBestMainThreadLayoutAndReturnOldLayout(); layoutStateUpdated = (mMainThreadLayoutState != oldMainThreadLayoutState); componentRootId = mRoot.getId(); } if (toRelease != null) { toRelease.releaseRef(); toRelease = null; } if (!layoutStateUpdated) { return; } // We defer until measure if we don't yet have a width/height int viewWidth = mLithoView.getMeasuredWidth(); int viewHeight = mLithoView.getMeasuredHeight(); if (viewWidth == 0 && viewHeight == 0) { // The host view has not been measured yet. return; } final boolean needsAndroidLayout = !isCompatibleComponentAndSize( mMainThreadLayoutState, componentRootId, viewWidth, viewHeight); if (needsAndroidLayout) { mLithoView.requestLayout(); } else { mountComponentIfDirty(); } } void attach() { assertMainThread(); if (mLithoView == null) { throw new IllegalStateException("Trying to attach a ComponentTree without a set View"); } LayoutState toRelease; int componentRootId; synchronized (this) { // We need to track that we are attached regardless... mIsAttached = true; // ... and then we do state transfer toRelease = setBestMainThreadLayoutAndReturnOldLayout(); componentRootId = mRoot.getId(); } if (toRelease != null) { toRelease.releaseRef(); toRelease = null; } // We defer until measure if we don't yet have a width/height int viewWidth = mLithoView.getMeasuredWidth(); int viewHeight = mLithoView.getMeasuredHeight(); if (viewWidth == 0 && viewHeight == 0) { // The host view has not been measured yet. return; } final boolean needsAndroidLayout = !isCompatibleComponentAndSize( mMainThreadLayoutState, componentRootId, viewWidth, viewHeight); if (needsAndroidLayout || mLithoView.isMountStateDirty()) { mLithoView.requestLayout(); } else { mLithoView.rebind(); } } private static boolean hasSameBaseContext(Context context1, Context context2) { return getBaseContext(context1) == getBaseContext(context2); } private static Context getBaseContext(Context context) { Context baseContext = context; while (baseContext instanceof ContextWrapper) { baseContext = ((ContextWrapper) baseContext).getBaseContext(); } return baseContext; } @ThreadConfined(ThreadConfined.UI) boolean isMounting() { return mIsMounting; } private boolean mountComponentIfDirty() { if (mLithoView.isMountStateDirty()) { if (mIncrementalMountEnabled) { incrementalMountComponent(); } else { mountComponent(null); } return true; } return false; } void incrementalMountComponent() { assertMainThread(); if (!mIncrementalMountEnabled) { throw new IllegalStateException("Calling incrementalMountComponent() but incremental mount" + " is not enabled"); } // Per ComponentTree visible area. Because LithoViews can be nested and mounted // not in "depth order", this variable cannot be static. final Rect currentVisibleArea = ComponentsPools.acquireRect(); if (getVisibleRect(currentVisibleArea)) { mountComponent(currentVisibleArea); } // if false: no-op, doesn't have visible area, is not ready or not attached ComponentsPools.release(currentVisibleArea); } private boolean getVisibleRect(Rect visibleBounds) { assertMainThread(); getLocationAndBoundsOnScreen(mLithoView, sCurrentLocation, visibleBounds); final ViewParent viewParent = mLithoView.getParent(); if (viewParent instanceof View) { View parent = (View) viewParent; getLocationAndBoundsOnScreen(parent, sParentLocation, sParentBounds); if (!visibleBounds.setIntersect(visibleBounds, sParentBounds)) { return false; } } visibleBounds.offset(-sCurrentLocation[0], -sCurrentLocation[1]); return true; } private static void getLocationAndBoundsOnScreen(View view, int[] location, Rect bounds) { assertMainThread(); view.getLocationOnScreen(location); bounds.set( location[0], location[1], location[0] + view.getWidth(), location[1] + view.getHeight()); } void mountComponent(Rect currentVisibleArea) { assertMainThread(); mIsMounting = true; // currentVisibleArea null or empty => mount all mLithoView.mount(mMainThreadLayoutState, currentVisibleArea); mIsMounting = false; } void detach() { assertMainThread(); synchronized (this) { mIsAttached = false; mHasViewMeasureSpec = false; } } /** * Set a new LithoView to this ComponentTree checking that they have the same context and * clear the ComponentTree reference from the previous LithoView if any. * Be sure this ComponentTree is detach first. */ void setLithoView(@NonNull LithoView view) { assertMainThread(); // It's possible that the view associated with this ComponentTree was recycled but was // never detached. In all cases we have to make sure that the old references between // lithoView and componentTree are reset. if (mIsAttached) { if (mLithoView != null) { mLithoView.setComponentTree(null); } else { detach(); } } else if (mLithoView != null) { // Remove the ComponentTree reference from a previous view if any. mLithoView.clearComponentTree(); } if (!hasSameBaseContext(view.getContext(), mContext)) { // This would indicate bad things happening, like leaking a context. throw new IllegalArgumentException( "Base view context differs, view context is: " + view.getContext() + ", ComponentTree context is: " + mContext); } mLithoView = view; } void clearLithoView() { assertMainThread(); // Crash if the ComponentTree is mounted to a view. if (mIsAttached) { throw new IllegalStateException( "Clearing the LithoView while the ComponentTree is attached"); } mLithoView = null; } void measure(int widthSpec, int heightSpec, int[] measureOutput, boolean forceLayout) { assertMainThread(); Component component = null; LayoutState toRelease; synchronized (this) { mIsMeasuring = true; // This widthSpec/heightSpec is fixed until the view gets detached. mWidthSpec = widthSpec; mHeightSpec = heightSpec; mHasViewMeasureSpec = true; toRelease = setBestMainThreadLayoutAndReturnOldLayout(); if (forceLayout || !isCompatibleComponentAndSpec(mMainThreadLayoutState)) { // Neither layout was compatible and we have to perform a layout. // Since outputs get set on the same object during the lifecycle calls, // we need to copy it in order to use it concurrently. component = mRoot.makeShallowCopy(); } } if (toRelease != null) { toRelease.releaseRef(); toRelease = null; } if (component != null) { // TODO: We should re-use the existing CSSNodeDEPRECATED tree instead of re-creating it. if (mMainThreadLayoutState != null) { // It's beneficial to delete the old layout state before we start creating a new one since // we'll be able to re-use some of the layout nodes. LayoutState localLayoutState; synchronized (this) { localLayoutState = mMainThreadLayoutState; mMainThreadLayoutState = null; } localLayoutState.releaseRef(); } // We have no layout that matches the given spec, so we need to compute it on the main thread. LayoutState localLayoutState = calculateLayoutState( mLayoutLock, mContext, component, widthSpec, heightSpec, mIsLayoutDiffingEnabled, mLastShouldAnimateTransitions, null); final StateHandler layoutStateStateHandler = localLayoutState.consumeStateHandler(); synchronized (this) { if (layoutStateStateHandler != null) { mStateHandler.commit(layoutStateStateHandler); ComponentsPools.release(layoutStateStateHandler); } mMainThreadLayoutState = localLayoutState; localLayoutState = null; } // We need to force remount on layout mLithoView.setMountStateDirty(); } measureOutput[0] = mMainThreadLayoutState.getWidth(); measureOutput[1] = mMainThreadLayoutState.getHeight(); int layoutScheduleType = SCHEDULE_NONE; Component root = null; synchronized (this) { mIsMeasuring = false; if (mScheduleLayoutAfterMeasure != SCHEDULE_NONE) { layoutScheduleType = mScheduleLayoutAfterMeasure; mScheduleLayoutAfterMeasure = SCHEDULE_NONE; root = mRoot.makeShallowCopy(); } } if (layoutScheduleType != SCHEDULE_NONE) { // shouldAnimateTransitions - This is a scheduled layout from a state update, so we animate it setRootAndSizeSpecInternal( root, SIZE_UNINITIALIZED, SIZE_UNINITIALIZED, layoutScheduleType == SCHEDULE_LAYOUT_ASYNC, true /* = shouldAnimateTransitions */, null /*output */); } } /** * Returns {@code true} if the layout call mounted the component. */ boolean layout() { assertMainThread(); return mountComponentIfDirty(); } /** * Returns whether incremental mount is enabled or not in this component. */ public boolean isIncrementalMountEnabled() { return mIncrementalMountEnabled; } synchronized Component getRoot() { return mRoot; } /** * Update the root component. This can happen in both attached and detached states. In each case * we will run a layout and then proxy a message to the main thread to cause a * relayout/invalidate. */ public void setRoot(Component<?> rootComponent) { setRoot(rootComponent, false); } /** * Sets a new component root, specifying whether to animate transitions where transition * animations have been specified. * * @see #setRoot */ public void setRoot(Component<?> rootComponent, boolean shouldAnimateTransitions) { if (rootComponent == null) { throw new IllegalArgumentException("Root component can't be null"); } setRootAndSizeSpecInternal( rootComponent, SIZE_UNINITIALIZED, SIZE_UNINITIALIZED, false /* isAsync */, shouldAnimateTransitions, null /* output */); } public void preAllocateMountContent() { assertMainThread(); final LayoutState toPrePopulate; if (mMainThreadLayoutState != null) { toPrePopulate = mMainThreadLayoutState.acquireRef(); } else { synchronized (this) { toPrePopulate = mBackgroundLayoutState; if (toPrePopulate == null) { return; } toPrePopulate.acquireRef(); } } final ComponentsLogger logger = mContext.getLogger(); LogEvent event = null; if (logger != null) { event = logger.newPerformanceEvent(EVENT_PRE_ALLOCATE_MOUNT_CONTENT); event.addParam(PARAM_LOG_TAG, mContext.getLogTag()); } toPrePopulate.preAllocateMountContent(); if (logger != null) { logger.log(event); } toPrePopulate.releaseRef(); } public void setRootAsync(Component<?> rootComponent) { if (rootComponent == null) { throw new IllegalArgumentException("Root component can't be null"); } setRootAndSizeSpecInternal( rootComponent, SIZE_UNINITIALIZED, SIZE_UNINITIALIZED, true /* isAsync */, false /* shouldAnimateTransitions */, null /* output */); } synchronized void updateStateLazy(String componentKey, StateUpdate stateUpdate) { if (mRoot == null) { return; } mStateHandler.queueStateUpdate(componentKey, stateUpdate); } void updateState(String componentKey, StateUpdate stateUpdate) { updateStateInternal(componentKey, stateUpdate, false); } void updateStateAsync(String componentKey, StateUpdate stateUpdate) { if (!mIsAsyncUpdateStateEnabled) { throw new RuntimeException("Triggering async state updates on this component tree is " + "disabled, use sync state updates."); } updateStateInternal(componentKey, stateUpdate, true); } void updateStateInternal(String key, StateUpdate stateUpdate, boolean isAsync) { final Component<?> root; synchronized (this) { if (mRoot == null) { return; } mStateHandler.queueStateUpdate(key, stateUpdate); if (mIsMeasuring) { // If the layout calculation was already scheduled to happen synchronously let's just go // with a sync layout calculation. if (mScheduleLayoutAfterMeasure == SCHEDULE_LAYOUT_SYNC) { return; } mScheduleLayoutAfterMeasure = isAsync ? SCHEDULE_LAYOUT_ASYNC : SCHEDULE_LAYOUT_SYNC; return; } root = mRoot.makeShallowCopy(); } setRootAndSizeSpecInternal( root, SIZE_UNINITIALIZED, SIZE_UNINITIALIZED, isAsync, true /* shouldAnimateTransitions */, null /*output */); } /** * Update the width/height spec. This is useful if you are currently detached and are responding * to a configuration change. If you are currently attached then the HostView is the source of * truth for width/height, so this call will be ignored. */ public void setSizeSpec(int widthSpec, int heightSpec) { setSizeSpec(widthSpec, heightSpec, null); } /** * Same as {@link #setSizeSpec(int, int)} but fetches the resulting width/height * in the given {@link Size}. */ public void setSizeSpec(int widthSpec, int heightSpec, Size output) { setRootAndSizeSpecInternal( null, widthSpec, heightSpec, false /* isAsync */, false /* shouldAnimateTransitions */, output /* output */); } public void setSizeSpecAsync(int widthSpec, int heightSpec) { setRootAndSizeSpecInternal( null, widthSpec, heightSpec, true /* isAsync */, false /* shouldAnimateTransitions */, null /* output */); } /** * Compute asynchronously a new layout with the given component root and sizes */ public void setRootAndSizeSpecAsync(Component<?> root, int widthSpec, int heightSpec) { setRootAndSizeSpecAsync(root, widthSpec, heightSpec, false); } /** * Like {@link #setRootAndSizeSpecAsync}, allowing specification of whether transitions should be * animated where transition animations have been specified. */ public void setRootAndSizeSpecAsync( Component<?> root, int widthSpec, int heightSpec, boolean shouldAnimateTransitions) { if (root == null) { throw new IllegalArgumentException("Root component can't be null"); } setRootAndSizeSpecInternal( root, widthSpec, heightSpec, true /* isAsync */, shouldAnimateTransitions, null /* output */); } /** * Compute a new layout with the given component root and sizes */ public void setRootAndSizeSpec(Component<?> root, int widthSpec, int heightSpec) { setRootAndSizeSpec(root, widthSpec, heightSpec, false); } /** * Like {@link #setRootAndSizeSpec}, allowing specification of whether transitions should be * animated where transition animations have been specified. */ public void setRootAndSizeSpec( Component<?> root, int widthSpec, int heightSpec, boolean shouldAnimateTransitions) { if (root == null) { throw new IllegalArgumentException("Root component can't be null"); } setRootAndSizeSpecInternal( root, widthSpec, heightSpec, false /* isAsync */, shouldAnimateTransitions, null /* output */); } public void setRootAndSizeSpec(Component<?> root, int widthSpec, int heightSpec, Size output) { if (root == null) { throw new IllegalArgumentException("Root component can't be null"); } setRootAndSizeSpecInternal( root, widthSpec, heightSpec, false /* isAsync */, false /* shouldAnimateTransitions */, output); } /** * @return the {@link LithoView} associated with this ComponentTree if any. */ @Keep @Nullable public LithoView getLithoView() { assertMainThread(); return mLithoView; } /** * Provides a new instance from the StateHandler pool that is initialized with the information * from the StateHandler currently held by the ComponentTree. Once the state updates have been * applied and we are back in the main thread the state handler gets released to the pool. * @return a copy of the state handler instance held by ComponentTree. */ public synchronized StateHandler getStateHandler() { return StateHandler.acquireNewInstance(mStateHandler); } private void setRootAndSizeSpecInternal( Component<?> root, int widthSpec, int heightSpec, boolean isAsync, boolean shouldAnimateTransitions, Size output) { synchronized (this) { mLastShouldAnimateTransitions = shouldAnimateTransitions; final Map<String, List<StateUpdate>> pendingStateUpdates = mStateHandler.getPendingStateUpdates(); if (pendingStateUpdates != null && pendingStateUpdates.size() > 0 && root != null) { root = root.makeShallowCopyWithNewId(); } final boolean rootInitialized = root != null; final boolean widthSpecInitialized = widthSpec != SIZE_UNINITIALIZED; final boolean heightSpecInitialized = heightSpec != SIZE_UNINITIALIZED; if (mHasViewMeasureSpec && !rootInitialized) { // It doesn't make sense to specify the width/height while the HostView is attached and it // has been measured. We do not throw an Exception only because there can be race conditions // that can cause this to happen. In such race conditions, ignoring the setSizeSpec call is // the right thing to do. return; } final boolean widthSpecDidntChange = !widthSpecInitialized || widthSpec == mWidthSpec; final boolean heightSpecDidntChange = !heightSpecInitialized || heightSpec == mHeightSpec; final boolean sizeSpecDidntChange = widthSpecDidntChange && heightSpecDidntChange; final LayoutState mostRecentLayoutState = mBackgroundLayoutState != null ? mBackgroundLayoutState : mMainThreadLayoutState; final boolean allSpecsWereInitialized = widthSpecInitialized && heightSpecInitialized && mWidthSpec != SIZE_UNINITIALIZED && mHeightSpec != SIZE_UNINITIALIZED; final boolean sizeSpecsAreCompatible = sizeSpecDidntChange || (allSpecsWereInitialized && mostRecentLayoutState != null && LayoutState.hasCompatibleSizeSpec( mWidthSpec, mHeightSpec, widthSpec, heightSpec, mostRecentLayoutState.getWidth(), mostRecentLayoutState.getHeight())); final boolean rootDidntChange = !rootInitialized || root.getId() == mRoot.getId(); if (rootDidntChange && sizeSpecsAreCompatible) { // The spec and the root haven't changed. Either we have a layout already, or we're // currently computing one on another thread. if (output != null) { output.height = mostRecentLayoutState.getHeight(); output.width = mostRecentLayoutState.getWidth(); } return; } if (widthSpecInitialized) { mWidthSpec = widthSpec; } if (heightSpecInitialized) { mHeightSpec = heightSpec; } if (rootInitialized) { mRoot = root; } } if (isAsync && output != null) { throw new IllegalArgumentException("The layout can't be calculated asynchronously if" + " we need the Size back"); } else if (isAsync) { mLayoutThreadHandler.removeCallbacks(mCalculateLayoutRunnable); mLayoutThreadHandler.removeCallbacks(mAnimatedCalculateLayoutRunnable); mLayoutThreadHandler.post( shouldAnimateTransitions ? mAnimatedCalculateLayoutRunnable : mCalculateLayoutRunnable); } else { calculateLayout(output, shouldAnimateTransitions); } } /** * Calculates the layout. * @param output a destination where the size information should be saved * @param shouldAnimateTransitions whether component transitions should be animated */ private void calculateLayout(Size output, boolean shouldAnimateTransitions) { int widthSpec; int heightSpec; Component<?> root; LayoutState previousLayoutState = null; // Cancel any scheduled requests we might have in the background queue since we are starting // a new layout computation. mLayoutThreadHandler.removeCallbacksAndMessages(null); synchronized (this) { // Can't compute a layout if specs or root are missing if (!hasSizeSpec() || mRoot == null) { return; } // Check if we already have a compatible layout. if (hasCompatibleComponentAndSpec()) { if (output != null) { final LayoutState mostRecentLayoutState = mBackgroundLayoutState != null ? mBackgroundLayoutState : mMainThreadLayoutState; output.width = mostRecentLayoutState.getWidth(); output.height = mostRecentLayoutState.getHeight(); } return; } widthSpec = mWidthSpec; heightSpec = mHeightSpec; root = mRoot.makeShallowCopy(); if (mMainThreadLayoutState != null) { previousLayoutState = mMainThreadLayoutState.acquireRef(); } } final ComponentsLogger logger = mContext.getLogger(); LogEvent layoutEvent = null; if (logger != null) { layoutEvent = logger.newPerformanceEvent(EVENT_LAYOUT_CALCULATE); layoutEvent.addParam(PARAM_LOG_TAG, mContext.getLogTag()); layoutEvent.addParam(PARAM_TREE_DIFF_ENABLED, String.valueOf(mIsLayoutDiffingEnabled)); layoutEvent.addParam(PARAM_IS_BACKGROUND_LAYOUT, String.valueOf(!ThreadUtils.isMainThread())); } LayoutState localLayoutState = calculateLayoutState( mLayoutLock, mContext, root, widthSpec, heightSpec, mIsLayoutDiffingEnabled, shouldAnimateTransitions, previousLayoutState != null ? previousLayoutState.getDiffTree() : null); if (output != null) { output.width = localLayoutState.getWidth(); output.height = localLayoutState.getHeight(); } if (previousLayoutState != null) { previousLayoutState.releaseRef(); previousLayoutState = null; } boolean layoutStateUpdated = false; synchronized (this) { // Make sure some other thread hasn't computed a compatible layout in the meantime. if (!hasCompatibleComponentAndSpec() && isCompatibleSpec(localLayoutState, mWidthSpec, mHeightSpec)) { if (localLayoutState != null) { final StateHandler layoutStateStateHandler = localLayoutState.consumeStateHandler(); if (layoutStateStateHandler != null) { if (mStateHandler != null) { // we could have been released mStateHandler.commit(layoutStateStateHandler); } ComponentsPools.release(layoutStateStateHandler); } } // Set the new layout state, and remember the old layout state so we // can release it. LayoutState tmp = mBackgroundLayoutState; mBackgroundLayoutState = localLayoutState; localLayoutState = tmp; layoutStateUpdated = true; } } if (localLayoutState != null) { localLayoutState.releaseRef(); localLayoutState = null; } if (layoutStateUpdated) { postBackgroundLayoutStateUpdated(); } if (logger != null) { logger.log(layoutEvent); } } /** * Transfer mBackgroundLayoutState to mMainThreadLayoutState. This will proxy * to the main thread if necessary. If the component/size-spec changes in the * meantime, then the transfer will be aborted. */ private void postBackgroundLayoutStateUpdated() { if (isMainThread()) { // We need to possibly update mMainThreadLayoutState. This call will // cause the host view to be invalidated and re-laid out, if necessary. backgroundLayoutStateUpdated(); } else { // If we aren't on the main thread, we send a message to the main thread // to invoke backgroundLayoutStateUpdated. sMainThreadHandler.obtainMessage(MESSAGE_WHAT_BACKGROUND_LAYOUT_STATE_UPDATED, this) .sendToTarget(); } } /** * The contract is that in order to release a ComponentTree, you must do so from the main * thread, or guarantee that it will never be accessed from the main thread again. Usually * HostView will handle releasing, but if you never attach to a host view, then you should call * release yourself. */ public void release() { LayoutState mainThreadLayoutState; LayoutState backgroundLayoutState; synchronized (this) { if (mLithoView != null) { mLithoView.setComponentTree(null); } mRoot = null; mainThreadLayoutState = mMainThreadLayoutState; mMainThreadLayoutState = null; backgroundLayoutState = mBackgroundLayoutState; mBackgroundLayoutState = null; // TODO t15532529 mStateHandler = null; } if (mainThreadLayoutState != null) { mainThreadLayoutState.releaseRef(); mainThreadLayoutState = null; } if (backgroundLayoutState != null) { backgroundLayoutState.releaseRef(); backgroundLayoutState = null; } } private boolean isCompatibleComponentAndSpec(LayoutState layoutState) { assertHoldsLock(this); return mRoot != null && isCompatibleComponentAndSpec( layoutState, mRoot.getId(), mWidthSpec, mHeightSpec); } // Either the MainThreadLayout or the BackgroundThreadLayout is compatible with the current state. private boolean hasCompatibleComponentAndSpec() { assertHoldsLock(this); return isCompatibleComponentAndSpec(mMainThreadLayoutState) || isCompatibleComponentAndSpec(mBackgroundLayoutState); } private boolean hasSizeSpec() { assertHoldsLock(this); return mWidthSpec != SIZE_UNINITIALIZED && mHeightSpec != SIZE_UNINITIALIZED; } private static synchronized Looper getDefaultLayoutThreadLooper() { if (sDefaultLayoutThreadLooper == null) { HandlerThread defaultThread = new HandlerThread(DEFAULT_LAYOUT_THREAD_NAME, DEFAULT_LAYOUT_THREAD_PRIORITY); defaultThread.start(); sDefaultLayoutThreadLooper = defaultThread.getLooper(); } return sDefaultLayoutThreadLooper; } private static boolean isCompatibleSpec( LayoutState layoutState, int widthSpec, int heightSpec) { return layoutState != null && layoutState.isCompatibleSpec(widthSpec, heightSpec) && layoutState.isCompatibleAccessibility(); } private static boolean isCompatibleComponentAndSpec( LayoutState layoutState, int componentId, int widthSpec, int heightSpec) { return layoutState != null && layoutState.isCompatibleComponentAndSpec(componentId, widthSpec, heightSpec) && layoutState.isCompatibleAccessibility(); } private static boolean isCompatibleComponentAndSize( LayoutState layoutState, int componentId, int width, int height) { return layoutState != null && layoutState.isComponentId(componentId) && layoutState.isCompatibleSize(width, height) && layoutState.isCompatibleAccessibility(); } public ComponentContext getContext() { return mContext; } private static class ComponentMainThreadHandler extends Handler { private ComponentMainThreadHandler() { super(Looper.getMainLooper()); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_WHAT_BACKGROUND_LAYOUT_STATE_UPDATED: ComponentTree that = (ComponentTree) msg.obj; that.backgroundLayoutStateUpdated(); break; default: throw new IllegalArgumentException(); } } } protected LayoutState calculateLayoutState( @Nullable Object lock, ComponentContext context, Component<?> root, int widthSpec, int heightSpec, boolean diffingEnabled, boolean shouldAnimateTransitions, @Nullable DiffNode diffNode) { final ComponentContext contextWithStateHandler; synchronized (this) { contextWithStateHandler = new ComponentContext(context, StateHandler.acquireNewInstance(mStateHandler)); } if (lock != null) { synchronized (lock) { return LayoutState.calculate( contextWithStateHandler, root, mId, widthSpec, heightSpec, diffingEnabled, shouldAnimateTransitions, diffNode, mCanPrefetchDisplayLists); } } else { return LayoutState.calculate( contextWithStateHandler, root, mId, widthSpec, heightSpec, diffingEnabled, shouldAnimateTransitions, diffNode, mCanPrefetchDisplayLists); } } /** * A default {@link LayoutHandler} that will use a {@link Handler} with a {@link Thread}'s * {@link Looper}. */ private static class DefaultLayoutHandler extends Handler implements LayoutHandler { private DefaultLayoutHandler(Looper threadLooper) { super(threadLooper); } } public static int generateComponentTreeId() { return sIdGenerator.getAndIncrement(); } /** * A builder class that can be used to create a {@link ComponentTree}. */ public static class Builder { // required private ComponentContext context; private Component<?> root; // optional private boolean incrementalMountEnabled = true; private boolean isLayoutDiffingEnabled = true; private LayoutHandler layoutThreadHandler; private Object layoutLock; private StateHandler stateHandler; private boolean asyncStateUpdates = true; private int overrideComponentTreeId = -1; private boolean canPrefetchDisplayLists = false; protected Builder() { } protected Builder(ComponentContext context, Component<?> root) { init(context, root); } protected void init(ComponentContext context, Component<?> root) { this.context = context; this.root = root; } protected void release() { context = null; root = null; incrementalMountEnabled = true; isLayoutDiffingEnabled = true; layoutThreadHandler = null; layoutLock = null; stateHandler = null; asyncStateUpdates = true; overrideComponentTreeId = -1; canPrefetchDisplayLists = false; } /** * Whether or not to enable the incremental mount optimization. True by default. * In order to use incremental mount you should disable mount diffing. * * @Deprecated We will remove this option soon, please consider turning it on (which is on by * default) */ public Builder incrementalMount(boolean isEnabled) { incrementalMountEnabled = isEnabled; return this; } /** * Whether or not to enable layout tree diffing. This will reduce the cost of * updates at the expense of using extra memory. True by default. * * @Deprecated We will remove this option soon, please consider turning it on (which is on by * default) */ public Builder layoutDiffing(boolean enabled) { isLayoutDiffingEnabled = enabled; return this; } /** * Specify the looper to use for running layouts on. Note that in rare cases * layout must run on the UI thread. For example, if you rotate the screen, * we must measure on the UI thread. If you don't specify a Looper here, the * Components default Looper will be used. */ public Builder layoutThreadLooper(Looper looper) { if (looper != null) { layoutThreadHandler = new DefaultLayoutHandler(looper); } return this; } /** * Specify the looper to use for running layouts on. Note that in rare cases * layout must run on the UI thread. For example, if you rotate the screen, * we must measure on the UI thread. If you don't specify a Looper here, the * Components default Looper will be used. */ public Builder layoutThreadHandler(LayoutHandler handler) { layoutThreadHandler = handler; return this; } /** * Specify a lock to be acquired during layout. This is an advanced feature * that can lead to deadlock if you don't know what you are doing. */ public Builder layoutLock(Object layoutLock) { this.layoutLock = layoutLock; return this; } /** * Specify an initial state handler object that the ComponentTree can use to set the current * values for states. */ public Builder stateHandler(StateHandler stateHandler) { this.stateHandler = stateHandler; return this; } /** * Specify whether the ComponentTree allows async state updates. This is enabled by default. */ public Builder asyncStateUpdates(boolean enabled) { this.asyncStateUpdates = enabled; return this; } /** * Gives the ability to override the auto-generated ComponentTree id: this is generally not * useful in the majority of circumstances, so don't use it unless you really know what you're * doing. */ public Builder overrideComponentTreeId(int overrideComponentTreeId) { this.overrideComponentTreeId = overrideComponentTreeId; return this; } /** * Specify whether the ComponentTree allows to prefetch display lists of its components * on idle time of UI thread. * * NOTE: To make display lists prefetching work, besides setting this flag * {@link com.facebook.litho.utils.DisplayListPrefetcherUtils#prefetchDisplayLists(View)} * should be called on scrollable surfaces like {@link android.support.v7.widget.RecyclerView} * during scrolling. */ public Builder canPrefetchDisplayLists(boolean canPrefetch) { this.canPrefetchDisplayLists = canPrefetch; return this; } /** * Builds a {@link ComponentTree} using the parameters specified in this builder. */ public ComponentTree build() { ComponentTree componentTree = new ComponentTree(this); ComponentsPools.release(this); return componentTree; } } }