The little Toast contains the principle (to solve the problem that the native Toast does not display when the notification is closed)

table of Contents

1. Toast member variables

2. Toast display process

1. Toast makeText(@NonNull Context context, @Nullable Looper looper,@NonNull CharSequence text, @Duration int duration) 

2.show()

3.NotificationManagerService

(1)public void enqueueToast(String pkg, ITransientNotification callback, int duration,int displayId)

(2)void showNextToastLocked()

(3)void scheduleDurationReachedLocked(ToastRecord r)

(4)handleDurationReached((ToastRecord) msg.obj)

(5 )void cancelToastLocked(int index)

Four, Toast cancellation process

Five, the problems of native Toast

1. Repeat to create Toast

2. The infinite loop of native Toast display

3. Native Toast is system-level Toast


A problem occurred in the recent project, that is, some mobile phones are turning off system notifications. As a result, the native Toast used in the project is not displayed on some mobile phones. Then I checked the system source code and found that the original native Toast was implemented based on NotificaitionManagerService. , No wonder some phones do not display. Those mobile phone manufacturers that showed up should have discovered this problem and modified the source code in the system. Specially record this process, and attach the source code that can solve this problem for your reference.

Usually when we use Toast, the following simple line of code can solve the problem, and it can be displayed in a Toast. From the point of view of source code, look at how this Toast is displayed step by step.

Toast.makeText(mContext, "原生Toast",Toast.LENGTH_SHORT).show();

The Toast class itself is just a tool class without any inheritance. To manage the addition/removal of Toast through an internal class TN, it is mainly to add/remove Toast View on WindowManager. NotificationManagerService controls when to add View to WindowManager and when to remove View from WindowManager.

public class Toast {
 
     private static class TN extends ITransientNotification.Stub {
    }
}

1. Toast member variables

The TN internal class is used to maintain a WindowManager to add and remove Views on Toast;

mNextView is the View of Toast to be displayed by Toast. If setView() is called, the View will be assigned to mNextView.

    //维护着一个WindowManager来添加/移除Toast的View
    final TN mTN;
    @UnsupportedAppUsage
    //延时时间
    int mDuration;
    //调用Toast的时候需要显示的View,初始化的时候就是默认的UI,用户也可以调用setView来重新设置这个View
    View mNextView;

2. Toast display process

1. Toast makeText(@NonNull Context context, @Nullable Looper looper,@NonNull CharSequence text, @Duration int duration) 

The main purpose is to initialize the View of Toast, the default is a TextView, and then assign mDuration and mNextView to the internal management class TN.

    /**
     * Make a standard toast to display using the specified looper.
     * If looper is null, Looper.myLooper() is used.
     * @hide
     */
    public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
            @NonNull CharSequence text, @Duration int duration) {
        Toast result = new Toast(context, looper);

        LayoutInflater inflate = (LayoutInflater)
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
        TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
        tv.setText(text);

        result.mNextView = v;
        result.mDuration = duration;

        return result;
    }

     The internal class TN itself inherits from ITransientNotification.Stub, and manages the display/hide of Toast's View. At the same time, NotificationServiceManager can access the process where Toast is located across processes.

  private static class TN extends ITransientNotification.Stub {

  }

 There are actually two methods in the ITransientNotification.aidl file, show()/hide(), and the inner class TN implements Toast's show()/hide().

/** @hide */
oneway interface ITransientNotification {
    void show();
    void hide();
}

 In these two methods, SHOW and HIDE messages are sent to the Handler.

        /**
         * schedule handleShow into the right thread
         */
        @Override
        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
        public void show(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
        }

        /**
         * schedule handleHide into the right thread
         */
        @Override
        public void hide() {
            if (localLOGV) Log.v(TAG, "HIDE: " + this);
            mHandler.obtainMessage(HIDE).sendToTarget();
        }

As you can see from the TN construction method, we can pass in the Looper object of a thread, and we can display the Toast in the current thread. In other words, Toast can be displayed in the thread, but a Looper object of the current child thread must be passed in.

          if (looper == null) {
                // Use Looper.myLooper() if looper is not specified.
                looper = Looper.myLooper();
                if (looper == null) {
                    throw new RuntimeException(
                            "Can't toast on a thread that has not called Looper.prepare()");
                }
            }
           

In addition, note that when displaying Toast in the child thread, pay attention to the show() method of Toast before Looper.loop(), otherwise the Handler in Toast cannot loop the messages in the queue, and the Toast is unable to display.

        new Thread() {
            @Override
            public void run() {
                super.run();
                Looper.prepare();
                Toast.makeText(ToastActivity.this,"22",Toast.LENGTH_SHORT).show();
                Looper.loop();
            }
        }.start();

