深入分享一下android.widget.Toast
問題來源
Toast是我們Android大開發中比不可少的顯示部分,相信大家很熟悉Toast的使用方式。之所以寫這篇文章,主要是我在專案中遇到了這樣一個問題,PM說的Toast在某個特定的頁面顯示的時長能不能合適一點,什麼意思?我們大Toast只有Toast.LENGTH_SHORT
和Toast.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.show()
其實遠端呼叫了NotificationManageService的enqueueToast
方法,在該方法中,存在一個Handler遍歷ToastQueue,ToastQueue中每一個ToastRecord將會呼叫callback.show()
和callback.hide()
方法,此方法最終將呼叫Toast的內部類TN物件的show()
和hide()
方法,而show
和hide
將分別呼叫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()
結果如下:
當然了,這裡Toast也只是一次性的,show()
一次,hide()
一次之後,就相當於廢了。如果要想重新show
,那麼需要重新new Object
了。
好了,這篇文章好長,也差不多寫完了,基本上把Toast流程分析了一遍。。。