1、Toast概念和問題的引出
Toast 中文名"土司",應該算是 Android 使用頻率很高的一個 widget 了,一般使用它來做一些操作的提示信息,例如我們在淘寶點擊收藏寶貝之后底部就會出現"收藏成功"這樣的提示信息,這種就是 Toast 的功能了。
先拋出一個問題哦:那就是多個應用或者多個線程都要在顯示 Toast 的時候系統是怎么去處理這個問題的?是串行執行還是并行執行呢?
2、如何使用 Toast
Toast 的使用可以說是非常簡單,簡單到一句話就可以顯示一個 Toast 了,下面是具體的代碼體現。
Toast.makeText(this, "hello Toast", Toast.LENGTH_SHORT).show();
3、構造 Toast 對象
下面就依據上面的那句代碼分析 Toast 顯示到手機屏幕上是需要經過什么過程。
主要入口為兩個:
- public static Toast makeText(Context context, CharSequence text, int duration)
- public void show()
3.1、makeText 源碼分析
makeText 方法內部主要做了這樣幾件事:
- 創建一個 Toast 對象;
- 加載一個系統布局,這個布局就是用顯示這個 Toast 的;
- 設置需要顯示的文本內容,就是我們在上面設置的 "hello Toast" 和設置這個 Toast 需要顯示的時間;
其實就根據傳入到 makeText 內部的幾個屬性構造出一個 Toast 對象。
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;
}
3.2、Toast 構造方法
Toast 的構造方法內部很簡單,主要是創建一個 TN 對象,并且賦值給 mTN,這個 TN 對象非常重要,在下面分析中再慢慢說明它的作用。因為 TN 對象 是在 Toast 構造中創建的,因此一個 Toast 對象的創建就對應創建一個 TN 對象,這句話也很重要。
public Toast(Context context) {
mContext = context;
//創建一個 TN 對象
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);
}
4、TN 類的分析
看 TN 類先看它的聲明,學過 AIDL 的同學應該都知道,TN 繼承了 ITransientNotification.Stub 類,說明它是遠程服務真正干活的類。這什么意思呢?其實是這樣的,當客戶端需要顯示一個土司,那么這個要求是要交給遠程服務 NotificationManagerService 去統一調度的【這里是第一次 IPC 過程:用戶進程訪問服務進程 NotificationManagerService 】。
而 NotificationManagerService 調度完成之后是怎么通知用戶進程去 show / hide 土司的呢?遠程服務進程通過 mTN 與用戶進程(當前進程)進行交互(show,hide等操作)【這里是第二次 IPC 過程:服務進程 NotificationManagerService 訪問 用戶進程 TN】。
private static class TN extends ITransientNotification.Stub {
final Runnable mHide = new Runnable() {
@Override
public void run() {
handleHide();
// Don't do this in handleHide() because it is also invoked by handleShow()
mNextView = null;
}
};
private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams()
final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
IBinder token = (IBinder) msg.obj;
handleShow(token);
}
};
int mGravity;
int mX, mY;
float mHorizontalMargin;
float mVerticalMargin;
View mView;
View mNextView;
int mDuration;
WindowManager mWM;
static final long SHORT_DURATION_TIMEOUT = 5000;
static final long LONG_DURATION_TIMEOUT = 1000;
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;
}
/**
* schedule handleShow into the right thread
*/
@Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(0, windowToken).sendToTarget();
}
/**
* schedule handleHide into the right thread
*/
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.post(mHide);
}
public void handleShow(IBinder windowToken) {
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().getConfigura
final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDi
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;
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);
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
}
}
private void trySendAccessibilityEvent() {
AccessibilityManager accessibilityManager =
AccessibilityManager.getInstance(mView.getContext());
if (!accessibilityManager.isEnabled()) {
return;
}
// treat toasts as notifications since they are used to
// announce a transient piece of information to the user
AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
event.setClassName(getClass().getName());
event.setPackageName(mView.getContext().getPackageName());
mView.dispatchPopulateAccessibilityEvent(event);
accessibilityManager.sendAccessibilityEvent(event);
}
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);
}
mView = null;
}
}
}
5、顯示一個Toast
在 show 方法中首先判斷 mNextView 是否為空,這個 mNextView 就是加載出來需要展示 Toast 的布局啦,如果連布局都沒有,那么顯示什么土司,直接就 throw 了。
獲取包名 pkg ,為什么需要包名呢,因為在遠程服務它會拿這個包名去判斷需要顯示的 Toast 是不是來至同一個進程的(一個進程對應一個包名),具體的判斷過程待會再分析。
給 tn 賦值 mTN ,還記得這個 mTN 不?他就是在 Toast.makeText 內部方法中創建的(也就是在Toast構造中創建),我在前面說過一個 Toast 對象對應一個 mTN 對象。
調用 IPC 通知遠程服務 NotificationServiceManager 去enqueueToast,這個過程非常重要。我們看到它將一個 tn 作為參數傳入,這是干嘛用呢?我們先前在介紹 TN 這個類時說過,這個類是用于進程間通訊的,因為顯示和隱藏 Toast 是需要由遠程的 NMS 去統一調度的,它內部維護一個集合存放一個個 Toast ,當到了指定時間可以顯示這個 Toast 了,那么 NMS 就可以通過 TN 這個代理類去通知客戶端去真正顯示一個 Toast 。
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
//獲取遠程服務 INotificationManager
INotificationManager service = getService();
//獲取包名 pkg
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
5.1、INotificationManager.Stub.Proxy.enqueueToast
service.enqueueToast(pkg, tn, mDuration) 這個方法的調用實際上是通過
INotificationManager的代理對象去執行的,具體執行代碼就在下面:主要就是將數據寫入到 Parcel _data 中,然后調用 mRemote.transact 進行 RPC 遠程調用操作,之后通過 _reply 獲取返回的數據。
//INotificationManager.Stub.Proxy.enqueueToast 代碼
public void enqueueToast(java.lang.String pkg, android.app.ITransientNotification callback, int duration) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeString(pkg);
//callback就是 tn 對象,這里需要傳遞給遠程服務,因此轉化為 Bidner
_data.writeStrongBinder((((callback!=null))?(callback.asBinder()):(null)));
_data.writeInt(duration);
mRemote.transact(Stub.TRANSACTION_enqueueToast, _data, _reply, 0);
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}
6、開始遠程服務調用 Toast
[ NMS 的源碼] (https://android.googlesource.com/platform/frameworks/base/+/f76a50c/services/java/com/android/server/NotificationManagerService.java) 有一個網站 grepcode 是可以查閱源碼的,有需要可以上這里看看源碼。
在 enqueueToast 方法內部主要做了這幾件事:
- synchronize 加鎖判斷,這里為什么要加鎖?
- indexOfToastLocked校驗操作;
- 根據 indexOfToastLocked 的校驗結果進行后續的操作。
public void enqueueToast(String pkg, ITransientNotification callback, int duration) {
//pkg 和 callback 的空校驗 代碼省略...
//加鎖判斷
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
//查找隊列中是否由該 ToastRecord
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.
//表示在隊列中找到對應的 ToastRecord
if (index >= 0) {
//更新這個 ToastRecord 的顯示時間,這種情況是在同一個用用中,使用同一個 Toast 對象,在極短時間內多次調用show方法,就會出現這個情況。
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 (!"android".equals(pkg)) {//不是系統彈的土司
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++;
//控制 toast 的次數 50
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
Slog.e(TAG, "Package has already posted " + count
+ " toasts. Not showing more. Package=" + pkg);
return;
}
}
}
}
//沒有找到的情況就創建一個 ToastReord 對象。
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();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
6.1、enqueueToast 加鎖判斷
這里要思考一下為什么這里要加鎖?我們知道鎖就是為了怕并發訪問出現數據的不同步問題,NMS 是系統服務,那么它的對象就只有一個,多個進程(多個 APP) 或者說這多個線程都可以獲取到這個服務,并且可以并發地去調用該服務的 enqueueToast 方法,這就像多個線程操作同一個對象的共享變量出現的數據不同步問題了,因此這里需要加鎖。還有就是 enqueueToast 方法是服務端的方法,它會在Binder 線程池中去調用。
6.2、indexOfToastLocked的校驗操作
檢測當前pag 包名和 callback 是否在 mToastQueue 中已經存在?在分析之前首先了解一下 callback 是什么呢?我們在先前創建 Toast 構造提到過,一個 Toast 對象會對應一個 mTN 對象。這個 callback 就是這個 mTN 對象的代理對象,為什么?因為它是需要進行進程間通訊的,因此需要將 mTN 對象轉化為 Binder 才能傳輸,而在 NMS 中獲取到這個 Binder 之后會將其轉化為 ITransientNotification.Proxy 代理對象。
該方法會返回一個整數,-1表示沒有找到,其他值表示找到了。
private 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);
//包名和binder都一樣時才表示是同一個 ToastRecord
if (r.pkg.equals(pkg) && r.callback.asBinder() == cbak) {
return i;
}
}
return -1;
}
6.3、根據 indexOfToastLocked 的校驗結果進行后續的操作。
1.如果找到了那么返回值不為-1,那么就直接更新一下 duration 并且不會去修改該 ToastRecord 為 mToastQueue 中的位置。意思是什么呢?這里舉個栗子:這里 for 循環 500 次,每次都調用 toast.show() 方法,其運行結果只會顯示一次,為什么呢?因為我們在上面提到過,使用 indexOfToastLocaked 內部是根據 pkg 和 callback 進行判斷的,而這兩個值是一樣的,因此該方法返回的值不為 -1 ,因此就算是 show 500 次其 mToastQueue 中也只有一個 ToastRecord 對象。
Toast toast = Toast.makeText(this, "Toast", Toast.LENGTH_SHORT);
for (int i = 0; i < 500; i++) {
toast.show();
}
2.如果找到的值為-1表示需要創建一個新的 ToastRecord 并且添加到 mToastQueue 中。因為之前得到的 index 為 -1 ,因此在這里需要重新計算 index = mToastQueue.size()-1,這個 index 就表示當前新創建 ToastRecord 在隊列中的位置,當這個位置為 0 時表示只有隊列中只有一個 ToastRecord ,直接執行 showNextToastLocked 即可。
3.注意這里,如果沒有知道的情況,并且同一個 pkg 下的 ToastRecord 的數量超過了 MAX_PACKAGE_NOTIFICATIONS 那么就直接 return 了,官方表示這是為了避免 DOS attacks。
4.這里注意下面這一種情況,執行結果是 toast 會執行 50 次,多余都被拋棄了,原因看上面第 3 點。但是重點這種寫法跟第 1 點的寫法的區別就是 Toast 的創建時機不一樣,這里是每循環一次就創建一個,也就是 Toast 的構造就會走一次,還記得上面說過 mTN 是在 Toast 構造創建的,每次調用 makeText 都會創建一個新的 Toast 對象,也就是 mTN 也是一個新的對象。那么傳遞給 NMS 的 mTN 就不一樣了,那么在進行 indexOfToastLocked 就會返回 -1 因此這里就是這段代碼的區別。
for (int i = 0; i < 500; i++) {
Toast toast = Toast.makeText(this, "Toast", Toast.LENGTH_SHORT).show();
}
7、遠程服務端調度顯示和隱藏操作
7.1、showNextToastLocked()
這個方法主要做了一下幾件事:
從隊列中取出一個 ToastRecord 并且通過 IPC 調用用戶進程的 TN 中的 show 方法真正去顯示一個 Toast 。
通過 Handler 發送一個延時任務,時間為 duration ,在這個時間過后就取消顯示這個 Toast。
private void showNextToastLocked() {
//從隊列中取出第一個 ToastRecord 對象
ToastRecord record = mToastQueue.get(0);
//while 循環是怕應用進程出現異常 RemoteException
while (record != null) {
if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
try {
//通知應用進程去執行顯示操作,實際上會調用 TN 的 show() 方法。
record.callback.show();
//在指定時間隱藏這個 Toast
scheduleTimeoutLocked(record, false);
return;
} catch (RemoteException e) {
//出現異常的情況,那么就從隊列中移除
// remove it from the list and let the process die
int index = mToastQueue.indexOf(record);
if (index >= 0) {
mToastQueue.remove(index);
}
keepProcessAliveLocked(record.pid);
//從隊列中再取出下一個 ToastRecord 去執行。
if (mToastQueue.size() > 0) {
record = mToastQueue.get(0);
} else {
//如果沒有就置null,退出循環
record = null;
}
}
}
}
7.2、record.callback.show();
record.callback 就是用戶進程傳遞過來服務進程的 ITransientNotification 的代理對象,通過調用該代理對象的 show() 方法,這個過程是一個 IPC 的過程,這個最終會調用用戶進程的 TN 中的 show() 方法。
public void show() {
//通過 handler 去 post 一個任務
mHandler.post(mShow);
}
final Runnable mShow = new Runnable() {
public void run() {
handleShow();
}
};
//通過 WindowManager 將 View 顯示出來
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;
mWM = WindowManagerImpl.getDefault();
final int gravity = mGravity;
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;
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();
}
}
7.3、 scheduleTimeoutLocked
使用 Handler 發送一個延時消息,時間就是 Toast 設置的 duration 屬性。
private void scheduleTimeoutLocked(ToastRecord r, boolean immediate)
{
Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
long delay = immediate ? 0 : (r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY);
mHandler.removeCallbacksAndMessages(r);
mHandler.sendMessageDelayed(m, delay);
}
7.4、 handleTimeout
private final class WorkerHandler extends Handler
{
@Override
public void handleMessage(Message msg)
{
switch (msg.what)
{
case MESSAGE_TIMEOUT:
handleTimeout((ToastRecord)msg.obj);
break;
}
}
}
private void handleTimeout(ToastRecord record)
{
synchronized (mToastQueue) {
int index = indexOfToastLocked(record.pkg, record.callback);
if (index >= 0) {
cancelToastLocked(index);
}
}
}
7.5、cancelToastLocked
當通過 Handler 發送的延時任務的時間到了那么就需要通知應用進程去 hide 這個 Toast 。具體操作還是通過 IPC 過程去真正調用 TN 的 hide 方法實現真正的隱藏操作。
當操作完成之后判斷隊列中是否還有 ToastRecord 沒有執行,如果有那么就調用 showNextToastLocked 方法再次的去取出 ToastRecord 去執行。
private void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
try {
//遠程調用應用進程的 TN 的 hide() 方法隱藏 toast
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
}
mToastQueue.remove(index);
keepProcessAliveLocked(record.pid);
//如果隊列中還有數據那么就再次調用 showNextToastLocked 方法流程跟上面是一樣的。
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.
showNextToastLocked();
}
}