本文主要是介绍NMS Toast,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
0x00 NMS Toast
Toast.makeText(Context, "Toast message content.", Toast.LENGTH_SHORT).show();
以下代码分析基于Android 8.1.0
0x01 Toast
Toast类只有500多行,逻辑比较简单,主要有三部分组成: Toast,INotificationManager和TN。Toast类负责构造Toast对象;NotificationManager 负责与 NotificationManagerService交互;TN负责Toast最终的显示。
首先构建一个Toast对象:
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,@NonNull CharSequence text, @Duration int duration) {// 创建一个Toast对象Toast result = new Toast(context, looper);// 加载布局,设置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);tv.setText(text);// 将加载的View设给Toastresult.mNextView = v;result.mDuration = duration;return result;
}
调用Toast.show()方法:
public void show() {// mNextView为最终要展示的View,不能为null,Toast创建的时候默认创建一个,也可通过Toast.setView(View view)设置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 {// 将TN发给NotificationManagerServiceservice.enqueueToast(pkg, tn, mDuration);} catch (RemoteException e) {// Empty}
}
几行代码,获取 INotificationManager 服务。
// =======================================================================================
// All the gunk below is the interaction with the Notification Service, which handles
// the proper ordering of these system-wide.
// =======================================================================================private static INotificationManager sService;static private INotificationManager getService() {if (sService != null) {return sService;}sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));return sService;
}
看到 INotificationManager.Stub.asInterface() 很自然的会去搜 NotificationManagerService,嗯嗯,AOSP
中全局搜索就可以了。至此,Toast的工作做完一半,下来的工作进入 NotificationManagerService。
0x02 NotificationManagerService
frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
我们来看enqueueToast():
...
final ArrayList<ToastRecord> mToastQueue = new ArrayList<>();
...
private final IBinder mService = new INotificationManager.Stub() {// Toasts// ============================================================================@Overridepublic 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) {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 timeif (!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) {// Toast已经存在record = mToastQueue.get(index);record.update(duration);record.update(callback);} else {// Toast不存在Binder token = new Binder();// 将这个token添加到系统,否则无法正常显示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);}}}@GuardedBy("mToastQueue")void showNextToastLocked() {// 获取要展示的ToastToastRecord record = mToastQueue.get(0);while (record != null) {if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);try {// 显示Toast,这个callback是上面的TN对象,可以看到,这个时候又调回了Toast.TN.show(IBinder windowToken)record.callback.show(record.token);// 处理Toast显示时间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 dieint index = mToastQueue.indexOf(record);if (index >= 0) {mToastQueue.remove(index);}keepProcessAliveIfNeededLocked(record.pid);if (mToastQueue.size() > 0) {record = mToastQueue.get(0);} else {record = null;}}}}@GuardedBy("mToastQueue")private void scheduleTimeoutLocked(ToastRecord r){// 从这里可以看到,是依赖Handler实现的mHandler.removeCallbacksAndMessages(r);Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);// 这里定义Toast的显示时间,LONG:3.5s, SHORT:2s。注意区分显示时的hideTimeoutMillisecondslong delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;mHandler.sendMessageDelayed(m, delay);}
...
};
0x03 Toast.TN
再回到Toast里,这次看里面的TN,从show(IBinder windowToken)开始。
private static class TN extends ITransientNotification.Stub {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;int mGravity;int mX, mY;float mHorizontalMargin;float mVerticalMargin;View mView;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;// 子线程显示Toast就会抛这个异常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) {@Overridepublic void handleMessage(Message msg) {switch (msg.what) {case SHOW: {// (2) 拿到token,展示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;}}}};}/*** (1) schedule handleShow into the right thread*/@Overridepublic void show(IBinder windowToken) {if (localLOGV) Log.v(TAG, "SHOW: " + this);// 发送显示Toast的MessagemHandler.obtainMessage(SHOW, windowToken).sendToTarget();}/*** schedule handleHide into the right thread*/@Overridepublic void hide() {if (localLOGV) Log.v(TAG, "HIDE: " + this);mHandler.obtainMessage(HIDE).sendToTarget();}/*** hide 和 cancel逻辑相同*/public void cancel() {if (localLOGV) Log.v(TAG, "CANCEL: " + this);mHandler.obtainMessage(CANCEL).sendToTarget();}/*** (3)真正展示Toast的地方,最终还是WindowManager.addView()* @param windowToken 展示Toast的token*/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;}// Toast没有显示过或者当前不再显示状态if (mView != mNextView) {// remove the old view if necessary// 移除之前的ViewhandleHide();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// 以下配置Toast的显示参数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;// 要显示,token是必须的mParams.token = windowToken;// 这种方法可以判断当前View是否正在显示,如果显示就remove掉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 */}}}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 userAccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);event.setClassName(getClass().getName());event.setPackageName(mView.getContext().getPackageName());mView.dispatchPopulateAccessibilityEvent(event);accessibilityManager.sendAccessibilityEvent(event);}/*** hide逻辑比较简单,直接remove掉就可以了*/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被置空mView = null;}}
}
至此,分析完毕。
0x03 疑问
- Toast为什么设计成一个远程调用?
- Toast和Notification什么关系?
这两个问题需要站在系统的角度考虑。简单谈谈我的理解。Toast和Notification都是系统提供的展示通知的基础组件,所有的APP都可以调用。所有的APP都可以调用,那系统就需要有一定的控制权,不然可能会被滥用。有些ROM显示Toast是需要系统授权的从侧面印证了这点。
0x04 答疑
- Toast能改显示时间吗?
不可以,Toast的显示时间由系统决定,使用者只能从Toast.LENGTH_SHORT(4s)/Toast.LENGTH_LONG(7s)两种方式中选择一种。
- 能不能改显示动画?
不可以,Toast.getWindowParams()为hide方法,不可以直接调用。
- 创建Toast时对传入的Context有要求吗?
这个context有两个作用,inflate出Toast要展示的内容和获取包名等信息。这两个操作对Context都无特殊要求,Application,Activity,Service,BroadcastReceiver的Context都可以。
- 如果我有两个屏幕,可以在第二个屏幕上显示Toast吗?
显示Toast的Token在创建时使用的DEFAULT_DISPLAY,因此不可以显示在第二块屏幕上。
0xFF 参考
- https://blog.csdn.net/lmj623565791/article/details/40481055
这篇关于NMS Toast的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!