溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Toast源碼深度分析

發(fā)布時間:2020-07-03 20:05:47 來源:網(wǎng)絡(luò) 閱讀:778 作者:楊充 欄目:移動開發(fā)
目錄介紹
  • 1.最簡單的創(chuàng)建方法
    • 1.1 Toast構(gòu)造方法
    • 1.2 最簡單的創(chuàng)建
    • 1.3 簡單改造避免重復(fù)創(chuàng)建
    • 1.4 為何會出現(xiàn)內(nèi)存泄漏
    • 1.5 吐司是系統(tǒng)級別的
  • 2.源碼分析
    • 2.1 Toast(Context context)構(gòu)造方法源碼分析
    • 2.2 show()方法源碼分析
    • 2.3 mParams.token = windowToken是干什么用的
    • 2.4 scheduleTimeoutLocked吐司如何自動銷毀的
    • 2.5 TN類中的消息機(jī)制
    • 2.6 普通應(yīng)用的Toast顯示數(shù)量是有限制的
    • 2.7 為何Activity銷毀后Toast仍會顯示
  • 3.經(jīng)典總結(jié)
    • 3.1 判斷應(yīng)用程序獲取通知權(quán)限是否開啟
    • 3.2 使用Toast注意事項
    • 3.3 Toast的顯示和隱藏重點邏輯
    • 3.4 Snackbar和Toast比較
  • 4.Toast封裝庫介紹
    • 4.1 能夠滿足的需求
    • 4.2 具有的優(yōu)勢
  • 5.Toast遇到的問題
    • 5.1 Toast偶爾報錯Unable to add window
    • 5.2 Toast運行在子線程問題
    • 5.3 Toast如何添加系統(tǒng)窗口的權(quán)限
    • 5.4 token null is not valid

好消息

  • 博客筆記大匯總【16年3月到至今】,包括Java基礎(chǔ)及深入知識點,Android技術(shù)博客,Python學(xué)習(xí)筆記等等,還包括平時開發(fā)中遇到的bug匯總,當(dāng)然也在工作之余收集了大量的面試題,長期更新維護(hù)并且修正,持續(xù)完善……開源的文件是markdown格式的!同時也開源了生活博客,從12年起,積累共計47篇[近20萬字],轉(zhuǎn)載請注明出處,謝謝!
  • 鏈接地址:https://github.com/yangchong211/YCBlogs
  • 如果覺得好,可以star一下,謝謝!當(dāng)然也歡迎提出建議,萬事起于忽微,量變引起質(zhì)變!
  • Toast封裝庫項目地址:https://github.com/yangchong211/YCDialog
  • 02.Toast源碼深度分析
    • 最簡單的創(chuàng)建,簡單改造避免重復(fù)創(chuàng)建,show()方法源碼分析,scheduleTimeoutLocked吐司如何自動銷毀的,TN類中的消息機(jī)制是如何執(zhí)行的,普通應(yīng)用的Toast顯示數(shù)量是有限制的,用代碼解釋為何Activity銷毀后Toast仍會顯示,Toast偶爾報錯Unable to add window是如何產(chǎn)生的,Toast運行在子線程問題,Toast如何添加系統(tǒng)窗口的權(quán)限等等
  • 03.DialogFragment源碼分析
    • 最簡單的使用方法,onCreate(@Nullable Bundle savedInstanceState)源碼分析,重點分析彈窗展示和銷毀源碼,使用中show()方法遇到的IllegalStateException分析
  • 05.PopupWindow源碼分析
    • 顯示PopupWindow,注意問題寬和高屬性,showAsDropDown()源碼,dismiss()源碼分析,PopupWindow和Dialog有什么區(qū)別?為何彈窗點擊一下就dismiss呢?
  • 06.Snackbar源碼分析
    • 最簡單的創(chuàng)建,Snackbar的make方法源碼分析,Snackbar的show顯示與點擊消失源碼分析,顯示和隱藏中動畫源碼分析,Snackbar的設(shè)計思路,為什么Snackbar總是顯示在最下面
  • 07.彈窗常見問題
    • DialogFragment使用中show()方法遇到的IllegalStateException,什么常見產(chǎn)生的?Toast偶爾報錯Unable to add window,Toast運行在子線程導(dǎo)致崩潰如何解決?

1.最簡單的創(chuàng)建方法

1.1 Toast構(gòu)造方法
  • Toast只會彈出一段信息,告訴用戶某某事情已經(jīng)發(fā)生了,過一段時間后就會自動消失。它不會阻擋用戶的任何操作。
  • Toast是沒有焦點,而且Toast顯示的時間有限,過一定的時間就會自動消失。
    • 通過new Toast(context)直接創(chuàng)建,除了將mContext = context,還有一步重要的操作,創(chuàng)建TN,下面會說到……
      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);
      }
