Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android源碼解析——Toast

Android源碼解析——Toast

編輯:關於Android編程

簡介

Toast是一種向用戶快速提供少量信息的視圖。當它顯示時,它會浮在整個應用層的上面,並且不會獲取到焦點。它的設計思想是能夠向用戶展示些信息,但又能盡量不顯得唐突。本篇我們來研讀一下Toast的源碼,並探明它的顯示及隱藏機制。

源碼解析

Toast

我們從Toast的最簡單調用開始,它的調用代碼是:

Toast.makeText(context,"Show toast",Toast.LENGTH_LONG).show();

在上面的代碼中,我們是先調用Toast的靜態方法來創建一個Toast,然後調用其show()方法將其顯示出來。其中makeText的源碼如下:

    public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
        Toast result = new Toast(context);

        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;
    }

可以看到只是new出一個Toast,並設置要顯示的View及時間。這裡默認使用的Toast布局transient_notification.xml也在SDK,代碼如下:



    

它只是一個簡單的LinearLayout套一個TextView。順便說一句,從布局上我們可以知道這個Toast的背景顏色是可以配置的,通過在theme中配置toastFrameBackground屬性。

回到Toast,我們來看一下它的屬性定義及構造方法。

public class Toast {
    final Context mContext;
    final TN mTN;
    int mDuration;
    View mNextView;

    public Toast(Context context) {
        mContext = context;
        mTN = new TN();
        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);
    }

它的屬性很簡單,只有四個,分別是上下文對象mContextTN對象mTN,表示顯示時長的mDuration,以及表示將要顯示的視圖mNextView。在它的構造方法中,初始化mTN及它的兩個成員變量。
留意一下Toast中顯示的View的命名為mNextView

我們再往下翻一下Toast的一些成員方法,會發現它的許多行為的實現,都是把值賦給了mTN所對應的屬性,比如設置gravity,水平外邊距,x軸或y軸的偏移等等。而它的showcancel方法,也都是通過其來實現,如下:

    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;

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

    public void cancel() {
        mTN.hide();

        try {
            getService().cancelToast(mContext.getPackageName(), mTN);
        } catch (RemoteException e) {
            // Empty
        }
    }

那麼TN究竟是個什麼東西?

TN

我們來看一下TN的源碼,可以知道,它是Toast裡的一個靜態內部類,繼承自ITransientNotification.Stub,並實現了其顯示和隱藏的方法:

    private static class TN extends ITransientNotification.Stub {
        // 顯示命令
        final Runnable mShow = new Runnable() {
            @Override
            public void run() {
                handleShow();
            }
        };
        //隱藏命令
        final Runnable mHide = new Runnable() {
            @Override
            public void run() {
                handleHide();
                // Don't do this in handleHide() because it is also invoked by handleShow()
                mNextView = null;
            }
        };

        private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();//懸浮窗口的參數
        final Handler mHandler = new Handler();    //用於將操作命令添加到線程的隊列中

        //顯示的View的相關布局參數
        int mGravity;
        int mX, mY;
        float mHorizontalMargin;
        float mVerticalMargin;


        View mView;    //當前顯示的View
        View mNextView;// 下一個要顯示的View

        WindowManager mWM;//用於顯示懸浮窗口

        TN() {
            // XXX This should be changed to use a Dialog, with a Theme.Toast
            // defined that sets up the layout params appropriately.
            //設置懸浮窗口的相關參數
            final WindowManager.LayoutParams params = mParams;
            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
            params.width = WindowManager.LayoutParams.WRAP_CONTENT;
            params.format = PixelFormat.TRANSLUCENT;
            params.windowAnimations = com.android.internal.R.style.Animation_Toast;
            params.type = WindowManager.LayoutParams.TYPE_TOAST;
            params.setTitle("Toast");
            params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
        }

        /**
         * schedule handleShow into the right thread
         */
        @Override
        public void show() {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.post(mShow);//將顯示命令添加到線程隊列當中
        }

        /**
         * schedule handleHide into the right thread
         */
        @Override
        public void hide() {
            if (localLOGV) Log.v(TAG, "HIDE: " + this);
            mHandler.post(mHide);//將隱藏命令添加到線程隊列當中
        }
        // ...其他代碼
    }

從上面的代碼中,我們可以看到,一個Toast的顯示和隱藏,並不是在Toast類本身中實現的,而是交給了TN,由它去實現。TN則是包裝了一個Toast的內容及行為。TN的顯示及隱藏的具體實現handleShow()handleHide(),就是向WindowManager添加和隱藏View,就和我們平時寫懸浮窗口的實現一樣,這裡略過。
我們還可以看到,TN繼承的ITransientNotification.Stub,它是AIDL接口的實現,該接口為ITransientNotification.aidl。AIDL是Android中用於進程間通信的一種機制。也就是對於我們的Toast,最終是交給另一個進程去顯示及隱藏的。
我們回到剛才讀Toast時的show()cancel()的代碼,可以看到先是獲取一個服務INotificationManager service = getService();,顯示時調用其service.enqueueToast(pkg, tn, mDuration);來顯示,隱藏時調用mTNhide()方法,並接著調用getService().cancelToast(mContext.getPackageName(), mTN);來取消,其中,mTN是它的進程通信的回調

##INotificationManager
INotificationManager也是一個AIDL接口,它定義一些通知管理服務的API,位於Android源碼中的frameworks\base\core\java\android\app,並沒有包含在Android SDK中。代碼如下:

interface INotificationManager
{
    /** @deprecated use {@link #enqueueNotificationWithTag} instead */
    void enqueueNotification(String pkg, int id, in Notification notification, inout int[] idReceived);
    /** @deprecated use {@link #cancelNotificationWithTag} instead */
    void cancelNotification(String pkg, int id);
    void cancelAllNotifications(String pkg);