The Handler in the inner class TN is to process different messages. Finally, the sending of these messages is maintained in the NotificationManagerService.

mHandler = new Handler(looper, null) {
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                        case SHOW: {
                            IBinder token = (IBinder) msg.obj;
                            handleShow(token);
                            break;
                        }
                        case HIDE: {
                            handleHide();
                            // Don't do this in handleHide() because it is also invoked by
                            // handleShow()
                            mNextView = null;
                            break;
                        }
                        case CANCEL: {
                            handleHide();
                            // Don't do this in handleHide() because it is also invoked by
                            // handleShow()
                            mNextView = null;
                            try {
                                getService().cancelToast(mPackageName, TN.this);
                            } catch (RemoteException e) {
                            }
                            break;
                        }
                    }
                }
            };
        }

You can also see from the source code that handleShow()/handleHide() is to add/remove View in Toast from WindowManager. In the handleShow() method, a logical judgment of if (mView != mNextView) is mainly mentioned. This mNextView is set by the developer after calling setView(). If the developer does not actively call this method, then it is directly calling makeText () Instantiate Toast's layout View.

  public void handleShow(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                    + " mNextView=" + mNextView);
            // If a cancel/hide is pending - no need to show - at this point
            // the window token is already invalid and no need to do any work.
            if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
                return;
            }
            if (mView != mNextView) {
                // remove the old view if necessary
                handleHide();
                mView = mNextView;
                Context context = mView.getContext().getApplicationContext();
                String packageName = mView.getContext().getOpPackageName();
                if (context == null) {
                    context = mView.getContext();
                }
                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                // 省略一些代码。。。。主要就是设置mWM中的一些参数
                //。。。。。。。。。
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeView(mView);
                }
                //将mView添加到WindowManager上面
                try {
                    mWM.addView(mView, mParams);
                    trySendAccessibilityEvent();
                } catch (WindowManager.BadTokenException e) {
                    /* ignore */
                }
            }
        }

In handleHide(), mView is removed and left blank. So mNextView is the View to be displayed, and then use mView to save this View and display it.

 @UnsupportedAppUsage
        public void handleHide() {
            if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
            if (mView != null) {
                // note: checking parent() just to make sure the view has
                // been added...  i have seen cases where we get here when
                // the view isn't yet added, so let's try not to crash.
                //将mView从WindowManager移除
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeViewImmediate(mView);
                }
                // Now that we've removed the view it's safe for the server to release
                // the resources.
                try {
                    getService().finishToken(mPackageName, this);
                } catch (RemoteException e) {
                }

                mView = null;
            }
        }

2.show()

 The first makeText() method is to instantiate the Toast object, then show() is to display the Toast. We can see from the source code that this is mainly through the NotificationManagerService to manage the added Toast queue

public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }

        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;
        final int displayId = mContext.getDisplayId();

        try {
            service.enqueueToast(pkg, tn, mDuration, displayId);
        } catch (RemoteException e) {
            // Empty
        }
    }

We can see from the source code that Toast has been displayed through NotificationManagerService at this time.

3.NotificationManagerService

(1)public void enqueueToast(String pkg, ITransientNotification callback, int duration,int displayId)

The main logic of this method is: First, it will judge the added Toast, if it has been added to the mToastQueue before, then directly update the duration of the Toast (this situation occurs when the Toast is instantiated first, and then passed When the instance calls show() in different places, the Toast passed in at this time will follow the logic), otherwise the Toast will be added to the mToastQueue.

When adding a Toast to mToastQueue, it will also determine whether it is a system Toast. If it is not a system Toast, then up to 25 Toasts can be added at the same time under the corresponding application.