1.2 最簡單的創(chuàng)建
  • 一行代碼調(diào)用,十分方便,但是這樣存在一種弊端。
    • 使用中遇到的問題:例如,當(dāng)點擊有些按鈕,需要吐司進(jìn)行提示時;快速連續(xù)點擊了多次按鈕,Toast就觸發(fā)了多次。系統(tǒng)會將這些Toast信息提示框放到隊列中,等前一個Toast信息提示框關(guān)閉后才會顯示下一個Toast信息提示框??赡軐?dǎo)致Toast就長時間關(guān)閉不掉了。又或者我們其實已在進(jìn)行其他操作了,應(yīng)該彈出新的Toast提示,而上一個Toast卻還沒顯示結(jié)束
      Toast.makeText(this,"吐司",Toast.LENGTH_SHORT).show();
1.3 簡單改造避免重復(fù)創(chuàng)建
  • 為了解決1.2中的重復(fù)創(chuàng)建問題,則可以這樣解決
    • 如下所示,簡易型代碼,需要注意問題,這里傳遞的上下文context需要是activity.getApplicationContext()全局上下文,避免靜態(tài)toast對象內(nèi)存泄漏
      /**
      * 吐司工具類    避免點擊多次導(dǎo)致吐司多次,最后導(dǎo)致Toast就長時間關(guān)閉不掉了
      * 注意:這里如果傳入context會報內(nèi)存泄漏;傳遞activity..getApplicationContext()
      * @param content       吐司內(nèi)容
      */
      private static Toast toast;
      @SuppressLint("ShowToast")
      public static void showToast(String content) {
      checkContext();
      if (toast == null) {
          toast = Toast.makeText(mApp, content, Toast.LENGTH_SHORT);
      } else {
          toast.setText(content);
      }
      toast.show();
      }
  • 這樣用的原理
    • 先判斷Toast對象是否為空,如果是空的情況下才會調(diào)用makeText()方法來去生成一個Toast對象,否則就直接調(diào)用setText()方法來設(shè)置顯示的內(nèi)容,最后再調(diào)用show()方法將Toast顯示出來。由于不會每次調(diào)用的時候都生成新的Toast對象,因此剛才我們遇到的問題在這里就不會出現(xiàn)
1.4 為何會出現(xiàn)內(nèi)存泄漏
  • 原因在于:如果在 Toast 消失之前,Toast 持有了當(dāng)前 Activity,而此時,用戶點擊了返回鍵,導(dǎo)致 Activity 無法被 GC 銷毀, 這個 Activity 就引起了內(nèi)存泄露。
1.5 吐司是系統(tǒng)級別的
  • 經(jīng)??吹降囊粋€場景就是你在你的應(yīng)用出調(diào)用了多次 Toast.show函數(shù),然后退回到桌面,結(jié)果發(fā)現(xiàn)桌面也會彈出 Toast,就是因為系統(tǒng)的 Toast 使用了系統(tǒng)窗口,具有高的層級

2.源碼分析

2.1 Toast(Context context)構(gòu)造方法源碼分析
  • 在構(gòu)造方法中,創(chuàng)建了NT對象,那么有人便會問,NT是什么東西呢?于是帶著好奇心便去看看NT的源碼,可以發(fā)現(xiàn)NT實現(xiàn)了ITransientNotification.Stub,提到這個感覺是不是很熟悉,沒錯,在aidl中就會用到這個。
    • 針對aidl,如果有人不明白,可以參考我的這邊文章Aidl進(jìn)程間通信詳細(xì)介紹主要是Aidl相關(guān)屬性介紹,實際開發(fā)中案例操作,部分源碼解析,客戶端綁定服務(wù)端service原理
      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);
      }
    • Toast源碼深度分析
  • 在TN類中,可以看到,實現(xiàn)了AIDL的show與hide方法

    • TN是Toast內(nèi)部的一個私有靜態(tài)類,繼承自ITransientNotification.Stub,ITransientNotification.Stub是出現(xiàn)在服務(wù)端實現(xiàn)的Service中,就是一個Binder對象,也就是對一個aidl文件的實現(xiàn)而已
      
      /**
      * schedule handleShow into the right thread
      */
      @Override
      public void show(IBinder windowToken) {
      if (localLOGV) Log.v(TAG, "SHOW: " + this);
      mHandler.obtainMessage(0, windowToken).sendToTarget();
      }

    /**

    • schedule handleHide into the right thread*/
      @Override
      br/>*/
      @Override
      if (localLOGV) Log.v(TAG, "HIDE: " + this);
      mHandler.post(mHide);
      }
  • 接著看下這個ITransientNotification.aidl文件
    /** @hide */
    oneway interface ITransientNotification {
        void show();
        void hide();
    }
