1
Author: vivo Internet front-end team - Zhang Xichen

1. Background and Problems

A certain SDK has PopupWindow and dynamic effects. Due to the requirements of the business scenario, for the App, the popup timing of the SDK's popup window is random.

When the pop-up window pops up, if the app happens to also have animation effects, the main thread may draw two animation effects at the same time, which will cause a freeze, as shown in the figure below.

We simulate the ongoing motion effects of the App (such as page switching) with horizontally moving blocks; it can be seen that when the Snackabr pop-up window pops up, the block motion is obviously stuck (moving to about 1/3).

The root cause of this problem can be briefly described as: uncontrollable dynamic conflict (business randomness) + main thread time-consuming method that cannot be placed (pop-up window instantiation, view infalte).

Therefore, we need to find a solution to solve the stuttering problem caused by the conflict of motion effects. We know that the Android coding specification requires that child threads cannot manipulate the UI, but must this be the case?

Through our optimization, we can finally achieve the perfect effect, with smooth motion and non-interference:

2. Optimization measures

[Optimization method 1]: Dynamically set the delayed instantiation and display time of the pop-up window to avoid business dynamic effects.

Conclusion: It works, but it's not elegant enough. Used as a back-up solution.

[Optimization method 2]: Can the time-consuming operations (such as instantiation, infalte) of the pop-up window be moved to the sub-thread to run, and only executed in the main thread during the display phase (calling the show method)?

Conclusion: Yes. The view operation before attach, strictly speaking, is not a UI operation, but a simple property assignment.

[Optimization method 3]: Can the instantiation, display, and interaction of the entire Snackbar be placed in sub-threads for execution?

Conclusion: Yes, but there are some constraints. Although the "UI thread" can be understood as the "main thread" most of the time, in the strict sense, the "UI thread" has never been defined in the Android source code as the "main thread".

3. Principle analysis

Next, we analyze the feasibility principle of the second and third options.

3.1 Conceptual Analysis

[Main thread]: The thread that instantiates the ActivityThread, and each Activity instantiates the thread.

[UI thread]: The thread that instantiates ViewRootImpl, and finally executes the thread that involves UI operations such as View's onMeasure/onLayout/onDraw.

[Sub-thread]: Relative concept, any other thread is a sub-thread relative to the main thread. The same is true for the UI thread.

3.2 Where does CalledFromWrongThreadException come from

As we all know, when we update the interface element, if it is not executed on the main thread, the system will throw CalledFromWrongThreadException, observe the exception stack, it is not difficult to find that the exception is thrown from the ViewRootImpl#checkThread method.

 // ViewRootImpl.java
void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

As you can see from the method reference, the ViewRootImpl#checkThread method is called in almost all view update methods to prevent multi-threaded UI operations.

图片

In order to facilitate in-depth analysis, we take the TextView#setText method as an example to further observe what was done before the exception was triggered.

By looking at the method call chain (Android Studio: alt + ctrl + H), we can see the UI update operation, reaching the invalidate method of the common parent class of VIew.

In fact, this method is a necessary method to trigger UI update. After View#invalidate is called, the redrawing of the View will be performed step by step in subsequent operations.

 ViewRootImpl.checkThread()  (android.view)
  ViewRootImpl.invalidateChildInParent(int[], Rect)  (android.view)
    ViewGroup.invalidateChild(View, Rect)  (android.view)
      ViewRootImpl.invalidateChild(View, Rect)  (android.view)
        View.invalidateInternal(int, int, int, int, boolean, boolean)  (android.view)
          View.invalidate(boolean)  (android.view)
            View.invalidate()  (android.view)
              TextView.checkForRelayout()(2 usages)  (android.widget)
                TextView.setText(CharSequence, BufferType, boolean, int)  (android.widget)

3.3 Understanding the View#invalidate method

Take a deep look at the source code of this method, we ignore the unimportant code, the invalidate method actually marks the dirty area, and continues to pass it to the parent View, and finally the top View performs the real invalidate operation.