Finally, it is judged whether the Toast in the mToastQueue collection is the first one, and the Toast will only be displayed when it is the first element in the collection.

 @Override
        public void enqueueToast(String pkg, ITransientNotification callback, int duration,
                int displayId)
        {

            if (pkg == null || callback == null) {
                Slog.e(TAG, "Not enqueuing toast. pkg=" + pkg + " callback=" + callback);
                return ;
            }

           //。。。。省略部分非重要逻辑。。。。
           //mToastQueue维护着加入到队列中的所有Toast的集合
            synchronized (mToastQueue) {
                int callingPid = Binder.getCallingPid();
                long callingId = Binder.clearCallingIdentity();
                try {
                    // ToastRecord 是对Toast的封装、含有Toast的所在的包名、延时时间等
                    ToastRecord record;
                    //找到该Toast对应的索引值
                    int index = indexOfToastLocked(pkg, callback);
                    // If it's already in the queue, we update it in place, we don't
                    // move it to the end of the queue.
                    //(1)如果该Toast已经存在在队列中,则只更新Toast显示的时间
                    if (index >= 0) {
                        record = mToastQueue.get(index);
                        record.update(duration);
                    } else {
                    //(2)说明该Toast没有被加入到队列中,后面的逻辑就是将Toast加入到Toast队列 
                    中,并显示第一个Toast
                    //(2.1)如果不是系统的Toast,那么每个应用下只能加入MAX_PACKAGE_NOTIFICATIONS个Toast,超过这个数量之后则不在显示
                        if (!isSystemToast) {
                            int count = 0;
                            final int N = mToastQueue.size();
                            for (int i=0; i<N; i++) {
                                 final ToastRecord r = mToastQueue.get(i);
                                 if (r.pkg.equals(pkg)) {
                                     count++;
                                     if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                                         Slog.e(TAG, "Package has already posted " + count
                                                + " toasts. Not showing more. Package=" + pkg);
                                         return;
                                     }
                                 }
                            }
                        }

                        (2.2)把符合条件的将Toast封装成ToastRecord,并且加入到mToastQueue中
                        Binder token = new Binder();
                        mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, displayId);
                        record = new ToastRecord(callingPid, pkg, callback, duration, token,
                                displayId);
                        mToastQueue.add(record);
                        //(2.3)把当前的索引值index指向刚加入的这个Toast的位置
                        index = mToastQueue.size() - 1;
                        keepProcessAliveIfNeededLocked(callingPid);
                    }
                    // If it's at index 0, it's the current toast.  It doesn't matter if it's
                    // new or just been updated.  Call back and tell it to show itself.
                    // If the callback fails, this will remove it from the list, so don't
                    // assume that it's valid after this.
                    //(3)如果刚加入的这个Toast恰好是该队列中的第一个,则将该Toast显示出来
                    if (index == 0) {
                        showNextToastLocked();
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
        }

Now there is a problem, if I call the Toast many times when the current Toast is not displayed.

Toast.makeText(mContext, "原生Toast",Toast.LENGTH_SHORT).show();

So there are already multiple Toasts in the mToastQueue collection at this time, so how can other Toasts be displayed? With this question, look at the code below.

(2)void showNextToastLocked()

From the above method, we can see that if there is a Toast in mToastQueue at this time, this method will be called to display the Toast.

 void showNextToastLocked() {
       //(1)取出该Toast封装成的对象ToastRecord
        ToastRecord record = mToastQueue.get(0);
        //这里使用的是一个无限循环,我觉得是有点浪费资源的,不知道源码在写的时候采用这种方式有什么好处,所以在自定义Toast的时候,已经将该逻辑改掉了。
        while (record != null) {
            if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
            try {
                //(2)调用该Toast的内部类TN中的show()显示该Toast
                record.callback.show(record.token);
                //(3)这个就是向NotificationManagerService中维护的Handler中发送duration消息来隐藏 
                 该Toast
                scheduleDurationReachedLocked(record);
                return;
            } catch (RemoteException e) {
                Slog.w(TAG, "Object died trying to show notification " + record.callback
                        + " in package " + record.pkg);
                // remove it from the list and let the process die
                int index = mToastQueue.indexOf(record);
                if (index >= 0) {
                    mToastQueue.remove(index);
                }
                keepProcessAliveIfNeededLocked(record.pid);
                if (mToastQueue.size() > 0) {
                    record = mToastQueue.get(0);
                } else {
                    record = null;
                }
            }
        }
    }

(3)void scheduleDurationReachedLocked(ToastRecord r)

The code logic of this method is very simple, that is, according to Toast.LENGTH_LONG or Toast.LENGTH_SHORT to get the corresponding delay time and send it to the Handler object to hide the Toast

    private void scheduleDurationReachedLocked(ToastRecord r)
    {
        mHandler.removeCallbacksAndMessages(r);
        Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r);
        //(1)根据Toast.LENGTH_LONG还是Toast.LENGTH_SHORT来获得对应的延时时间
        int delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
        // Accessibility users may need longer timeout duration. This api compares original delay
        // with user's preference and return longer one. It returns original delay if there's no
        // preference.
        delay = mAccessibilityManager.getRecommendedTimeoutMillis(delay,
                AccessibilityManager.FLAG_CONTENT_TEXT);
        //(2)发送延时消息来来隐藏Toast
        mHandler.sendMessageDelayed(m, delay);
    }

Then look at the corresponding content in the Handler

(4)handleDurationReached((ToastRecord) msg.obj)