2.2 show()方法源碼分析
  • 通過AIDL(Binder)通信拿到NotificationManagerService的服務(wù)訪問接口,然后把TN對象和一些參數(shù)傳遞到遠(yuǎn)程NotificationManagerService中去

    • 當(dāng) Toast在show的時候,然后把這個請求放在 NotificationManager 所管理的隊列中,并且為了保證 NotificationManager 能跟進(jìn)程交互,會傳遞一個TN類型的 Binder對象給NotificationManager系統(tǒng)服務(wù),接著看下面getService方法做了什么?

      public void show() {
      if (mNextView == null) {
          throw new RuntimeException("setView must have been called");
      }
      
      //通過AIDL(Binder)通信拿到NotificationManagerService的服務(wù)訪問接口,當(dāng)前Toast類相當(dāng)于上面例子的客戶端!?。∠喈?dāng)重要?。。?INotificationManager service = getService();
      String pkg = mContext.getOpPackageName();
      TN tn = mTN;
      tn.mNextView = mNextView;
      
      try {
          //把TN對象和一些參數(shù)傳遞到遠(yuǎn)程NotificationManagerService中去
          service.enqueueToast(pkg, tn, mDuration);
      } catch (RemoteException e) {
          // Empty
      }
      }
  • 接著看看getService方法
    • 通過單利模式獲取sService對象。
      //遠(yuǎn)程NotificationManagerService的服務(wù)訪問接口
      private static INotificationManager sService;
      static private INotificationManager getService() {
      //單例模式
      if (sService != null) {
          return sService;
      }
      //通過AIDL(Binder)通信拿到NotificationManagerService的服務(wù)訪問接口
      sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
      return sService;
      }
  • 接下來看看service.enqueueToast(pkg, tn, mDuration)這段代碼,相信有的小伙伴會質(zhì)疑,這段代碼報紅色,如何查看呢?
    • Toast源碼深度分析
    • 于是,我直接在studio中全局搜索NotificationManagerService,終于給找到了,如下所示:
    • Toast源碼深度分析
    • 下面就到重點呢……注意:record是將Toast封裝成ToastRecord對象,放入mToastQueue中。通過下面代碼可以得知:通過isSystemToast判斷是否為系統(tǒng)Toast。如果當(dāng)前Toast所屬的進(jìn)程的包名為“android”,則為系統(tǒng)Toast。如果是系統(tǒng)Toast一定可以進(jìn)入到系統(tǒng)Toast隊列中,不會被黑名單阻止。
      synchronized (mToastQueue) {
      int callingPid = Binder.getCallingPid();
      long callingId = Binder.clearCallingIdentity();
      try {
          ToastRecord record;
          int index;
          //判斷是否是系統(tǒng)級別的吐司
          if (!isSystemToast) {
              index = indexOfToastPackageLocked(pkg);
          } else {
              index = indexOfToastLocked(pkg, callback);
          }
          if (index >= 0) {
              record = mToastQueue.get(index);
              record.update(duration);
              record.update(callback);
          } else {
              //創(chuàng)建一個Binder類型的token對象
              Binder token = new Binder();
              //生成一個Toast窗口,并且傳遞token等參數(shù)
              mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
              record = new ToastRecord(callingPid, pkg, callback, duration, token);
              //添加到吐司隊列之中
              mToastQueue.add(record);
              //對當(dāng)前索引重新進(jìn)行賦值
              index = mToastQueue.size() - 1;
          }
          //將當(dāng)前Toast所在的進(jìn)程設(shè)置為前臺進(jìn)程
          keepProcessAliveIfNeededLocked(callingPid);
          if (index == 0) {
              //如果index為0,說明當(dāng)前入隊的Toast在隊頭,需要調(diào)用showNextToastLocked方法直接顯示
              showNextToastLocked();
          }
      } finally {
          Binder.restoreCallingIdentity(callingId);
      }
      }
  • 接下來看一下showNextToastLocked()方法中的源代碼,看看這個方法中做了什么……
    • 首先獲取吐司消息隊列中第一個ToastRecord對象,然后判斷該對象如果不為null的話,就開始通過callback進(jìn)行show,且傳遞了token參數(shù),注意這個show是通知進(jìn)程顯示。然后再調(diào)用scheduleTimeoutLocked(record)方法執(zhí)行超時后自動取消的邏輯【下面詳細(xì)分析】。同時需要注意的時,如果出現(xiàn)了異常,則會從吐司消息隊列中移除該record……
    • 那么callback是干嘛的呢,一般印象中callback是處理回調(diào)的?從ITransientNotification callback得知,這個callback哥們竟然是是一個 ITransientNotification 類型的對象,也就是前面說到的TN的Binder代理對象,那么他傳遞的這個token參數(shù)是干什么用的呢?這里我們程序員小伙伴可以接著往下看哈!
    • Toast源碼深度分析