As you can see, for the code to start recursively executing, several necessary conditions need to be met:

  • Parent View is not empty: This condition is obvious. When the parent view is empty, the ParentView#invalidateChild method cannot be called.
  • Dirty zone coordinates are legal: equally obvious.
  • AttachInfo is not empty: the only variable at present, when the method is empty, invalidate will not be executed.

Then, when conditions 1 and 2 are obvious, why judge the AttachInfo object one more time? What information is in this AttachInfo object?

 void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
    // ...
 
    // Propagate the damage rectangle to the parent view.
    final AttachInfo ai = mAttachInfo; // 此处何时赋值
    final ViewParent p = mParent;
    if (p != null && ai != null && l < r && t < b) { // 此处逻辑若不通过,实际也不会触发invalidate
        final Rect damage = ai.mTmpInvalRect;
        damage.set(l, t, r, b);
        p.invalidateChild(this, damage);
    }
 
    // ...
 
}

What's in mAttachInfo?

Note description: attachInfo is a series of information that a view is assigned when attaching to its parent window.

Some of the key content can be seen in it:

  1. Window (Window) related classes, information and IPC classes.
  2. ViewRootImpl object: This class is the source that will trigger CalledFromWrongThreadException.
  3. other information.

In fact, through the information on the calling chain of the TextView#setText method above, we already know that all successfully executed view#invalidate methods will eventually go to the method in ViewRootImpl, and check the thread that tries to update the UI in ViewRootImpl.

That is to say, when a View is associated with the ViewRootImpl object, it is possible to trigger the CalledFromWrongThreadException exception, so attachInfo is a necessary object for the View to continue to effectively execute the invalidate method.

 // android.view.view
 
/**
 * A set of information given to a view when it is attached to its parent
 * window.
 */
final static class AttachInfo {
 
    // ...
 
    final IBinder mWindowToken;
 
    /**
     * The view root impl.
     */
    final ViewRootImpl mViewRootImpl;
 
    // ...
 
    AttachInfo(IWindowSession session, IWindow window, Display display,
            ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,
            Context context) {
 
        // ...
 
        mViewRootImpl = viewRootImpl;
 
        // ...
    }
}

As described in the comments, combined with the source code observation, the mAttachInfo assignment time is indeed only the attach and detach of the view.

So we further speculate: the UI update operation of the view before attach will not trigger an exception. Can we complete the time-consuming operations such as instantiation in the child thread before attaching?

When did the view attach to the window?

图片

Just as we write the layout file, the construction of the view tree is constructed through the addView method one by one VIewGroup. Observe the ViewGroup#addViewInner method, and you can see the code that binds the child view to the attachInfo relationship.

ViewGroup#addView →ViewGroup#addViewInner

 // android.view.ViewGroup
 
private void addViewInner(View child, int index, LayoutParams params,
        boolean preventRequestLayout) {
    // ...                                                                      
    AttachInfo ai = mAttachInfo;
    if (ai != null && (mGroupFlags & FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW) == 0) {
 
        // ...
        child.dispatchAttachedToWindow(mAttachInfo, (mViewFlags&VISIBILITY_MASK));
        // ...
    }
    // ...
}

In our background case, the layout inflate operation of the pop-up window is time-consuming. Is the attachWindow operation completed when this operation is executed?

In fact, when infalte, the developer can freely control whether to execute the attach operation, and all the overloaded methods of infalte will eventually be executed to LayoutInfaltor#tryInflatePrecompiled.

That is to say, we can perform the inflate operation and the addView operation in two steps, and the former can be done in the child thread.

(In fact, the AsyncLayoutInflater in the Androidx package provided by google also operates in this way).

 private View tryInflatePrecompiled(@LayoutRes int resource, Resources res, @Nullable ViewGroup root,
    boolean attachToRoot) {
    // ...
    if (attachToRoot) {
        root.addView(view, params);
    } else {
        view.setLayoutParams(params);
    }
    // ...
}

So far, it seems that everything is relatively clear, everything is related to ViewRootImpl, so let's take a closer look at it:

