凉白开 2019-06-28
1.最简单的创建方法
2.源码分析
3.经典总结
4.Toast封装库介绍
5.Toast遇到的问题
Toast是没有焦点,而且Toast显示的时间有限,过一定的时间就会自动消失。
public Toast(Context context) { mContext = context; 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); }
一行代码调用,十分方便,但是这样存在一种弊端。
Toast.makeText(this,"吐司",Toast.LENGTH_SHORT).show();
为了解决1.2中的重复创建问题,则可以这样解决
/** * 吐司工具类 避免点击多次导致吐司多次,最后导致Toast就长时间关闭不掉了 * 注意:这里如果传入context会报内存泄漏;传递activity..getApplicationContext() * @param content 吐司内容 */ private static Toast toast; @SuppressLint("ShowToast") public static void showToast(String content) { checkContext(); if (toast == null) { toast = Toast.makeText(mApp, content, Toast.LENGTH_SHORT); } else { toast.setText(content); } toast.show(); }
这样用的原理
在构造方法中,创建了NT对象,那么有人便会问,NT是什么东西呢?于是带着好奇心便去看看NT的源码,可以发现NT实现了ITransientNotification.Stub,提到这个感觉是不是很熟悉,没错,在aidl中就会用到这个。
public Toast(Context context) { mContext = context; 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); }
在TN类中,可以看到,实现了AIDL的show与hide方法
/** * 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); }
接着看下这个ITransientNotification.aidl文件
/** @hide */ oneway interface ITransientNotification { void show(); void hide(); }
通过AIDL(Binder)通信拿到NotificationManagerService的服务访问接口,然后把TN对象和一些参数传递到远程NotificationManagerService中去
public void show() { if (mNextView == null) { throw new RuntimeException("setView must have been called"); } //通过AIDL(Binder)通信拿到NotificationManagerService的服务访问接口,当前Toast类相当于上面例子的客户端!!!相当重要!!! INotificationManager service = getService(); String pkg = mContext.getOpPackageName(); TN tn = mTN; tn.mNextView = mNextView; try { //把TN对象和一些参数传递到远程NotificationManagerService中去 service.enqueueToast(pkg, tn, mDuration); } catch (RemoteException e) { // Empty } }
接着看看getService方法
//远程NotificationManagerService的服务访问接口 private static INotificationManager sService; static private INotificationManager getService() { //单例模式 if (sService != null) { return sService; } //通过AIDL(Binder)通信拿到NotificationManagerService的服务访问接口 sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification")); return sService; }
接下来看看service.enqueueToast(pkg, tn, mDuration)这段代码,相信有的小伙伴会质疑,这段代码报红色,如何查看呢?
synchronized (mToastQueue) { int callingPid = Binder.getCallingPid(); long callingId = Binder.clearCallingIdentity(); try { ToastRecord record; int index; //判断是否是系统级别的吐司 if (!isSystemToast) { index = indexOfToastPackageLocked(pkg); } else { index = indexOfToastLocked(pkg, callback); } if (index >= 0) { record = mToastQueue.get(index); record.update(duration); record.update(callback); } else { //创建一个Binder类型的token对象 Binder token = new Binder(); //生成一个Toast窗口,并且传递token等参数 mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY); record = new ToastRecord(callingPid, pkg, callback, duration, token); //添加到吐司队列之中 mToastQueue.add(record); //对当前索引重新进行赋值 index = mToastQueue.size() - 1; } //将当前Toast所在的进程设置为前台进程 keepProcessAliveIfNeededLocked(callingPid); if (index == 0) { //如果index为0,说明当前入队的Toast在队头,需要调用showNextToastLocked方法直接显示 showNextToastLocked(); } } finally { Binder.restoreCallingIdentity(callingId); } }
接下来看一下showNextToastLocked()方法中的源代码,看看这个方法中做了什么……
如果你仔细一点,你可以看到在handleShow(IBinder windowToken)这个方法中,将windowToken赋值给mParams.token,那么就会思考这个token是干什么用的呢?它是哪里传递过来的呢?
接下来再来看看scheduleTimeoutLocked(record)这部分代码,这个主要是超时监听消息逻辑
既然发送了消息,那肯定有地方接收消息并且处理消息呀。接着看下面代码,重点看cancelToastLocked源码!
cancelToastLocked源码逻辑主要是
当创建TN对象的时候,就创建了handler和runnable对象。
同时,当toast执行show之后,过了一会儿会自动销毁,那么这又是为啥呢?那么是哪里调用了hide方法呢?
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; } }
如何判断是否是系统吐司呢?如果当前Toast所属的进程的包名为“android”,则为系统Toast,或者调用isCallerSystem()方法
final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
接着看看isCallerSystem()方法源码,isCallerSystem的源码也比较简单,就是判断当前Toast所属进程的uid是否为SYSTEM_UID、0、PHONE_UID中的一个,如果是,则为系统Toast;如果不是,则不为系统Toast。
private static boolean isUidSystem(int uid) { final int appid = UserHandle.getAppId(uid); return (appid == Process.SYSTEM_UID || appid == Process.PHONE_UID || uid == 0); } private static boolean isCallerSystem() { return isUidSystem(Binder.getCallingUid()); }
为什么要这样判断是否是系统吐司呢?从源码可知:首先系统Toast一定可以进入到系统Toast队列中,不会被黑名单阻止。然后系统Toast在系统Toast队列中没有数量限制,而普通pkg所发送的Toast在系统Toast队列中有数量限制。
记得以前昊哥问我,为何toast在activity销毁后仍然会弹出呢,我毫不思索地说,因为toast是系统级别的呀。那么是如何实现的呢,我就无言以对呢……今天终于可以回答呢!
具体可以参考我的弹窗封装库:https://github.com/yangchong2...
//判断是否有权限 NotificationManagerCompat.from(context).areNotificationsEnabled() //如果没有通知权限,则直接跳转设置中心设置 @SuppressLint("ObsoleteSdkInt") private static void toSetting(Context context) { Intent localIntent = new Intent(); localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if (Build.VERSION.SDK_INT >= 9) { localIntent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS"); localIntent.setData(Uri.fromParts("package", context.getPackageName(), null)); } else if (Build.VERSION.SDK_INT <= 8) { localIntent.setAction(Intent.ACTION_VIEW); localIntent.setClassName("com.android.settings", "com.android.setting.InstalledAppDetails"); localIntent.putExtra("com.android.settings.ApplicationPkgName", context.getPackageName()); } context.startActivity(localIntent); }
为了避免静态toast对象内存泄漏,固可以使用应用级别的上下文context。所以这里我就直接采用了应用级别Application上下文,需要在application进行初始化一下。即可调用……
//初始化 ToastUtils.init(this); //可以自由设置吐司的背景颜色,默认是纯黑色 ToastUtils.setToastBackColor(this.getResources().getColor(R.color.color_7f000000)); //直接设置最简单吐司,只有吐司内容 ToastUtils.showRoundRectToast("自定义吐司"); //设置吐司标题和内容 ToastUtils.showRoundRectToast("吐司一下","他发的撒经济法的解放军"); //第三种直接设置自定义布局的吐司 ToastUtils.showRoundRectToast(R.layout.view_layout_toast_delete); //或者直接采用bulider模式创建 ToastUtils.Builder builder = new ToastUtils.Builder(this.getApplication()); builder .setDuration(Toast.LENGTH_SHORT) .setFill(false) .setGravity(Gravity.CENTER) .setOffset(0) .setDesc("内容内容") .setTitle("标题") .setTextColor(Color.WHITE) .setBackgroundColor(this.getResources().getColor(R.color.blackText)) .build() .show();
因为看到网上有许多toast的封装,需要传递上下文,后来感觉是不是不需要传递这个参数,直接统一初始化一下就好呢。所以才有了这个toast的改良版。
/** * 检查上下文不能为空,必须先进性初始化操作 */ private static void checkContext(){ if(mApp==null){ throw new NullPointerException("ToastUtils context is not null,please first init"); } }
报错日志,是不是有点眼熟呀?更多可以看我的开源项目:https://github.com/yangchong211
android.view.WindowManager$BadTokenException Unable to add window -- token android.os.BinderProxy@7f652b2 is not valid; is your activity running?
查询报错日志是从哪里来的
发生该异常的原因
Toast.makeText(this,"潇湘剑雨-yc",Toast.LENGTH_SHORT).show(); try { Thread.sleep(20000); } catch (InterruptedException e) { e.printStackTrace(); }
解决办法,目前见过好几种,思考一下那种比较好……
第二种,抛出异常增加try-catch,代码如下所示,最后仍然无法解决问题
哪些情况会发生该问题?
先来看看问题代码,会出现什么问题呢?
new Thread(new Runnable() { @Override public void run() { ToastUtils.showRoundRectToast("潇湘剑雨-杨充"); } }).start();
然后找找报错日志从哪里来的
子线程中吐司的正确做法,代码如下所示
new Thread(new Runnable() { @Override public void run() { Looper.prepare(); ToastUtils.showRoundRectToast("潇湘剑雨-杨充"); Looper.loop(); } }).start();
得出的结论
需要注意:WindowManager检查当前窗口的token是否有效,如果有效,则添加窗口展示Toast;如果无效,则抛出异常,会发生5.1这种类型的异常。