2.3 mParams.token = windowToken是干什么用的
  • 如果你仔細(xì)一點,你可以看到在handleShow(IBinder windowToken)這個方法中,將windowToken賦值給mParams.token,那么就會思考這個token是干什么用的呢?它是哪里傳遞過來的呢?
    • 這個所需要的這個系統(tǒng)窗口 token ,是由我們的 NotificationManager 系統(tǒng)服務(wù)所生成,由于系統(tǒng)服務(wù)具有高權(quán)限,果真是厲害呀。
    • 上文2.3中我已經(jīng)分析了showNextToastLocked()方法部分源碼record.callback.show(record.token),可以知道callback對象的show方法中需要傳遞的參數(shù) record.token實際上就是上面所說的NotificationManager服務(wù)所生成的窗口的 token。
    • Toast源碼深度分析
    • Toast源碼深度分析
  • 這個顯示窗口的方法比較簡單,就是將所傳遞過來的窗口 token 賦值給窗口屬性對象 mParams, 然后通過調(diào)用 WindowManager.addView 方法,將 Toast中的mView對象納入WindowManager中,而WindowManager看源碼可知是一個接口,具體是放在WindowManagerService中處理。
2.4 scheduleTimeoutLocked吐司如何自動銷毀的
  • 接下來再來看看scheduleTimeoutLocked(record)這部分代碼,這個主要是超時監(jiān)聽消息邏輯
    • 通過看這段代碼知道,handler延遲delay時間后發(fā)送消息,并且這個delay時間只有原生自帶的兩種時間類型,無法開發(fā)者自己定義。
    • Toast源碼深度分析
  • 既然發(fā)送了消息,那肯定有地方接收消息并且處理消息呀。接著看下面代碼,重點看cancelToastLocked源碼
    • 可以看到當(dāng)接收到消息時,先判斷是否吐司,如果是有的話,也就是索引index>=0,那么就去cancel,在cancelToastLocked(int index)這段源碼里面,我們終于可以看到record.callback.hide()這個方法了,前面我們知道callback是前面提到TN的binder代理對象,所以這個方法是調(diào)用了TN類中的hide()方法,下面2.5中將詳細(xì)講解TN中的消息機(jī)制。
    • 同時結(jié)束吐司之后,移除消息隊列中對象,同時判斷吐司消息隊列中是否還有剩下的消息,如果是有的話,則會接著調(diào)用showNextToastLocked()繼續(xù)彈吐司,關(guān)于showNextToastLocked()可以看2.3中的源碼分析。
    • Toast源碼深度分析
    • Toast源碼深度分析
    • Toast源碼深度分析
    • Toast源碼深度分析
  • cancelToastLocked源碼邏輯主要是
    • 調(diào)用 ITransientNotification.hide 方法,通知客戶端隱藏窗口,并且移除隊列中對象
    • 將給Toast 生成的窗口Token從WMS 服務(wù)中刪除
    • 判斷吐司消息隊列中是否存在消息,如果存在消息,則繼續(xù)開始show吐司……
2.5 TN類中的消息機(jī)制
  • 看源碼可知,TN中的消息機(jī)制也是通過handler消息機(jī)制實現(xiàn)的。如果對handler 消息機(jī)制還不太熟悉,可以查看我的這篇博客:Handler消息機(jī)制
  • 當(dāng)創(chuàng)建TN對象的時候,就創(chuàng)建了handler和runnable對象。
    • 然后看看show與hide方法,在show方法中發(fā)送消息,當(dāng)mHandler接受到消息之后,就調(diào)用handleShow(token)處理邏輯,通過WindowManager將view添加進(jìn)來,同時在該方法中也設(shè)置了大量的布局屬性。
    • 在把Toast的View添加之前發(fā)現(xiàn)Toast的View已經(jīng)被添加過(有partent)則刪掉;把Toast的View添加到窗口,其中mParams.type在構(gòu)造函數(shù)中賦值為TYPE_TOAST!
    • Toast源碼深度分析
    • Toast源碼深度分析
  • 同時,當(dāng)toast執(zhí)行show之后,過了一會兒會自動銷毀,那么這又是為啥呢?那么是哪里調(diào)用了hide方法呢?

    • 回調(diào)了Toast的TN的show,當(dāng)timeout可能就是hide呢。從上面我分析NotificationManagerService源碼中的showNextToastLocked()的scheduleTimeoutLocked(record)源碼,可以知道在NotificationManagerService通過handler延遲delay時間發(fā)送消息,然后通過callback調(diào)用hide,由于callback是TN中Binder的代理對象, 所以便可以調(diào)用到TN中的hide方法達(dá)到銷毀吐司的目的。handleHide()源碼如下所示

      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.removeViewImmediate(mView);
          }
      
          mView = null;
      }
      }