First of all, where does ViewRootImpl come from? -- in WindowManager#addView

When we can add a new window through WindowManager#addView, the implementation of this method will instantiate ViewRootImpl in WindowManagerGlobal#addView, and set the newly instantiated ViewRootImpl as the Parent of the added View, and the View is also identified as rootView.

 // android.view.WindowManagerGlobal
 
public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
    // ...
 
    root = new ViewRootImpl(view.getContext(), display);
 
    // ...
 
    try {
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
        // ...
    }
}
 
 
// android.view.RootViewImpl
 
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    // ...
    mView = view;
    // ...
    mAttachInfo.mRootView = view;
    // ...
    view.assignParent(this);
    // ...
}

Let's observe the calling relationship of the WindowManagerGlobal#addView method again, and we can see the calling moments of many familiar classes:

 WindowManagerGlobal.addView(View, LayoutParams, Display, Window)  (android.view)
    WindowManagerImpl.addView(View, LayoutParams)  (android.view)
        Dialog.show()  (android.app) // Dialog的显示方法
        PopupWindow.invokePopup(LayoutParams)  (android.widget)
            PopupWindow.showAtLocation(IBinder, int, int, int)  (android.widget) // PopupWindow的显示方法
        TN in Toast.handleShow(IBinder)  (android.widget) // Toast的展示方法

From the calling relationship, we can see that, such as Dialog, PopupWindow, Toast, etc., all attach the window and associate it with RootViewImpl when the display method is called. Therefore, in theory, we only need to ensure that the show method is called on the main thread.

In addition, for the pop-up window scene, the Androidx material package will also provide Snackbar. Let's observe the attach timing and logic of Snackbar in the material package:

It can be found that this pop-up window is actually bound to the existing view tree directly through the addView method in the View passed in by the business, and is not displayed by adding a new window through WindowManager. The timing of its attach is also the moment when show is called.

 // com.google.android.material.snackbar.BaseTransientBottomBar
 
final void showView() {
 
    // ...
 
    if (this.view.getParent() == null) {
      ViewGroup.LayoutParams lp = this.view.getLayoutParams();
     
      if (lp instanceof CoordinatorLayout.LayoutParams) {
        setUpBehavior((CoordinatorLayout.LayoutParams) lp);
      }
     
      extraBottomMarginAnchorView = calculateBottomMarginForAnchorView();
      updateMargins();
     
      // Set view to INVISIBLE so it doesn't flash on the screen before the inset adjustment is
      // handled and the enter animation is started
      view.setVisibility(View.INVISIBLE);
      targetParent.addView(this.view);
    }
 
    // ...
 
}

At this point, we can draw the first conclusion: the instantiation of an unattached View and the operation of its attributes, since there is no viewRootImpl object in its top-level parent, no matter what method is called, checkThread will not be triggered, so It can be done in a child thread.

Only when the view is attached to the window, it will be used as part of the UI (mounted to the ViewTree) and needs to be controlled, updated and other management operations by a fixed thread.

If a view wants to attach to a window, there are two ways:

  1. The addView method is called by a parent View that has attachedWindow, and the child view is also attached to the same window, thus owning viewRootImpl. (material Snackbar method)
  2. Through WindowManager#addView, build a Window and ViewRootImpl to complete the attach operation of view and window. (PopupWindow method)

How to understand Window and View and ViewRootImpl?

Window is an abstract concept. Each Window corresponds to a View and a ViewRootImpl. Window and View are connected through ViewRootImpl. —— "Exploring the Art of Android Development"

// Understanding: Each Window corresponds to a ViewTree, its root node is ViewRootImpl, ViewRootImpl controls everything in ViewTree from top to bottom (event & drawing & updating)

Here comes the question: So, does the fixed thread that controls the View have to be the main thread?

 /**
 * Invalidate the whole view. If the view is visible,
 * {@link #onDraw(android.graphics.Canvas)} will be called at some point in
 * the future.
 * <p>
 * This must be called from a UI thread. To call from a non-UI thread, call
 * {@link #postInvalidate()}.
 */
