Bootstrap

Android应用Activity、Dialog、PopWindow、Toast窗口添加机制及源码分析 《五》-Toast

5 Android应用Toast窗口添加显示机制源码
5-1 基础知识准备

在开始分析这几个窗口之前需要脑补一点东东,我们从应用层开发来直观脑补,这样下面分析源码时就不蛋疼了。如下是一个我们写的两个应用实现Service跨进程调用服务ADIL的例子,客户端调运远程Service的start与stop方法控制远程Service的操作。

Android系统中的应用程序都运行在各自的进程中,进程之间是无法直接交换数据的,但是Android为开发者提供了AIDL跨进程调用Service的功能。其实AIDL就相当于双方约定的一个规则而已。

先看下在Android Studio中AIDL开发的工程目录结构,如下:
这里写图片描述

由于AIDL文件中不能出现访问修饰符(如public),同时AIDL文件在两个项目中要完全一致而且只支持基本类型,所以我们定义的AIDL文件如下:

ITestService.aidl

package io.github.yanbober.myapplication;

interface ITestService {
    void start(int id);
    void stop(int id);
}

再来看下依据aidl文件自动生成的ITestService.java文件吧,如下:

/*
 * This file is auto-generated.  DO NOT MODIFY.
 */
package io.github.yanbober.myapplication;
public interface ITestService extends android.os.IInterface
{
    //Stub类是ITestService接口的内部静态抽象类,该类继承了Binder类
    public static abstract class Stub extends android.os.Binder implements io.github.yanbober.myapplication.ITestService
    {
        ......
        //这是抽象静态Stub类中的asInterface方法,该方法负责将service返回至client的对象转换为ITestService.Stub
        //把远程Service的Binder对象传递进去,得到的是远程服务的本地代理
        public static io.github.yanbober.myapplication.ITestService asInterface(android.os.IBinder obj)
        {
            ......
        }
        ......
        //远程服务的本地代理,也会继承自ITestService
        private static class Proxy implements io.github.yanbober.myapplication.ITestService
        {
            ......
            @Override
            public void start(int id) throws android.os.RemoteException
            {
                ......
            }

            @Override
            public void stop(int id) throws android.os.RemoteException
            {
                ......
            }
        }
        ......
    }
    //两个方法是aidl文件中定义的方法
    public void start(int id) throws android.os.RemoteException;
    public void stop(int id) throws android.os.RemoteException;
}

这就是自动生成的java文件,接下来我们看看服务端的Service源码,如下:

//记得在AndroidManifet.xml中注册Service的<action android:name="io.github.yanbober.myapplication.aidl" />

public class TestService extends Service {
    private TestBinder mTestBinder;

    //该类继承ITestService.Stub类而不是Binder类,因为ITestService.Stub是Binder的子类
    //进程内的Service定义TestBinder内部类是继承Binder类
    public class TestBinder extends ITestService.Stub {

        @Override
        public void start(int id) throws RemoteException {
            Log.i(null, "Server Service is start!");
        }

        @Override
        public void stop(int id) throws RemoteException {
            Log.i(null, "Server Service is stop!");
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        //返回Binder
        return mTestBinder;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        //实例化Binder
        mTestBinder = new TestBinder();
    }
}

现在服务端App的代码已经OK,我们来看下客户端的代码。客户端首先也要像上面的工程结构一样,把AIDL文件放好,接着在客户端使用远程服务端的Service代码如下:

public class MainActivity extends Activity {
    private static final String REMOT_SERVICE_ACTION = "io.github.yanbober.myapplication.aidl";

    private Button mStart, mStop;

    private ITestService mBinder;

    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //获得另一个进程中的Service传递过来的IBinder对象
            //用IMyService.Stub.asInterface方法转换该对象
            mBinder = ITestService.Stub.asInterface(service);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mStart = (Button) this.findViewById(R.id.start);
        mStop = (Button) this.findViewById(R.id.stop);

        mStart.setOnClickListener(clickListener);
        mStop.setOnClickListener(clickListener);
        //绑定远程跨进程Service
        bindService(new Intent(REMOT_SERVICE_ACTION), connection, BIND_AUTO_CREATE);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //取消绑定远程跨进程Service
        unbindService(connection);
    }

    private View.OnClickListener clickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            调用远程Service中的start与stop方法
            switch (v.getId()) {
                case R.id.start:
                    try {
                        mBinder.start(0x110);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                    break;
                case R.id.stop:
                    try {
                        mBinder.stop(0x120);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                    break;
            }
        }
    };
}

到此你对应用层通过AIDL使用远程Service的形式已经很熟悉了,至于实质的通信使用Binder的机制我们后面会写文章一步一步往下分析。到此的准备知识已经足够用来理解下面我们的源码分析了。

5-2 Toast窗口源码分析

我们常用的Toast窗口其实和前面分析的Activity、Dialog、PopWindow都是不同的,因为它和输入法、墙纸类似,都是系统窗口。

我们还是按照最常用的方式来分析源码吧。