The logic here is to find the index value of the corresponding Toast, and then call the cancelToastLocked(index) method to hide the Toast.

    private void handleDurationReached(ToastRecord record)
    {
        if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback);
        synchronized (mToastQueue) {
            int index = indexOfToastLocked(record.pkg, record.callback);
            if (index >= 0) {
                cancelToastLocked(index);
            }
        }
    }

(5 )void cancelToastLocked(int index)

The main thing is to find the corresponding ToastRecord and call back to the hide() method in the Toast inner class to hide the Toast. Then remove the Toast from the mToastQueue queue, remove the message corresponding to the Toast from the Handler, and finally determine whether there are undisplayed Toasts in the mToastQueue collection. If there are still, repeat the second method and put them in the collection in turn The 0th Toast is displayed. This also answers the question in the first method enqueueToast(), and completes the logic of displaying all Toasts in the mToastQueue collection in turn.

 void cancelToastLocked(int index) {
        ToastRecord record = mToastQueue.get(index);
        //(1)找到对应的ToastRecord,然后调用Toast中的hide()
        try {
            record.callback.hide();
        } catch (RemoteException e) {
            Slog.w(TAG, "Object died trying to hide notification " + record.callback
                    + " in package " + record.pkg);
            // don't worry about this, we're about to remove it from
            // the list anyway
        }
        //(2)将该Toast从队列中移除
        ToastRecord lastToast = mToastQueue.remove(index);
        mWindowManagerInternal.removeWindowToken(lastToast.token, false /* removeWindows */,
                lastToast.displayId);
        // We passed 'false' for 'removeWindows' so that the client has time to stop
        // rendering (as hide above is a one-way message), otherwise we could crash
        // a client which was actively using a surface made from the token. However
        // we need to schedule a timeout to make sure the token is eventually killed
        // one way or another.
        //(3)将该Toast对应的消息从Handler中移除
        scheduleKillTokenTimeout(lastToast);

        keepProcessAliveIfNeededLocked(record.pid);
        //(4)如果集合中仍有Toast还没有显示完,那么就在重复第2个方法进行依次显示集合中的第0个元素
        if (mToastQueue.size() > 0) {
            // Show the next one. If the callback fails, this will remove
            // it from the list, so don't assume that the list hasn't changed
            // after this point.
            showNextToastLocked();
        }
    }

Four, Toast cancellation process

If we are using Toast, we need to cancel the Toast before it is displayed. At this time, we need to call cancel(). In fact, we will eventually call the cancel() method of the internal management class TN.

    /**
     * Close the view if it's showing, or don't show it if it isn't showing yet.
     * You do not normally have to call this.  Normally view will disappear on its own
     * after the appropriate duration.
     */
    public void cancel() {
        mTN.cancel();
    }

The cancel() method in TN ultimately sends a CANCEL message to the Handler, specifically to the canelToast() in the NotificationManagerService.

                    handleHide();
                    // Don't do this in handleHide() because it is also invoked by
                     // handleShow()
                    mNextView = null;
                     try {
                          getService().cancelToast(mPackageName, TN.this);
                     } catch (RemoteException e) {
                       }

