彻底理解Toast原理和解决小米MIUI系统上没法弹Toast的问题
1、Toast的基本使用
Toast在Android中属于系统消息通知,用来提示用户完成了什么操作、或者给用户一个必要的提醒。Toast的官方定义是这样的:
A toast provides simple feedback about an operation in a small popup. It only fills the amount of space required for the message and the current activity remains visible and interactive.
它仅仅用作一个简单的反馈机制。使用也比较简单:
Context context = getApplicationContext();
CharSequence text = "Hello toast!";
int duration = Toast.LENGTH_SHORT; Toast toast = Toast.makeText(context, text, duration);
toast.show();
一般情况下,我们传入一个String就基本上满足大多数的需求。但要想自定义一个View,然后通过Toast进行显示,也仅仅多了设置View的操作。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/toast_layout_root"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:padding="8dp"
android:background="#DAAA"
>
<ImageView android:src="@drawable/droid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
/>
<TextView android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#FFF"
/>
</LinearLayout>
我们把这个文件命名为toast_layout.xml,然后在代码中加载它。
LayoutInflater inflater = getLayoutInflater();
View layout = inflater.inflate(R.layout.toast_layout,
(ViewGroup) findViewById(R.id.toast_layout_root)); TextView text = (TextView) layout.findViewById(R.id.text);
text.setText("This is a custom toast"); Toast toast = new Toast(getApplicationContext());
toast.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
toast.setDuration(Toast.LENGTH_LONG);
toast.setView(layout);
toast.show();
其实就是这么简单。
2、Toast原理解剖
但现实是,产品需求说你给我控制Toast显示的时间。咋一看好像也不难嘛。
不是有个setDuration方法么?当你翻看源码的时候,你会发现它的描述参数只有以下两种:
LENGTH_SHORT
LENGTH_LONG
这两个常量对应着2秒和3.5秒,你传个其它数字进入,效果并不是你所预料。其实这两个常量仅仅是个flag,并不是我们想的多少秒。官方API文档告诉我们:
This time could be user-definable.
但,它又不提供一个公开的方法让你设置。抓狂!先看一下Toast的显示和隐藏在代码层面做了什么事情。
/**
* Show the view for the specified duration.
*/
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);
} catch (RemoteException e) {
// Empty
}
}
/**
* Close the view if it's showing, or don't show it if it isn't showing yet.
* You do not normally have to call this. Normally view will disappear on its own
* after the appropriate duration.
*/
public void cancel() {
mTN.hide(); try {
getService().cancelToast(mContext.getPackageName(), mTN);
} catch (RemoteException e) {
// Empty
}
}
理解这两个方法,需要深挖getService()到底调用了那个类做enqueueToast的操作?TN类是干什么的?继续跟踪代码。
static private INotificationManager getService() {
if (sService != null) {
return sService;
}
sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
return sService;
}
看到Stub.asInterface,我们知道这是利用Binder进行跨进程调用了。而TN类就是遵循AIDL的实现。
private static class TN extends ITransientNotification.Stub
TN类内部使用Handler机制:post一个mShow和mHide:
final Runnable mShow = new Runnable() {
@Override
public void run() {
handleShow();
}
}; final Runnable mHide = new Runnable() {
@Override
public void run() {
handleHide();
// Don't do this in handleHide() because it is also invoked by handleShow()
mNextView = null;
}
};
再来看handleShow()方法的实现:
public void handleShow() {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
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;
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);
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
}
}
大概意思就是通过WindowManager的addView方法实现Toast的显示。其中trySendAccessibilityEvent()方法会把当前的类名、应用的包名通过AccessibilityManager来做进一步的分发,以供后续的处理。
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 user
AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
event.setClassName(getClass().getName());
event.setPackageName(mView.getContext().getPackageName());
mView.dispatchPopulateAccessibilityEvent(event);
accessibilityManager.sendAccessibilityEvent(event);
}
先回到前面的enqueueToast方法,看它做了什么事情。前面的INotificationManager service = getService()返回的就是NotificationManagerService,所以enqueueToast方法的最终实现在NotificationManagerService类中。
@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 = isCallerSystem() || ("android".equals(pkg)); if (ENABLE_BLOCKED_TOASTS && !noteNotificationOp(pkg, Binder.getCallingUid())) {
if (!isSystemToast) {
Slog.e(TAG, "Suppressing toast from package " + pkg + " by user request.");
return;
}
} synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
int index = indexOfToastLocked(pkg, 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++;
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
Slog.e(TAG, "Package has already posted " + count
+ " toasts. Not showing more. Package=" + pkg);
return;
}
}
}
} record = new ToastRecord(callingPid, pkg, callback, duration);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
keepProcessAliveLocked(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);
}
}
}
static final int MAX_PACKAGE_NOTIFICATIONS = 50;
static final int LONG_DELAY = 3500; // 3.5 seconds
static final int SHORT_DELAY = 2000; // 2 seconds
这段代码主要做了以下几件事情:
- 获取当前进程的Id。
- 查看这个Toast是否在队列中,有的话直接返回,并更新显示时间。
- 如果是非系统的Toast(通过应用包名进行判断),且Toast的总数大于等于50,不再把新的Toast放入队列。
- 最后通过keepProcessAliveLocked(callingPid)方法来设置对应的进程为前台进程,保证不被销毁。
- 如果index = 0,说明Toast就处于队列的头部,直接进行显示。
- 我们在NotificationManagerService类中确认了前面提到的LENGTH_SHORT和LENGTH_LONG的显示时长。
关于上述的第四点,我们通过Toast类型的定义来印证代码:
/**
* Window type: transient notifications.
* In multiuser systems shows only on the owning user's window.
*/
public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;
所以一旦应用被销毁,它对应的Toast也将不会再显示:shows only on the owning user's window. 再来看这个keepProcessAliveLocked方法:
// lock on mToastQueue
void keepProcessAliveLocked(int pid)
{
int toastCount = 0; // toasts from this pid
ArrayList<ToastRecord> list = mToastQueue;
int N = list.size();
for (int i=0; i<N; i++) {
ToastRecord r = list.get(i);
if (r.pid == pid) {
toastCount++;
}
}
try {
mAm.setProcessForeground(mForegroundToken, pid, toastCount > 0);
} catch (RemoteException e) {
// Shouldn't happen.
}
}
其中mAm是一个ActivityManagerService实例,所以调用最终进入到ActivityManagerService的setProcessForeground方法进行再次处理。下面我用一张序列图展示整个调用流程:
其中第八步的scheduleTimeoutLocked()实质上就是利用Handler延时发送一个Message,回调TN类的hide()方法,最终通过WindowManager的removeView()来隐藏之前显示的Toast。
private void scheduleTimeoutLocked(ToastRecord r)
{
mHandler.removeCallbacksAndMessages(r);
Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
mHandler.sendMessageDelayed(m, delay);
}
至此,Toast的显示和隐藏已经分析完毕。原理搞清楚了,让我们回到一开始提到的问题,如何控制Toast的显示时长?
思路1:通过反射的方式调用TN类中的show和hide方法。
代码大概像这样:
Object obj = message.obj;
Method method = obj.getClass().getDeclaredMethod("hide", null);
method.invoke(obj, null);
但是很可惜,Method method = obj.getClass().getDeclaredMethod("hide", null); 这种方法在4.0之上已经不适用了。
思路2:不让Toast进入系统队列,我们自己维护一个队列。
这种方式其实仿照一下TN类中的实现,结合LinkedBlockingQueue和WindowManager就可以了。关于如何实现,后面有相应的源码链接。
3、Toast在某些系统无法显示问题
此问题常见于小米系统。MIUI上可能是出于“绿化”的考虑,在维护Toast队列的时候,Toast只能在自己进程运行在顶端的时候才能弹出来,否则就“invisible to user”。乱改系统行为,简直丧心病狂有木有,最终苦的是广大Android开发人员。不过有了上面的理论准备,要解决也是没有问题的,参照思路2。
对于这个问题,已经有人给出了源码实现,请参考问题描述:解决小米MIUI系统上后台应用没法弹Toast的问题,Github源码地址:https://github.com/zhitaocai/ToastCompat
本来到这里就可以结束了,但笔者在实际开发中遭遇了一个小小的坑。
mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
这个坑就是上面的mContext,它必须是ApplicationContext,不然在小米3或小米Note(Android 4.4.4)无法起作用!
以上。
参考:
Toast相关源码
彻底理解Toast原理和解决小米MIUI系统上没法弹Toast的问题的更多相关文章
- 小米miui系统怎么关闭文件管理里的热门视频和表情?
小米miui系统怎么关闭文件管理里的热门视频和表情? 打开"文件管理"后,切换到"手机"选项卡. 然后,点击屏幕右上角的一排竖点. . 在弹出的菜单中点击&qu ...
- 解决windows64位系统上安装mysql-python报错
解决windows64位系统上安装mysql-python报错 2018年03月12日 13:08:24 一个CD包 阅读数:1231 版权声明:本文为博主原创文章,未经博主允许不得转载. ht ...
- 【踩坑速记】MIUI系统BUG,调用系统相机拍照可能会带给你的一系列坑,将拍照适配方案进行到底!
一.写在前面 前几天也是分享了一些学习必备干货(还没关注的,赶紧入坑:传送门),也好久没有与大家探讨技术方案了,心里也是挺痒痒的,这不,一有点闲暇之时,就迫不及待把最近测出来的坑分享给大家. 提起An ...
- MIUI系统如何获取ROOT权限
MIUI系统有么好方法启用了Root超级权限?各位都清楚,Android手机有Root超级权限,一旦手机启用了root相关权限,就能够实现更多的功能,举例子,各位公司的营销部门的同事,使用大多数营销工 ...
- Java 集合系列04之 fail-fast总结(通过ArrayList来说明fail-fast的原理、解决办法)
概要 前面,我们已经学习了ArrayList.接下来,我们以ArrayList为例,对Iterator的fail-fast机制进行了解.内容包括::1 fail-fast简介2 fail-fast示例 ...
- [转载] fail-fast总结(通过ArrayList来说明fail-fast的原理、解决办法)
说明: 转载自http://www.cnblogs.com/skywang12345/p/3308762.html概要 前面,我们已经学习了ArrayList.接下来,我们以ArrayList为例,对 ...
- Java集合系列:-----------04fail-fast总结(通过ArrayList来说明fail-fast的原理以及解决办法)
前面,我们已经学习了ArrayList.接下来,我们以ArrayList为例,对Iterator的fail-fast机制进行了解.内容包括::1 fail-fast简介2 fail-fast示例3 f ...
- ToastMiui【仿MIUI的带有动画的Toast】
版权声明:本文为HaiyuKing原创文章,转载请注明出处! 前言 仿MIUI的带有动画的Toast 效果图 代码分析 ToastMiui类基于WindowManager 为了和Toast用法保持一致 ...
- 深入理解FFM原理与实践
原文:http://tech.meituan.com/deep-understanding-of-ffm-principles-and-practices.html 深入理解FFM原理与实践 del2 ...
随机推荐
- 烂泥:mysql5.5主从同步复制配置
本文由秀依林枫提供友情赞助,首发于烂泥行天下. 在上篇文章<烂泥:学习mysql数据库主从同步复制原理>中,我们介绍了有关mysql主从复制的基本原理.在这篇文章中,我们来实际测试下mys ...
- LINUX内核笔记:自旋锁
目录 自旋锁作用与基本使用方法? 在SMP和UP上的不同表现? 自旋锁与上下文 使用spin_lock()后为什么不能睡眠? 强调:锁什么? 参考 1.自旋锁作用与基本使用方法? 与其他锁一样,自 ...
- gvim的菜单乱码解决方法
gvim的菜单乱码解决方法: (乱码是由于系统内码不兼容导致,系统内码包括gb2312 gb18030 utf-8 utf-16[unicode]等) 生成文件 ~/.gvimrc 并添加如下语句:s ...
- 设置输入域(input/textarea)中文本光标的位置
以前记录了一篇 将光标定位于输入框最右侧的实现方式 ,实现光标定位在文本的最末.这种需求往往在修改现有的文本.有时可能还需要把光标定位在首位,或者中间某个位置,这就需要实现一个更通用的方法. 这个方法 ...
- JavaScript为select添加option,select选项变化时的处理,获取slelect被选中的值
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...
- [转]Oracle 修改或者删除临时表 ORA-14452: 试图创建, 更改或删除正在使用的临时表中的索引
本文转自:http://blog.csdn.net/treasurelifelhf/article/details/7290729 由于存储过程出现问题,导致前台页面无法显示数据.执行存储过程发现临时 ...
- POJ1065Wooden Sticks[DP LIS]
Wooden Sticks Time Limit: 1000MS Memory Limit: 10000K Total Submissions: 21902 Accepted: 9353 De ...
- 关于反射blog
非常好的Java反射例子 疯狂java 在学习编程的过程中,我觉得不止要获得课本的知识,更多的是通过学习技术知识提高解决问题的能力,这样我们才能走在最前方,更多Java学习,请浏览疯狂java ...
- Zookeeper的学习材料
https://www.ibm.com/developerworks/cn/opensource/os-cn-zookeeper/ https://www.zhihu.com/question/351 ...
- 南邮CTF隐写之丘比龙的女神
刚开始下载下图片来 习惯性的binwalk一下 没发现东西 formost一下也没分离出来 扔进c32asm中发现有nvshen.jpg 于是改后缀名字为.zip 解压nvshen.jpg发现无法解压 ...