我们先看下Toast的静态makeText方法吧,如下:

   public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
        //new一个Toast对象
        Toast result = new Toast(context);
        //获取前面有篇文章分析的LayoutInflater
        LayoutInflater inflate = (LayoutInflater)
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        //加载解析Toast的布局,实质transient_notification.xml是一个LinearLayout中套了一个@android:id/message的TextView而已
        View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
        //取出布局中的TextView
        TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
        //把我们的文字设置到TextView上
        tv.setText(text);
        //设置一些属性
        result.mNextView = v;
        result.mDuration = duration;
        //返回新建的Toast
        return result;
    }

可以看见,这个方法构造了一个Toast,然后把要显示的文本放到这个View的TextView中,然后初始化相关属性后返回这个新的Toast对象。

当我们有了这个Toast对象之后,可以通过show方法来显示出来,如下看下show方法源码:

 public void show() {
        ......
        //通过AIDL(Binder)通信拿到NotificationManagerService的服务访问接口,当前Toast类相当于上面例子的客户端!!!相当重要!!!
        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;

        try {
            //把TN对象和一些参数传递到远程NotificationManagerService中去
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }

我们看看show方法中调运的getService方法,如下:

  //远程NotificationManagerService的服务访问接口
    private static INotificationManager sService;

    static private INotificationManager getService() {
        //单例模式
        if (sService != null) {
            return sService;
        }
        //通过AIDL(Binder)通信拿到NotificationManagerService的服务访问接口
        sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
        return sService;
    }

通过上面我们的基础脑补实例你也能看懂这个getService方法了吧。那接着我们来看mTN吧,好像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);
    }

可以看见mTN确实是在构造函数中实例化的,那我们就来看看这个TN类,如下:

  //类似于上面例子的服务端实例化的Service内部类Binder
    private static class TN extends ITransientNotification.Stub {
        ......
        //实现了AIDL的show与hide方法
        @Override
        public void show() {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.post(mShow);
        }

        @Override
        public void hide() {
            if (localLOGV) Log.v(TAG, "HIDE: " + this);
            mHandler.post(mHide);
        }
        ......
    }

看见没有,TN是Toast内部的一个私有静态类,继承自ITransientNotification.Stub。你这时指定好奇ITransientNotification.Stub是个啥玩意,对吧?其实你在上面的脑补实例中见过它的,他出现在服务端实现的Service中,就是一个Binder对象,也就是对一个aidl文件的实现而已,我们看下这个ITransientNotification.aidl文件,如下:

package android.app;

/** @hide */
oneway interface ITransientNotification {
    void show();
    void hide();
}

看见没有,和我们上面的例子很类似吧。

再回到上面分析的show()方法中可以看到,我们的Toast是传给远程的NotificationManagerService管理的,为了NotificationManagerService回到我们的应用程序(回调),我们需要告诉NotificationManagerService我们当前程序的Binder引用是什么(也就是TN)。是不是觉得和上面例子有些不同,这里感觉Toast又充当客户端,又充当服务端的样子,实质就是一个回调过程而已。

继续来看Toast中的show方法的service.enqueueToast(pkg, tn, mDuration);语句,service实质是远程的NotificationManagerService,所以enqueueToast方法就是NotificationManagerService类的,如下:

  private final IBinder mService = new INotificationManager.Stub() {
        // Toasts
        // ============================================================================

        @Override
        public void enqueueToast(String pkg, ITransientNotification callback, int duration)
        {
            ......
            synchronized (mToastQueue) {
                int callingPid = Binder.getCallingPid();
                long callingId = Binder.clearCallingIdentity();
                try {
                    ToastRecord record;
                    //查看该Toast是否已经在队列当中
                    int index = indexOfToastLocked(pkg, callback);
                    // If it's already in the queue, we update it in place, we don't
                    // move it to the end of the queue.
                    //注释说了,已经存在则直接取出update
                    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封装成ToastRecord对象,放入mToastQueue中
                        record = new ToastRecord(callingPid, pkg, callback, duration);
                        //把他添加到ToastQueue队列中
                        mToastQueue.add(record);
                        index = mToastQueue.size() - 1;
                        //将当前Toast所在的进程设置为前台进程
                        keepProcessAliveLocked(callingPid);
                    }
                    //如果index为0,说明当前入队的Toast在队头,需要调用showNextToastLocked方法直接显示
                    if (index == 0) {
                        showNextToastLocked();
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
        }
   }

继续看下该方法中调运的showNextToastLocked方法,如下:

   void showNextToastLocked() {
        //取出ToastQueue中队列最前面的ToastRecord
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            try {
                //Toast类中实现的ITransientNotification.Stub的Binder接口TN,调运了那个类的show方法
                record.callback.show();
                scheduleTimeoutLocked(record);
                return;
            } catch (RemoteException e) {
                ......
            }
        }
    }

继续先看下该方法中调运的scheduleTimeoutLocked方法,如下:

 private void scheduleTimeoutLocked(ToastRecord r)
    {
        //移除上一条消息
        mHandler.removeCallbacksAndMessages(r);
        //依据Toast传入的duration参数LENGTH_LONG=1来判断决定多久发送消息
        Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
        long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
        //依据设置的MESSAGE_TIMEOUT后发送消息
        mHandler.sendMessageDelayed(m, delay);
    }

可以看见这里先回调了Toast的TN的show,下面timeout可能就是hide了。接着还在该类的mHandler处理了这条消息,然后调运了如下处理方法:

 private void handleTimeout(ToastRecord record)
    {
        ......
        synchronized (mToastQueue) {
            int index = indexOfToastLocked(record.pkg, record.callback);
            if (index >= 0) {
                cancelToastLocked(index);
            }
        }
    }

我们继续看cancelToastLocked方法,如下:

 void cancelToastLocked(int index) {
        ToastRecord record = mToastQueue.get(index);
        try {
            //回调Toast的TN中实现的hide方法
            record.callback.hide();
        } catch (RemoteException e) {
            ......
        }
        //从队列移除当前显示的Toast
        mToastQueue.remove(index);
        keepProcessAliveLocked(record.pid);
        if (mToastQueue.size() > 0) {
            //如果当前的Toast显示完毕队列里还有其他的Toast则显示其他的Toast
            showNextToastLocked();
        }
    }

到此可以发现,Toast的远程管理NotificationManagerService类的处理实质是通过Handler发送延时消息显示取消Toast的,而且在远程NotificationManagerService类中又远程回调了Toast的TN类实现的show与hide方法。

现在我们就回到Toast的TN类再看看这个show与hide方法,如下:

 private static class TN extends ITransientNotification.Stub {

        //仅仅是实例化了一个Handler,非常重要!!!!!!!!
        final Handler mHandler = new Handler(); 
        ......
        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;
            }
        };

        //实现了AIDL的show与hide方法
        @Override
        public void show() {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.post(mShow);
        }

        @Override
        public void hide() {
            if (localLOGV) Log.v(TAG, "HIDE: " + this);
            mHandler.post(mHide);
        }

    }