It can be seen from NotificationManagerService that the logic of hiding Toast after the delay is consistent with the previous Toast, but the previous one is to call the cancelToastLocked(index) method after the delay time is up, and here is to call the cancel() method When, call the cancelToastLocked(index) method immediately.

  @Override
        public void cancelToast(String pkg, ITransientNotification callback) {
            Slog.i(TAG, "cancelToast pkg=" + pkg + " callback=" + callback);

            if (pkg == null || callback == null) {
                Slog.e(TAG, "Not cancelling notification. pkg=" + pkg + " callback=" + callback);
                return ;
            }

            synchronized (mToastQueue) {
                long callingId = Binder.clearCallingIdentity();
                try {
                    int index = indexOfToastLocked(pkg, callback);
                    if (index >= 0) {
                        cancelToastLocked(index);
                    } else {
                        Slog.w(TAG, "Toast already cancelled. pkg=" + pkg
                                + " callback=" + callback);
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
        }

Five, the problems of native Toast

1. Repeat to create Toast

Usually when we use Toast, we will directly call the following line of code to display Toast. From the display process we analyzed in the second part, we can see:

Toast.makeText(mContext, "原生Toast",Toast.LENGTH_SHORT).show();

Each time this line of code is called, a Toast will be instantiated and then added to the mToastQueue queue of the NotificationServiceManager. If this line of code happens to be called when the button is clicked, it is easy to call this line of code multiple times, causing repeated Toast creation. Sometimes in a project, in order to avoid repeated creation of Toasts, a Toast instance is usually created, and this Toast instance is called globally, for example:

private static Toast toast;
public static void showToast(Context context, String content) {
    if (toast == null) {
        toast = Toast.makeText(context, content, Toast.LENGTH_SHORT);
    } else {
        toast.setText(content);
    }
    toast.show();
}

The above line of code actually has a memory leak problem. For example, when the Toast is displayed, it will hold the Activity object. When it has not disappeared, closing the Activity will cause the Activity object to be unable to be recycled, causing memory leaks in the Activity. So in response to this problem, improvements have been made in the custom Toast. As you can see from the source code, every time it is displayed, the 0th element in mToastQueue is actually taken to display, and the element is not deleted from the collection until the display is complete. Then we can completely add the Toast before To determine whether the text content displayed by the Toast is consistent with the text content of the current Toast, if they are consistent, you can not add it to the mToastQueue queue first. The custom code ( code path link   GitHub address is https://github.com/wenjing-bonnie/toast.git ) has been optimized for this point. Specifically in PowerfulToastManagerService:

    protected void enqueueToast(PowerfulToast toast, String content, ITransientPowerfulToast callBack, int duration) {
            //。。。。。。省略其他代码
            //如果与正在显示的Toast的内容一致,则不将该Toast加入到Toast队列中;
            //(1)恰好该workHandler的延时MESSAGE_DURATION_REACHED到了在执行remove操作的时候,此时为null,会向下加入这个Toast
            //(2)只要这个Toast没有显示完,则取出来的值不为空,则不会加入到显示mToastQueue队列中
            if (mToastQueue != null && !mToastQueue.isEmpty()) {
                PowerfulToastRecord curRecord = mToastQueue.get(0);
                if (curRecord != null && content.equals(curRecord.content)) {
                    return;
                }
            }
            //将新增的toast加入到队列中
            record = new PowerfulToastRecord(toast, content, callBack, duration);
            mToastQueue.add(record);
         //。。。。。。省略其他代码
}

2. The infinite loop of native Toast display

When the native Toast is displayed, the 0th element of the mToastQueue is taken out here, and then displayed until it disappears. This loop has been executed until the Toast is displayed. The loop always exists. In fact, why is it not directly here? Is it enough to use an if(record!=null) to make a judgment? There is no reason why this source code is used in this way. So when customizing Toast, the logic has been changed, and if (record!=null) is used directly to judge.

    void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);
        //这块为什么会要用一个死循环的方式呢?
        while (record != null) {
            
        }
     }

3. Native Toast is system-level Toast

When the native Toast is displayed, when the WindowManager.LayoutParams is set, the following type is used, but for the custom Toast,

   params.type = WindowManager.LayoutParams.TYPE_TOAST;

The types of WindowManager are divided into application Window, child Window, and system Window. For an Activity corresponding to the application Window, the child Window cannot exist alone, and must be attached to the parent Window, and when the system Window is in use, the permissions must be declared.

Therefore, we cannot use this type in the custom Toast, because the notification permission is set to TYPE_TOAST after the notification permission is closed, and the exception of android.view.WindowManager$BadTokenException will be thrown. And the type of system Window, when used, will prompt the user to give the corresponding permissions, so the user experience is very poor, so only the application Window can be used, then there will be another problem when using the application Window type, if When the Toast does not disappear, when the Activity is closed, android.view.WindowLeaked: Activity will be thrown. So in order to avoid this situation, we monitor the life cycle of the Activity and cancel all Toasts in the mToastQueue when the Activity is closed. Therefore, when using a custom Toast, you need to register the Toast first.

public class PowerfulToastManagerService implements Application.ActivityLifecycleCallbacks {   
 /**
     * 将application传入用来管理Activity的生命周期
     *
     * @param application
     */
    protected void registerToast(Application application) {
        application.registerActivityLifecycleCallbacks(this);
    }
 /**
     * {@link android.app.Application.ActivityLifecycleCallbacks}
     */

    @Override
    public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
    }

    @Override
    public void onActivityStarted(@NonNull Activity activity) {
    }

    @Override
    public void onActivityResumed(@NonNull Activity activity) {
    }

    @Override
    public void onActivityPaused(@NonNull Activity activity) {
        Log.logV(TAG, activity.getClass().getSimpleName() + " , is paused ! " + " , size is " + mToastQueue.size());
        cancelAllPowerfulToast();
    }

    @Override
    public void onActivityStopped(@NonNull Activity activity) {
    }

    @Override
    public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
    }

    @Override
    public void onActivityDestroyed(@NonNull Activity activity) {

    }
}

 

 

 

 

Guess you like

Origin blog.csdn.net/nihaomabmt/article/details/108104146