今天看了一下Toast的原始碼,或者如果你對AIDL感興趣,可以看下去。
Toast不會同一時間顯示多個,好像所有Toast都是排隊了一樣,而且不同的App都是排的一個佇列一樣。是的,這裡有一個佇列。而且因為不同App都是使用這一個佇列,這裡就用到了AIDL跨程式通訊。
跨程式有一個C/S的概念,就是服務端和客戶端的概念,客戶端呼叫服務端的服務,服務端把結果返回給客戶端。
顯示一個Toast有2個過程:
-
1.一個toast包裝好,去佇列裡排隊。
這個過程,佇列是服務端,toast.show()是客戶端。 複製程式碼
-
2.佇列中排到這個Toast顯示了,就會呼叫toast去自己顯示。
這個過程,佇列是客戶端,toast.show()是服務端。 複製程式碼
想要顯示一個Toast,程式碼如下:
Toast.makeText(context, text, duration).show();
複製程式碼
先看看makeText()
方法,它只是一個準備過程,載入好佈局,然後要顯示的內容設定好,要顯示的時長設定好。結果仍是返回一個Toast物件。
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;
}
複製程式碼
在Toast result = new Toast(context, looper);
中初始化了一個TN物件mTN:
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);
}
複製程式碼
這裡把縱向座標和重力方向設定到了mTN中。
然後就是呼叫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
}
}
複製程式碼
這裡getService()
取到了服務,然後呼叫service.enqueueToast(pkg, tn, mDuration);
去排隊。
這裡的getService()實際上是拿到了一個Service的本地代理:
static private INotificationManager getService() {
if (sService != null) {
return sService;
}
sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
return sService;
}
複製程式碼
***.Stub.asInterface
就是AIDL中的一個典型的寫法。
這裡插一下AIDL的相關內容
我自己新建了一個AIDL,定義了2個方法。
// IMyTest.aidl
package top.greendami.aidl;
// Declare any non-default types here with import statements
interface IMyTest {
/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
int add(int a, int b);
String hello(String s);
}
複製程式碼
然後build一下,下面程式碼由AS自動生成,在build資料夾下面:
package top.greendami.aidl;
public interface IMyTest extends android.os.IInterface {
public static abstract class Stub extends android.os.Binder implements top.greendami.aidl.IMyTest {
private static final java.lang.String DESCRIPTOR = "top.greendami.aidl.IMyTest";
/**
* Construct the stub at attach it to the interface.
*/
public Stub() {
this.attachInterface(this, DESCRIPTOR);
}
/**
* Cast an IBinder object into an top.greendami.aidl.IMyTest interface,
* generating a proxy if needed.
*/
public static top.greendami.aidl.IMyTest asInterface(android.os.IBinder obj) {
···
}
@Override
public android.os.IBinder asBinder() {
return this;
}
@Override
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
···
}
private static class Proxy implements top.greendami.aidl.IMyTest {
···
}
static final int TRANSACTION_add = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_hello = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
}
public int add(int a, int b) throws android.os.RemoteException;
public java.lang.String hello(java.lang.String s) throws android.os.RemoteException;
}
複製程式碼
先看看上面用到的asInterface
方法:
public static top.greendami.aidl.IMyTest asInterface(android.os.IBinder obj) {
if ((obj == null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin != null) && (iin instanceof top.greendami.aidl.IMyTest))) {
return ((top.greendami.aidl.IMyTest) iin);
}
return new top.greendami.aidl.IMyTest.Stub.Proxy(obj);
}
複製程式碼
這裡是先在本地查詢看看有沒有物件,如果有就說明沒有跨程式,直接返回本地物件。如果沒有就要返回一個代理了。這裡可以看做服務端為客戶端準備一個‘假’的自己,讓客戶端看起來就像擁有一個真正的服務端物件。
Proxy(obj)
中把代理中每個方法都進行了處理,如果有add和hello兩個方法:
private static class Proxy implements top.greendami.aidl.IMyTest {
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote) {
mRemote = remote;
}
@Override
public android.os.IBinder asBinder() {
return mRemote;
}
public java.lang.String getInterfaceDescriptor() {
return DESCRIPTOR;
}
@Override
public int add(int a, int b) throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
int _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(a);
_data.writeInt(b);
mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);
_reply.readException();
_result = _reply.readInt();
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
@Override
public java.lang.String hello(java.lang.String s) throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
java.lang.String _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeString(s);
mRemote.transact(Stub.TRANSACTION_hello, _data, _reply, 0);
_reply.readException();
_result = _reply.readString();
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
}
static final int TRANSACTION_add = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_hello = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
}
複製程式碼
大致就是把需要傳遞的引數序列化,然後呼叫方法的真正實現,然後拿到返回的結果並且返回。我們看到真正的方法實現在mRemote.transact
這裡,這個Proxy就真的只是代理,和客戶端互動,傳遞一下引數而已。
在onTransact
方法中實現了真正的呼叫:
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
switch (code) {
case INTERFACE_TRANSACTION: {
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_add: {
data.enforceInterface(DESCRIPTOR);
int _arg0;
_arg0 = data.readInt();
int _arg1;
_arg1 = data.readInt();
int _result = this.add(_arg0, _arg1);
reply.writeNoException();
reply.writeInt(_result);
return true;
}
case TRANSACTION_hello: {
data.enforceInterface(DESCRIPTOR);
java.lang.String _arg0;
_arg0 = data.readString();
java.lang.String _result = this.hello(_arg0);
reply.writeNoException();
reply.writeString(_result);
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
複製程式碼
這裡呼叫的this.add(_arg0, _arg1);
和this.hello(_arg0);
就是在AIDL中定義好的介面。因為abstract class Stub
中實現了top.greendami.aidl.IMyTest
介面(當然它也是一個Bind),在top.greendami.aidl.IMyTest
介面中宣告瞭add
和hello
這兩個方法,所以這兩個方法是在實現Stub
這個類的時候實現的。
一般情況下,在Service
的onBind
方法中返回一個Stub
物件,new
這個Stub
物件的時候就實現了這兩個方法。這樣服務端就準備好了。
在客戶端呼叫的時候bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
這裡有個mServiceConnection
回撥,在回撥中public void onServiceConnected(ComponentName name, IBinder service)
可以拿到service
物件,通過***.Stub.asInterface(service)
就可以拿到代理物件,然後就可以呼叫方法了。
回到Toast
sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
複製程式碼
通過這樣,拿到了‘排隊’的服務,然後呼叫service.enqueueToast(pkg, tn, mDuration);
就去排隊了。這裡的tn
是一個TN
型別的型別,儲存了這個Toast相關資訊,包括View,顯示時長等等。同時TN
也是一個ITransientNotification.Stub
實現。這裡就是第二步的時候作為服務端被呼叫(類似回撥的作用)。
看看NotificationManagerService.java
中enqueueToast()
方法:
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
...
final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg));
final boolean isPackageSuspended =
isPackageSuspendedForUser(pkg, Binder.getCallingUid());
if (ENABLE_BLOCKED_TOASTS && !isSystemToast &&
(!areNotificationsEnabledForPackage(pkg, Binder.getCallingUid())
|| isPackageSuspended)) {
...
return;
}
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
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.
if (index >= 0) {
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 (!isSystemToast) {
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++;
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
Slog.e(TAG, "Package has already posted " + count
+ " toasts. Not showing more. Package=" + pkg);
return;
}
}
}
}
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);
}
}
}
複製程式碼
這個方法首先校驗了包名和回撥是不是空,是的話就返回(程式碼省略)。 然後看看是不是被系統禁止顯示通知的App(通過包名判斷)。
前面的校驗都通過了,就是開始排隊了synchronized (mToastQueue)
,首先是拿到程式號,然後看看這個相同的App和回撥之前有沒有在佇列中,如果在就更新一下顯示時間,如果沒在還要看看這個App有多少Toast,超過50就不讓排隊。如果這些都滿足條件就進入佇列排隊。
if (index == 0) {
showNextToastLocked();
}
複製程式碼
如果佇列裡面只有一個成員,就立馬去顯示(如果不是,就說明佇列已經在迴圈了),看看showNextToastLocked()
方法:
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);
scheduleTimeoutLocked(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) {
mToastQueue.remove(index);
}
keepProcessAliveIfNeededLocked(record.pid);
if (mToastQueue.size() > 0) {
record = mToastQueue.get(0);
} else {
record = null;
}
}
}
}
複製程式碼
顯示的就是下面這句了,
record.callback.show(record.token);
下面這句是用handler
啟用一個延時,取消顯示
scheduleTimeoutLocked(record);
。這裡的record.callback
就是之前傳進來的TN
物件了,看看Toast
中TN
的實現:
private static class TN extends ITransientNotification.Stub {
...
static final long SHORT_DURATION_TIMEOUT = 4000;
static final long LONG_DURATION_TIMEOUT = 7000;
TN(String packageName, @Nullable Looper looper) {
...
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();
mNextView = null;
break;
}
case CANCEL: {
handleHide();
mNextView = null;
try {
getService().cancelToast(mPackageName, TN.this);
} catch (RemoteException e) {
}
break;
}
}
}
};
}
/**
* schedule handleShow into the right thread
*/
@Override
public void show(IBinder windowToken) {
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
/**
* schedule handleHide into the right thread
*/
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.obtainMessage(HIDE).sendToTarget();
}
public void cancel() {
if (localLOGV) Log.v(TAG, "CANCEL: " + this);
mHandler.obtainMessage(CANCEL).sendToTarget();
}
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);
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;
mParams.token = windowToken;
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeView(mView);
}
try {
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}
...
public void handleHide() {
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;
}
}
}
複製程式碼
這裡就是簡單的Handler的呼叫了。通過addView
進行顯示。

Toast移除過程:
scheduleTimeoutLocked
傳送訊息交給Handler處理。
private void scheduleTimeoutLocked(ToastRecord r)
{
mHandler.removeCallbacksAndMessages(r);
Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
mHandler.sendMessageDelayed(m, delay);//這裡的延時就是顯示時長
}
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)
{
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);
}
}
}
private void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
try {
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);
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();
}
}
複製程式碼
兄弟都看到這兒了,不點個收藏嗎?(回覆666,有彩蛋哦!)