可以看见,这里实现aidl接口的方法实质是通过handler的post来执行的一个方法,而这个Handler仅仅只是new了一下,也就是说,如果我们写APP时使用Toast在子线程中则需要自行准备Looper对象,只有主线程Activity创建时帮忙准备了Looper(关于Handler与Looper如果整不明白请阅读《Android异步消息处理机制详解及源码分析》)。

那我们重点关注一下handleShow与handleHide方法,如下:

public void handleShow() {
            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                    + " mNextView=" + mNextView);
            if (mView != mNextView) {
                // remove the old view if necessary
                //如果有必要就通过WindowManager的remove删掉旧的
                handleHide();
                mView = mNextView;
                Context context = mView.getContext().getApplicationContext();
                String packageName = mView.getContext().getOpPackageName();
                if (context == null) {
                    context = mView.getContext();
                }
                //通过得到的context(一般是ContextImpl的context)获取WindowManager对象(上一篇文章分析的单例的WindowManager)
                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                ......
                //在把Toast的View添加之前发现Toast的View已经被添加过(有partent)则删掉
                if (mView.getParent() != null) {
                    ......
                    mWM.removeView(mView);
                }
                ......
                //把Toast的View添加到窗口,其中mParams.type在构造函数中赋值为TYPE_TOAST!!!!!!特别重要
                mWM.addView(mView, mParams);
                ......
            }
        }

        public void handleHide() {
            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.
                //注释说得很清楚了,不解释,就是remove
                if (mView.getParent() != null) {
                    mWM.removeView(mView);
                }
                mView = null;
            }
        }

到此Toast的窗口添加原理就分析完毕了,接下来我们进行总结。
5-3 Toast窗口源码分析总结及应用开发技巧

经过上面的分析我们总结如下:
这里写图片描述

通过上面分析及上图直观描述可以发现,之所以Toast的显示交由远程的NotificationManagerService管理是因为Toast是每个应用程序都会弹出的,而且位置和UI风格都差不多,所以如果我们不统一管理就会出现覆盖叠加现象,同时导致不好控制,所以Google把Toast设计成为了系统级的窗口类型,由NotificationManagerService统一队列管理。

在我们开发应用程序时使用Toast注意事项:

通过分析TN类的handler可以发现,如果想在非UI线程使用Toast需要自行声明Looper,否则运行会抛出Looper相关的异常;UI线程不需要,因为系统已经帮忙声明。

在使用Toast时context参数尽量使用getApplicationContext(),可以有效的防止静态引用导致的内存泄漏。

有时候我们会发现Toast弹出过多就会延迟显示,因为上面源码分析可以看见Toast.makeText是一个静态工厂方法,每次调用这个方法都会产生一个新的Toast对象,当我们在这个新new的对象上调用show方法就会使这个对象加入到NotificationManagerService管理的mToastQueue消息显示队列里排队等候显示;所以如果我们不每次都产生一个新的Toast对象(使用单例来处理)就不需要排队,也就能及时更新了。

6 Android应用Activity、Dialog、PopWindow、Toast窗口显示机制总结

可以看见上面无论Acitivty、Dialog、PopWindow、Toast的实质其实都是如下接口提供的方法操作:

public interface ViewManager
{
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}

整个应用各种窗口的显示都离不开这三个方法而已,只是token及type与Window是否共用的问题。

【工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重劳动成果】

;