当前位置: 移动技术网 > 移动技术>移动开发>Android > Android开发关于Toast的源码分析

Android开发关于Toast的源码分析

2018年09月13日  | 移动技术网移动技术  | 我要评论

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的完整显示和消失就讲解结束了。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网