    void enqueueToast(String pkg, ITransientNotification callback, int duration);
    void cancelToast(String pkg, ITransientNotification callback);
    void enqueueNotificationWithTag(String pkg, String tag, int id, in Notification notification, inout int[] idReceived);
    void cancelNotificationWithTag(String pkg, String tag, int id);
}

可以看到它管理的內容包含Toast提示及通知,下面我們只關注Toast相關的兩個接口。根據它的名字,我們找到這個服務的對應實現sources\android-23\android\app\NotificationManager.java

NotificationManagerService

在該類的代碼中,有INotificationManager.Stub的匿名內部類實例,主要代碼如下:

    private final IBinder mService = new INotificationManager.Stub() {
        // Toasts
        // ============================================================================

        @Override
        public void enqueueToast(String pkg, ITransientNotification callback, int duration)
        {
            if (DBG) {
                Slog.i(TAG, "enqueueToast pkg=" + pkg + " callback=" + callback
                        + " duration=" + duration);
            }

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

            final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));

            if (ENABLE_BLOCKED_TOASTS && !noteNotificationOp(pkg, Binder.getCallingUid())) {
                if (!isSystemToast) {
                    Slog.e(TAG, "Suppressing toast from package " + pkg + " by user request.");
                    return;
                }
            }
            //對Toast隊列加鎖
            synchronized (mToastQueue) {
                int callingPid = Binder.getCallingPid();//獲取調用進程id
                long callingId = Binder.clearCallingIdentity();//重置當前線程上進來的IPC的ID
                try {
                    ToastRecord record;
                    int index = indexOfToastLocked(pkg, callback);//判斷我們的mTN是否存在隊列中
                    // 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.
                        //如果不是系統Toast,則限制toast的數量,以避免DOS攻擊及內存洩露。
                        if (!isSystemToast) {
                            int count = 0;
                            final int N = mToastQueue.size();
                            for (int i=0; i= MAX_PACKAGE_NOTIFICATIONS) {
                                         Slog.e(TAG, "Package has already posted " + count
                                                + " toasts. Not showing more. Package=" + pkg);
                                         return;
                                     }
                                 }
                            }
                        }

                        // 創建一個ToastRecord對象,並加入隊列。
                        record = new ToastRecord(callingPid, pkg, callback, duration);
                        mToastQueue.add(record);
                        index = mToastQueue.size() - 1;
                        keepProcessAliveLocked(callingPid);//設置調用者的進程的活動狀態。它會根據該進程的Toast的數量來設置是否為前台進程。
                    }
                    // 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.
                    //如果最後一個下標為0,則表示它是當前的toast,那麼將不會管它是新創建的還是剛剛被更新過,
                    //而是回調告訴mTN把自己顯示出來。
                    if (index == 0) {
                        showNextToastLocked();
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
        }

        @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 {
                    //獲取Toast的在隊列中的下標
                    int index = indexOfToastLocked(pkg, callback);
                    if (index >= 0) {//如果存在
                        //從隊列中移除,該方法還會設置調用進程的活動狀態,並顯示下一條要顯示的Toast
                        cancelToastLocked(index);
                    } else {
                        Slog.w(TAG, "Toast already cancelled. pkg=" + pkg
                                + " callback=" + callback);
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
        }
        //其他接口的實現代碼略
    }

其中顯示toast的代碼如下:

    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();
                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);
                }
                keepProcessAliveLocked(record.pid);
                if (mToastQueue.size() > 0) {
                    record = mToastQueue.get(0);
                } else {
                    record = null;
                }
            }
        }
    }
````
可以看到它會調用`TN`對象的`show()`方法,並接著調用`scheduleTimeoutLocked(record);`。
該方法代碼如下:






```java
    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);
    }

也就是在delay時間之後發送一個MESSAGE_TIMEOUT的消息來通知Toast隱藏。delay的時間根據Toast設置的duration是否為Toast.LENGTH_LONG來決定是LONG_DELAY(3.5秒)還是SHORT_DELAY(2秒)
NotificationManagerService的實現代碼很長,為避免篇幅因貼上代碼的()() 關系變得太長,這裡只介紹了主要流程的代碼,具體每個步驟的方法實現,可以自己翻閱該類代碼,文件位於SDK的sources\android-23\com\android\server\notification1`。

    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);
    }

也就是在delay時間之後發送一個MESSAGE_TIMEOUT的消息來通知Toast隱藏。delay的時間根據Toast設置的duration是否為Toast.LENGTH_LONG來決定是LONG_DELAY(3.5秒)還是SHORT_DELAY(2秒)。
NotificationManagerService的實現代碼很長,為避免篇幅因貼上代碼的()() 關系變得太長,這裡只介紹了主要流程的代碼,具體每個步驟的方法實現,可以自己翻閱該類代碼,文件位於SDK的sources\android-23\com\android\server\notification1`。

總結

對於Toast代碼的分析在此告一段落,從上面的分析中我們可以得到以下結論:

默認的Toast布局的背景可以在theme中配置。 Toast的顯示及隱藏是通過NotificationManagerService來管理的,它跨進程,使用AIDL來實現進程間通信。 所有Toast都會加到NotificationManagerService的隊列中,對於非系統程序,它會限制Toast的數量(當前我所讀的代碼中該值為50)以防止DOS攻擊及內存洩露的問題。 Toast的顯示及隱藏命令通過new出來的handler來發送。所以沒有隊列的線程是不能顯示Toast的。 Toast的顯示的時間只有兩個,duration相當於一個標志位,用於標志顯示的時間是長還是短,而不是具體的顯示時間。 當有Toast要顯示時,其所在進程會被設為前台進程。
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved