Toast 實現(xiàn)原理解析

關(guān)于Toast我們開發(fā)中最常用,但是他的實現(xiàn)原理往往被忽略嬉荆,大概知道是通過WindowManager直接加載顯示的鬼贱。
但是,不知道讀者是否思考過以下問題:
1.為什么同一個應(yīng)用不同線程琉闪,調(diào)用Toast.show()的時候,是有序顯示.
2.不同應(yīng)用之間Toast調(diào)用show()的時候砸彬,為什么不沖突颠毙,不會覆蓋顯示,而且同樣也是有序的砂碉。
3.怎樣實現(xiàn)非UI線程調(diào)用Toast.show().而不產(chǎn)生崩潰蛀蜜。
4.退出應(yīng)用的時候,Toast.show()還在顯示绽淘,如何做到退出應(yīng)用后涵防,不顯示Toast

Toast是用來提示用戶信息一個view闹伪,這個View顯示在Window上沪铭,通過WindowManager直接加載,而依賴于應(yīng)用中的任何View上偏瓤。
首先前兩個問題杀怠,要分析Toast的實現(xiàn)原理。
當我們這樣顯示一個Toast:
Toast.makeText(MainActivity.this,"今天天氣很好哦!" ,Toast.LENGTH_LONG).show();
首先makeText()厅克,實例化一個Toast赔退。并inflate布局transient_notification,使得

public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
        @NonNull CharSequence text, @Duration int duration) {
    Toast result = new Toast(context, looper);

    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硕旗。并inflate布局transient_notification,

<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>

并設(shè)置要顯示的文字信息女责。實例化的Toast漆枚,實際上實例化靜態(tài)對象TN。

public Toast(@NonNull Context context, @Nullable Looper looper) {
  mContext = context;
  mTN = new TN(context.getPackageName(), looper);
......
}
TN類繼承自ITransientNotification.Stub抵知,如下是TN的源碼:
private static class TN extends ITransientNotification.Stub {
  private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();

  private static final int SHOW = 0;
  private static final int HIDE = 1;
  private static final int CANCEL = 2;
  final Handler mHandler;

  int mGravity;
  int mX, mY;
  float mHorizontalMargin;
  float mVerticalMargin;


  View mView;
  View mNextView;
  int mDuration;

  WindowManager mWM;

  String mPackageName;

  static final long SHORT_DURATION_TIMEOUT = 4000;
  static final long LONG_DURATION_TIMEOUT = 7000;

  TN(String packageName, @Nullable Looper looper) {
      // 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;

      mPackageName = packageName;

      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()");
          }
      }
      mHandler = new Handler(looper, null) {
          @Override
          public void handleMessage(Message msg) {
              switch (msg.what) {
                  case SHOW: {
                      IBinder token = (IBinder) msg.obj;
                      handleShow(token);
                      break;
                  }
                  case HIDE: {
                      handleHide();
                      // Don't do this in handleHide() because it is also invoked by
                      // handleShow()
                      mNextView = null;
                      break;
                  }
                  case CANCEL: {
                      handleHide();
                      // Don't do this in handleHide() because it is also invoked by
                      // handleShow()
                      mNextView = null;
                      try {
                          getService().cancelToast(mPackageName, TN.this);
                      } catch (RemoteException e) {
                      }
                      break;
                  }
              }
          }
      };
  }

  /**
   * schedule handleShow into the right thread
   */
  @Override
  public void show(IBinder windowToken) {
      if (localLOGV) Log.v(TAG, "SHOW: " + this);
      mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
  }

  /**
   * schedule handleHide into the right thread
   */
  @Override
  public void hide() {
      if (localLOGV) Log.v(TAG, "HIDE: " + this);
      mHandler.obtainMessage(HIDE).sendToTarget();
  }

  public void cancel() {
      if (localLOGV) Log.v(TAG, "CANCEL: " + this);
      mHandler.obtainMessage(CANCEL).sendToTarget();
  }

  public void handleShow(IBinder windowToken) {
      if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
              + " mNextView=" + mNextView);
      // If a cancel/hide is pending - no need to show - at this point
      // the window token is already invalid and no need to do any work.
      if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
          return;
      }
      if (mView != mNextView) {
          // remove the old view if necessary
          handleHide();
          mView = mNextView;
          Context context = mView.getContext().getApplicationContext();
          String packageName = mView.getContext().getOpPackageName();
          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;
          mParams.packageName = packageName;
          mParams.hideTimeoutMilliseconds = mDuration ==
              Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
          mParams.token = windowToken;
          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);
          // Since the notification manager service cancels the token right
          // after it notifies us to cancel the toast there is an inherent
          // race and we may attempt to add a window after the token has been
          // invalidated. Let us hedge against that.
          try {
              mWM.addView(mView, mParams);
              trySendAccessibilityEvent();
          } catch (WindowManager.BadTokenException e) {
              /* ignore */
          }
      }
  }