2.6 普通應(yīng)用的Toast顯示數(shù)量是有限制的
  • 如何判斷是否是系統(tǒng)吐司呢?如果當(dāng)前Toast所屬的進(jìn)程的包名為“android”,則為系統(tǒng)Toast,或者調(diào)用isCallerSystem()方法
    final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
  • 接著看看isCallerSystem()方法源碼,isCallerSystem的源碼也比較簡單,就是判斷當(dāng)前Toast所屬進(jìn)程的uid是否為SYSTEM_UID、0、PHONE_UID中的一個,如果是,則為系統(tǒng)Toast;如果不是,則不為系統(tǒng)Toast。

    private static boolean isUidSystem(int uid) {
        final int appid = UserHandle.getAppId(uid);
        return (appid == Process.SYSTEM_UID || appid == Process.PHONE_UID || uid == 0);
    }
    
    private static boolean isCallerSystem() {
        return isUidSystem(Binder.getCallingUid());
    }
  • 為什么要這樣判斷是否是系統(tǒng)吐司呢?從源碼可知:首先系統(tǒng)Toast一定可以進(jìn)入到系統(tǒng)Toast隊列中,不會被黑名單阻止。然后系統(tǒng)Toast在系統(tǒng)Toast隊列中沒有數(shù)量限制,而普通pkg所發(fā)送的Toast在系統(tǒng)Toast隊列中有數(shù)量限制。
    • 那么關(guān)于數(shù)量限制這個結(jié)果從何而來,大概是多少呢?查看將要入隊的Toast是否已經(jīng)在系統(tǒng)Toast隊列中。這是通過比對pkg和callback來實現(xiàn)的。通過下面源碼分析可知:只要Toast的pkg名稱和tn對象是一致的,則系統(tǒng)把這些Toast認(rèn)為是同一個Toast。
    • 然后再看看下面這個源碼截圖,可知,非系統(tǒng)Toast,每個pkg在當(dāng)前mToastQueue中Toast有總數(shù)限制,不能超過MAX_PACKAGE_NOTIFICATIONS,也就是50
    • Toast源碼深度分析
    • Toast源碼深度分析
2.7 為何Activity銷毀后Toast仍會顯示
  • 記得以前昊哥問我,為何toast在activity銷毀后仍然會彈出呢,我毫不思索地說,因為toast是系統(tǒng)級別的呀。那么是如何實現(xiàn)的呢,我就無言以對呢……今天終于可以回答呢!
    • 還是回到NotificationManagerService類中的enqueueToast方法中,直接查看keepProcessAliveIfNeededLocked(callingPid)方法。這段代碼的意思是將當(dāng)前Toast所在進(jìn)程設(shè)置為前臺進(jìn)程,這里的mAm = ActivityManager.getService(),調(diào)用了setProcessImportant方法將當(dāng)前pid的進(jìn)程置為前臺進(jìn)程,保證不會系統(tǒng)殺死。這也就解釋了為什么當(dāng)我們finish當(dāng)前Activity時,Toast還可以顯示,因為當(dāng)前進(jìn)程還在執(zhí)行。
    • Toast源碼深度分析

3.經(jīng)典總結(jié)

