凉白开 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这种类型的异常。