.......
}
ITransientNotification是AIDL進程間通訊的接口墙基,ITransientNotification.aidl位于frameworks/base/core/java/android/app/ITransientNotification.aidl,源碼如下:
在線源碼:
[ITransientNotification](http://androidxref.com/7.1.2_r36/xref/frameworks/base/core/java/android/app/ITransientNotification.aidl)
[java] view plain copy
package android.app;  

/** @hide */  
oneway interface ITransientNotification {  
  void show();  
  void hide();  
}  
當我們調(diào)用show()的時候刷喜,通過INotificationManager將消息加入隊列中残制。
/**
* Show the view for the specified duration.
*/
public void show() {
  if (mNextView == null) {
      throw new RuntimeException("setView must have been called");
  }

  INotificationManager service = getService();
  String pkg = mContext.getOpPackageName();
  TN tn = mTN;
  tn.mNextView = mNextView;

  try {
      service.enqueueToast(pkg, tn, mDuration);
  } catch (RemoteException e) {
      // Empty
  }
}

static private INotificationManager getService() {
  if (sService != null) {
      return sService;
  }
  sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
  return sService;
}
INotificationManager是 INotificationManager.aidl接口的實現(xiàn)。源碼:[INotificationManager.aidl](http://androidxref.com/7.1.2_r36/xref/frameworks/base/core/java/android/app/INotificationManager.aidl)

NotificationManagerService服務(wù)開啟后掖疮,就會實例化一個Binder:
private final IBinder mService = new INotificationManager.Stub() {
1269        // Toasts
1270        // ============================================================================
1271
1272        @Override
1273        public void enqueueToast(String pkg, ITransientNotification callback, int duration)
1274        {
1275            if (DBG) {
1276                Slog.i(TAG, "enqueueToast pkg=" + pkg + " callback=" + callback
1277                        + " duration=" + duration);
1278            }
1279
1280            if (pkg == null || callback == null) {
1281                Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
1282                return ;
1283            }
1284
1285            final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
1286            final boolean isPackageSuspended =
1287                    isPackageSuspendedForUser(pkg, Binder.getCallingUid());
1288
1289            if (ENABLE_BLOCKED_TOASTS && (!noteNotificationOp(pkg, Binder.getCallingUid())
1290                    || isPackageSuspended)) {
1291                if (!isSystemToast) {
1292                    Slog.e(TAG, "Suppressing toast from package " + pkg
1293                            + (isPackageSuspended
1294                                    ? " due to package suspended by administrator."
1295                                    : " by user request."));
1296                    return;
1297                }
1298            }
1299
1300            synchronized (mToastQueue) {
1301                int callingPid = Binder.getCallingPid();
1302                long callingId = Binder.clearCallingIdentity();
1303                try {
1304                    ToastRecord record;
1305                    int index = indexOfToastLocked(pkg, callback);
1306                    // If it's already in the queue, we update it in place, we don't
1307                    // move it to the end of the queue.
1308                    if (index >= 0) {
1309                        record = mToastQueue.get(index);
1310                        record.update(duration);
1311                    } else {
1312                        // Limit the number of toasts that any given package except the android
1313                        // package can enqueue.  Prevents DOS attacks and deals with leaks.
1314                        if (!isSystemToast) {
1315                            int count = 0;
1316                            final int N = mToastQueue.size();
1317                            for (int i=0; i<N; i++) {
1318                                 final ToastRecord r = mToastQueue.get(i);
1319                                 if (r.pkg.equals(pkg)) {
1320                                     count++;
1321                                     if (count >= MAX_PACKAGE_NOTIFICATIONS) {
1322                                         Slog.e(TAG, "Package has already posted " + count
1323                                                + " toasts. Not showing more. Package=" + pkg);
1324                                         return;
1325                                     }
1326                                 }
1327                            }
1328                        }
1329
1330                        Binder token = new Binder();
1331                        mWindowManagerInternal.addWindowToken(token,
1332                                WindowManager.LayoutParams.TYPE_TOAST);
1333                        record = new ToastRecord(callingPid, pkg, callback, duration, token);
1334                        mToastQueue.add(record);
1335                        index = mToastQueue.size() - 1;
1336                        keepProcessAliveIfNeededLocked(callingPid);
1337                    }
1338                    // If it's at index 0, it's the current toast.  It doesn't matter if it's
1339                    // new or just been updated.  Call back and tell it to show itself.
1340                    // If the callback fails, this will remove it from the list, so don't
1341                    // assume that it's valid after this.
1342                    if (index == 0) {
1343                        showNextToastLocked();
1344                    }
1345                } finally {
1346                    Binder.restoreCallingIdentity(callingId);
1347                }
1348            }
1349        }
1350
1351        @Override
1352        public void cancelToast(String pkg, ITransientNotification callback) {
1353           
1360            synchronized (mToastQueue) {
1361                long callingId = Binder.clearCallingIdentity();
1362                try {
1363                    int index = indexOfToastLocked(pkg, callback);
1364                    if (index >= 0) {
1365                        cancelToastLocked(index);
1366                    } else {
1367                        Slog.w(TAG, "Toast already cancelled. pkg=" + pkg
1368                                + " callback=" + callback);
1369                    }
1370                } finally {
1371                    Binder.restoreCallingIdentity(callingId);
1372                }
1373            }
1374        }
             ........
1375}

INotificationManager.Stub() 實現(xiàn) enqueueToast()通過 showNextToastLocked()初茶,cancelToast()通過 cancelToastLocked(index)方法來回調(diào)ITransientNotification的show(),hide()。

void showNextToastLocked() {
2995        ToastRecord record = mToastQueue.get(0);
2996        while (record != null) {
2997            if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
2998            try {
2999                record.callback.show(record.token);
3000                scheduleTimeoutLocked(record);
3001                return;
3002            } catch (RemoteException e) {
3003                Slog.w(TAG, "Object died trying to show notification " + record.callback
3004                        + " in package " + record.pkg);
3005                // remove it from the list and let the process die
3006                int index = mToastQueue.indexOf(record);
3007                if (index >= 0) {
3008                    mToastQueue.remove(index);
3009                }
3010                keepProcessAliveIfNeededLocked(record.pid);
3011                if (mToastQueue.size() > 0) {
3012                    record = mToastQueue.get(0);
3013                } else {
3014                    record = null;
3015                }
3016            }
3017        }
3018    }
3019
3020    void cancelToastLocked(int index) {
3021        ToastRecord record = mToastQueue.get(index);
3022        try {
3023            record.callback.hide();
3024        } catch (RemoteException e) {
3025            Slog.w(TAG, "Object died trying to hide notification " + record.callback
3026                    + " in package " + record.pkg);
3027            // don't worry about this, we're about to remove it from
3028            // the list anyway
3029        }
3030
3031        ToastRecord lastToast = mToastQueue.remove(index);
3032        mWindowManagerInternal.removeWindowToken(lastToast.token, true);
3033
3034        keepProcessAliveIfNeededLocked(record.pid);
3035        if (mToastQueue.size() > 0) {
3036            // Show the next one. If the callback fails, this will remove
3037            // it from the list, so don't assume that the list hasn't changed
3038            // after this point.
3039            showNextToastLocked();
3040        }
3041    }

TN是ITransientNotification的子類浊闪,通過自身的Handler將消息處理恼布,handshow() 中mWM.addView(mView, mParams)添加吐葵。

總結(jié):

1.Toast.show(),Toast.cancel()是通過跨進程通訊(IPC通訊機制)實現(xiàn)的桥氏,全局一個系統(tǒng)服務(wù)NotificationManagerService管理Toast消息隊列温峭。所以異步線程,跨進程調(diào)用都是有序字支,不會覆蓋的凤藏。
2.盡管每次實例化一個TN,每個線程下的Handler持有的Looper相同線程是一樣的,處理各自的消息隊列里的SHOW,HIDE消息。
3.要實現(xiàn)非主線程調(diào)用不要忘記Looper.prepare()實例化looper:

new Thread(){
    @Override
    public void run() {
        super.run();
        Looper.prepare();
        Toast.makeText(MainActivity.this,"今天天氣很好哦!" + (++indexToast),Toast.LENGTH_LONG).show();
        Looper.loop();
    }
}.start();

4.應(yīng)用在后臺工作以后堕伪,要記得Toast.cancel()取消顯示揖庄。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市欠雌,隨后出現(xiàn)的幾起案子蹄梢,更是在濱河造成了極大的恐慌,老刑警劉巖富俄,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件禁炒,死亡現(xiàn)場離奇詭異,居然都是意外死亡霍比,警方通過查閱死者的電腦和手機幕袱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來悠瞬,“玉大人们豌,你說我怎么就攤上這事∏匙保” “怎么了望迎?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長凌外。 經(jīng)常有香客問我辩尊,道長,這世上最難降的妖魔是什么趴乡? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任对省,我火速辦了婚禮,結(jié)果婚禮上晾捏,老公的妹妹穿的比我還像新娘蒿涎。我一直安慰自己,他們只是感情好惦辛,可當我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布劳秋。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪玻淑。 梳的紋絲不亂的頭發(fā)上嗽冒,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天,我揣著相機與錄音补履,去河邊找鬼添坊。 笑死,一個胖子當著我的面吹牛箫锤,可吹牛的內(nèi)容都是我干的贬蛙。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼谚攒,長吁一口氣:“原來是場噩夢啊……” “哼阳准!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起馏臭,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤野蝇,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后括儒,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體绕沈,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年塑崖,在試婚紗的時候發(fā)現(xiàn)自己被綠了七冲。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡规婆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蝉稳,到底是詐尸還是另有隱情抒蚜,我是刑警寧澤,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布耘戚,位于F島的核電站嗡髓,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏收津。R本人自食惡果不足惜饿这,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望撞秋。 院中可真熱鬧长捧,春花似錦、人聲如沸吻贿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至肌割,卻和暖如春卧蜓,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背把敞。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工弥奸, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人奋早。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓其爵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親伸蚯。 傳聞我的和親對象是個殘疾皇子摩渺,可洞房花燭夜當晚...
    茶點故事閱讀 43,452評論 2 348

推薦閱讀更多精彩內(nèi)容