天天看點

Android之Toast源碼解析

前言:在最近的面試過程中,有一次偶然的問到了候選者的Toast的源碼的問題,我發現很多人基本上對Toast怎麼使用了然于心,但是它的原理卻是不怎麼關注,是以我今天打算寫篇文章來介紹一下Toast的源碼原理,希望給那些想要了解Toast原理的同學看看。

這篇文章隻是從解析源碼的角度給大家分析Toast的,是以可能枯燥一點,請用點耐心開始看吧。。。

由于懶得大量的手敲資料了,是以這篇文章引用了一些其他文章的資料,在此先表示萬分感謝!!!

我們平時對Toast的正常調用就是:

Toast.makeText(this,"meng",Toast.LENGTH_LONG).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;
    }
           

對于這裡的Toast的makeText的實作,首先,第一行代碼,Toast result = new Toast(context),去調用Toast的構造方法來初始化了一個Toast,是怎麼初始化的呢?

大家來看看Toast構造方法的代碼是怎麼樣的:

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

從上面的源碼中我們可以看到,Toast的初始化需要上下文,這也是我們在調用Toast的時候,都必須擷取上下文的原因。然後建立了一個mTN對象,mTN是一個binder對象,用于IPC。

在這部分代碼實作中,這個TN的初始化操作主要是進行了Window布局元素的建立,具體的邏輯請看下面的代碼:

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

在完成了Toast的初始化之後,我們接着跳出到makeText()方法裡面接着看下面的代碼:

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

在這部分的代碼中其實就是加載了一個布局,這個布局相當簡單,就是一個TextView,然後找到布局中的TextView,然後将我們需要顯示的Toast顯示在這個地方。 

然後用下面的兩行代碼将之前我們所初始化的view,以及我們傳入的需要延時的時長傳入。 

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

到此為止,我們對makeText方法裡的代碼邏輯就分析完了。

簡單總結就是: 正如方法名所顯示的那樣,其主要就是确定Toast需要顯示的位置(使用window确定),然後确定Toast所需要顯示的View,并且綁定到Toast中。

可能你會有疑問到這Toast建立完畢了,但是并沒有show出來呀?

是以,咱們接下來就開始分析show()方法啦。直接深入show()方法,就會看到:

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

在這個代碼裡面,是首先判斷我們建立的view是否為null,為null直接抛出。 然後擷取NotificationManager 的IPC接口,INotificationManager 。 擷取到包名,再将我們之前所處理好的view傳遞給布局的binder對象mTN。 最後,用接口發起跨程序通信。

執行到後面,在服務端,我們來看看這個enqueueToast(pkg, tn, mDuration)的實作,我們隻看主要的代碼:

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

                        record = new ToastRecord(callingPid, pkg, callback, duration);
                        mToastQueue.add(record);
                        index = mToastQueue.size() - 1;
                        keepProcessAliveLocked(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();
                    }
           

在這個方法裡面,首先涉及到了一個同步過程,其所用的鎖是一個ArrayList mToastQueue,從名字也可以看出來,它儲存的東西是ToastRecord。從這裡可以看出一個問題,為啥系統中,應用中如此多的Toast,卻能一一顯示,并不出現問題,它們是儲存在一個隊列中的,會一個一個地取出來顯示。

接下來我們首先看: int index = indexOfToastLocked(pkg, callback) ;

int indexOfToastLocked(String pkg, ITransientNotification callback)
    {
        IBinder cbak = callback.asBinder();
        ArrayList<ToastRecord> list = mToastQueue;
        int len = list.size();
        for (int i=0; i<len; i++) {
            ToastRecord r = list.get(i);
            if (r.pkg.equals(pkg) && r.callback.asBinder() == cbak) {
                return i;
            }
        }
        return -1;
    }
           

在這個方法中主要操作是周遊了mToastQueue将我們傳入的pkg以及mTN與其中的ToastRecord對比,若是存在,那麼直接傳回這一條記錄的index,若是不存在那麼傳回-1。 

再看下面的代碼:

if (index >= 0) {
                        record = mToastQueue.get(index);
                        record.update(duration);
                    } 
           

之前代碼已經說明了,在這個if中處理的就是能在mToastQueue中找到的情況: 首先擷取到這條記錄,然後更新,其中傳入的參數是延時時間,這個函數沒有做什麼其他的操作,僅僅隻是指派了新的時間。

接下來再看看沒有加入的情況,這部分才是重點:

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

從代碼的實作很容易可以看出,這個方法是用來限制Toast的數量的: 首先若其不是系統應用,那麼才會執行這個方法的下面的實作,是先擷取到enqueue的大小N,設定count計數器,然後周遊,若是周遊項的pkg和我們傳入的pkg相同,那麼count++。即是說,統計一個package中的Toast的數量,Android隻允許最多50個Toast的數量,不過現實的情況是,咱們的應用基本不會用到那麼多滴。是吧 ,老鐵們!

繼續接下來的源碼解析:

record = new ToastRecord(callingPid, pkg, callback, duration);
                        mToastQueue.add(record);
                        index = mToastQueue.size() - 1;
                        keepProcessAliveLocked(callingPid);
           

這裡首先是新建立了ToastRecord,然後将這條記錄添加到mToastQueue,index是用來判斷是否是目前Toast的。

接下來:

if (index == 0) {
                        showNextToastLocked();
                    }
           

做了判斷,若是目前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;
                }
            }
        }
    }
           

簡單分析這段代碼就會發現:因為是目前Toast,那麼直接擷取到第一個 ToastRecord即可。接下來就調用record.callback.show(),看來我們離終點越來越近了,這裡的callback就是TN對象,在這裡,show()方法又是一次IPC。

那麼深入這個show()方法:

public void show() {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.post(mShow);
        }
           

可以看到,這裡就是實際進行顯示的地方,Handler這個東西就不再多說了,說說這個mShow,其是一個Runnable,實際切換線程之後,運作的是它:

final Runnable mShow = new Runnable() {
            @Override
            public void run() {
                handleShow();
            }
        };
           

咱們直接進入 handleShow()方法看其實作:

public void handleShow() {
            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                    + " mNextView=" + mNextView);
            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);
                // We can resolve the Gravity here by using the Locale for getting
                // the layout direction
                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;
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeView(mView);
                }
                if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
                mWM.addView(mView, mParams);
                trySendAccessibilityEvent();
            }
        }
           

這裡的代碼有些多,但是我們隻需要關注的核心代碼就是:

mWM.addView(mView, mParams);

文章到此,view終于被加載到了window上,Toast終于在螢幕上顯示出來啦!

本文over啦,see you