Android开发关于Toast的源码分析
toast实现
toast入口
我们在应用中使用toast提示的时候,一般都是一行简单的代码调用,如下所示:
toast.maketext(context, msg, toast.length_short).show();
maketext就是toast的入口,我们从maketext的源码来深入理解toast的实现。源码如下(frameworks/base/core/java/android/widget/toast.java):
public static toast maketext(context context, charsequence text, 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;
}
从maketext的源码里,我们可以看出toast的布局文件是transient_notification.xml,位于frameworks/base/core/res/res/layout/transient_notification.xml:
<?xml version="1.0" encoding="utf-8"?>
<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>
toast的布局文件非常简单,就是在垂直布局的linearlayout里放置了一个textview。接下来,我们继续跟到show()方法,研究一下布局形成之后的展示代码实现:
public void show() {
if (mnextview == null) {
throw new runtimeexception("setview must have been called");
}
inotificationmanager service = getservice();
string pkg = mcontext.getpackagename();
tn tn = mtn;
tn.mnextview = mnextview;
try {
service.enqueuetoast(pkg, tn, mduration);
} catch (remoteexception e) {
// empty
}
}
show方法中有两点是需要我们注意的。(1)tn是什么东东?(2)inotificationmanager服务的作用。带着这两个问题,继续我们toast源码的探索。
tn源码
很多问题都能通过源码找到答案,关键在与你是否有与之匹配的耐心和坚持。mtn的实现在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);
}
接下来,我们就从tn类的源码出发,探寻tn的作用。tn源码如下:
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();
int mgravity;
int mx, my;
float mhorizontalmargin;
float mverticalmargin;
view mview;
view mnextview;
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;
/// m: [alps00517576] support multi-user
params.privateflags = windowmanager.layoutparams.private_flag_show_for_all_users;
}
/**
* 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);
}
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();
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;
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();
}
}
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);
}
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.removeview(mview);
}
mview = null;
}
}
}
通过源码,我们能很明显的看到继承关系,tn类继承自itransientnotification.stub,用于进程间通信。这里假设读者都有android进程间通信的基础(不太熟的建议学习罗升阳关于binder进程通信的一系列博客)。既然tn是用于进程间通信,那么我们很容易想到tn类的具体作用应该是toast类的回调对象,其他进程通过调用tn类的具体对象来操作toast的显示和消失。
tn类继承自itransientnotification.stub,itransientnotification.aidl位于frameworks/base/core/java/android/app/itransientnotification.aidl,源码如下:
package android.app;
/** @hide */
oneway interface itransientnotification {
void show();
void hide();
}
itransientnotification定义了两个方法show()和hide(),它们的具体实现就在tn类当中。tn类的实现为:
/**
* 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的show和hide方法实现是基于handler机制。而tn类中的handler实现是:
final handler mhandler = new handler();
而且,我们在tn类中没有发现任何looper.perpare()和looper.loop()方法。说明,mhandler调用的是当前所在线程的looper对象。所以,当我们在主线程(也就是ui线程中)可以随意调用toast.maketext方法,因为android系统帮我们实现了主线程的looper初始化。但是,如果你想在子线程中调用toast.maketext方法,就必须先进行looper初始化了,不然就会报出java.lang.runtimeexception: can't create handler inside thread that has not called looper.prepare() 。handler机制的学习可以参考我之前写过的一篇博客:http://blog.csdn.net/wzy_1988/article/details/38346637。
接下来,继续跟一下mshow和mhide的实现,它俩的类型都是runnable。
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;
}
};
可以看到,show和hide的真正实现分别是调用了handleshow()和handlehide()方法。我们先来看handleshow()的具体实现:
public void handleshow() {
if (mview != mnextview) {
// remove the old view if necessary
handlehide();
mview = mnextview;
context context = mview.getcontext().getapplicationcontext();
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;
if (mview.getparent() != null) {
mwm.removeview(mview);
}
mwm.addview(mview, mparams);
trysendaccessibilityevent();
}
}
从源码中,我们知道toast是通过windowmanager调用addview加载进来的。因此,hide方法自然是windowmanager调用removeview方法来将toast视图移除。
总结一下,通过对tn类的源码分析,我们知道了tn类是回调对象,其他进程调用tn类的show和hide方法来控制这个toast的显示和消失。
notificationmanagerservice
回到toast类的show方法中,我们可以看到,这里调用了getservice得到inotificationmanager服务,源码如下:
private static inotificationmanager sservice;
static private inotificationmanager getservice() {
if (sservice != null) {
return sservice;
}
sservice = inotificationmanager.stub.asinterface(servicemanager.getservice("notification"));
return sservice;
}
得到inotificationmanager服务后,调用了enqueuetoast方法将当前的toast放入到系统的toast队列中。传的参数分别是pkg、tn和mduration。也就是说,我们通过toast.maketext(context, msg, toast.length_show).show()去呈现一个toast,这个toast并不是立刻显示在当前的window上,而是先进入系统的toast队列中,然后系统调用回调对象tn的show和hide方法进行toast的显示和隐藏。
这里inofiticationmanager接口的具体实现类是notificationmanagerservice类,位于frameworks/base/services/java/com/android/server/notificationmanagerservice.java。
首先,我们来分析一下toast入队的函数实现enqueuetoast,源码如下:
public void enqueuetoast(string pkg, itransientnotification callback, int duration)
{
// packagename为null或者tn类为null,直接返回,不进队列
if (pkg == null || callback == null) {
return ;
}
// (1) 判断是否为系统toast
final boolean issystemtoast = iscallersystem() || ("android".equals(pkg));
// 判断当前toast所属的pkg是否为系统不允许发生toast的pkg.notificationmanagerservice有一个hashset数据结构,存储了不允许发生toast的包名
if (enable_blocked_toasts && !notenotificationop(pkg, binder.getcallinguid()) && !arenotificationsenabledforpackageint(pkg)) {
if (!issystemtoast) {
return;
}
}
synchronized (mtoastqueue) {
int callingpid = binder.getcallingpid();
long callingid = binder.clearcallingidentity();
try {
toastrecord record;
// (2) 查看该toast是否已经在队列当中
int index = indexoftoastlocked(pkg, callback);
// 如果toast已经在队列中,我们只需要更新显示时间即可
if (index >= 0) {
record = mtoastqueue.get(index);
record.update(duration);
} else {
// 非系统toast,每个pkg在当前mtoastqueue中toast有总数限制,不能超过max_package_notifications
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;
}
}
}
}
// 将toast封装成toastrecord对象,放入mtoastqueue中
record = new toastrecord(callingpid, pkg, callback, duration);
mtoastqueue.add(record);
index = mtoastqueue.size() - 1;
// (3) 将当前toast所在的进程设置为前台进程
keepprocessalivelocked(callingpid);
}
// (4) 如果index为0,说明当前入队的toast在队头,需要调用shownexttoastlocked方法直接显示
if (index == 0) {
shownexttoastlocked();
}
} finally {
binder.restorecallingidentity(callingid);
}
}
}
可以看到,我对上述代码做了简要的注释。代码相对简单,但是还有4点标注代码需要我们来进一步探讨。
(1) 判断是否为系统toast。如果当前toast所属的进程的包名为“android”,则为系统toast,否则还可以调用iscallersystem()方法来判断。该方法的实现源码为:
boolean isuidsystem(int uid) {
final int appid = userhandle.getappid(uid);
return (appid == process.system_uid || appid == process.phone_uid || uid == 0);
}
boolean iscallersystem() {
return isuidsystem(binder.getcallinguid());
}
iscallersystem的源码也比较简单,就是判断当前toast所属进程的uid是否为system_uid、0、phone_uid中的一个,如果是,则为系统toast;如果不是,则不为系统toast。
是否为系统toast,通过下面的源码阅读可知,主要有两点优势:
系统toast一定可以进入到系统toast队列中,不会被黑名单阻止。
系统toast在系统toast队列中没有数量限制,而普通pkg所发送的toast在系统toast队列中有数量限制。
(2) 查看将要入队的toast是否已经在系统toast队列中。这是通过比对pkg和callback来实现的,具体源码如下所示:
private int indexoftoastlocked(string pkg, itransientnotification callback)
{
ibinder cbak = callback.asbinder();
arraylist<toastrecord> list = mtoastqueue;
int len = list.size();
for (int i=0; i<len; i++) {
toastrecord r = list.get(i);
if (r.pkg.equals(pkg) && r.callback.asbinder() == cbak) {
return i;
}
}
return -1;
}
通过上述代码,我们可以得出一个结论,只要toast的pkg名称和tn对象是一致的,则系统把这些toast认为是同一个toast。
(3) 将当前toast所在进程设置为前台进程。源码如下所示:
private 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=activitymanagernative.getdefault(),调用了setprocessforeground方法将当前pid的进程置为前台进程,保证不会系统杀死。这也就解释了为什么当我们finish当前activity时,toast还可以显示,因为当前进程还在执行。
(4) index为0时,对队列头的toast进行显示。源码如下:
private void shownexttoastlocked() {
// 获取队列头的toastrecord
toastrecord record = mtoastqueue.get(0);
while (record != null) {
try {
// 调用toast的回调对象中的show方法对toast进行展示
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;
}
}
}
}
这里toast的回调对象callback就是tn对象。接下来,我们看一下,为什么系统toast的显示时间只能是2s或者3.5s,关键在于scheduletimeoutlocked方法的实现。原理是,调用tn的show方法展示完toast之后,需要调用scheduletimeoutlocked方法来将toast消失。(如果大家有疑问:不是说tn对象的hide方法来将toast消失,为什么要在这里调用scheduletimeoutlocked方法将toast消失呢?是因为tn类的hide方法一执行,toast立刻就消失了,而平时我们所使用的toast都会在当前activity停留几秒。如何实现停留几秒呢?原理就是scheduletimeoutlocked发送message_timeout消息去调用tn对象的hide方法,但是这个消息会有一个delay延迟,这里也是用了handler消息机制)。
private static final int long_delay = 3500; // 3.5 seconds
private static final int short_delay = 2000; // 2 seconds
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);
}
首先,我们看到这里并不是直接发送了message_timeout消息,而是有个delay的延迟。而delay的时间从代码中“long delay = r.duration == toast.length_long ? long_delay : short_delay;”看出只能为2s或者3.5s,这也就解释了为什么系统toast的呈现时间只能是2s或者3.5s。自己在toast.maketext方法中随意传入一个duration是无作用的。
接下来,我们来看一下workerhandler中是如何处理message_timeout消息的。mhandler对象的类型为workerhandler,源码如下:
private final class workerhandler extends handler
{
@override
public void handlemessage(message msg)
{
switch (msg.what)
{
case message_timeout:
handletimeout((toastrecord)msg.obj);
break;
}
}
}
可以看到,workerhandler对message_timeout类型的消息处理是调用了handlertimeout方法,那我们继续跟踪handletimeout源码:
private void handletimeout(toastrecord record)
{
synchronized (mtoastqueue) {
int index = indexoftoastlocked(record.pkg, record.callback);
if (index >= 0) {
canceltoastlocked(index);
}
}
}
handletimeout代码中,首先判断当前需要消失的toast所属toastrecord对象是否在队列中,如果在队列中,则调用canceltoastlocked(index)方法。真相就要浮现在我们眼前了,继续跟踪源码:
private void canceltoastlocked(int index) {
toastrecord record = mtoastqueue.get(index);
try {
record.callback.hide();
} catch (remoteexception e) {
// don't worry about this, we're about to remove it from
// the list anyway
}
mtoastqueue.remove(index);
keepprocessalivelocked(record.pid);
if (mtoastqueue.size() > 0) {
// show the next one. if the callback fails, this will remove
// it from the list, so don't assume that the list hasn't changed
// after this point.
shownexttoastlocked();
}
}
哈哈,看到这里,我们回调对象的hide方法也被调用了,同时也将该toastrecord对象从mtoastqueue中移除了。到这里,一个toast的完整显示和消失就讲解结束了。