// 咬文嚼字:「from a UI thread」,不是「from the UI thread」
public void invalidate() {
    invalidate(true);
}

3.4 In-depth observation of ViewRootImpl and Android screen refresh mechanism

Let's rephrase the question: Is it safe not to update a View in the main thread? Can we have multiple UI threads?

To return to this question, we still have to return to the origin of CalledFromWrongThreadException.

 // ViewRootImpl.java
 
void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

Looking at the code again, we can see that the judgment condition of the checkThread method is to judge whether the mThread object is consistent with the Thread object of the current code, so the ViewRootImpl.mThread member variable must be mainThread?

In fact, it is not. Looking at the ViewRootImpl class, there is only one assignment of the mThread member variable, that is, in the ViewRootImpl object constructor, the current thread object is obtained when it is instantiated.

 // ViewRootImpl.java
 
public ViewRootImpl(Context context, Display display) {
    // ...
    mThread = Thread.currentThread();
    // ...
    mChoreographer = Choreographer.getInstance();
}

Therefore, we can infer that the checkThread method determines whether the thread when ViewRootImpl is instantiated is consistent with the thread of the UI update operation. Rather than strong constraints are applied to the main process.

In the previous article, we have explained that the instantiation of the ViewRootImpl object is called by WindowManager#addView → WindowManagerGlobal#addView → new ViewRootImpl, and these methods can be triggered in the child thread.

In order to verify our inference, we first do a step analysis from the source code level.

First, let's observe the annotation description of ViewRootImpl:

The top of a view hierarchy, implementing the needed protocol between View and the WindowManager. This is for the most part an internal implementation detail of WindowManagerGlobal.

The documentation points out that ViewRootImpl is the top object of the view tree and implements the necessary protocols between View and WindowManager. As most of the internal implementation in WindowManagerGlobal. That is, most of the important methods in WindowManagerGlobal eventually came to the implementation of ViewRootImpl.

There are several very important member variables and methods in the ViewRootImpl object, which control the mapping operation of the view tree. 图片

Here we briefly introduce the mechanism of Android screen refresh and how it interacts with the above-mentioned core objects and methods, so that we can better analyze it further.

Understand the Android screen refresh mechanism

We know that when the View is drawn, it is triggered by the invalidate method, and eventually it will go to its onMeasure, onLayout, and onDraw methods to complete the drawing. The process during this period plays an important role in our understanding of UI thread management.

Let's take a look at the Android drawing process through the source code:

First, the View#invalidate method is triggered, passed to the parent View level by level, and finally passed to the ViewRootImpl object at the top level of the view tree to complete the marking of the dirty area.

 // ViewRootImpl.java
 
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
 
    // ...
                                                                        
    invalidateRectOnScreen(dirty);
                                                                        
    return null;
}
 
private void invalidateRectOnScreen(Rect dirty) {
 
    // ...
     
    if (!mWillDrawSoon && (intersected || mIsAnimating)) {
        scheduleTraversals();
    }
}

ViewRootImpl will then execute the scheduleTraversal method to plan the UI view tree drawing task:

  1. First, a synchronous message barrier will be added to the message queue of the UI thread to ensure the priority execution of subsequent drawing asynchronous messages;
  2. After that, a Runnable object will be registered with Choreographer, and the former will decide when to call the run method of Runnable;
  3. The Runnable object is the doTraversal method, that is, the method that actually performs the traversal and drawing of the view tree.
 // ViewRootImpl.java
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
 
void scheduleTraversals() {
    // ...
    mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
    mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    // ...
}

After the Choreographer is called, it will go through the following methods, and finally call DisplayEventReceiver#scheduleVsync, and finally call the nativeScheduleVsync method to register and accept the vertical synchronization signal at the bottom of the system.

Choreographer#postCallback →postCallbackDelayed →

postCallbackDelayedInternal→mHandler#sendMessage→MSG\_DO\_SCHEDULE_CALLBACK