3.1 判斷應(yīng)用程序獲取通知權(quán)限是否開啟
  • 一行代碼調(diào)用即可:DialogUtils.requestMsgPermission(this);
  • 大部分手機(jī)通知權(quán)限是開啟的。如果關(guān)閉了,則吐司是無法顯示的,但是仍有部分手機(jī),比如某型號小米手機(jī),錘子手機(jī)等就權(quán)限需要手動開啟。
  • Toast的展示是由NMS服務(wù)控制的,NMS服務(wù)會做一些權(quán)限、token等的校驗,當(dāng)通知權(quán)限一旦關(guān)閉,Toast將不再彈出。
  • 具體可以參考我的彈窗封裝庫:https://github.com/yangchong211/YCDialog

    • 自定義對話框,其中包括:自定義Toast,采用builder模式,支持設(shè)置吐司多個屬性;自定義dialog控件,仿IOS底部彈窗;自定義DialogFragment彈窗,支持自定義布局,也支持填充recyclerView布局;自定義PopupWindow彈窗,輕量級,還有自定義Snackbar等等;還有自定義loading加載窗,簡單便用。
      
      //判斷是否有權(quán)限
      NotificationManagerCompat.from(context).areNotificationsEnabled()

    //如果沒有通知權(quán)限,則直接跳轉(zhuǎn)設(shè)置中心設(shè)置@SuppressLint("ObsoleteSdkInt")
    br/>@SuppressLint("ObsoleteSdkInt")
    Intent localIntent = new Intent();
    localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    if (Build.VERSION.SDK_INT >= 9) {
    localIntent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
    localIntent.setData(Uri.fromParts("package", context.getPackageName(), null));
    } else if (Build.VERSION.SDK_INT <= 8) {
    localIntent.setAction(Intent.ACTION_VIEW);
    localIntent.setClassName("com.android.settings",
    "com.android.setting.InstalledAppDetails");
    localIntent.putExtra("com.android.settings.ApplicationPkgName", context.getPackageName());
    }
    context.startActivity(localIntent);
    }

3.2 使用Toast注意事項
  • 通過分析TN類的handler可以發(fā)現(xiàn),如果想在非UI線程使用Toast需要自行聲明Looper,否則運行會拋出Looper相關(guān)的異常;UI線程不需要,因為系統(tǒng)已經(jīng)幫忙聲明。
  • 在使用Toast時context參數(shù)盡量使用getApplicationContext(),可以有效的防止靜態(tài)引用導(dǎo)致的內(nèi)存泄漏。
  • 有時候我們會發(fā)現(xiàn)Toast彈出過多就會延遲顯示,因為上面源碼分析可以看見Toast.makeText是一個靜態(tài)工廠方法,每次調(diào)用這個方法都會產(chǎn)生一個新的Toast對象,當(dāng)我們在這個新new的對象上調(diào)用show方法就會使這個對象加入到NotificationManagerService管理的mToastQueue消息顯示隊列里排隊等候顯示;所以如果我們不每次都產(chǎn)生一個新的Toast對象(使用單例來處理)就不需要排隊,也就能及時更新呢。
3.3 Toast的顯示和隱藏重點邏輯
  • Toast調(diào)用show方法 ,其實就是是將自己納入到NotificationManager的Toast管理中去,期間傳遞了一個本地的TN類型或者是 ITransientNotification.Stub的Binder對象
  • NotificationManager 收到 Toast 的顯示請求后,將生成一個 Binder 對象,將它作為一個窗口的 token 添加到 WMS 對象,并且類型是 TOAST
  • NotificationManager 將這個窗口token通過ITransientNotification的show方法傳遞給遠(yuǎn)程的TN對象,并且拋出一個超時監(jiān)聽消息 scheduleTimeoutLocked
  • TN 對象收到消息以后將往 Handler 對象中 post 顯示消息,然后調(diào)用顯示處理函數(shù)將 Toast 中的 View 添加到了 WMS 管理中,Toast窗口顯示
  • NotificationManager的WorkerHandler收到MESSAGE_TIMEOUT消息, NotificationManager遠(yuǎn)程調(diào)用hide方法進(jìn)程隱藏Toast 窗口,然后將窗口token從WMS中刪除,并且判斷吐司消息隊列中是否還有消息,如果有,則繼續(xù)吐司!
3.4 Snackbar和Toast比較
  • 可以使用snackBar替代Toast,即使用戶禁掉了通知權(quán)限,也可以顯示出來。SnackBar,其實就是使用View系統(tǒng)去模擬一個窗口行為,而且還能更加快速的實現(xiàn)動畫效果,是不是很棒。
  • Snackbar是Android自5.0系統(tǒng)推出MaterialDesign后官方推薦的控件,在交互友好性方面比Toast要好

4.Toast封裝庫介紹

4.1 能夠滿足的需求
  • 可以設(shè)置吐司的位置,偏移,吐司文字顏色,吐司背景顏色等等。簡單的代碼就可以實現(xiàn)你需要的多種場景。也可以設(shè)置定義布局的吐司。項目地址:https://github.com/yangchong211/YCDialog
