Android Toast原理及显示流程(基于Android9.
2019-05-23 本文已影响0人
android_coder
本文主要是研究下Toast是怎么显示出来的,讲解下其整个流程,本文基于Android9.0
Toast的基本用法
1:首先我们来看一下Toast的弹出的写法:
Toast.makeText(MainActivity.this,"test",Toast.LENGTH_LONG).show();
2:Toast的makeText方法
我们先来看下这个方法主要做了哪些处理
makeText是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);------------------->创建一个toast对象,应用端的Toast对象
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);---------->message控件
tv.setText(text);-------------------------------------------------------------------------------------------->文本设置
result.mNextView = v;
result.mDuration = duration;
return result;
}
调用的是同名的方法
Toast构造方法
public Toast(Context context) {
this(context, null);
}
/**
* 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);--------------->创建一个TN对象
mTN.mY = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.toast_y_offset);------------------>y轴方向的偏移量
mTN.mGravity = context.getResources().getInteger(
com.android.internal.R.integer.config_toastDefaultGravity);
}
我们可以看到一个toast对应一个TN对象
TN对象
private static class TN extends ITransientNotification.Stub {
@UnsupportedAppUsage
private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
private static final int SHOW = 0;
private static final int HIDE = 1;
private static final int CANCEL = 2;
final Handler mHandler;
@UnsupportedAppUsage
int mGravity;
int mX;
@UnsupportedAppUsage
int mY;
float mHorizontalMargin;
float mVerticalMargin;
@UnsupportedAppUsage
View mView;
@UnsupportedAppUsage
View mNextView;
int mDuration;
WindowManager mWM;
String mPackageName;
static final long SHORT_DURATION_TIMEOUT = 4000;
static final long LONG_DURATION_TIMEOUT = 7000;
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;
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.myLooper();
if (looper == null) {
throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");
}
}
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;
}
}
}
};
}
我们可以看到TN是一个binder对象,控制着toast的显示,隐藏,取消操作,并且toast是一个系统窗口,类型为toast
Toast的show方法
调用了makeText创建了一个Toast对象和TN对象,我们接着看下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);---------->调用enqueueToast方法,传递了包名和tb对象以及时长
} catch (RemoteException e) {
// Empty
}
}
NotificationManagerService的enqueueToast的实现
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) {------------------->ToastQueue队列
int callingPid = Binder.getCallingPid();----------->调用的进程id
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;---------------->创建一个服务端的Toast对象ToastRecord对象
int index = indexOfToastLocked(pkg, callback);--------->根据包名来查找当前在toast队列中的位置,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++;
//如果调用者不是system或是Phone用户,或者包名不是android,当前的toast队列中最多只允许50个toast
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
Slog.e(TAG, "Package has already posted " + count
+ " toasts. Not showing more. Package=" + pkg);
return;
}
}
}
}
//创建一个binder对象,toekn对象传递给wms,以便wms可以管理toast对应的窗口
Binder token = new Binder();
mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
record = new ToastRecord(callingPid, pkg, callback, duration, token);
mToastQueue.add(record);-------------------------------->加入到Toast队列中
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();-------------------->显示toast
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
NotificationManagerService之showNextToastLocked
void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);-------------------->取最近添加到toast队列中的toast对象
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);
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;
}
}
}
}
进入到TN的show方法
TN之show方法
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();-------->发送到对应的线程中
}
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW: {
IBinder token = (IBinder) msg.obj;-------------->在NotificationmanagerService端创建的token对象
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;
}
}
}
};
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;
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);-------------------->最后调用windowmanager的addView方法添加到窗口管理器
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}
这样toast就显示出来了