Have a look at Toast’s source code today, or if you are interested in AIDL, read on.

Multiple Toasts will not be displayed at the same time, as if all toasts are queued and different apps are in a queue. Yeah, there’s a queue. And because different apps use this same queue, AIDL cross-process communication is used here.

There is a concept of C/S across processes, which is the concept of a server and a client. The client calls the service of the server, and the server returns the results to the client.

There are two processes for displaying a Toast:

  • 1. Wrap up a toast and go to the queue.

    In this process, the queue is the server and toast.show() is the client.Copy the code
  • 2. If the Toast is displayed in the queue, the system calls the Toast to display the Toast itself.

    In this process, the queue is the client and toast.show() is the server.Copy the code

To display a Toast, the code looks like this:

Toast.makeText(context, text, duration).show();
Copy the code

Take a look at the makeText() method, which is just a preparation process, loading the layout, then setting up what to display, and how long to display. The result is still to return a Toast object.

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;
    }
Copy the code

Toast result = new Toast(context, looper); Initializing a TN object mTN:

public Toast(@NonNull Context context, @Nullable Looper looper) {
        mContext = context;
        mTN = new TN(context.getPackageName(), looper);
        mTN.mY = context.getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.toast_y_offset);
        mTN.mGravity = context.getResources().getInteger(
                com.android.internal.R.integer.config_toastDefaultGravity);
    }
Copy the code

So I’m going to set the vertical coordinates and the gravity direction in mTN. The next call to show() shows:

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

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

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

Here getService() fetches the service and calls service.enqueuetoast (PKG, TN, mDuration); Go to line up.

Here getService() actually gets a local proxy for a Service:

static private INotificationManager getService() {
        if(sService ! = null) {return sService;
        }
        sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
        return sService;
    }
Copy the code

***.stub. asInterface is a typical form of AIDL.

Here’s an AIDL reference

I created my own AIDL and defined two methods.

// IMyTest.aidl
package top.greendami.aidl;

// Declare any non-default types here with import statements

interface IMyTest {
    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    int add(int a, int b);

    String hello(String s);
}

Copy the code

The following code is automatically generated by AS under the build folder:

package top.greendami.aidl;

public interface IMyTest extends android.os.IInterface {
    public static abstract class Stub extends android.os.Binder implements top.greendami.aidl.IMyTest {
        private static final java.lang.String DESCRIPTOR = "top.greendami.aidl.IMyTest";

        /**
         * Construct the stub at attach it to the interface.
         */
        public Stub() {
            this.attachInterface(this, DESCRIPTOR);
        }