MessageQueue#next→ mHandler#handleMessage →MSG\_DO\_SCHEDULE_CALLBACK→ doScheduleCallback→scheduleFrameLocked → scheduleVsyncLocked→DisplayEventReceiver#scheduleVsync

 // android.view.DisplayEventReceiver
 
/**
 * Schedules a single vertical sync pulse to be delivered when the next
 * display frame begins.
 */
@UnsupportedAppUsage
public void scheduleVsync() {
    if (mReceiverPtr == 0) {
        Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
                + "receiver has already been disposed.");
    } else {
        nativeScheduleVsync(mReceiverPtr);
    }
}

The bottom layer of the system will generate a Vsync (vertical synchronization) signal every 16.6ms to ensure stable screen refresh. After the signal is generated, the DisplayEventReceiver#onVsync method will be called back.

After receiving the onSync callback, the internal implementation class of Choreographer FrameDisplayEventReceiver will send an asynchronous message in the message queue of the UI thread and call the Choreographer#doFrame method.

 // android.view.Choreographer
 
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
        implements Runnable {
 
    // ...
 
    @Override
    public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
        // ...
        // Post the vsync event to the Handler.
        Message msg = Message.obtain(mHandler, this);
        msg.setAsynchronous(true);
        mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
    }
 
    @Override
    public void run() {
        mHavePendingVsync = false;
        doFrame(mTimestampNanos, mFrame);
    }
 
}

When the Choreographer#doFrame method is executed, it will then call the doCallbacks(Choreographer.CALLBACK_TRAVERSAL, ...) method to execute the mTraversalRunnable registered by ViewRootImpl, that is, the ViewRootImpl#doTraversal method.

 // android.view.Choreographer
 
void doFrame(long frameTimeNanos, int frame) {
    // ...
    try {
        // ...
        doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
        // ...
    } finally {
        // ...
    }
}

ViewRootImpl#doTraversal then removes the synchronization signal barrier, continues to execute the ViewRootImpl#performTraversals method, and finally calls the View#measure, View#layout, and View#draw methods to execute the drawing.

 // ViewRootImpl.java
 
void doTraversal() {
    // ...
    mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
    // ...                                                          
    performTraversals();                                                            
    // ...
}
 
private void performTraversals() {
    // ...
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    // ...
    performLayout(lp, desiredWindowWidth, desiredWindowHeight);
    // ...
    performDraw();
}

So is the UI thread consistent throughout the drawing process? Is there a situation in which the main thread (mainThread) is forcibly taken during the drawing process?

Looking at the entire drawing process, both ViewRootImpl and Choreographer use Handler objects. Let's observe how their Handlers and their Loopers come from:

First of all, the Handler in ViewRootImpl is implemented by its internal inheritance from the Handler object, and does not overload the constructor of the Handler, or explicitly pass in the Looper.

 // ViewRootImpl.java
 
final class ViewRootHandler extends Handler {
    @Override
    public String getMessageName(Message message) {
        // ...
    }
                                                                                               
    @Override
    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        // ...
    }
                                                                                               
    @Override
    public void handleMessage(Message msg) {
        // ...
    }
}
                                                                                               
final ViewRootHandler mHandler = new ViewRootHandler();

Let's look at the constructor of the Handler object. If Looper is not specified, Looper.myLooper() is used by default, and myLooper is used to obtain the looper object of the current thread from ThreadLocal.

Combined with the mThread of the ViewRootImpl object we discussed earlier is the thread where it is instantiated, we know that the mHandler thread of ViewRootImpl is the same thread as the instantiation thread.

 // andriod.os.Handler
public Handler(@Nullable Callback callback, boolean async) {
    // ...
    mLooper = Looper.myLooper();
    // ...
    mQueue = mLooper.mQueue;
    // ...
}
 
// andriod.os.Looper
/**
 * Return the Looper object associated with the current thread.  Returns
 * null if the calling thread is not associated with a Looper.
 */
public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

Let's observe which thread is the Handler thread in the mChoreographer object held inside ViewRootImpl.

