详解Android中的Toast源码
toast源码实现
toast入口
我们在应用中使用toast提示的时候,一般都是一行简单的代码调用,如下所示:
[java] view plaincopyprint?在code上查看代码片派生到我的代码片
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的完整显示和消失就讲解结束了。
上一篇: 值类型和引用类型的区别深入理解
下一篇: 4种java复制文件的方式