        /**
         * Cast an IBinder object into an top.greendami.aidl.IMyTest interface,
         * generating a proxy ifMen. * / public static top. Greendami. Aidl. IMyTest asInterface (android. OS. IBinder obj) {...} @ Override public android.os.IBinderasBinder() {
            returnthis; } @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, Int flags) throws android. OS. RemoteException {...} private static class Proxy implements top. Greendami. Aidl. IMyTest { ···} static final int TRANSACTION_add = (Android.os.ibinder.first_call_transaction + 0); static final int TRANSACTION_hello = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1); } public int add(int a, int b) throws android.os.RemoteException; public java.lang.String hello(java.lang.String s) throws android.os.RemoteException; }Copy the code

Let’s look at the asInterface method used above:

public static top.greendami.aidl.IMyTest asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if(((iin ! = null) && (iin instanceof top.greendami.aidl.IMyTest))) {return ((top.greendami.aidl.IMyTest) iin);
            }
            return new top.greendami.aidl.IMyTest.Stub.Proxy(obj);
        }

Copy the code

In this case, the local object is first searched to see if there is any object, if there is no cross-process, directly return the local object. If not, return a proxy. This can be seen as the server preparing a ‘fake’ version of itself for the client, making it look like the client has a real server object.

Proxy(obj) handles every method in the Proxy, if there are two methods add and Hello:

private static class Proxy implements top.greendami.aidl.IMyTest {
            private android.os.IBinder mRemote;

            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }

            @Override
            public android.os.IBinder asBinder(a) {
                return mRemote;
            }

            public java.lang.String getInterfaceDescriptor(a) {
                return DESCRIPTOR;
            }

            @Override
            public int add(int a, int b) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                int _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeInt(a);
                    _data.writeInt(b);
                    mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.readInt();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }

            @Override
            public java.lang.String hello(java.lang.String s) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                java.lang.String _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeString(s);
                    mRemote.transact(Stub.TRANSACTION_hello, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.readString();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return_result; }}static final int TRANSACTION_add = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        static final int TRANSACTION_hello = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
    }
Copy the code

It basically serializes the arguments that need to be passed, then calls the actual implementation of the method, then gets the result returned and returns. We see the real method implementation in mremote.transact, where the Proxy is really just the Proxy, interacting with the client and passing parameters.

The real call is implemented in the onTransact method:

public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
            switch (code) {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(DESCRIPTOR);
                    return true;
                }
                case TRANSACTION_add: {
                    data.enforceInterface(DESCRIPTOR);
                    int _arg0;
                    _arg0 = data.readInt();
                    int _arg1;
                    _arg1 = data.readInt();
                    int _result = this.add(_arg0, _arg1);
                    reply.writeNoException();
                    reply.writeInt(_result);
                    return true;
                }
                case TRANSACTION_hello: {
                    data.enforceInterface(DESCRIPTOR);
                    java.lang.String _arg0;
                    _arg0 = data.readString();
                    java.lang.String _result = this.hello(_arg0);
                    reply.writeNoException();
                    reply.writeString(_result);
                    return true; }}return super.onTransact(code, data, reply, flags);
        }
Copy the code

This. add(_arg0, _arg1); And this. Hello (_arg0); That’s the interface defined in AIDL. Because of the abstract class implements in the Stub top. Greendami. Aidl. IMyTest interface (of course it is also a Bind), at the top. Greendami. Aidl. IMyTest interface declared in the add and hello this two methods, So these two methods are implemented when the Stub class is implemented.

In general, a Service’s onBind method returns a Stub object, which implements both methods when new. Then the server is ready.

BindService (Intent, mServiceConnection, context.bind_auto_create); There’s an mServiceConnection callback, and in the callback public void onServiceConnected(ComponentName name, IBinder Service) can get the service object, Using ***.stub.asInterface (service) to get the proxy object, the method can then be called.

Back to Toast

sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
Copy the code

By doing so, we get the ‘queued’ service and call service.enqueuetoast (PKG, TN, mDuration); I went to wait in line. Here tn is a type of TN that holds the toast-related information, including View, display duration, and so on. At the same time TN is also a ITransientNotification. Stub implementation. This is where the second step is invoked as a server (callback like function).

See NotificationManagerService. In Java enqueueToast () method:

public void enqueueToast(String pkg, ITransientNotification callback, int duration)
        {...final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg));
            final boolean isPackageSuspended =
                    isPackageSuspendedForUser(pkg, Binder.getCallingUid());

            if(ENABLE_BLOCKED_TOASTS && ! isSystemToast && (! areNotificationsEnabledForPackage(pkg, Binder.getCallingUid()) || isPackageSuspended)) { ...return;
            }

            synchronized (mToastQueue) {
                int callingPid = Binder.getCallingPid();
                long callingId = Binder.clearCallingIdentity();
                try {
                    ToastRecord record;
                    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.
                    if (index >= 0) {
                        record = mToastQueue.get(index);
                        record.update(duration);
                    } else {
                        // Limit the number of toasts that any given package except the android
                        // package can enqueue. Prevents DOS attacks and deals with leaks.
                        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;
                                     }
                                 }
                            }
                        }

                        Binder token = new Binder();
                        mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
                        record = new ToastRecord(callingPid, pkg, callback, duration, token);
                        mToastQueue.add(record);
                        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.
                    if (index == 0) { showNextToastLocked(); }}finally{ Binder.restoreCallingIdentity(callingId); }}}Copy the code

This method first verifies that the package name and callback are null and returns if they are (code omitted). Then check to see if the App is prohibited from showing notifications by the package name.

Synchronized (mToastQueue), first get the process number, then check whether the same App and callback are in the queue, if yes, update the display time, if not, then check how many toasts this App has. No lines over 50. If all of these conditions are met, the queue is entered.

            if (index == 0) {
                showNextToastLocked();
            }
Copy the code

If there is only one member in the queue, it is displayed immediately (if not, the queue is already looping), and look at showNextToastLocked() :

void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);
        while(record ! = null) {if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
            try {
                record.callback.show(record.token);
                scheduleTimeoutLocked(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; }}}}Copy the code

Record.callback. show(record.token); This handler is used to enable a delay that cancels scheduleTimeoutLocked(record). . Callback (TN) {record. Callback (TN);

private static class TN extends ITransientNotification.Stub {
         ...
        static final long SHORT_DURATION_TIMEOUT = 4000;
        static final long LONG_DURATION_TIMEOUT = 7000;

        TN(String packageName, @Nullable Looper looper) {
            ...
            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();
                            mNextView = null;
                            break;
                        }
                        case CANCEL: {
                            handleHide();
                            mNextView = null;
                            try {
                                getService().cancelToast(mPackageName, TN.this);
                            } catch (RemoteException e) {
                            }
                            break; }}}}; } /** * schedule handleShow into the right thread */ @Override public void show(IBinder windowToken) { mHandler.obtainMessage(SHOW, windowToken).sendToTarget(); } /** * schedule handleHide into the right thread */ @Override public voidhide() {
            if (localLOGV) Log.v(TAG, "HIDE: " + this);
            mHandler.obtainMessage(HIDE).sendToTarget();
        }

