Android源码解析——Toast
简介
Toast是一种向用户快速展示少量信息的视图。当它显示时,它会浮在整个应用层的上面,并且不会获取到焦点。它的设计思想是能够向用户展示些信息,但又能尽量不显得唐突。本篇我们来研读一下Toast的源码,并探明它的显示及隐藏机制。
源码解析
Toast
我们从Toast的最简单调用开始,它的调用代码是:
Toast.makeText(context,"Show toast",Toast.LENGTH_LONG).show();
在上面的代码中,我们是先调用Toast
的静态方法来创建一个Toast
,然后调用其show()
方法将其显示出来。其中makeText
的源码如下:
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
Toast result = new Toast(context);
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);
result.mNextView = v;
result.mDuration = duration;
return result;
}
可以看到只是new出一个Toast,并设置要显示的View及时间。这里默认使用的Toast布局transient_notification.xml
也在SDK中,代码如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="?android:attr/toastFrameBackground">
<TextView
android:id="@android:id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_horizontal"
android:textAppearance="@style/TextAppearance.Toast"
android:textColor="@color/bright_foreground_dark"
android:shadowColor="#BB000000"
android:shadowRadius="2.75"
/>
</LinearLayout>
它只是一个简单的LinearLayout套一个TextView。顺便说一句,从布局上我们可以知道这个Toast的背景颜色是可以配置的,通过在theme中配置toastFrameBackground
属性。不过遗憾的是,我后来又查了一下,发现这个属性被导出到public.xml
当中,也就是我们还是不能在应用当中配置。
回到Toast,我们来看一下它的属性定义及构造方法。
public class Toast {
final Context mContext;
final TN mTN;
int mDuration;
View mNextView;
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);
}
它的属性很简单,只有四个,分别是上下文对象mContext
,TN
对象mTN
,表示显示时长的mDuration
,以及表示将要显示的视图mNextView
。在它的构造方法中,初始化mTN
及它的两个成员变量。
留意一下Toast中显示的View的命名为mNextView
。
我们再往下翻一下Toast的一些成员方法,会发现它的许多行为的实现,都是在操作mTN,比如设置gravity
,水平外边距,x轴或y轴的偏移等等。而它的show()
及cancel()
方法,也都是通过其来实现,如下:
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
}
}
public void cancel() {
mTN.hide();
try {
getService().cancelToast(mContext.getPackageName(), mTN);
} catch (RemoteException e) {
// Empty
}
}
那么TN究竟是个什么东西?
TN
我们来看一下TN的源码,可以知道,它是Toast里的一个静态内部类,继承自ITransientNotification.Stub
,并实现其显示和隐藏的方法:
private static class TN extends ITransientNotification.Stub {
// 显示命令
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;
}
};
private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();//悬浮窗口的参数
final Handler mHandler = new Handler(); //用于将操作命令添加到线程的队列中
//显示的View的相关布局参数
int mGravity;
int mX, mY;
float mHorizontalMargin;
float mVerticalMargin;
View mView; //当前显示的View
View mNextView;// 下一个要显示的View
WindowManager mWM;//用于显示悬浮窗口
TN() {
// 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;
}
/**
* schedule handleShow into the right thread
*/
@Override
public void show() {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.post(mShow);//将显示命令添加到线程队列当中
}
/**
* schedule handleHide into the right thread
*/
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.post(mHide);//将隐藏命令添加到线程队列当中
}
// ...其他代码
}
从上面的代码中,我们可以看到,一个Toast的显示和取消,并不是在Toast类本身中实现的,而是交给了TN,由它去实现。TN则是包装了一个Toast的内容及行为。TN的显示及隐藏的具体实现是handleShow()
和handleHide()
,这两个方法的内容是向WindowManager添加和隐藏View,和我们平时写悬浮窗口的实现一样,这里略过不谈。
我们还可以看到,TN继承的ITransientNotification.Stub
,它是AIDL接口的实现,该接口为ITransientNotification.aidl
。AIDL是Android中用于进程间通信的一种机制。也就是对于我们的Toast,最终是交给另一个进程去管理它的显示及隐藏。
我们回到刚才读Toast时的show()
及cancel()
的代码,可以看到先是获取一个服务INotificationManager service = getService();
,显示时调用其service.enqueueToast(pkg, tn, mDuration);
,隐藏时调用mTN
的hide()
方法,并接着调用getService().cancelToast(mContext.getPackageName(), mTN);
,其中,mTN
是它的进程通信的回调。
INotificationManager
INotificationManager
也是一个AIDL接口,它定义一些通知管理服务的行为,位于Android源码中的frameworks\base\core\java\android\app
,并没有包含在Android SDK中。代码如下:
interface INotificationManager
{
/** @deprecated use {@link #enqueueNotificationWithTag} instead */
void enqueueNotification(String pkg, int id, in Notification notification, inout int[] idReceived);
/** @deprecated use {@link #cancelNotificationWithTag} instead */
void cancelNotification(String pkg, int id);
void cancelAllNotifications(String pkg);
void enqueueToast(String pkg, ITransientNotification callback, int duration);
void cancelToast(String pkg, ITransientNotification callback);
void enqueueNotificationWithTag(String pkg, String tag, int id, in Notification notification, inout int[] idReceived);
void cancelNotificationWithTag(String pkg, String tag, int id);
}
可以看到它管理的内容包含Toast提示及Notification通知,下面我们只关注Toast相关的两个接口。根据它的名字,我们找到这个接口的对应实现sources\android-23\android\app\NotificationManager.java
。
NotificationManagerService
在该类的代码中,有INotificationManager.Stub
的匿名内部类实例,主要代码如下:
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 = 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;
}
}
//对Toast队列加锁
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();//获取调用进程id
long callingId = Binder.clearCallingIdentity();//重置当前线程上进来的IPC的ID
try {
ToastRecord record;
int index = indexOfToastLocked(pkg, callback);//判断我们的mTN是否存在队列中
// 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.
//如果不是系统Toast,则限制toast的数量,以避免DOS攻击及内存泄露。
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;
}
}
}
}
// 创建一个ToastRecord对象,并加入队列。
record = new ToastRecord(callingPid, pkg, callback, duration);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
keepProcessAliveLocked(callingPid);//设置调用者的进程的活动状态。它会根据该进程的Toast的数量来设置是否为前台进程。
}
// 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.
//如果最后一个下标为0,则表示它是当前的toast,那么将不会管它是新创建的还是刚刚被更新过,
//而是回调告诉mTN把自己显示出来。
if (index == 0) {
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
@Override
public void cancelToast(String pkg, ITransientNotification callback) {
Slog.i(TAG, "cancelToast pkg=" + pkg + " callback=" + callback);
if (pkg == null || callback == null) {
Slog.e(TAG, "Not cancelling notification. pkg=" + pkg + " callback=" + callback);
return ;
}
synchronized (mToastQueue) {
long callingId = Binder.clearCallingIdentity();
try {
//获取Toast的在队列中的下标
int index = indexOfToastLocked(pkg, callback);
if (index >= 0) {//如果存在
//从队列中移除,该方法还会设置调用进程的活动状态,并显示下一条要显示的Toast
cancelToastLocked(index);
} else {
Slog.w(TAG, "Toast already cancelled. pkg=" + pkg
+ " callback=" + callback);
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
//其他接口的实现代码略
}
其中显示Toast的代码如下:
void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
while (record != null) {
if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
try {
record.callback.show();
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 die
int index = mToastQueue.indexOf(record);
if (index >= 0) {
mToastQueue.remove(index);
}
keepProcessAliveLocked(record.pid);
if (mToastQueue.size() > 0) {
record = mToastQueue.get(0);
} else {
record = null;
}
}
}
}
````
可以看到它会调用`TN`对象的`show()`方法,并接着调用`scheduleTimeoutLocked(record);`。
该方法代码如下:
<div class="se-preview-section-delimiter"></div>
```java
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);
}
也就是在显示之后,会在delay
毫秒之后发送一个MESSAGE_TIMEOUT的消息来通知Toast隐藏,来让一个Toast只显示一小段时间。delay
的时间根据Toast设置的duration是否为Toast.LENGTH_LONG来决定是LONG_DELAY(3.5秒)还是SHORT_DELAY(2秒)。
NotificationManagerService
的实现代码很长,为避免因贴上太多代码而使篇幅变得太长,这里只介绍了主要流程的代码,具体每个步骤的方法实现,可以自己翻阅该类代码,文件位于SDK的sources\android-23\com\android\server\notification
文件夹。
总结
对于Toast代码的分析在此告一段落,从上面的分析中我们可以得到以下结论:
- 默认的Toast布局的背景可以在theme中配置。
- Toast的显示及取消是通过NotificationManagerService来管理的,它跨进程,使用AIDL来实现进程间通信。
- 所有Toast都会加到
NotificationManagerService
的队列中,对于非系统程序,它会限制Toast的数量(当前我所读的代码中该值为50)以防止DOS攻击及内存泄露的问题。 - Toast里的TN对象的显示及隐藏命令通过new出来的handler来发送。所以没有队列的线程是不能显示Toast的。
- Toast的显示的时间只有两个,duration相当于一个标志位,用于标志显示的时间是长还是短,而不是具体的显示时间。
- 当有Toast要显示时,其所在进程会被设为前台进程。
Android源码解析——Toast的更多相关文章
- Android源码解析系列
转载请标明出处:一片枫叶的专栏 知乎上看了一篇非常不错的博文:有没有必要阅读Android源码 看完之后痛定思过,平时所学往往是知其然然不知其所以然,所以为了更好的深入Android体系,决定学习an ...
- android源码解析(十七)-->Activity布局加载流程
版权声明:本文为博主原创文章,未经博主允许不得转载. 好吧,终于要开始讲讲Activity的布局加载流程了,大家都知道在Android体系中Activity扮演了一个界面展示的角色,这也是它与andr ...
- Android源码解析——LruCache
我认为在写涉及到数据结构或算法的实现类的源码解析博客时,不应该急于讲它的使用或马上展开对源码的解析,而是要先交待一下这个数据结构或算法的资料,了解它的设计,再从它的设计出发去讲如何实现,最后从实现的角 ...
- Android源码解析——AsyncTask
简介 AsyncTask 在Android API 3引入,是为了使UI线程能被正确和容易地使用.它允许你在后台进行一些操作,并且把结果带到UI线程中,而不用自己去操纵Thread或Handler.它 ...
- Android 源码解析 之 setContentView
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/41894125,本文出自:[张鸿洋的博客] 大家在平时的开发中,对于setCont ...
- Android源码解析——Handler、Looper与MessageQueue
本文的目的是来分析下 Android 系统中以 Handler.Looper.MessageQueue 组成的异步消息处理机制,通过源码来了解整个消息处理流程的走向以及相关三者之间的关系 需要先了解以 ...
- Android 源码解析之AsyncTask
AsyncTask相信大家都不陌生,它是为了简化异步请求.更新UI操作而诞生的.使用它不仅可以完成我们的网络耗时操作,而且还可以在完成耗时操作后直接的更新我们所需要的UI组件.这使得它在android ...
- 【Android源码解析】View.post()到底干了啥
emmm,大伙都知道,子线程是不能进行 UI 操作的,或者很多场景下,一些操作需要延迟执行,这些都可以通过 Handler 来解决.但说实话,实在是太懒了,总感觉写 Handler 太麻烦了,一不小心 ...
- Android 源码解析:单例模式-通过容器实现单例模式-懒加载方式
本文分析了 Android 系统服务通过容器实现单例,确保系统服务的全局唯一. 开发过 Android 的用户肯定都用过这句代码,主要作用是把布局文件 XML 加载到系统中,转换为 Android 的 ...
随机推荐
- 复习HTML+CSS(5)
n <meta>标记 <meta>的主要作用,是提供网页的源信息.比如:指定网页的搜索关键字 <meta>标记有两个属性:http-equiv和name. 1. ...
- jacascript 函数声明、函数表达式与声明提升(hoisting机制)
前言:这是笔者学习之后自己的理解与整理.如果有错误或者疑问的地方,请大家指正,我会持续更新! 声明.定义.初始化 声明的意思是宣称一个变量名的存在,定义则为这个变量分配存储空间,初始化则是给该变量名的 ...
- MySQL操作与修改表
插入数据(insert) insert语句的3个主要组成部分: 所要插入数据的表的名称: 表终需要使用的列的名称: 需要插入到列的值. 数字型主键生成机制 数字型主键生成机制,除了随机选择数字外,还可 ...
- Windows下使用console线连接思科交换机
在XP下可以直接使用内置工具"超级终端",在win7或者更高版本需要下载安装SecureCRT. 本文假设已经下载安装好了SecureCRT. 首先,将电脑连接console线.因 ...
- Java 内部类的意义及应用
众所周知,我们的 C++ 程序语言是多继承制的,而多继承明显的好处就是,相对而言只需要写较少的代码即可完成一个类的定义,因为我们可以通过继承其它类来获取别人的实现. 但是,它也有一个致命性的缺陷,容易 ...
- NC二次开发常用的表
常用的表: 收费清单:pr_cr_receivables 会计月份: bd_accperiodmonth 20180416
- 使用.Net+非关系型数据库MongoDB 实现LBS商家按距离排序_按离我最近排序
.Net MongoDB LBS地理位置定位 开发过程,实现商家按距离排序 前言: 在使用美团点外卖,看电影,找好吃的时候,经常会注意到软件有一个按距离排序,找离我最近的商家,心中有一些疑问,.Net ...
- PostgreSQL的insert注入
写这篇文是在昨夜的ctf中遇到的. ctf地址:bloody-feedback.quals.2017.volgactf.ru email存在注入,在ctf中发现注入就很好办了,只要找到能绕过的方法就行 ...
- Linux提示字符设置
当我们登陆linux后,显示的提示字符究竟是什么意思呢?又可不可以设置呢. 首先来看看默认的显示: 普通用户: [fuwh@localhost ~]$ root用户: [root@localhost ...
- [SDOI2011]计算器
Description 你被要求设计一个计算器完成以下三项任务: 1.给定y,z,p,计算Y^Z Mod P 的值: 2.给定y,z,p,计算满足xy≡ Z ( mod P )的最小非负整数: 3.给 ...