1. 程式人生 > >深入分享一下android.widget.Toast

深入分享一下android.widget.Toast

問題來源

  Toast是我們Android大開發中比不可少的顯示部分,相信大家很熟悉Toast的使用方式。之所以寫這篇文章,主要是我在專案中遇到了這樣一個問題,PM說的Toast在某個特定的頁面顯示的時長能不能合適一點,什麼意思?我們大Toast只有Toast.LENGTH_SHORTToast.LENGTH_LONG兩種,前者是2s,後者是顯示3.5s。嗯,有些頁面的確顯示有問題,short太短,long太短,搞得比較尷尬,所以需要分析一下,這Toast到底是怎麼執行的,我能不能修改這個時間,讓它要多長有多長呢? 我希望你讀完這篇文章之後,能瞭解深層次的Toast執行原理。

深入原始碼

  廢話不多說,我們一般使用Toast的方式如下:

Toast.makeText(activity, "hello world", Toast.LENGTH_SHORT).show();
//or
Toast.makeText(activity, "hello world", Toast.LENGTH_LONG).show();

通過原始碼分析,makeText()方法中,主要是獲取LayoutInflater獲取一個TextView,這個TextView就是我們的Toast需要顯示的View,這個很簡單,就不說了,主要來看一下show()方法:

    /**
     * 顯示特定時長的View
     */
    public void show() {
        //程式碼省略  

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

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

通過上面可以看出,首先Toast獲取了一個INotificationManager,因為後面叫service,看上去是一個服務,那麼我們看一下getService()方法:

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

一看INotificationManager.Stub.asInterface,就知道使用了aidl方法,這可有點扯淡了,一個簡單的Toast,也要用上aidl,我不太熟悉,怎麼辦?既然不熟悉,那麼我們先放一步說話吧,先看看

 service.enqueueToast(pkg, tn, mDuration);

方法,但是這個方法也是標紅的:
這裡寫圖片描述

以前,每次看到這裡,都有些失望,都不知道接下來該怎麼分析原始碼,因為這些原始碼都在framework層,也不好找,怎麼辦呢?別急,現在已經有辦法了,現在有這麼一個網站,專門介紹Android#framwork層的,叫http://androidxref.com/,大家可以有事沒事上去看看。說到這裡,我腦袋發熱,直接搜了一下enqueueToast方法,不搜不知道,一搜嚇一跳:
這裡寫圖片描述

我們居然搜到了INotificationManager.aidl,Toast還有NotificationManagerService.java三個檔案,想都不用想,真正的實現類一定是NotificationManagerService.java,我們進去看看:
這裡寫圖片描述

我稍微把程式碼整理一下,當然我建議你先不看這一大段程式碼,說實話沒什麼卵用,邊看解釋邊回來看效果更好:

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

            //無關緊要的程式碼ignore

            final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
            final boolean isPackageSuspended =
                    isPackageSuspendedForUser(pkg, Binder.getCallingUid());

            synchronized (mToastQueue) {
                int callingPid = Binder.getCallingPid();
                long callingId = Binder.clearCallingIdentity();
                try {
                    ToastRecord record;
                    //獲取
                    int index = indexOfToastLocked(pkg, callback);

                    if (index >= 0) {
                        record = mToastQueue.get(index);
                        record.update(duration);
                    } 
                        Binder token = new Binder();
                        mWindowManagerInternal.addWindowToken(token,WindowManager.LayoutParams.TYPE_TOAST);
                        record = new ToastRecord(callingPid, pkg, callback, duration, token);
                        mToastQueue.add(record);
                        index = mToastQueue.size() - 1;
                    }

                    if (index == 0) {
                        showNextToastLocked();
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
            }
          }

程式碼還是很長,不過我們慢慢分析,我們先看一下enqueueToast(String pkg, ITransientNotification callback, int duration)這個方法三個引數分別是:pkg,ITransientNotification介面的callback和duration,其實我現在還是最關心的就是這個duration,因為這個duration傳入的就是決定顯示的時間,見Toast.show()方法:
這裡寫圖片描述

不過,更重要的是需要知道這個TN,即ITransientNotification這個callback,TN是Toast的一個靜態內部類,繼承了ITransientNotification.Stub (這是是aidl方法產生的,此時也實現了ITransientNotification介面,這篇不是扯aidl的,所以我就當大家都明白), 大家可以先不管這個TN到底是個什麼玩意,先不扯,等用到了咋們在慢慢回來看。

對於上面的原始碼,我們就從最簡單的方式分析吧,那麼我們就直接分析這個方法:

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

我們就認為現在mToastQueue就一個ToastRecord,在分析這個方法之前,我們先看一下ToastRecord是怎麼生成的:

record = new ToastRecord(callingPid, pkg, callback, duration, token);

可見,此時的callback和duration是我們在傳入enqueueToast方法的引數,此時記住就行,等會要用到,現在我們需要分析showNextToastLocked()方法了:
方法不長,那我就直接截圖了:
這裡寫圖片描述

此時很明朗,其實是直接呼叫了record.callback.show(record.token) 通過剛才的分析,這個callback就是我們傳入ITransientNotification,當然實現者大家肯定都知道,就是Toast內部靜態類TN,那麼是時候看看TN的show()方法了:

@Override
public void show(IBinder windowToken) {
     mHandler.obtainMessage(0, windowToken).sendToTarget();
}

這個一看就明白,直接mHandler看原始碼了:
這裡寫圖片描述

這個簡單,直接看handleShow(token)方法了:


@Override
public void handleShow(IBinder windowToken) {          
       if (mView != mNextView) {            
                WindowManager  mWn = ... ;
                //程式碼省    
                mWM.addView(mView, mParams);
            }
 }

哦,終於知道Toast也是通過WindowManager.addView新增上去的,這個就叼了,最起碼我們分析了一遍Toast的生成過程,還是比較曲折的。 好了,既然掛上去了,那怎麼消失掉呢?消失的原始碼該怎麼分析呢?別急,咋們忘回看:showNextToastLocked()中還有一個scheduleTimeoutLocked(record)方法,我們進去看看:
這裡寫圖片描述

很簡單,如果使用的事Toast.LENGTH_LONG,就延遲3.5秒,否則就延遲2秒,那麼我們去看一下mHandler.MESSAGE_TIMEOUT:
這裡寫圖片描述

那我們就去看一下handleTimeOut方法:
這裡寫圖片描述

這次執行的方法是cancelToastLocked(index)方法,那麼此時這個方法在做什麼呢?
我們來看一下程式碼:

void cancelToastLocked(int index) {
       ToastRecord record = mToastQueue.get(index);
       //呼叫record.callback
       //即是剛剛認識的TN物件
        try {
            record.callback.hide();
        } catch (RemoteException ignore) {
            //...
        }

        ToastRecord lastToast = mToastQueue.remove(index);
        mWindowManagerInternal.removeWindowToken(lastToast.token, true);

        keepProcessAliveIfNeededLocked(record.pid);
        if (mToastQueue.size() > 0) {
            showNextToastLocked();
        }
    }

同樣道理,我們看到了record.callback.hide()方法,此時我們知道callback還是Toast中的TN物件,那麼它的hide方法為:

@Override
public void hide() {
    mHandler.post(mHide);
}

對於mHander.post(mHide),其中的mHide為一個Runnable物件:

 final Runnable mHide = new Runnable() {
    @Override
    public void run() {
        handleHide();
        mNextView = null;
        }
    };

直接去看handleHide()方法:

public void handleHide() {
    if (mView != null) {
        if (mView.getParent() != null) {
            //最終還是WindowManager移除了該View
            mWM.removeViewImmediate(mView);
        }

        mView = null;
        }
    }

最終還是熟悉的WindowManager移除了mView,那麼此時我們的Toast就會消失在螢幕之上了。
當然了,事情還沒有結束,在cancelToastLocked(index)方法中還有一個方法值得我們看一下:

 if (mToastQueue.size() > 0) {
        showNextToastLocked();
    }

如果ToastQueue不為空,那麼將繼續迴圈將ToastQueue每一個ToastRecord執行show()和hide()方法,直到所有Toast都顯示掉。當然,如果你不適用特殊的手段,按照Toast的執行意向,你將不會同時看到兩個Toast在一個螢幕上,因為前一個Toast沒有show()完成,不會去呼叫後面ToastRecord的方法的。

上個圖,把整個過程描述一下,如果有錯誤,請及時提出:

Toast顯示過程

Toast.show()其實遠端呼叫了NotificationManageService的enqueueToast方法,在該方法中,存在一個Handler遍歷ToastQueue,ToastQueue中每一個ToastRecord將會呼叫callback.show()callback.hide()方法,此方法最終將呼叫Toast的內部類TN物件的show()hide()方法,而showhide將分別呼叫WindowManager.addView()windowManager.removeView()方法,直至將Toast顯示或者移除在螢幕上。

結局

回到開頭,到了這裡,我們是否可以自己控制Toast的顯示時長呢?如果還是要NotificationManagerService參與,那就沒啥希望了,因為它內部的Handler.postDelay()只有兩種選擇,要麼是2s,要麼是3.5s。那麼我們不用NotificationManagerService這這尊大佛,直接呼叫Toast.TN.show()Toast.TN.Hide()是否可行呢?我的想法也是這樣的,也實現過了,程式碼如下,很簡單,只使用了簡單的反射:

public class AllTimeShowToast {
    private Toast mToast ;
    private Object TN ;

    private Method show ;
    private Method hide ;

    private TextView mLongTextView ;

    public AllTimeShowToast(Context context) {
        mToast = new Toast(context );
        initTextView(context);
        initTN();
    }

    private void initTextView(Context context) {
        mLongTextView = new TextView(context);
        mLongTextView.setText("all time show ");
    }

    private void initTN() {
        try {
            Field tnObj =  mToast.getClass().getDeclaredField("mTN");
            tnObj.setAccessible(true);

            TN = tnObj.get(mToast);
            show = TN.getClass().getMethod("show");
            hide = TN.getClass().getMethod("hide");

            Field tnParamsField = TN.getClass().getDeclaredField("mParams");
            tnParamsField.setAccessible(true);
            WindowManager.LayoutParams params = (WindowManager.LayoutParams) tnParamsField.get(TN);

            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
            params.width = WindowManager.LayoutParams.WRAP_CONTENT;
            params.format = PixelFormat.TRANSLUCENT;
            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;


            Field nextViewField = TN.getClass().getDeclaredField("mNextView");
            nextViewField.setAccessible(true);
            nextViewField.set(TN, mLongTextView);

        }catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void show() {
        try {
            show.invoke(TN);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void hide() {
        try {
            hide.invoke(TN) ;
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

呼叫方式為:

var showToast = AllTimeShowToast(this);

//show方法展示
showToast!!.show()

//hide方法展示
showToast!!.hide()

結果如下:

all_time_to_show

當然了,這裡Toast也只是一次性的,show()一次,hide()一次之後,就相當於廢了。如果要想重新show,那麼需要重新new Object了。
好了,這篇文章好長,也差不多寫完了,基本上把Toast流程分析了一遍。。。