The instantiation of mChoreographer is obtained through the Choreographer#getInstance method when the ViewRootImpl object is instantiated.

 // ViewRootImpl.java
 
public ViewRootImpl(Context context, Display display) {
    // ...
    mThread = Thread.currentThread();
    // ...
    mChoreographer = Choreographer.getInstance();
}

Looking at the Choreographer code, it can be seen that the getInstance method returns the current thread instance obtained through ThreadLocal;

The current thread instance also uses the current thread's looper (Looper#myLooper) instead of forcing the main thread Looper (Looper#getMainLooper).

From this, we conclude that throughout the drawing process,

From the triggering of the View#invalidate method, to the registration of the vertical synchronization signal monitor (DisplayEventReceiver#nativeScheduleVsync), and the vertical synchronization signal callback (DisplayEventReceiver#onVsync) to the View's measue/layout/draw method call, all in the same thread (UI thread), And the system does not restrict that the scene must be the main thread.
 // andriod.view.Choreographer
 
// Thread local storage for the choreographer.
private static final ThreadLocal<Choreographer> sThreadInstance =
        new ThreadLocal<Choreographer>() {
    @Override
    protected Choreographer initialValue() {
        Looper looper = Looper.myLooper();
        // ...
        Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
        if (looper == Looper.getMainLooper()) {
            mMainInstance = choreographer;
        }
        return choreographer;
    }
};
 
/**
 * Gets the choreographer for the calling thread.  Must be called from
 * a thread that already has a {@link android.os.Looper} associated with it.
 *
 * @return The choreographer for this thread.
 * @throws IllegalStateException if the thread does not have a looper.
 */
public static Choreographer getInstance() {
    return sThreadInstance.get();
}

The Android drawing process and UI thread control analyzed above can be summarized as the following figure:

图片

At this point, we can get an inference: the View displayed by the window (Window), its UI thread can be independent of the main thread of the App .

Let's verify it by coding practice.

4. Coding Verification and Practice

In fact, the drawing of screen content is never completely completed in one thread. The most common scenarios are:

  1. When the video is playing, the drawing of the video screen is not the main thread and UI thread of the App.
  2. The pop-up and other drawing of the system toast is controlled by the system level, and it is not drawn by the main thread or UI thread of the app itself.

Combined with the working case, we try to put the entire PopupWindow of the SDK on the sub-thread as a whole, that is, designate an independent UI thread for the PopupWindow of the SDK.

We use PopupWindow to implement a custom interactive Snackbar popup window. In the management class of the popup window, define and instantiate a custom UI thread and Handler;

Note that the execution of the showAtLocation method of PopupWindow will be thrown to the custom UI thread (dismiss is the same). In theory, the UI thread of the popup window will become our custom thread.

 // Snackbar弹窗管理类
public class SnackBarPopWinManager {
 
    private static SnackBarPopWinManager instance;
 
    private final Handler h; // 弹窗的UI线程Handler
 
    // ...
 
    private SnackBarPopWinManager() {
        // 弹窗的UI线程
        HandlerThread ht = new HandlerThread("snackbar-ui-thread");
        ht.start();
        h = new Handler(ht.getLooper());
    }
 
    public Handler getSnackbarWorkHandler() {
        return h;
    }
 
