1. 程式人生 > 程式設計 >Android9.0上針對Toast的特殊處理圖文詳解

Android9.0上針對Toast的特殊處理圖文詳解

前言

我們都清楚,Toast顯示時長有兩個選擇,長顯示是3.5秒,端顯示是2秒。那如果想要做到長時間顯示,該怎麼做呢?有個歷史遺留的app通過開一個執行緒,不斷呼叫show方法進行實現,這些年也沒出過問題,直到系統版本更新到了Android9.0。

實現方式大概如下:

mToast = new Toast(context);
mToast.setDuration(Toast.LENGTH_LONG);
mToast.setView(layout);
...
mToast.show(); //線上程裡不斷呼叫show方法,達到長時間顯示的目的

在Android9.0上,Toast閃現了一下就不見了,並沒有如預期那樣,長時間顯示。為什麼呢?

概述

這裡我們先來大概瞭解下Toast的顯示流程。

Toast使用

一般使用Toast的時候,比較簡單的就是如下方式:

Toast.makeText(mContext,"hello world",duration).show();

這樣就可以顯示一個toast。還有一種是自定義view的:

mToast = new Toast(context);
mToast.setDuration(Toast.LENGTH_LONG);
mToast.setView(layout);
mToast.show(); 

原理都一樣,先new 一個Toast,然後設定顯示時長,設定toast中要顯示的view(text也是view),然後就可以show出來。

Toast原理

Toast實現

先看看Toast的實現:

//frameworks/base/core/java/android/widget/Toast.java
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);
}

Toast的建構函式很簡單,主要就是mTN這個成員,後續對Toast的操作都在這裡進行。緊接著就是設定Toast顯示時長和顯示內容:

public void setView(View view) {
 mNextView = view;
}

public void setDuration(@Duration int duration) {
 mDuration = duration;
 mTN.mDuration = duration;
}

Android9.0上針對Toast的特殊處理圖文詳解

Toast顯示

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

Android9.0上針對Toast的特殊處理圖文詳解

show方法簡單,最終是呼叫了通知服務的enqueueToast方法:

frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java

public void enqueueToast(String pkg,ITransientNotification callback,int duration)
 {
  ...
  final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg));

  ...
   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
    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) {
     record = mToastQueue.get(index);
     record.update(duration);
     try {
      record.callback.hide();
     } catch (RemoteException e) {
     }
     record.update(callback);
    } else {
     Binder token = new Binder();
     mWindowManagerInternal.addWindowToken(token,TYPE_TOAST,DEFAULT_DISPLAY);
     record = new ToastRecord(callingPid,pkg,callback,duration,token);
     mToastQueue.add(record);
     index = mToastQueue.size() - 1;
    }
    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) {
     showNextToastLocked();
    }
   } finally {
    Binder.restoreCallingIdentity(callingId);
   }
  }
 }

Toast的管理是通過ToastRecord型別列表集中管理的,NotificationManagerService會將每一個Toast封裝為ToastRecord物件,並新增到mToastQueue中,mToastQueue的型別是ArrayList。在enqueueToast中,首先會判斷應用是否為系統應用,如果是系統應用,則通過indexOfToastLocked來尋找是否有滿足條件的Toast存在:

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().equals(cbak)) {
   return i;
  }
 }
 return -1;
}

判斷的依據是包名和callback,這裡的callback其實就是上文說到的TN類,這是一個Binder型別,繼承自ITransientNotification.Stub。如果條件符合,則返回對應索引,否則返回-1。首次show Toast的時候,肯定返回-1,則此時會new一個ToastRecord物件,並且加入到mToastQueue中,此時的index則為0:

record = new ToastRecord(callingPid,token);
mToastQueue.add(record);
index = mToastQueue.size() - 1;

那麼就會走到如下分支了:

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(record.token); //呼叫TN類的show方法
   scheduleDurationReachedLocked(record); //時間到就隱藏Toast
   return;
  } catch (RemoteException e) {
   ...
  }
 }
}

該方法也簡單,就是回撥TN類的show方法,上文提過,TN類對外提供show,hide, cancel等方法,在這些方法中,再通過內部handler進行處理:

//frameworks/base/core/java/android/widget/Toast.java
public void show(IBinder windowToken) {
  if (localLOGV) Log.v(TAG,"SHOW: " + this);
  mHandler.obtainMessage(SHOW,windowToken).sendToTarget();
}

