本系列博客基于android-28版本
【W(wǎng)indow系列】——Toast源碼解析
【W(wǎng)indow系列】——PopupWindow的前世今生
【W(wǎng)indow系列】——Dialog源碼解析
【W(wǎng)indow系列】——Window中的Token
前言
Toast組件應(yīng)該是接觸Android中使用率非常高的一個(gè)原生控件,其使用的便捷性一直是開發(fā)者選用的原因,短短的一行代碼就可以實(shí)現(xiàn)支持跨頁面的提示功能。但是隨著Google對于Android系統(tǒng)自身安全性的限制瞬矩,導(dǎo)致Toast組件目前在高版本上也出現(xiàn)了許多問題,例如當(dāng)關(guān)閉應(yīng)用的通知欄權(quán)限墓贿,全局的Toast就無法展示了祥款。本期博客就先從源碼角度分析Toast的實(shí)現(xiàn)原理较锡,只有了解了Toast的實(shí)現(xiàn)原理,才能想辦法解決問題伏钠。
源碼解析
我們使用Toast一般的使用方式如下:
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
所以我們來分別看一下兩個(gè)方法横漏。
/**
* Make a standard toast that just contains a text view.
*
* @param context The context to use. Usually your {@link android.app.Application}
* or {@link android.app.Activity} object.
* @param text The text to show. Can be formatted text.
* @param duration How long to display the message. Either {@link #LENGTH_SHORT} or
* {@link #LENGTH_LONG}
*
*/
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
return makeText(context, null, text, duration);
}
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;
}
這里有兩個(gè)注意點(diǎn):
1.可以看到這里注釋寫到了,延時(shí)duration
只能是變量LENGTH_SHORT
或LENGTH_LONG
具體原因后面源碼分析到再看熟掂。
2.我們每次使用Toast都會(huì)new一個(gè)新的Toast對象缎浇,而這個(gè)布局就是一個(gè)transient_notification.xml
文件
現(xiàn)在首先來看一下Toast的構(gòu)造函數(shù)
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);
}
可以看到這里創(chuàng)建了一個(gè)TN
對象,這個(gè)TN
后面會(huì)貫穿整個(gè)Toast的使用全過程赴肚,所以我們先看一下這是個(gè)什么對象素跺。
private static class TN extends ITransientNotification.Stub {
TN(String packageName, @Nullable Looper looper) {
// 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;
//type為TYPE_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;
mPackageName = packageName;
if (looper == null) {
// Use Looper.myLooper() if looper is not specified.
//獲取Looper對象
looper = Looper.myLooper();
//如果自線程,沒有創(chuàng)建Looper對象誉券,則拋異常
if (looper == null) {
throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");
}
}
//創(chuàng)建Handler對象
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();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
break;
}
case CANCEL: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
try {
getService().cancelToast(mPackageName, TN.this);
} catch (RemoteException e) {
}
break;
}
}
}
};
}
}
首先可以看到這個(gè)TN對象繼承了ITransientNotification.Stub
指厌,看到這個(gè)名字,如果了解過AIDL機(jī)制的話横朋,或者了解過Binder機(jī)制的仑乌,應(yīng)該對這個(gè)名字很熟悉百拓,這個(gè)不就是AIDL的實(shí)現(xiàn)類琴锭,所以可以看出Toast機(jī)制的底層實(shí)現(xiàn)肯定用到了Binder機(jī)制⊙么可以看到這里面有兩個(gè)方法被@Override
標(biāo)記决帖,show()
方法和hide()
方法,這不是正好和我們的顯示和隱藏對應(yīng)嗎蓖捶。
這里我注釋著重寫了幾個(gè)點(diǎn)
1.首先可以看到這里創(chuàng)建了WindowManager.LayoutParams
對象地回,并且設(shè)置了一系列熟悉,其中比較重要的一個(gè)是俊鱼,這里設(shè)置了一個(gè)type
屬性為TYPE_TOAST
刻像,這個(gè)標(biāo)記了這個(gè)Window的類型,而關(guān)閉通知欄權(quán)限導(dǎo)致Toast無法展示也是和這個(gè)屬性有關(guān)并闲,不影響本次原理分析细睡,所以暫不分析。
2.獲取Looper對象帝火,如果屬性Handler機(jī)制的話溜徙,應(yīng)該看到這個(gè)方法很熟悉,Looper.myLooper()
這個(gè)方法底層利用ThreadLocal獲取Looper對象犀填,而一般我們使用Toast都是在主線程使用蠢壹,主線程的main方法,已經(jīng)自動(dòng)完成了Looper.prepare()方法和Looper.loop()方法九巡,
所以已經(jīng)自動(dòng)完成了Looper的創(chuàng)建图贸。這里可以看到,如果沒有獲取到Looper對象,則會(huì)拋出異常疏日。所以這里我們也可以對應(yīng)分析一個(gè)問題:
自線程使用Toast對象會(huì)怎么樣乏盐?
如果熟悉Handler機(jī)制的話,應(yīng)該立馬能得出答案制恍,當(dāng)然是崩潰了父能,猶豫創(chuàng)建出來的自線程沒有創(chuàng)建Looper對象,所以這里無法獲取到Looper對象净神,那么就會(huì)拋異常何吝,導(dǎo)致崩潰。
那么自線程如何使用Toast呢鹃唯?
還是Handler機(jī)制爱榕,既然沒有Looper機(jī)制,那么就創(chuàng)建咯
new Thread(){
public void run(){
Looper.prepare();//給當(dāng)前線程初始化Looper
Toast.makeText(getApplicationContext(),"自線程Toast",0).show();//Toast初始化的時(shí)候會(huì)new Handler();無參構(gòu)造默認(rèn)獲取當(dāng)前線程的Looper坡慌,如果沒有prepare過黔酥,則拋出題主描述的異常。上一句代碼初始化過了洪橘,就不會(huì)出錯(cuò)跪者。
Looper.loop();//這句執(zhí)行,Toast排隊(duì)show所依賴的Handler發(fā)出的消息就有人處理了熄求,Toast就可以吐出來了渣玲。但是,這個(gè)Thread也阻塞這里了弟晚,因?yàn)閘oop()是個(gè)for (;;) ...
}
}.start();
3.后面就創(chuàng)建了Handler對象忘衍,所以如果是常規(guī)情況,那么在Handler中執(zhí)行的應(yīng)該是主線程的方法卿城。
看完了構(gòu)造函數(shù)枚钓,現(xiàn)在我們就來看一下Toast的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
}
}
static private INotificationManager getService() {
if (sService != null) {
return sService;
}
sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
return sService;
}
果然和上面分析的一樣,這里首先利用Binder獲取了NotificationManagerService
的代理瑟押,然后調(diào)用了它的enqueueToast()
方法搀捷,注意這里將剛才創(chuàng)建的TN
對象傳了過去,果然是利用了Binder勉耀,雙向通信指煎。
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 = isCallerSystemOrPhone() || ("android".equals(pkg));
final boolean isPackageSuspended =
isPackageSuspendedForUser(pkg, Binder.getCallingUid());
if (ENABLE_BLOCKED_TOASTS && !isSystemToast &&
(!areNotificationsEnabledForPackage(pkg, Binder.getCallingUid())
|| isPackageSuspended)) {
Slog.e(TAG, "Suppressing toast from package " + pkg
+ (isPackageSuspended
? " due to package suspended by administrator."
: " by user request."));
return;
}
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
int index;
// All packages aside from the android package can enqueue one toast at a time
//是否是系統(tǒng)應(yīng)用
if (!isSystemToast) {
index = indexOfToastPackageLocked(pkg);
} else {
index = indexOfToastLocked(pkg, callback);
}
// If the package already has a toast, we update its toast
// in the queue, we don't move it to the end of the queue.
if (index >= 0) {
//如果當(dāng)前隊(duì)列里已經(jīng)有Toast,直接更新
record = mToastQueue.get(index);
record.update(duration);
try {
record.callback.hide();
} catch (RemoteException e) {
}
record.update(callback);
} else {
//沒有便斥,則創(chuàng)建新的ToastRecord
Binder token = new Binder();
//生成一個(gè)Toast窗口至壤,并且傳遞token等參數(shù)
mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
//生產(chǎn)一個(gè)ToastRecord
record = new ToastRecord(callingPid, pkg, callback, duration, token);
//將Toast加入隊(duì)列
mToastQueue.add(record);
index = mToastQueue.size() - 1;
}
//設(shè)置當(dāng)前進(jìn)程為前臺(tái)進(jìn)程
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) {
//如果當(dāng)前Toast為隊(duì)頭,則顯示Toast
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
可以看到枢纠,果然利用了Binder像街,這里首先用isSystemToast
判斷了是否是系統(tǒng)應(yīng)用
final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg));
可以看到這里,兩個(gè)判斷條件一個(gè)是通過進(jìn)程Id判斷是否是系統(tǒng)進(jìn)程,一個(gè)是通過包名判斷是否"android"
镰绎,所以后面會(huì)的博客會(huì)介紹一種通過偽造包名的方式脓斩,以系統(tǒng)Toast的方式彈Toast。
后面在定位Toast在隊(duì)列中的位置的時(shí)候畴栖,如果隊(duì)列中已經(jīng)存在Toast的話随静,走的就是更新流程,而如果是一個(gè)新的Toast吗讶,則會(huì)首先創(chuàng)建一個(gè)Binder對象燎猛,然后生成一個(gè)ToastRecord
對象,并加入隊(duì)列照皆,這里注意創(chuàng)建的Token
對象會(huì)被保存在ToastRecord
對象中重绷。
接下來這個(gè)函數(shù)很重要:
void keepProcessAliveIfNeededLocked(int pid)
{
int toastCount = 0; // toasts from this pid
ArrayList<ToastRecord> list = mToastQueue;
int N = list.size();
for (int i=0; i<N; i++) {
ToastRecord r = list.get(i);
if (r.pid == pid) {
toastCount++;
}
}
try {
mAm.setProcessImportant(mForegroundToken, pid, toastCount > 0, "toast");
} catch (RemoteException e) {
// Shouldn't happen.
}
}
這里將當(dāng)前彈Toast的進(jìn)程設(shè)置為了前臺(tái)進(jìn)程,熟悉Toast的應(yīng)該都知道膜毁,Toast的特殊性在于它支持跨頁面顯示昭卓,甚至當(dāng)應(yīng)用關(guān)閉的時(shí)候,Toast仍然能夠展示瘟滨,就是這個(gè)函數(shù)發(fā)揮的作用候醒,這里利用AMS,還是通過Binder室奏,調(diào)用了setProcessImportant
火焰,將Toast所在的進(jìn)程設(shè)置為了前臺(tái)進(jìn)程劲装,保證了進(jìn)程的存活胧沫,所以當(dāng)頁面銷毀了,Toast還是可以正常顯示占业。
if (index == 0) {
//如果當(dāng)前Toast為隊(duì)頭绒怨,則顯示Toast
showNextToastLocked();
}
void showNextToastLocked() {
//取出隊(duì)列頭的Toast
ToastRecord record = mToastQueue.get(0);
//居然是個(gè)循環(huán)
while (record != null) {
if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
try {
//調(diào)用callback的show方法,傳入剛才創(chuàng)建的Token對象
record.callback.show(record.token);
//延時(shí)移除Toast
scheduleDurationReachedLocked(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) {
//移除Toast
mToastQueue.remove(index);
}
//喚醒進(jìn)程
keepProcessAliveIfNeededLocked(record.pid);
if (mToastQueue.size() > 0) {
//再次獲取
record = mToastQueue.get(0);
} else {
record = null;
}
}
}
}
最后如果是Toast為隊(duì)列頭谦疾,那么此時(shí)就會(huì)執(zhí)行showNextToastLocked()
方法南蹂,可以看到這里首先嘗試獲取隊(duì)列頭的Toast,后面居然是一個(gè)while
循環(huán)念恍,這塊我感覺Google有點(diǎn)過度嚴(yán)謹(jǐn)了六剥,可以看到如果沒有取到ToastRecord
,這里就移除后,再次執(zhí)行喚醒進(jìn)程峰伙,然后再次嘗試獲取疗疟,直到獲取到,但是這樣為了一個(gè)Toast的展示瞳氓,甚至可能導(dǎo)致這個(gè)循環(huán)一直再執(zhí)行策彤,感覺有些不值當(dāng)了,這只是我個(gè)人的看法,歡迎大家討論店诗。
當(dāng)取到ToastRecord
后裹刮,會(huì)執(zhí)行其callback
的show
方法,當(dāng)看到這個(gè)方法名的時(shí)候庞瘸,感覺很熟悉捧弃,那么這個(gè)callback
是什么對象呢,看一下ToastRecord
的構(gòu)造的地方擦囊。
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
record = new ToastRecord(callingPid, pkg, callback, duration, token);
}
}
還是剛才那個(gè)函數(shù)塔橡,可看到,callback就是入?yún)⒌膶ο笏冢敲丛倏匆幌?code>Toast的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
}
}
這樣整個(gè)流程就通了葛家,這個(gè)callback
就是最初的TN
對象,還是利用Binder的雙向通信泌类,所以這里就會(huì)回到TN
對象的show()
方法癞谒,這里要注意,再調(diào)用show
方法的時(shí)候刃榨,會(huì)把剛才創(chuàng)建的Token
對象弹砚,傳入。
@Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
這里有回到了最早分析的Handler對象枢希,這個(gè)Handler對象常規(guī)使用的話是在主線程創(chuàng)建的桌吃。
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();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
break;
}
case CANCEL: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
try {
getService().cancelToast(mPackageName, TN.this);
} catch (RemoteException e) {
}
break;
}
}
}
};
可以看到又調(diào)用了handleShow
方法。
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 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;
mParams.hideTimeoutMilliseconds = mDuration ==
Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
//設(shè)置token
mParams.token = windowToken;
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);
// Since the notification manager service cancels the token right
// after it notifies us to cancel the toast there is an inherent
// race and we may attempt to add a window after the token has been
// invalidated. Let us hedge against that.
try {
//利用WindowManager將View加入
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}
這里的代碼就比較簡單了苞轿,將基礎(chǔ)的屬性設(shè)置到了LayoutParams茅诱,這里比較重要的是將token設(shè)置到了LayoutParams
中(關(guān)于這個(gè)屬性后面可能會(huì)有一篇博客專門講解一下這個(gè)屬性值和權(quán)限的關(guān)系,本篇博客主要分析Toast的展示原理搬卒,就不拓展分析了)瑟俭,并且利用WindowManager
的addView
的上,這樣最終Toast
就顯示出來了契邀。
剩下了就是怎么移除這個(gè)Toast了摆寄,回到NMS,再show
后坯门,使用scheduleDurationReachedLocked(record);
方法微饥,就是移除操作。
private void scheduleDurationReachedLocked(ToastRecord r)
{
mHandler.removeCallbacksAndMessages(r);
Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r);
//顯示耗時(shí)只有兩種
long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
//通過Handler發(fā)送消息執(zhí)行
mHandler.sendMessageDelayed(m, delay);
}
這里第一個(gè)注意的點(diǎn)古戴,可以看到欠橘,這里delay
變量只有兩種可能,LONG_DELAY
和SHORT_DELAY
允瞧。這也就解釋了為什么我們平時(shí)使用Toast
組件简软,不支持自定義顯示時(shí)長蛮拔,只能有LONG
和SHORT
兩種時(shí)長。
然后通過Handler發(fā)送一個(gè)延時(shí)消息痹升,用于隱藏Toast組件建炫。
@Override
public void handleMessage(Message msg)
{
switch (msg.what)
{
case MESSAGE_DURATION_REACHED:
handleDurationReached((ToastRecord)msg.obj);
break;
...
}
}
private void handleDurationReached(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);
}
}
}
這里的邏輯很簡單,就是利用Handler的消息機(jī)制疼蛾,取出顯示的消息的位置肛跌,然后進(jìn)行取消操作。
@GuardedBy("mToastQueue")
void cancelToastLocked(int index) {
//取出消息
ToastRecord record = mToastQueue.get(index);
try {
//執(zhí)行隱藏邏輯
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
}
//移除操作
ToastRecord lastToast = mToastQueue.remove(index);
mWindowManagerInternal.removeWindowToken(lastToast.token, false /* removeWindows */,
DEFAULT_DISPLAY);
// We passed 'false' for 'removeWindows' so that the client has time to stop
// rendering (as hide above is a one-way message), otherwise we could crash
// a client which was actively using a surface made from the token. However
// we need to schedule a timeout to make sure the token is eventually killed
// one way or another.
scheduleKillTokenTimeout(lastToast.token);
keepProcessAliveIfNeededLocked(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.
//顯示下一個(gè)
showNextToastLocked();
}
}
知道了show
的邏輯后察郁,這個(gè)的原理就很相似了衍慎,這里首先取出ToastRecord
變量,其實(shí)我感覺這里Google可以優(yōu)化一下皮钠,剛才先是定位稳捆,然后這里又取出,相當(dāng)于兩次遍歷麦轰,其實(shí)可以合并為一次遍歷就可以乔夯。
- 然后利用Binder執(zhí)行
hide
方法。 - 將給Toast 生成的窗口Token從WMS 服務(wù)中刪除
- 判斷是否還有消息款侵,如果存在末荐,則繼續(xù)顯示Toast
這里再看一下hide
方法。同樣也是利用Handler,最終執(zhí)行handleHide()
方法新锈。
public void handleHide() {
if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
if (mView != null) {
// note: checking parent() just to make sure the view has
// been added... i have seen cases where 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);
}
// Now that we've removed the view it's safe for the server to release
// the resources.
try {
getService().finishToken(mPackageName, this);
} catch (RemoteException e) {
}
mView = null;
}
}
這里還是利用WMS將View移除甲脏,這里有個(gè)地方挺有意思,這里先判斷了一下view的parent
不為null,這里的注釋寫的很口語化,Google的工程師也挺有意思妹笆。
// note: checking parent() just to make sure the view has
// been added... i have seen cases where we get here when
// the view isn't yet added, so let's try not to crash.
至此块请,整個(gè)流程就分析完畢了。
總結(jié)
這里來回顧總結(jié)一下Toast
的展示原理
- 首先通過構(gòu)建Toast對象晾浴,內(nèi)部創(chuàng)建了
TN
對象负乡,這個(gè)對象是一個(gè)Binder對象。show
方法的實(shí)質(zhì)是調(diào)用NMS的代理脊凰,執(zhí)行enqueueToast
方法,并且傳入TN
對象用于雙向通信茂腥。- NMS中狸涌,將Toast的顯示構(gòu)建成了一個(gè)
ToastRecord
對象,并且有一個(gè)隊(duì)列用于保存最岗。- NMS將
ToastRecord
加入隊(duì)列后帕胆,最終利用TN
對象,執(zhí)行show
方法- TN對象的
show
方法般渡,最后是利用Handler
發(fā)送消息懒豹,最后執(zhí)行添加芙盘,就是利用WindowManager將Toast的View加入Window。- NMS中執(zhí)行完后脸秽,內(nèi)部也會(huì)利用Handler發(fā)送延時(shí)消息儒老,只有兩種
LONG
和SHORT
,消息收到后记餐,同樣也是通過TN
對象驮樊,執(zhí)行hide
方法- 同樣的流程,TN利用Handler發(fā)送消息片酝,最終執(zhí)行囚衔,同樣利用WindowManager,移除View雕沿。
- NMS執(zhí)行完移除操作后练湿,會(huì)判斷隊(duì)列中是否還有消息,如果有繼續(xù)執(zhí)行展示Toast的邏輯审轮。
本篇博客主要是針對Toast
組件的展示原理進(jìn)行講解鞠鲜,后面有時(shí)間會(huì)繼續(xù)分析Toast相關(guān)的問題,和Window相關(guān)的問題断国。