        public void cancel() {
            if (localLOGV) Log.v(TAG, "CANCEL: " + this);
            mHandler.obtainMessage(CANCEL).sendToTarget();
        }

        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 viewif 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);
                final Configuration config = mView.getContext().getResources().getConfiguration();
                final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
                mParams.gravity = gravity;
                if((gravity & gravity.HORIZONTAL_GRAVITY_MASK) == gravity.FILL_HORIZONTAL) {mparams.horizontalweight = 1.0f; }if((gravity & gravity.VERTICAL_GRAVITY_MASK) == gravity.FILL_VERTICAL) {mparams.verticalweight = 1.0f; } mParams.x = mX; mParams.y = mY; mParams.verticalMargin = mVerticalMargin; mParams.horizontalMargin = mHorizontalMargin; mParams.packageName = packageName; mParams.hideTimeoutMilliseconds = mDuration == Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT; mParams.token = windowToken;if(mView.getParent() ! = null) {if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeView(mView);
                }
                try {
                    mWM.addView(mView, mParams);
                    trySendAccessibilityEvent();
                } catch (WindowManager.BadTokenException e) {
                    /* ignore */
                }
            }
        }

            ...

        public void handleHide() {
            if(mView ! = null) { // note: checking parent() just to make sure the view has // been added... i have seen caseswhere we get here when
                // the view isn't yet added, so let's try not to crash.
                if(mView.getParent() ! = null) {if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in "+ this); mWM.removeViewImmediate(mView); } mView = null; }}}Copy the code

So here’s a simple call to the Handler. Display it through addView.

Toast removal process: scheduleTimeoutLocked sends the message to Handler.

private void scheduleTimeoutLocked(ToastRecord r) { mHandler.removeCallbacksAndMessages(r); Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r); long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY; mHandler.sendMessageDelayed(m, delay); } private final class extends Handler {@override public void handleMessage(Message MSG) { switch (msg.what) {case MESSAGE_TIMEOUT:
                    handleTimeout((ToastRecord)msg.obj);
                    break;
            }
        }
    }
private void handleTimeout(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);
            }
        }
    }
    
private void cancelToastLocked(int index) {
        ToastRecord record = mToastQueue.get(index);
        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
        }
        mToastQueue.remove(index);
        keepProcessAliveLocked(record.pid);
        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(); }}Copy the code

Hey, guys, you see this. You want a collection? (Reply 666, Easter eggs!)