4.2 具有的優(yōu)勢
  • 采用builder構(gòu)造者模式,鏈?zhǔn)骄幊?,一行代碼調(diào)用即可設(shè)置吐司Toast。
  • 為了避免靜態(tài)toast對象內(nèi)存泄漏,固可以使用應(yīng)用級別的上下文context。所以這里我就直接采用了應(yīng)用級別Application上下文,需要在application進(jìn)行初始化一下。即可調(diào)用……

    //初始化
    ToastUtils.init(this);
    
    //可以自由設(shè)置吐司的背景顏色,默認(rèn)是純黑色
    ToastUtils.setToastBackColor(this.getResources().getColor(R.color.color_7f000000));
    
    //直接設(shè)置最簡單吐司,只有吐司內(nèi)容
    ToastUtils.showRoundRectToast("自定義吐司");
    
    //設(shè)置吐司標(biāo)題和內(nèi)容
    ToastUtils.showRoundRectToast("吐司一下","他發(fā)的撒經(jīng)濟(jì)法的解放軍");
    
    //第三種直接設(shè)置自定義布局的吐司
    ToastUtils.showRoundRectToast(R.layout.view_layout_toast_delete);
    
    //或者直接采用bulider模式創(chuàng)建
    ToastUtils.Builder builder = new ToastUtils.Builder(this.getApplication());
    builder
            .setDuration(Toast.LENGTH_SHORT)
            .setFill(false)
            .setGravity(Gravity.CENTER)
            .setOffset(0)
            .setDesc("內(nèi)容內(nèi)容")
            .setTitle("標(biāo)題")
            .setTextColor(Color.WHITE)
            .setBackgroundColor(this.getResources().getColor(R.color.blackText))
            .build()
            .show();
  • 因為看到網(wǎng)上有許多toast的封裝,需要傳遞上下文,后來感覺是不是不需要傳遞這個參數(shù),直接統(tǒng)一初始化一下就好呢。所以才有了這個toast的改良版。
    • 如果沒有調(diào)用ToastUtils.init(this)初始化,則會提示報錯ToastUtils context is not null,please first init",具體看下面代碼。
      /**
      * 檢查上下文不能為空,必須先進(jìn)性初始化操作
      */
      private static void checkContext(){
      if(mApp==null){
          throw new NullPointerException("ToastUtils context is not null,please first init");
      }
      }

5.Toast遇到的異常問題

