您的位置:首页 > 移动开发 > Android开发

Android Toast源码实现

2016-02-23 16:22 441 查看
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:

转:http://www.2cto.com/kf/201502/374764.html

 

<!--?xml version="1.0" encoding="utf-8"?-->

<linearlayout android:background="?android:attr/toastFrameBackground" android:orientation="vertical" android:layout_height="match_parent" android:layout_width="match_parent" xmlns:android="http://schemas.android.com/apk/res/android">

 

    <textview android:id="@android:id/message" android:layout_height="wrap_content" android:layout_width="wrap_content" android:shadowradius="2.75" android:shadowcolor="#BB000000" android:textcolor="@color/bright_foreground_dark" android:textappearance="@style/TextAppearance.Toast"
android:layout_gravity="center_horizontal" android:layout_weight="1">

 

</textview></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; (count="" if="" count++;="" {="" (r.pkg.equals(pkg))="" r="mToastQueue.get(i);" toastrecord="" final="" i++)="">= 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);

        }

    }

}</n;>

 

可以看到,我对上述代码做了简要的注释。代码相对简单,但是还有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; if="" {="" r="list.get(i);" toastrecord="" i++)="" pre="" }<="" -1;="" return="" }="" i;="" cbak)="" r.callback.asbinder()="=" &&="" (r.pkg.equals(pkg)="">    通过上述代码,我们可以得出一个结论,只要Toast的pkg名称和tn对象是一致的,则系统把这些Toast认为是同一个Toast。

    (3) 将当前Toast所在进程设置为前台进程。源码如下所示:

<pre class="brush:java;">    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; if="" {="" r="list.get(i);" toastrecord="" i++)="" }="" toastcount="" pid,="" mam.setprocessforeground(mforegroundtoken,="" try="" toastcount++;="" pid)="" (r.pid="="> 0);

        } catch (RemoteException e) {

            // Shouldn't happen.

        }

    }</n;></toastrecord></pre>    这里的mAm=ActivityManagerNative.getDefault(),调用了setProcessForeground方法将当前pid的进程置为前台进程,保证不会系统杀死。这也就解释了为什么当我们finish当前Activity时,Toast还可以显示,因为当前进程还在执行。

    (4) index为0时,对队列头的Toast进行显示。源码如下:

<pre class="brush:java;">    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;

                }

            }

        }

    }</pre>    这里Toast的回调对象callback就是tn对象。接下来,我们看一下,为什么系统Toast的显示时间只能是2s或者3.5s,关键在于scheduleTimeoutLocked方法的实现。原理是,调用tn的show方法展示完Toast之后,需要调用scheduleTimeoutLocked方法来将Toast消失。(<strong>如果大家有疑问:不是说tn对象的hide方法来将Toast消失,为什么要在这里调用scheduleTimeoutLocked方法将Toast消失呢?是因为tn类的hide方法一执行,Toast立刻就消失了,而平时我们所使用的Toast都会在当前Activity停留几秒。如何实现停留几秒呢?原理就是scheduleTimeoutLocked发送MESSAGE_TIMEOUT消息去调用tn对象的hide方法,但是这个消息会有一个delay延迟,这里也是用了Handler消息机制</strong>)。<br>

 

 

<pre class="brush:java;">    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);

    }</pre>    首先,我们看到这里并不是直接发送了MESSAGE_TIMEOUT消息,而是有个delay的延迟。<strong>而delay的时间从代码中“long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;”看出只能为2s或者3.5s,这也就解释了为什么系统Toast的呈现时间只能是2s或者3.5s。自己在Toast.makeText方法中随意传入一个duration是无作用的。</strong>

    接下来,我们来看一下WorkerHandler中是如何处理MESSAGE_TIMEOUT消息的。mHandler对象的类型为WorkerHandler,源码如下:

<pre class="brush:java;">    private final class WorkerHandler extends Handler

    {

        @Override

        public void handleMessage(Message msg)

        {

            switch (msg.what)

            {

                case MESSAGE_TIMEOUT:

                    handleTimeout((ToastRecord)msg.obj);

                    break;

            }

        }

    }</pre>    可以看到,WorkerHandler对MESSAGE_TIMEOUT类型的消息处理是调用了handlerTimeout方法,那我们继续跟踪handleTimeout源码:

<pre class="brush:java;">    private void handleTimeout(ToastRecord record)

    {

        synchronized (mToastQueue) {

            int index = indexOfToastLocked(record.pkg, record.callback);

            if (index >= 0) {

                cancelToastLocked(index);

            }

        }

    }</pre>    handleTimeout代码中,首先判断当前需要消失的Toast所属ToastRecord对象是否在队列中,如果在队列中,则调用cancelToastLocked(index)方法。真相就要浮现在我们眼前了,继续跟踪源码:

<pre class="brush:java;">    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();

        }

    }</pre>    哈哈,看到这里,我们回调对象的hide方法也被调用了,同时也将该ToastRecord

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Android Toast 源码 实现