//貼出部分handleMessage方法
case SHOW: {
 IBinder token = (IBinder) msg.obj;
 handleShow(token);
 break;
}

public void handleShow(IBinder windowToken) {

 ...
 if (mView != mNextView) {
  // remove the old view if necessary
  handleHide();
  mView = mNextView;
  ...
  mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
  ...
  try {
   mWM.addView(mView,mParams); //交給WMS進行下一步的操作,最終顯示出我們的view
   trySendAccessibilityEvent();
  } catch (WindowManager.BadTokenException e) {
   /* ignore */
  }
 }

}

呼叫show方法,最終會呼叫到handleshow方法,在該方法中使用WMS服務將view顯示出來。

Toast隱藏

顯示說完了,什麼時候隱藏消失?在scheduleDurationReachedLocked方法中:

//frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
private void scheduleDurationReachedLocked(ToastRecord r)
{
  mHandler.removeCallbacksAndMessages(r);
  Message m = Message.obtain(mHandler,MESSAGE_DURATION_REACHED,r);
  long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
  mHandler.sendMessageDelayed(m,delay);
}

這裡也是使用了一個handler來進行處理,delay的時長取決於我們之前設定的Toast顯示時長。長時間為3.5秒,短時間為2秒。

MESSAGE_DURATION_REACHED訊息處理如下:

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

void cancelToastLocked(int index) {

  ToastRecord record = mToastQueue.get(index);
  try {
    record.callback.hide(); //隱藏掉該Toast
  } catch (RemoteException e) {
    ...
  }

  ToastRecord lastToast = mToastQueue.remove(index); //已經顯示完畢的Toast,從列表中移除掉
  ...
  if (mToastQueue.size() > 0) { //如果還有待顯示Toast
    // 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.
    showNextToastLocked();
  }
}

該方法呼叫TN的hide方法隱藏掉Toast,然後再將Toast從列表中移除。看看隱藏的過程:

case HIDE: {
  handleHide();
  // Don't do this in handleHide() because it is also invoked by
  // handleShow()
  mNextView = null; //這裡會把view清掉
  break;
}

public void handleHide() {
    if (localLOGV) Log.v(TAG,"HANDLE HIDE: " + this + " mView=" + mView);
    if (mView != null) {
      ...
      mWM.removeViewImmediate(mView);
      ...
      mView = null;
    }
}

隱藏的過程,其實也簡單,將view從視窗中移除,然後將mNextView和mView置Null。

到此Toast的顯示和隱藏已經講完。下面說說多次show為什麼會導致Toast消失。

Toast的消失

想象一個場景,如果一個全域性Toast(此次出問題的app中就是一個全域性Toast),我們不斷的去呼叫Toast的show方法,那麼就意味著上文說的mToastQueue列表不為空,存在Toast,就會走到如下分支:

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) {
    record = mToastQueue.get(index);
    record.update(duration);
    try {
      record.callback.hide(); //如果存在已經顯示的Toast,這裡會先進行hide
    } catch (RemoteException e) {
    }
    record.update(callback);
  }
}

hide的流程我們已經清楚,會將資源釋放,將mNextView和mView置為Null。執行到這裡會導致第一個Toast消失,之後呼叫showNextToastLocked()方法顯示第二個Toast,最終呼叫到TN的handleShow方法:

public void handleShow(IBinder windowToken) {
  // ...
  if (mView != mNextView) {
    // ...
    mView = mNextView;
    // ...
    mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    // ...
    mWM.addView(mView,mParams);
    // ...
  }
}

由於所有的Toast都對應一個TN物件,因此此時mView和mNextView均為null,不會執行mWM.addView(),Toast也就不會顯示。

解決方法

在Android9.0中如果想要一直顯示某個Toast,怎麼做?使用區域性Toast,不要使用全域性Toast。

但有一點比較奇怪的是,查看了Android10.0程式碼,發現Android10.0將這個機制回滾了。即Android10.0上又可以一直顯示Toast:

//這裡就不執行hide的操作了
if (index >= 0) {
  record = mToastQueue.get(index);
  record.update(duration);
}

結語

Android多個系統版本中,唯獨Android9.0做了這個特殊處理,無非就是禁用應用長時間顯示Toast。但10.0版本又取消了這個處理,難道是發現這樣處理並不合適?

到此這篇關於Android9.0上針對Toast的特殊處理的文章就介紹到這了,更多相關Android9.0對Toast的特殊處理內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!