    public void presentPopWin(final SnackBarPopWin snackBarPopWin) {
        // UI操作抛至自定义的UI线程
        h.postDelayed(new SafeRunnable() {
            @Override
            public void safeRun() {
                // ..
                // 展示弹窗
                snackBarPopWin.getPopWin().showAtLocation(dependentView, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, y);
                // 定时自动关闭
                snackBarPopWin.dismissAfter(5000);
                // ...
        });
    }
 
    public void dismissPopWin(final SnackBarPopWin snackBarPopWin) {
        // UI操作抛至自定义的UI线程
        h.postDelayed(new SafeRunnable() {
            @Override
            public void safeRun() {
                // ...
                // dismiss弹窗
                snackBarPopWin.getPopWin().dismiss();
                // ...
        });
    }
 
    // ...
}

After that, we define the pop-up window itself, and its pop-up, disappearance and other methods are implemented through the management class.

 // Snackbar弹窗本身(通过PopupWindow实现)
public class SnackBarPopWin extends PointSnackBar implements View.OnClickListener {
 
    private PopupWindow mPopWin;
 
    public static SnackBarPopWin make(String alertText, long points, String actionId) {
        SnackBarPopWin instance = new SnackBarPopWin();
        init(instance, alertText, actionId, points);
        return instance;
    }
 
    private SnackBarPopWin() {
        // infalte等耗时操作
        // ...
        View popView = LayoutInflater.from(context).inflate(R.layout.popwin_layout, null);
        // ...
        mPopWin = new PopupWindow(popView, ...);
        // ...
    }
 
    // 用户的UI操作,回调应该也在UI线程
    public void onClick(View v) {
        int id = v.getId();
        if (id == R.id.tv_popwin_action_btn) {
            onAction();
        } else if (id == R.id.btn_popwin_cross) {
            onClose();
        }
    }
 
    public void show(int delay) {
        // ...
        SnackBarPopWinManager.getInstance().presentPopWin(SnackBarPopWin.this);
    }
 
    public void dismissAfter(long delay) {
        // ...
        SnackBarPopWinManager.getInstance().dismissPopWin(SnackBarPopWin.this);
    }
 
    // ...
 
}

At this point, we instantiate the popup window in the child thread, and after 2s, we also change the TextView content in the child thread.

 // MainActivity.java
 
public void snackBarSubShowSubMod(View view) {
 
    WorkThreadHandler.getInstance().post(new SafeRunnable() {
        @Override
        public void safeRun() {
            String htmlMsg = "已读新闻<font color=#ff1e02>5</font>篇,剩余<font color=#00af57>10</font>次,延迟0.3s";
            final PointSnackBar snackbar = PointSnackBar.make(htmlMsg, 20, "");
            if (null != snackbar) {
                snackbar.snackBarBackgroundColor(mToastColor)
                        .buttonBackgroundColor(mButtonColor)
                        .callback(new PointSnackBar.Callback() {
                    @Override
                    public void onActionClick() {
                        snackbar.onCollectSuccess();
                    }
                }).show();
            }
 
            // 在自定义UI线程中更新视图
            SnackBarPopWinManager.getInstance().getSnackbarWorkHandler().postDelayed(new SafeRunnable() {
                @Override
                public void safeRun() {
                    try {
                        snackbar.alertText("恭喜完成<font color='#ff00ff'>“UI更新”</font>任务,请领取积分");
                    } catch (Exception e) {
                        DemoLogUtils.e(TAG, "error: ", e);
                    }
                }
            }, 2000);
        }
    });
}

The display effect, the UI displays the interaction normally, and because the UI is drawn in different threads, it will not affect the operation and dynamic effects of the main thread of the App:

The response thread that observes the click event is the custom UI thread, not the main thread:

图片

(Note: The code in practice is not actually online. The UI thread of PopupWindow in the online version of the SDK is still the same as the App, using the main thread).

V. Summary

A deeper understanding of the inability of Android sub-threads to operate UI: the thread that controls View drawing and the thread that notifies View update must be the same thread, that is, the UI thread is the same.

For scenarios such as pop-up windows that are relatively independent from other services of the app, you can consider multi-UI thread optimization.

In the follow-up work, clearly distinguish the concepts of UI thread, main thread, and sub-thread, and try not to mix them.

Of course, there are also some inapplicable scenarios for multiple UI threads, such as the following logic:

  1. All method calls of Webview must be on the main thread, because the main thread verification is enforced in its code. For example, the built-in Webview in PopupWindow does not apply to multiple UI threads.
  2. The use of Activity must be in the main thread, because the Handler used in its creation and other operations is also forced to be designated as mainThreadHandler.

refer to:

  1. Android screen refresh mechanism
  2. Why Android has to update UI on main thread

vivo互联网技术
3.3k 声望10.2k 粉丝