美文网首页
Toast 源码学习

Toast 源码学习

作者: leilifengxingmw | 来源:发表于2019-01-08 13:46 被阅读13次

    我们使用Toast的时候,通常都是这样

    Toast.makeText(this, "Hello world", Toast.LENGTH_SHORT).show()
    

    首先我们从Toast的静态方法makeText开始

    public static Toast makeText(Context context, CharSequence text, 
                                 @Duration int duration) {
        return makeText(context, null, text, duration);
    }
    
    public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
            @NonNull CharSequence text, @Duration int duration) {
        //构建Toast实例
        Toast result = new Toast(context, looper);
    
        LayoutInflater inflate = (LayoutInflater)
                    context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        //获取默认的布局文件,就是要展示的View
        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;
        //返回创建的Toast实例
        return result;
    }
    

    Toast的构造函数

    public Toast(@NonNull Context context, @Nullable Looper looper) {
            mContext = context;
            //1. mTN是一个本地对象,用于进程间通信
            mTN = new TN(context.getPackageName(), looper);
            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是一个本地对象,用于进程间通信

    private static class TN extends ITransientNotification.Stub {
        //...
        TN(String packageName, @Nullable Looper looper) {
            //1. 初始化looper 对象
            if (looper == null) {
                // Use Looper.myLooper() if looper is not specified.
                looper = Looper.myLooper();
                if (looper == null) {
                    throw new RuntimeException(
                            "Can't toast on a thread that has not called
                             Looper.prepare()");
                }
            }
        }
    }
    

    注意:在TN的构造函数中,我们发现有初始化looper对象的一段逻辑。如果我们要在子线程弹出Toast的时候必须先调用Looper.prepare();来初始化一个Looper对象或者传入一个不为null的Looper对象。

    Toast的show方法

    public void show() {
            //mNextView不能为null
            if (mNextView == null) {
                throw new RuntimeException("setView must have been called");
            }
            //获取系统服务NotificationManagerService
            INotificationManager service = getService();
            String pkg = mContext.getOpPackageName();
            TN tn = mTN;
            tn.mNextView = mNextView;
    
            try {
                //将show toast的请求加入NotificationManagerService的toast队列
                service.enqueueToast(pkg, tn, mDuration);
            } catch (RemoteException e) {
                // Empty
            }
        }
    
    @Override
    public void enqueueToast(String pkg, ITransientNotification callback, int duration)
        {
         //...
         synchronized (mToastQueue) {
         int callingPid = Binder.getCallingPid();
         long callingId = Binder.clearCallingIdentity();
         try {
               ToastRecord record;
                int index;
                //除了android 包以外,其他应用同一时刻只能把一个toast加入队列 
                if (!isSystemToast) {//不是系统级别的toast
                      index = indexOfToastPackageLocked(pkg);
                 } else {
                       index = indexOfToastLocked(pkg, callback);
                 }
    
                 // 如果当前的应用已经有一个ToastRecord对象了,我们就更新这个对象
                 if (index >= 0) {
                      record = mToastQueue.get(index);
                      record.update(duration);
                      try {
                          record.callback.hide();
                      } catch (RemoteException e) {
    
                      }
                      record.update(callback);
                  } else {
                        Binder token = new Binder();
                        mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
                        //创建一个新的ToastRecord对象,并加入列表中
                        record = new ToastRecord(callingPid, pkg, callback, duration, token);
                        mToastQueue.add(record);
                        //获取列表中最后一个ToastRecord对象的下标
                        index = mToastQueue.size() - 1;
                    }
                    keepProcessAliveIfNeededLocked(callingPid);
                    // 如果index是0,就显示这个toast
                    if (index == 0) {
                        showNextToastLocked();
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
        }
    
    void showNextToastLocked() {
            ToastRecord record = mToastQueue.get(0);
            while (record != null) {
                //...
                //1. Toast是如何显示的      
                record.callback.show(record.token);
                //2. Toast是如何消失的     
                scheduleDurationReachedLocked(record);
                return;
                //...
           }
        }
    
    

    Toast是如何显示的

    record.callback.show(record.token);
    

    这里record.callback是一个 ITransientNotification 类型的对象,而这个对象实际上就是我们刚才所说的 TN 类型对象的代理对象。当我们调用 TN 代理对象的 show 方法的时候,相当于 RPC 调用了 TN 的 show 方法。

    TN的show方法

     @Override
      public void show(IBinder windowToken) {
          if (localLOGV) Log.v(TAG, "SHOW: " + this);
          mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
      }
    
     mHandler = new Handler(looper, null) {
                    @Override
                    public void handleMessage(Message msg) {
                        switch (msg.what) {
                            case SHOW: {
                                IBinder token = (IBinder) msg.obj;
                                handleShow(token);
                                break;
                            }              
                        }
                    }
                };
    
    public void handleShow(IBinder windowToken) {
         //...
         if (mView != mNextView) {
            // 如果必要,先移除老的View
            handleHide();
            //...
           mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
           //...
           mParams.token = windowToken;
           //...
          mWM.addView(mView, mParams);
          }
     }
    

    显示窗口的方法,就是将所传递过来的窗口 token 赋值给窗口属性对象 mParams, 然后通过调用 WindowManager.addView 方法,将 Toast 中的 mView 对象纳入 WMS 的管理。

    Toast 是如何消失的

    scheduleDurationReachedLocked(record);
    
    private void scheduleDurationReachedLocked(ToastRecord r){
        mHandler.removeCallbacksAndMessages(r);
        //构建一个到期的消息
        Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r);
        long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
        //发送消息
        mHandler.sendMessageDelayed(m, delay);
    }
    

    mHandler是一个WorkerHandler对象

     protected class WorkerHandler extends Handler{
    
            public WorkerHandler(Looper looper) {
                super(looper);
            }
    
            @Override
            public void handleMessage(Message msg){
                switch (msg.what){
                    case MESSAGE_DURATION_REACHED:
                        //处理到期的消息
                        handleDurationReached((ToastRecord)msg.obj);
                        break;
                     //...
                }
            }
    
          //...
    
        }
    

    NotificationManagerService的handleDurationReached方法

    private void handleDurationReached(ToastRecord record) {
     
        synchronized (mToastQueue) {
            int index = indexOfToastLocked(record.pkg, record.callback);
            if (index >= 0) {
                  cancelToastLocked(index);
            }
        }
     }
    
    void cancelToastLocked(int index) {
            ToastRecord record = mToastQueue.get(index);
            try {
                //隐藏toast
                record.callback.hide();
            } catch (RemoteException e) {
                // ...
            }
    
            ToastRecord lastToast = mToastQueue.remove(index);
    
            mWindowManagerInternal.removeWindowToken(lastToast.token, false /* removeWindows */,
                    DEFAULT_DISPLAY);
            //...
            //如果队列中还有toast,就展示下一个
            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();
            }
        }
    
    
     //隐藏toast
     record.callback.hide();
    

    这里record.callback是一个 ITransientNotification 类型的对象,而这个对象实际上就是我们刚才所说的 TN 类型对象的代理对象。当我们调用 TN 代理对象的 hide方法的时候,相当于 RPC 调用了 TN 的 hide方法。

    TN 的 hide方法

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

    mHandler关于HIDE情况的处理

     case HIDE: {
              handleHide();
              mNextView = null;
              break;
    }
    
    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.
                if (mView.getParent() != null) {
                    //WindowManager移除Toast的View
                    mWM.removeViewImmediate(mView);
                }
                // Now that we've removed the view it's safe for the server to release
                // the resources.
                try {
                    getService().finishToken(mPackageName, this);
                } catch (RemoteException e) {
    
                }
                mView = null;
            }
        }
    

    自定义Toast

    如果想自定义Toast,可以使用Toast的setView方法设置要展示的View。如下所示:

    private fun showCustomToast(text: String) {
            val view = LayoutInflater.from(this).inflate(R.layout.custom_toast, null)
            val tvMessage = view.findViewById<TextView>(R.id.toast_tv)
            tvMessage.text = text
            //注意这里没有使用Toast.makeText方法
            val toast = Toast(this)
            toast.view = view
            toast.show()
        }
    

    布局文件

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">
    
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:background="@color/colorPrimary">
    
            <TextView
                android:id="@+id/toast_tv"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="20dp"
                android:layout_marginTop="10dp"
                android:layout_marginRight="20dp"
                android:layout_marginBottom="10dp"
                android:gravity="center"
                android:textColor="#fff"
                android:textSize="18sp"
                tools:text="一段很长的测试文字" />
    
        </LinearLayout>
    
        <ImageView
            android:id="@+id/toast_iv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_star" />
    
    </RelativeLayout>
    
    

    具体的例子可以参考 Android自定义Toast

    参考链接:

    1. [Android] Toast问题深度剖析(一)
    2. Android自定义Toast
    3. 面试题:Toast 只能在主线程使用吗?

    相关文章

      网友评论

          本文标题:Toast 源码学习

          本文链接:https://www.haomeiwen.com/subject/bsfvrqtx.html