5.1 Toast偶爾報錯Unable to add window
  • 報錯日志,是不是有點眼熟呀?更多可以看我的開源項目:https://github.com/yangchong211
    android.view.WindowManager$BadTokenException
        Unable to add window -- token android.os.BinderProxy@7f652b2 is not valid; is your activity running?
  • 查詢報錯日志是從哪里來的
    • Toast源碼深度分析
  • 發(fā)生該異常的原因
    • 這個異常發(fā)生在Toast顯示的時候,原因是因為token失效。通常情況下,一般是不會出現(xiàn)這種異常。但是由于在某些情況下, Android進(jìn)程某個UI線程的某個消息阻塞。導(dǎo)致 TN 的 show 方法 post 出來 0 (顯示) 消息位于該消息之后,遲遲沒有執(zhí)行。這時候,NotificationManager 的超時檢測結(jié)束,刪除了 WMS 服務(wù)中的 token 記錄。刪除 token 發(fā)生在 Android 進(jìn)程 show 方法之前。這就導(dǎo)致了上面的異常。
    • 測試代碼。模擬一下異常的發(fā)生場景,其實很容易,只需要這樣做就可以出現(xiàn)上面這個問題
      Toast.makeText(this,"瀟湘劍雨-yc",Toast.LENGTH_SHORT).show();
      try {
          Thread.sleep(20000);
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
  • 解決辦法,目前見過好幾種,思考一下那種比較好……
    • 第一種,既然是報is your activity running,那可以不可以在吐司之前先判斷一下activity是否running呢?
    • 第二種,拋出異常增加try-catch,代碼如下所示,最后仍然無法解決問題
      • 按照源碼分析,異常是發(fā)生在下一個UI線程消息中,因此在上一個ui線程消息中加入try-catch是沒有意義的。而且用到吐司地方這么多,這樣做也不方便啦!
    • 第三種,那就是自定義類似吐司Toast的view控件。個人建議除非要求非常高,不然不要這樣做。畢竟發(fā)生這種異常還是比較少見的
  • 哪些情況會發(fā)生該問題?
    • UI 線程執(zhí)行了一條非常耗時的操作,比如加載圖片等等,就類似上面用 sleep 模擬情況
    • 進(jìn)程退后臺或者息屏了,系統(tǒng)為了減少電量或者某種原因,分配給進(jìn)程的cpu時間減少,導(dǎo)致進(jìn)程內(nèi)的指令并不能被及時執(zhí)行,這樣一樣會導(dǎo)致進(jìn)程看起來”卡頓”的現(xiàn)象
    • 當(dāng)TN拋出消息的時候,前面有大量的 UI 線程消息等待執(zhí)行,而每個 UI 線程消息雖然并不卡頓,但是總和如果超過了 NotificationManager 的超時時間,還是會出現(xiàn)問題
5.2 Toast運行在子線程問題
  • 先來看看問題代碼,會出現(xiàn)什么問題呢?
    new Thread(new Runnable() {
        @Override
        public void run() {
            ToastUtils.showRoundRectToast("瀟湘劍雨-楊充");
        }
    }).start();
    • 報錯日志如下所示:
    • Toast源碼深度分析
  • 然后找找報錯日志從哪里來的
    • ![image]()
  • 子線程中吐司的正確做法,代碼如下所示
    new Thread(new Runnable() {
        @Override
        public void run() {
            Looper.prepare();
            ToastUtils.showRoundRectToast("瀟湘劍雨-楊充");
            Looper.loop();
        }
    }).start();
  • 得出的結(jié)論
    • Toast也可以在子線程執(zhí)行,不過需要手動提供Looper環(huán)境的。
    • Toast在調(diào)用show方法顯示的時候,內(nèi)部實現(xiàn)是通過Handler執(zhí)行的,因此自然是不阻塞Binder線程,另外,如果addView的線程不是Loop線程,執(zhí)行完就結(jié)束了,當(dāng)然就沒機(jī)會執(zhí)行后續(xù)的請求,這個是由Hanlder的構(gòu)造函數(shù)保證的??梢钥纯磆andler的構(gòu)造函數(shù),如果Looper==null就會報錯,而Toast對象在實例化的時候,也會為自己實例化一個Hanlder,這就是為什么說“一定要在主線程”,其實準(zhǔn)確的說應(yīng)該是 “一定要在Looper非空的線程”。
    • Handler的構(gòu)造函數(shù)如下所示:
    • Toast源碼深度分析
    • Toast源碼深度分析
5.3 Toast如何添加系統(tǒng)窗口的權(quán)限
  • 作為程序員,都知道任何視圖的顯示都要依賴于一個視圖窗口Window,同樣Toast的顯示也需要一個窗口,而且它還是一個系統(tǒng)窗口,這個窗口最終會被WindowManagerService(WMS)標(biāo)記管理。當(dāng)顯示一個Toast時,調(diào)用show方法后,會通過TN 類中的handleShow方法處理展示的邏輯,同時WMS會生成一個token,而我們知道WMS本身就是一個系統(tǒng)級的服務(wù),所以由它生成的token必然擁有權(quán)限添加系統(tǒng)窗口,最后WMS調(diào)用addView方法將view和mParams參數(shù)帶進(jìn)來,這樣就可以展示吐司呢。
  • 需要注意:WindowManager檢查當(dāng)前窗口的token是否有效,如果有效,則添加窗口展示Toast;如果無效,則拋出異常,會發(fā)生5.1這種類型的異常。
    • 在那個地方檢查token呢?在mWM.addView(mView, mParams)這里檢查token,點擊去可以發(fā)現(xiàn)ViewManager是個接口,這時候可以去看WindowManagerImpl類,繼承ViewManager。
    • Toast源碼深度分析
    • Toast源碼深度分析
5.4 token null is not valid
  • 看了美團(tuán)的技術(shù)文檔分享得知,這個異常其實并非是Toast的異常,而是Google對WindowManage的一些限制導(dǎo)致的。Android從7.1.1版本開始,對WindowManager做了一些限制和修改,特別是TYPE_TOAST類型的窗口,必須要傳遞一個token用于權(quán)限校驗才允許添加。在stackoverflow上搜索,也較少得到這方面的解答,這塊有點難以解決這個問題。

關(guān)于其他內(nèi)容介紹

01.關(guān)于博客匯總鏈接
  • 1.技術(shù)博客匯總
  • 2.開源項目匯總
  • 3.生活博客匯總
  • 4.喜馬拉雅音頻匯總
  • 5.其他匯總
02.關(guān)于我的博客
  • 我的個人站點:www.yczbj.org,www.ycbjie.cn
  • github:https://github.com/yangchong211
  • 知乎:https://www.zhihu.com/people/yang-chong-69-24/pins/posts
  • 簡書:http://www.jianshu.com/u/b7b2c6ed9284
  • csdn:http://my.csdn.net/m0_37700275
  • 喜馬拉雅聽書:http://www.ximalaya.com/zhubo/71989305/
  • 開源中國:https://my.oschina.net/zbj1618/blog
  • 泡在網(wǎng)上的日子:http://www.jcodecraeer.com/member/content_list.php?channelid=1
  • 郵箱:yangchong211@163.com
  • 阿里云博客:https://yq.aliyun.com/users/article?spm=5176.100- 239.headeruserinfo.3.dT4bcV
  • segmentfault頭條:https://segmentfault.com/u/xiangjianyu/articles
向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI