总结:
- 首先Toast的make方法主要就是封装TN(Binder)这个对象,这里有几点需要注意
- Toast默认的是TextView,必须需要有个显示信息的UI,否则会报错
- 当前线程必须有相对应的Looper,否则报错,因为里面的显示机制还是跟Handler有关的,主线程是默认有MainLooper的
- show方法:其实是系统内部维护着一个队列,通过不断的从该队列中取出ToastRecord(对TN的封装),这个队列是通过AIDL进行通讯的,从Toast–通过AIDL–TN,然后再通过WidowManager显示到界面上
- 值得注意的是,我们无法对系统内部的队列进行操作。回顾整个流程,其实我们无法控制的是AIDL通讯的部分,如果我们需要完全控制Toast的话,完全可以把AIDL这部分取消掉。关于这点我们以后再单独讨论
源码分析
我们通常这样使用Toast:
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
return makeText(context, null, text, duration);
}
/**
* Make a standard toast to display using the specified looper.
* If looper is null, Looper.myLooper() is used.
* @hide
*/
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);
//这个布局就只有一个TextView
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;
}
/**
* Constructs an empty Toast object. If looper is null, Looper.myLooper() is used.
* @hide
*/
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的makeText方法其实就是先新建一个Toast对象,然后其各个赋好值,这里有个TN类:
TN extends ITransientNotification.Stub
TN(String packageName, @Nullable Looper looper) {
.......
//这里也说明了为什么直接在子线程中使用Toast会报错了,除非先执行looper.prepare
if (looper == null) {
// Use Looper.myLooper() if looper is not specified.
looper = Looper.myLooper();
if (looper == null) {
throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");
}
}
mHandler = new Handler(looper, null) {
.........
}
我们知道TN是一个继承自ITransientNotification.Stub的类,以及looper不能为空,为的是Handler的实例化,这里我们暂时只用知道他的继承关系就好了,知道其是一个Binder对象,可以用于进程间通讯,然后回到我们的makeText方法,在调用了Toast的构造方法创建了Toast对象之后,我们又通过context.getSystemService方法获取到LayoutInflater,然后通过调用LayoutInflater的inflate方法加载到了Toast的布局文件,这个布局文件其实就是一个LinearLayout+TextView,我们来看看show方法的具体实现:
/**
* Show the view for the specified duration.
*/
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
//NotificationManagerService
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
这里的INotificationManager是服务器端NotificationManagerService的Binder客户端,然后我们调用了service.enqueueToast方法
//callback也就是传进来的TN对象
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
.......
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
//是否是系统Toast
if (!isSystemToast) {
//里面实现很简单,其实就是在一个List<ToastRecord>中遍历去查找,找到第一个与pkg相同的ToastRecord,如果没有返回-1
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);
record.update(callback);
} else {
//也就是第一次把Toast加入进来,mToastQueue里面是为空
Binder token = new Binder();
mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
//这里我们知道ToastRecord也就是属性的封装
record = new ToastRecord(callingPid, pkg, callback, duration, token);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
}
keepProcessAliveIfNeededLocked(callingPid);
//如果不等于0的话就排队
if (index == 0) {
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
这里的callback就是上面说的TN对象,如果没找到,则新建ToastRecord对象:
ToastRecord(int pid, String pkg, ITransientNotification callback, int duration,
Binder token) {
this.pid = pid;
this.pkg = pkg;
this.callback = callback;
this.duration = duration;
this.token = token;
}
ToastRecord其实就是记录一些信息的类罢了,callback指的是TN这个类的对象,接着我们看看showNextToastLocked:
void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
while (record != null) {
try {
//调用TN对象的show方法
record.callback.show(record.token);
//从mToastQueue队列中移除掉record,继续处理剩下的record
scheduleDurationReachedLocked(record);
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);
}
keepProcessAliveIfNeededLocked(record.pid);
if (mToastQueue.size() > 0) {
record = mToastQueue.get(0);
} else {
record = null;
}
}
}
}
上面已经说了,callback其实就是传进来的TN对象,然后就会调用TN的show方法:
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
mHandler在TN构造函数中就定义了:
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;
}
}
}
};
}
show方法传的是SHOW,执行的是handleShow:
public void handleShow(IBinder windowToken) {
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;
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 {
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}
一开始mView默认是为null的,mNextView在Toast的makeToast中已经有默认的值,或者使用Toast对象的setView方法,而且在Toast的show方法中明确的指出mNextView为null是会抛出异常的,所以这时候mView!=mNextView,接着看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;
}
}
}
很明显,先将View从Window移除( mWM.removeViewImmediate(mView);然后释放这个View资源,接着看handleShow方法,剩下也就是配置一些属性值,然后通过WindowManager把View加到Window中,回到NotificationManagerService类法,后面还有scheduleDurationReachedLocked这个方法执行:
private void scheduleDurationReachedLocked(ToastRecord r)
{
mHandler.removeCallbacksAndMessages(r);
Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
//这里有个需要的注意点,就是Toast显示的时间长度只能有这两种情况
long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
mHandler.sendMessageDelayed(m, delay);
}
duration也就是makeToast中的时间,what=MESSAGE_TIMEOUT,将会执行的方法: handleTimeout((ToastRecord)msg.obj):
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);
}
}
}
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
}
ToastRecord lastToast = mToastQueue.remove(index);
mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY);
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.
showNextToastLocked();
}
很明显,又调回了TN的hide方法:handleHide();同时在MToastQueue移除掉该ToastRecord,如果MToastQueue的大小还大于0,则继续执行showNextToastLocked操作