Android之Window和彈窗問題

楊充發表於2019-05-08

目錄介紹

  • 10.0.0.1 Window是什麼?如何通過WindowManager新增Window(程式碼實現)?WindowManager的主要功能是什麼?
  • 10.0.0.2 Window概念解析?WindowSession的建立過程是怎樣的?WindowSession的作用?Token的使用場景?
  • 10.0.0.3 Activity、View、Window三者之間的關係,Window有哪幾種型別?
  • 10.0.0.5 Activity的啟動過程是怎樣的?Activity建立和Dialog建立過程的異同?
  • 10.0.0.6 如何處理快速連續點選了多次按鈕時Toast就觸發了多次而關閉不掉?
  • 10.0.0.7 DecorView何時才被WindowManager真正新增到Window中?Window的addView原始碼分析?
  • 10.0.0.8 Dialog的Window建立過程?為什麼Dialog不能用Application的Context?
  • 10.0.0.9 什麼是DecorView?如何獲取到DecorView?DecorView的職責是什麼?DecorView如何被載入到Window中?
  • 10.0.1.0 DecorView如何顯示出來,為什麼setContentView()設定的介面,為什麼在onResume()之後才對使用者可見呢?
  • 10.0.1.1 什麼是ViewRoot?ViewRoot屬於View樹的一份子嗎?ViewRoot的工作流程是怎麼樣的?
  • 10.0.1.2 吐司為何會出現記憶體洩漏?在Toast構造方法中建立NT物件是幹什麼用的?Toast是怎麼show出來的?
  • 10.0.1.3 連續吐司是如何確定吐司的先後順序?為什麼Toast執行show後過了一會兒就自動銷燬?
  • 10.0.1.4 如何理解普通應用的Toast顯示數量是有限制的?為什麼要判斷是否是系統吐司?為何Activity銷燬後Toast仍會顯示?
  • 10.0.1.5 為什麼說Toast儘量用全域性上下文?說一下Toast的顯示和隱藏重點邏輯,說下你的理解?
  • 10.0.1.6 Toast報錯Unable to add window是什麼意思?Toast執行在子執行緒會問題,在子執行緒或者service中能執行嗎?
  • 10.0.1.7 為什麼建議用DialogFragment替代Dialog?如何定義DialogFragment樣式?使用dialogFragment有何好處?
  • 10.0.1.8 Dialog的Window建立過程是怎樣的?為什麼Dialog不能用Application的Context,說一下原因?
  • 10.0.1.9 Dialog和Window有什麼關係?Dialog的dismiss和cancel()方法都可銷燬彈窗,它們有什麼區別?
  • 10.0.2.0 PopupWindow中不設定為什麼必須設定寬高?PopupWindow和Dialog有什麼區別?說下建立和銷燬的大概流程?
  • 10.0.2.1 Snackbar與吐司有何區別在哪裡?Snackbar控制元件show時為何從下往上移出來?為什麼顯示在最下面?
  • 10.0.2.2 說一下Snackbar和SnackbarManager類的設計有哪些奧妙的地方,如何處理訊息的顯示順序?

彈窗部落格筆記彙總

  • 02.Toast原始碼深度分析
    • 最簡單的建立,簡單改造避免重複建立,show()方法原始碼分析,scheduleTimeoutLocked吐司如何自動銷燬的,TN類中的訊息機制是如何執行的,普通應用的Toast顯示數量是有限制的,用程式碼解釋為何Activity銷燬後Toast仍會顯示,Toast偶爾報錯Unable to add window是如何產生的,Toast執行在子執行緒問題,Toast如何新增系統視窗的許可權等等
  • 03.DialogFragment原始碼分析
    • 最簡單的使用方法,onCreate(@Nullable Bundle savedInstanceState)原始碼分析,重點分析彈窗展示和銷燬原始碼,使用中show()方法遇到的IllegalStateException分析
  • 04.Dialog原始碼分析
    • AlertDialog原始碼分析,通過AlertDialog.Builder物件設定屬性,Dialog生命週期,Dialog中show方法展示彈窗分析,Dialog的dismiss銷燬彈窗,Dialog彈窗問題分析等等
  • 05.PopupWindow原始碼分析
    • 顯示PopupWindow,注意問題寬和高屬性,showAsDropDown()原始碼,dismiss()原始碼分析,PopupWindow和Dialog有什麼區別?為何彈窗點選一下就dismiss呢?
  • 06.Snackbar原始碼分析
    • 最簡單的建立,Snackbar的make方法原始碼分析,Snackbar的show顯示與點選消失原始碼分析,顯示和隱藏中動畫原始碼分析,Snackbar的設計思路,為什麼Snackbar總是顯示在最下面
  • 07.彈窗常見問題
    • DialogFragment使用中show()方法遇到的IllegalStateException,什麼常見產生的?Toast偶爾報錯Unable to add window,Toast執行在子執行緒導致崩潰如何解決?
  • 08.Builder模式
    • 你會發現,在這個彈窗封裝庫中,很多地方用到了builder模式,那麼可以先了解下Builder模式使用場景,簡單案例,Builder模式實際案例Demo展示,看看AlertDialog.Builder原始碼如何實現,為什麼AlertDialog要使用builder模式呢?builder模式優缺點分析。

10.0.0.1 Window是什麼?如何通過WindowManager新增Window(程式碼實現)?WindowManager的主要功能是什麼?

  • Window是什麼?
    • 表示一個視窗的概念,是所有View的直接管理者,任何檢視都通過Window呈現(點選事件由Window->DecorView->View; Activity的setContentView底層通過Window完成)
    • Window是一個抽象類,具體實現是PhoneWindow
    • 建立Window需要通過WindowManager建立
    • WindowManager是外界訪問Window的入口
    • Window具體實現位於WindowManagerService中
    • WindowManager和WindowManagerService的互動是通過IPC完成
  • 如何通過WindowManager新增Window(程式碼實現)?
    • 如下所示
      //1. 控制元件 
      Button button = new Button(this); 
      button.setText("Window Button"); 
      //2. 佈局引數 
      WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, 0, 0, PixelFormat.TRANSPARENT); 
      layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; 
      layoutParams.gravity = Gravity.LEFT | Gravity.TOP; 
      layoutParams.x = 100; 
      layoutParams.y = 300; 
      // 必須要有type不然會異常: the specified window type 0 is not valid 
      layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR; 
      //3. 獲取WindowManager並新增控制元件到Window中 
      WindowManager windowManager = getWindowManager(); 
      windowManager.addView(button, layoutParams);
      複製程式碼
  • WindowManager的主要功能是什麼?
    • 新增、更新、刪除View
      public interface ViewManager{ 
          public void addView(View view, ViewGroup.LayoutParams params); 
          //新增View 
          public void updateViewLayout(View view, ViewGroup.LayoutParams params); 
          //更新View 
          public void removeView(View view); 
          //刪除View 
      }
      複製程式碼

10.0.0.2 Window概念解析?WindowSession的建立過程是怎樣的?WindowSession的作用?Token的使用場景?

  • Window概念解析?
    • Window和View通過ViewRootImpl建立聯絡
    • Window並不是實際存在的,而是以View的形式存在
    • WindowManager的三個介面方法也是針對View的
    • 實際使用中無法直接訪問Window,必須通過WindowManager
    • View是檢視的呈現方式,但是不能單獨存在,必須依附在Window這個抽象的概念上
    • WMS把所有的使用者訊息發給View/ViewGroup,但是在View/ViewGroup處理訊息的過程中,有一些操作是公共的, Window把這些公共行為抽象出來, 這就是Window。
  • WindowSession的建立過程是怎樣的?
    • 在WindowManager的addView中會建立ViewRootImpl,內部會通過WMS去獲取WindowSession
    • WindowSession的型別是IWindowSession,本身是Binder物件,真正實現類是Session
    • image
  • WindowSession的作用?部落格
    • 表示一個Active Client Session
    • 每個程式一般都有一個Session物件
    • 用於WindowManager互動
  • Token的使用場景?
    • Popupwindow的showAtLocation第一個引數需要傳入View,這個View就是用來獲取Token的。
    • Android 5.0新增空間SnackBar同理也需要一個View來獲取Token
  • Token是什麼?
    • 型別為IBinder,是一個Binder物件。
    • 主要分兩種Token:
      • 指向Window的token: 主要是實現WmS和應用所在程式通訊。
      • 指向ActivityRecord的token: 主要是實現WmS和AmS通訊的。
  • Activity中的Token
    • ActivityRecord是AmS中用來儲存一個Activity資訊的輔助類。
    • AMS中需要根據Token去找到對應的ActivityRecord。

10.0.0.3 Activity、View、Window三者之間的關係,Window有哪幾種型別?

  • Activity、View、Window三者之間的關係
    • 在Activity啟動過程其中的attach()方法中初始化了PhoneWindow,而PhoneWindow是Window的唯一實現類,然後Activity通過setContentView將View設定到了PhoneWindow上,而View通過WindowManager的addView()、removeView()、updateViewLayout()對View進行管理。
  • Window有哪幾種型別
    • 應用Window:對應一個Activity。
    • 子Window:不能單獨存在,需附屬特定的父Window。如Dialog。
    • 系統Window: 需申明許可權才能建立。如Toast。
  • Activity 與 PhoneWindow 與 DecorView 關係圖
    • image

10.0.0.5 Activity的啟動過程是怎樣的?Activity的檢視載入的原始碼分析?Activity建立和Dialog建立過程的異同?

  • Activity的啟動過程是怎樣的?
    • 最終會由ActivityThread中的performLauchActivity來完成整個啟動過程
    • performLauchActivity內部會通過類載入器建立Activity的例項物件
    • 併為Activity的例項物件呼叫attach方法,為其關聯執行過程中所以來的上下文環境變數
    • attch方法中,系統會建立Activity所屬的Window物件,併為其設定回撥介面
    • Window物件的建立是通過PolicyManager的makeNewWindow方法實現。部落格
    • Activity實現了window的callback介面,因此外界狀態改變時會回撥Activity的方法(onAttachedToWindow、dispatchTouchEvent等等)
  • Activity的檢視載入的原始碼分析
    • image
  • Dialog的Window建立過程
    • 建立WindowDialog。和Activity類似,同樣是通過PolicyManager.makeNewWindow() 來實現。
    • 初始化DecorView並將Dialog的檢視新增到DecorView中去。和Activity類似,同樣是通過Window.setContentView() 來實現。
    • 將DecorView新增到Window中顯示。和Activity一樣,都是在自身要出現在前臺時才會將新增Window。
      • Dialog.show() 方法:完成DecorView的顯示。
      • WindowManager.remoteViewImmediate() 方法:當Dialog被dismiss時移除DecorView。

10.0.0.6 如何處理快速連續點選了多次按鈕時Toast就觸發了多次而關閉不掉?

  • 使用中遇到的問題
    • 例如:當點選有些按鈕,需要吐司進行提示時;快速連續點選了多次按鈕,Toast就觸發了多次。可能導致Toast就長時間關閉不掉了。又或者我們其實已在進行其他操作了,應該彈出新的Toast提示,而上一個Toast卻還沒顯示結束。部落格
  • 解決的辦法
    建立工具類:
    /**
    * 吐司工具類    避免點選多次導致吐司多次,最後導致Toast就長時間關閉不掉了
    * @param context
    * @param content
    */
    private static Toast toast;
    public static void showToast(Context context, String content) {
        if (toast == null) {
            toast = Toast.makeText(context.getApplicationContext(), content, Toast.LENGTH_SHORT);
        } else {
            toast.setText(content);
        }
        toast.show();
    }
    複製程式碼
  • 這樣用的原理
    • 先判斷Toast物件是否為空,如果是空的情況下才會呼叫makeText()方法來去生成一個Toast物件,否則就直接呼叫setText()方法來設定顯示的內容,最後再呼叫show()方法將Toast顯示出來。由於不會每次呼叫的時候都生成新的Toast物件,因此剛才我們遇到的問題在這裡就不會出現

10.0.0.7 DecorView何時才被WindowManager真正新增到Window中?Window的addView原始碼分析?

  • DecorView何時才被WindowManager真正新增到Window中?
    • 即使Activity的佈局已經成功新增到DecorView中,DecorView此時還沒有新增到Window中
    • ActivityThread的handleResumeActivity方法中,首先會呼叫Activity的onResume方法,接著呼叫Activity的makeVisible()方法
    • makeVisible()中完成了DecorView的新增和顯示兩個過程
    • image
  • Window的addView原始碼分析?
    • WindowManager是一個介面,真正實現類是WindowManagerImpl,並最終以代理模式交給WindowManagerGlobal實現。
    • addView: 1-建立ViewRootImpl;2-將ViewRoor、DecorView、佈局引數儲存到WM的內部列表中;3-ViewRoot.setView()建立ViewRoot和DecorView的聯絡。
    • setView:1-進行View繪製三大流程;2-會通過WindowSession完成Window的新增過程(一次IPC呼叫)
    • requestLayout:內部呼叫scheduleTraversals(), 底層通過mChoreographer去監聽下一幀的重新整理訊號。
    • mWindowSession.addToDisplay: 執行WindowManangerService的addWindow
    • addWindow: 檢查引數等設定;檢查Token;將Token、Window儲存到WMS中;將WindowState儲存到Session中。
  • Window的remove原始碼與解析
    • WindowManager中提供了兩種刪除介面:removeView非同步刪除、removeViewImmediate同步刪除(不建議使用)
    • 呼叫WMGlobal的removeView
    • 呼叫到WMGlobal的removeViewLocked進行真正的移除
    • 執行ViewRoot的die方法(): 1-同步方法直接呼叫doDie 2-非同步方法直接傳送Message
    • doDie(): 呼叫dispatchDetachedFromWindow()和WindowManagerGlobal.getInstance().doRemoveView(this)
    • dispatchDetachedFromWindow:部落格
      • 1回撥onDetachedFromeWindow;
      • 2垃圾回收相關操作;
      • 3通過Session的remove()在WMS中刪除Window;
      • 4通過Choreographer移除監聽器

10.0.0.8 Dialog的Window建立過程?為什麼Dialog不能用Application的Context?

  • Dialog的Window建立過程?
    • 建立Window——同樣是通過PolicyManager的makeNewWindow方法完成,與Activity建立過程一致
    • 初始化DecorView並將Dialog的檢視新增到DecorView中——和Activity一致(setContentView)
    • 將DecorView新增到Window中並顯示——在Dialog的show方法中,通過WindowManager將DecorView新增到Window中(mWindowManager.addView(mDecor, 1))
    • Dialog關閉時會通過WindowManager來移除DecorView:mWindowManager.removeViewImmediate(mDecor)
    • Dialog必須採用Activity的Context,因為有應用token(Application的Context沒有應用token),也可以將Dialog的Window通過type設定為系統Window就不再需要token。
  • 為什麼Dialog不能用Application的Context?
    • Dialog本身的Token為null,在初始化時如果是使用Application或者Service的Context,在獲取到WindowManager時,獲取到的token依然是null。
    • Dialog如果採用Activity的Context,獲取到的WindowManager是在activity.attach()方法中建立,token指向了activity的token。
    • 因為通過Application和Service的Context將無法獲取到Token從而導致失敗。

10.0.0.9 什麼是DecorView?如何獲取到DecorView?DecorView的職責是什麼?DecorView如何被載入到Window中?

  • 什麼是DecorView
    • DecorView是FrameLayout的子類,它可以被認為是Android檢視樹的根節點檢視。
    • DecorView作為頂級View,一般情況下它內部包含一個豎直方向的LinearLayout,在這個LinearLayout裡面有上下三個部分,上面是個ViewStub,延遲載入的檢視(應該是設定ActionBar,根據Theme設定),中間的是標題欄(根據Theme設定,有的佈局沒有),下面的是內容欄。
  • 如何獲取到DecorView
    • 在Activity中通過setContentView所設定的佈局檔案其實就是被加到內容欄之中的,成為其唯一子View,就是上面的id為content的FrameLayout中,在程式碼中可以通過content來得到對應載入的佈局。部落格
    ViewGroup content = (ViewGroup)findViewById(android.R.id.content);
    ViewGroup rootView = (ViewGroup) content.getChildAt(0);
    複製程式碼
  • DecorView的職責是什麼
    • 通過原始碼瞭解可以知道,Activity就像個控制器,不負責檢視部分。Window像個承載器,裝著內部檢視。DecorView就是個頂層檢視,是所有View的最外層佈局。ViewRoot像個聯結器,負責溝通,通過硬體的感知來通知檢視,進行使用者之間的互動。
  • DecorView如何被載入到Window中?部落格
    • 從Activity中的setContentView()開始。在Activity中的attach()方法中,生成了PhoneWindow例項。既然有了Window物件,那麼我們就可以**設定DecorView給Window物件了。
    • 從中獲取mContentParent。獲得到之後,然後通過installDecor方法,然後生成DecorView,不過這裡操作很複雜,大概流程先從主題中獲取樣式,然後根據樣式,載入對應的佈局到DecorView中,為mContentParent新增View,即Activity中的佈局。
    • 具體可以看這篇文章:10.DecorView介紹

10.0.1.0 DecorView如何顯示出來,為什麼setContentView()設定的介面,為什麼在onResume()之後才對使用者可見呢?

  • 通過setContentView()設定的介面,為什麼在onResume()之後才對使用者可見呢?這就要從ActivityThread開始說起。
    private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    
        //就是在這裡呼叫了Activity.attach()呀,接著呼叫了Activity.onCreate()和Activity.onStart()生命週期,
        //但是由於只是初始化了mDecor,新增了佈局檔案,還沒有把
        //mDecor新增到負責UI顯示的PhoneWindow中,所以這時候對使用者來說,是不可見的
        Activity a = performLaunchActivity(r, customIntent);
    
        ......
    
        if (a != null) {
        //這裡面執行了Activity.onResume()
        handleResumeActivity(r.token, false, r.isForward,
                            !r.activity.mFinished && !r.startsNotResumed);
    
        if (!r.activity.mFinished && r.startsNotResumed) {
            try {
                    r.activity.mCalled = false;
                    //執行Activity.onPause()
                    mInstrumentation.callActivityOnPause(r.activity);
                    }
            }
        }
    }
    複製程式碼
  • 重點看下handleResumeActivity(),在這其中,DecorView將會顯示出來,同時重要的一個角色:ViewRoot也將登場。
    • 這個方法裡面會呼叫performResumeActivity方法,這個時候,Activity.onResume()已經呼叫了,但是現在介面還是不可見的
    • 接著講decorView新增進WindowManager了,但是這個時候,還是不可見的
    • 最後執行makeVisible,執行了重要的操作,使得DecorView可見
  • 當我們執行了Activity.makeVisible()方法之後,介面才對我們是可見的。部落格
    void makeVisible() {
       if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());//將DecorView新增到WindowManager
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);//DecorView可見
    }
    複製程式碼
    • 到此DecorView便可見,顯示在螢幕中。但是在這其中,wm.addView(mDecor, getWindow().getAttributes());起到了重要的作用,因為其內部建立了一個ViewRootImpl物件,負責繪製顯示各個子View。
  • 最後通過WindowManagerImpl的addView方法將DecorView載入出來
    • 看到其中例項化了ViewRootImpl物件,然後呼叫其setView()方法。其中setView()方法經過一些列折騰,最終呼叫了performTraversals()方法,然後依照下圖流程層層呼叫,完成繪製,最終介面才顯示出來。
    • img
    • 具體更加詳細的過程,可以看10.DecorView介紹

10.0.1.1 什麼是ViewRoot?ViewRoot屬於View樹的一份子嗎?ViewRoot的工作流程是怎麼樣的?

  • 什麼是ViewRoot
    • ViewRoot可能比較陌生,但是其作用非常重大。所有View的繪製以及事件分發等互動都是通過它來執行或傳遞的。
    • ViewRoot對應ViewRootImpl類,它是連線WindowManagerService和DecorView的紐帶,View的三大流程(測量(measure),佈局(layout),繪製(draw))均通過ViewRoot來完成。
  • ViewRoot屬於View樹的一份子嗎?
    • ViewRoot並不屬於View樹的一份子。
    • 從原始碼實現上來看,它既非View的子類,也非View的父類,但是,它實現了ViewParent介面,這讓它可以作為View的名義上的父檢視。RootView繼承了Handler類,可以接收事件並分發,Android的所有觸屏事件、按鍵事件、介面重新整理等事件都是通過ViewRoot進行分發的。部落格
  • 下面結構圖可以清晰的揭示四者之間的關係:
    • img

10.0.1.2 吐司為何會出現記憶體洩漏?在Toast構造方法中建立NT物件是幹什麼用的?Toast是怎麼show出來的?

  • 吐司為何會出現記憶體洩漏
    • 原因在於:如果在 Toast 消失之前,Toast 持有了當前 Activity,而此時,使用者點選了返回鍵,導致 Activity 無法被 GC 銷燬, 這個 Activity 就引起了記憶體洩露。
  • 在Toast構造方法中建立NT物件是幹什麼用的?
    • TN是屬於Toast內部一個私有靜態類,它是通過aidl進行通訊,主要作用是實現吐司的show和hide功能。
    • 在構造方法中,建立了NT物件,那麼有人便會問,NT是什麼東西呢?看看NT的原始碼,可以發現NT實現了ITransientNotification.Stub,提到這個感覺是不是很熟悉,沒錯,在aidl中就會用到這個。
      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);
      }
      複製程式碼
      • image
    • 在TN類中,可以看到,實現了AIDL的show與hide方法
      • TN是Toast內部的一個私有靜態類,繼承自ITransientNotification.Stub,ITransientNotification.Stub是出現在服務端實現的Service中,就是一個Binder物件,也就是對一個aidl檔案的實現而已
      @Override
      public void show(IBinder windowToken) {
          if (localLOGV) Log.v(TAG, "SHOW: " + this);
          mHandler.obtainMessage(0, windowToken).sendToTarget();
      }
      
      @Override
      public void hide() {
          if (localLOGV) Log.v(TAG, "HIDE: " + this);
          mHandler.post(mHide);
      }
      複製程式碼
    • 接著看下這個ITransientNotification.aidl檔案
      /** @hide */
      oneway interface ITransientNotification {
          void show();
          void hide();
      }
      複製程式碼
  • Toast是怎麼show出來的?
    • 通過AIDL(Binder)通訊拿到NotificationManagerService的服務訪問介面,然後把TN物件和一些引數傳遞到遠端NotificationManagerService中去
    • 當 Toast在show的時候,然後把這個請求放在 NotificationManager 所管理的佇列中,並且為了保證 NotificationManager 能跟程式互動,會傳遞一個TN型別的Binder物件給NotificationManager系統服
    • 然後通過service.enqueueToast方法,record是將Toast封裝成ToastRecord物件,放入mToastQueue中。通過下面程式碼可以得知:通過isSystemToast判斷是否為系統Toast。如果當前Toast所屬的程式的包名為“android”,則為系統Toast。如果是系統Toast一定可以進入到系統Toast佇列中,不會被黑名單阻止。

10.0.1.3 連續吐司是如何確定吐司的先後順序?為什麼Toast執行show後過了一會兒就自動銷燬?

  • 連續吐司是如何確定吐司的先後順序?
    • 主要是說一下showNextToastLocked()方法中的原始碼
      • 首先獲取吐司訊息佇列中第一個ToastRecord物件,然後判斷該物件如果不為null的話,就開始通過callback進行show,且傳遞了token引數,注意這個show是通知程式顯示。然後再呼叫scheduleTimeoutLocked(record)方法執行超時後自動取消的邏輯。同時需要注意的時,如果出現了異常,則會從吐司訊息佇列中移除該record……
      • 那麼callback是幹嘛的呢,一般印象中callback是處理回撥的?從ITransientNotification callback得知,這個callback哥們竟然是是一個 ITransientNotification 型別的物件,也就是前面說到的TN的Binder代理物件。
    • 簡而言之,也就是說,TN中的訊息機制也是通過handler進行實現的。在show方法中傳送訊息,當mHandler接受到訊息之後,就呼叫handleShow(token)處理邏輯,通過WindowManager將view新增進來,同時在該方法中也設定了大量的佈局屬性。
  • 為什麼Toast執行show後過了一會兒就自動銷燬?部落格
    • 回撥了Toast的TN的show,當timeout可能就是hide呢。分析NotificationManagerService原始碼中的showNextToastLocked()的scheduleTimeoutLocked(record)原始碼,可以知道在NotificationManagerService通過handler延遲delay時間傳送訊息,然後通過callback呼叫hide,由於callback是TN中Binder的代理物件, 所以便可以呼叫到TN中的hide方法達到銷燬吐司的目的。
    • handleHide()原始碼如下所示,可知當銷燬後先將view移除,然後在置空操作。
    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;
        }
    }
    複製程式碼

10.0.1.4 如何理解普通應用的Toast顯示數量是有限制的?為什麼要判斷是否是系統吐司?為何Activity銷燬後Toast仍會顯示?

  • 如何理解普通應用的Toast顯示數量是有限制的?
    • 如何判斷是否是系統吐司呢?如果當前Toast所屬的程式的包名為“android”,則為系統Toast,或者呼叫isCallerSystem()方法
    final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
    複製程式碼
    • 接著看看isCallerSystem()方法原始碼,isCallerSystem的原始碼也比較簡單,就是判斷當前Toast所屬程式的uid是否為SYSTEM_UID、0、PHONE_UID中的一個,如果是,則為系統Toast;如果不是,則不為系統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());
    }
    複製程式碼
  • 為什麼要判斷是否是系統吐司?
    • 從原始碼可知:首先系統Toast一定可以進入到系統Toast佇列中,不會被黑名單阻止。然後系統Toast在系統Toast佇列中沒有數量限制,而普通pkg所傳送的Toast在系統Toast佇列中有數量限制。
    • 那麼關於數量限制這個結果從何而來,大概是多少呢?檢視將要入隊的Toast是否已經在系統Toast佇列中。這是通過比對pkg和callback來實現的。通過下面原始碼分析可知:只要Toast的pkg名稱和tn物件是一致的,則系統把這些Toast認為是同一個Toast。
    • 然後再看看下面這個原始碼截圖,可知,非系統Toast,每個pkg在當前mToastQueue中Toast有總數限制,不能超過MAX_PACKAGE_NOTIFICATIONS,也就是50
    • image
    • image
  • 為何Activity銷燬後Toast仍會顯示
    • 記得以前昊哥問我,為何toast在activity銷燬後仍然會彈出呢,我毫不思索地說,因為toast是系統級別的呀。那麼是如何實現的呢,我就無言以對呢……今天終於可以回答呢!
    • 還是回到NotificationManagerService類中的enqueueToast方法中,直接檢視keepProcessAliveIfNeededLocked(callingPid)方法。這段程式碼的意思是將當前Toast所在程式設定為前臺程式,這裡的mAm = ActivityManager.getService(),呼叫了setProcessImportant方法將當前pid的程式置為前臺程式,保證不會系統殺死。這也就解釋了為什麼當我們finish當前Activity時,Toast還可以顯示,因為當前程式還在執行。
    • image

10.0.1.5 為什麼說Toast儘量用全域性上下文?說一下Toast的顯示和隱藏重點邏輯,說下你的理解?

  • 為什麼說Toast儘量用全域性上下文?
    • 在使用Toast時context引數儘量使用getApplicationContext(),可以有效的防止靜態引用導致的記憶體洩漏。
    • 有時候我們會發現Toast彈出過多就會延遲顯示,因為上面原始碼分析可以看見Toast.makeText是一個靜態工廠方法,每次呼叫這個方法都會產生一個新的Toast物件,當我們在這個新new的物件上呼叫show方法就會使這個物件加入到NotificationManagerService管理的mToastQueue訊息顯示佇列裡排隊等候顯示;所以如果我們不每次都產生一個新的Toast物件(使用單例來處理)就不需要排隊,也就能及時更新呢。
  • 說一下Toast的顯示和隱藏重點邏輯,說下你的理解?部落格
    • Toast呼叫show方法 ,其實就是是將自己納入到NotificationManager的Toast管理中去,期間傳遞了一個本地的TN型別或者是 ITransientNotification.Stub的Binder物件
    • NotificationManager 收到 Toast 的顯示請求後,將生成一個 Binder 物件,將它作為一個視窗的 token 新增到 WMS 物件,並且型別是 TOAST
    • NotificationManager 將這個視窗token通過ITransientNotification的show方法傳遞給遠端的TN物件,並且丟擲一個超時監聽訊息 scheduleTimeoutLocked
    • TN 物件收到訊息以後將往 Handler 物件中 post 顯示訊息,然後呼叫顯示處理函式將 Toast 中的 View 新增到了 WMS 管理中,Toast視窗顯示
    • NotificationManager的WorkerHandler收到MESSAGE_TIMEOUT訊息, NotificationManager遠端呼叫hide方法程式隱藏Toast 視窗,然後將視窗token從WMS中刪除,並且判斷吐司訊息佇列中是否還有訊息,如果有,則繼續吐司!

10.0.1.6 Toast報錯Unable to add window是什麼意思?Toast執行在子執行緒會問題,在子執行緒或者service中能執行嗎?

  • Toast偶爾報錯Unable to add window
    • 報錯日誌,是不是有點眼熟呀?更多可以看我的開源專案:github.com/yangchong21…
      android.view.WindowManager$BadTokenException
          Unable to add window -- token android.os.BinderProxy@7f652b2 is not valid; is your activity running?
      複製程式碼
    • 查詢報錯日誌是從哪裡來的
      • image
    • 發生該異常的原因
      • 這個異常發生在Toast顯示的時候,原因是因為token失效。通常情況下,一般是不會出現這種異常。但是由於在某些情況下, Android程式某個UI執行緒的某個訊息阻塞。導致 TN 的 show 方法 post 出來 0 (顯示) 訊息位於該訊息之後,遲遲沒有執行。這時候,NotificationManager 的超時檢測結束,刪除了 WMS 服務中的 token 記錄。刪除 token 發生在 Android 程式 show 方法之前。這就導致了上面的異常。
      • 測試程式碼。模擬一下異常的發生場景,其實很容易,只需要這樣做就可以出現上面這個問題
       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,程式碼如下所示,最後仍然無法解決問題
        • 按照原始碼分析,異常是發生在下一個UI執行緒訊息中,因此在上一個ui執行緒訊息中加入try-catch是沒有意義的。而且用到吐司地方這麼多,這樣做也不方便啦!
      • 第三種,那就是自定義類似吐司Toast的view控制元件。個人建議除非要求非常高,不然不要這樣做。畢竟發生這種異常還是比較少見的
    • 哪些情況會發生該問題?
      • UI 執行緒執行了一條非常耗時的操作,比如載入圖片等等,就類似上面用 sleep 模擬情況
      • 程式退後臺或者息屏了,系統為了減少電量或者某種原因,分配給程式的cpu時間減少,導致程式內的指令並不能被及時執行,這樣一樣會導致程式看起來”卡頓”的現象
      • 當TN丟擲訊息的時候,前面有大量的 UI 執行緒訊息等待執行,而每個 UI 執行緒訊息雖然並不卡頓,但是總和如果超過了 NotificationManager 的超時時間,還是會出現問題
  • Toast執行在子執行緒問題
    • 先來看看問題程式碼,會出現什麼問題呢?
    new Thread(new Runnable() {
        @Override
        public void run() {
            ToastUtils.showRoundRectToast("瀟湘劍雨-楊充");
        }
    }).start();
    複製程式碼
    • 報錯日誌如下所示:
    • image
  • 子執行緒中吐司的正確做法,程式碼如下所示
    new Thread(new Runnable() {
        @Override
        public void run() {
            Looper.prepare();
            ToastUtils.showRoundRectToast("瀟湘劍雨-楊充");
            Looper.loop();
        }
    }).start();
    複製程式碼
  • 得出的結論
    • Toast也可以在子執行緒執行,不過需要手動提供Looper環境的。
    • Toast在呼叫show方法顯示的時候,內部實現是通過Handler執行的,因此自然是不阻塞Binder執行緒,另外,如果addView的執行緒不是Loop執行緒,執行完就結束了,當然就沒機會執行後續的請求,這個是由Hanlder的建構函式保證的。可以看看handler的建構函式,如果Looper==null就會報錯,而Toast物件在例項化的時候,也會為自己例項化一個Hanlder,這就是為什麼說“一定要在主執行緒”,其實準確的說應該是 “一定要在Looper非空的執行緒”。部落格

10.0.1.7 為什麼建議用DialogFragment替代Dialog?如何定義DialogFragment樣式?使用dialogFragment有何好處?

  • 為什麼建議用DialogFragment替代Dialog
    • Android比較推薦採用DialogFragment實現對話方塊,它完全能夠實現Dialog的所有需求,並且還能複用Fragment的生命週期管理,被後臺殺死後,可以恢復重建。
    • 在手機配置變化導致 Activity 需要重新建立時,例如旋轉螢幕,基於 DialogFragment 的對話方塊將會由 FragmentManager 自動重建,然而基於 Dialog 實現的對話方塊卻沒有這樣的能力。
  • 如何定義DialogFragment樣式
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (local == BOTTOM) {
            setStyle(DialogFragment.STYLE_NO_TITLE, R.style.BottomDialog);
        } else if (local == CENTER || local == TOP) {
            setStyle(DialogFragment.STYLE_NO_TITLE, R.style.CenterDialog);
        }
    }
    複製程式碼
  • 建立theme主題樣式,並且進行設定
    • 設定樣式,以DialogFragment為例,只需要在onCreate中setStyle(DialogFragment.STYLE_NO_TITLE, R.style.CenterDialog)即可。
    • 注意,CenterDialog中可以設定彈窗的動畫效果。
    • 注意一下style常量,這裡只是展示常用的。
      STYLE_NORMAL:會顯示一個普通的dialog
      STYLE_NO_TITLE:不帶標題的dialog
      STYLE_NO_FRAME:無框的dialog
      STYLE_NO_INPUT:無法輸入內容的dialog,即不接收輸入的焦點,而且觸控無效。
      複製程式碼
    • 注意動畫設定如下所示
      <style name="CenterDialog" parent="@android:style/Theme.Dialog">
          <item name="android:windowTitleStyle">@null</item>
          <item name="android:windowBackground">@android:color/transparent</item>
          <item name="android:colorBackgroundCacheHint">@null</item>
          <item name="android:windowAnimationStyle">@style/CenterDialogAnimationStyle</item>
          <item name="android:windowSoftInputMode">stateUnspecified|adjustPan</item>
      </style>
      複製程式碼
  • 使用dialogFragment有何好處?
    • DialogFragment是繼承Fragment,具有Fragment的生命週期,本質上說就是Fragment,只是其內部還有一個dialog而已。你既可以當它是Dialog使用,也可以把它作為Fragment使用。
    • onCreateView可以載入客戶化更高的對話方塊,onCreateDialog載入系統AlertDialog型別對話方塊比較合適。
    • DialogFragmnet對話方塊橫屏時對話方塊不會關閉,因為DailogFragment有Fragment屬性,會在螢幕發生變化時重新建立DialogFragment。部落格
    • setStyle的呼叫點,要放在onCreateView前,一般是放在onCreat方法中執行,否則,設定的style和theme將不起作用!setStyle中,style的引數是不可以相互一起使用的,只能用一個,如果還不滿足你使用,可以通過設定theme來滿足。

10.0.1.8 Dialog的Window建立過程是怎樣的?為什麼Dialog不能用Application的Context,說一下原因?

  • Dialog的Window建立過程是怎樣的?
    • 建立Window——同樣是通過PolicyManager的makeNewWindow方法完成,與Activity建立過程一致
    • 初始化DecorView並將Dialog的檢視新增到DecorView中——和Activity一致(setContentView)
    • 將DecorView新增到Window中並顯示——在Dialog的show方法中,通過WindowManager將DecorView新增到Window中(mWindowManager.addView(mDecor, 1))
    • Dialog關閉時會通過WindowManager來移除DecorView:mWindowManager.removeViewImmediate(mDecor)
    • Dialog必須採用Activity的Context,因為有應用token(Application的Context沒有應用token),也可以將Dialog的Window通過type設定為系統Window就不再需要token。
  • 為什麼Dialog不能用Application的Context,說一下原因?
    • Dialog本身的Token為null,在初始化時如果是使用Application或者Service的Context,在獲取到WindowManager時,獲取到的token依然是null。
    • Dialog如果採用Activity的Context,獲取到的WindowManager是在activity.attach()方法中建立,token指向了activity的token。
    • 因為通過Application和Service的Context將無法獲取到Token從而導致失敗。

10.0.1.9 Dialog和Window有什麼關係?Dialog的dismiss和cancel()方法都可銷燬彈窗,它們有什麼區別?

  • Dialog和Window有什麼關係?
    • 看原始碼可知在Dialog的構造方法中直接直接構造了一個PhoneWindow,並賦值給Dialog的成員變數mWindow,從這裡可以看出其實Dialog和Activity的顯示邏輯都是類似的,都是通過對應的Window變數來實現視窗的載入與顯示的。然後我們執行了一些Window物件的初始化操作,比如設定回撥函式為本身,然後呼叫Window類的setWindowManager方法,並傳入了WindowManager。然後建立一個對話方塊監聽handler物件。
    Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
        if (createContextThemeWrapper) {
            if (themeResId == 0) {
                final TypedValue outValue = new TypedValue();
                context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
                themeResId = outValue.resourceId;
            }
            //建立一個Context
            mContext = new ContextThemeWrapper(context, themeResId);
        } else {
            mContext = context;
        }
    
        //獲取一個WindowManager物件
        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        //建立一個Window物件
        final Window w = new PhoneWindow(mContext);
        //將Window物件w賦值給mWindow
        mWindow = w;
        //為Windowd物件設定回撥,並且它本身實現了這些回撥函式
        w.setCallback(this);
        w.setOnWindowDismissedCallback(this);
        //為Window物件設定WindowManager物件
        w.setWindowManager(mWindowManager, null, null);
        w.setGravity(Gravity.CENTER);
        //建立一個對話方塊監聽Handler
        mListenersHandler = new ListenersHandler(this);
    }
    複製程式碼
  • Dialog的dismiss和cancel()方法都可銷燬彈窗,它們有什麼區別?
    • 呼叫alertDialog.cancel()或者alertDialog.dismiss()都可以達到銷燬彈窗的效果。
    • 如果沒有設定setOnCancelListener事件,則兩個方法是等效的。為什麼這樣說呢?
    • 首先看一下Dialog的cannel方法的具體實現:可以看到方法體中,若當前Dialog沒有取消,並且設定了取消message,則呼叫Message.obtain(mCancelMessage).sendToTarget()。而這個mCancelMessage則是在setOnCancelListener方法中建立的。呼叫的是設定的OnCancelListener的onCancel方法,也就是說呼叫dialog.cancel方法時首先會判斷dialog是否呼叫了setOnCancelListener若設定了,則先呼叫OnCancelListener的onCancel方法,然後再次執行dismiss方法,若我們沒有為Dialog.Builder設定OnCancelListener那麼cancel方法和dismiss方法是等效的。部落格
    public void cancel() {
        if (!mCanceled && mCancelMessage != null) {
            mCanceled = true;
            // Obtain a new message so this dialog can be re-used
            Message.obtain(mCancelMessage).sendToTarget();
        }
        dismiss();
    }
    
    public void setOnCancelListener(final OnCancelListener listener) {
        if (listener != null) {
            mCancelMessage = mListenersHandler.obtainMessage(CANCEL, listener);
        } else {
            mCancelMessage = null;
        }
    }
    
    private static final class ListenersHandler extends Handler {
        private WeakReference<DialogInterface> mDialog;
    
        public ListenersHandler(Dialog dialog) {
            mDialog = new WeakReference<DialogInterface>(dialog);
        }
    
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case DISMISS:
                    ((OnDismissListener) msg.obj).onDismiss(mDialog.get());
                    break;
                case CANCEL:
                    ((OnCancelListener) msg.obj).onCancel(mDialog.get());
                    break;
                case SHOW:
                    ((OnShowListener) msg.obj).onShow(mDialog.get());
                    break;
            }
        }
    }
    複製程式碼
  • dismiss方法主要是做了什麼?
    • 可以看到,這裡首先判斷當前執行緒的Looper是否是主執行緒的Looper(由於mHandler是在主執行緒中建立的,所以mHandler.getLooper返回的是主執行緒中建立的Looper物件),若是的話,則直接執行dismissDialog()方法,否則的話,通過mHandler傳送非同步訊息至主執行緒中,簡單來說就是判斷當前執行緒是否是主執行緒,若是主執行緒則執行dismissDialog方法否則傳送非同步訊息。而這裡的非同步訊息最終也是呼叫的dismissDialog方法
    public void dismiss() {
        if (Looper.myLooper() == mHandler.getLooper()) {
            dismissDialog();
        } else {
            mHandler.post(mDismissAction);
        }
    }
    複製程式碼

10.0.2.0 PopupWindow中不設定為什麼必須設定寬高?PopupWindow和Dialog有什麼區別?說下建立和銷燬的大概流程?

  • PopupWindow中不設定為什麼必須設定寬高?
    • 先看問題程式碼,下面這個不會出現彈窗,思考:為什麼?
      PopupWindow popupWindow = new PopupWindow(this);
      View inflate = LayoutInflater.from(this).inflate(R.layout.view_pop_custom, null);
      popupWindow.setContentView(inflate);
      popupWindow.setAnimationStyle(R.style.BottomDialog);
      popupWindow.showAsDropDown(mTv1);
      複製程式碼
    • 注意:必須設定寬和高,否則不顯示任何東西
      • 這裡的WRAP_CONTENT可以換成fill_parent 也可以是具體的數值,它是指PopupWindow的大小,也就是contentview的大小,注意popupwindow根據這個大小顯示你的View,如果你的View本身是從xml得到的,那麼xml的第一層view的大小屬性將被忽略。相當於popupWindow的width和height屬性直接和第一層View相對應。
  • PopupWindow和Dialog有什麼區別?
    • 兩者最根本的區別在於有沒有新建一個window,PopupWindow沒有新建,而是將view加到DecorView;Dialog是新建了一個window,相當於走了一遍Activity中建立window的流程
    • 從原始碼中可以看出,PopupWindow最終是執行了mWindowManager.addView方法,全程沒有新建window
  • 說下建立和銷燬的大概流程?
    • 建立PopupWindow的時候,先建立WindowManager,因為WIndowManager擁有控制view的新增和刪除、修改的能力。這一點關於任主席的藝術探索書上寫的很詳細……部落格
    • 然後是setContentView,儲存contentView,這個步驟就做了這個
    • 顯示PopupWindow,這個步驟稍微複雜點,建立並初始化LayoutParams,設定相關引數,作為以後PopupWindow在應用DecorView裡哪裡顯示的憑據。然後建立PopupView,並且將contentView插入其中。最後使用WindowManager將PopupView新增到應用DecorView裡。
    • 銷燬PopupView,WindowManager把PopupView移除,PopupView再把contentView移除,最後把物件置為null
  • 為何彈窗點選一下就dismiss呢?
    • PopupWindow通過為傳入的View新增一層包裹的佈局,並重寫該佈局的點選事件,實現點選PopupWindow之外的區域PopupWindow消失的效果

10.0.2.1 Snackbar與吐司有何區別在哪裡?Snackbar控制元件show時為何從下往上移出來?為什麼顯示在最下面?

  • Snackbar與吐司有何區別
    • 與Toast進行比較,SnackBar有優勢:
    • 1.SnackBar可以自動消失,也可以手動取消(側滑取消,但是需要在特殊的佈局中,後面會仔細說)
    • 2.SnackBar可以通過setAction()來與使用者進行互動
    • 3.通過CallBack我們可以獲取SnackBar的狀態
    • image
  • Snackbar控制元件show時為何從下往上移出來?
    • 至於說Snackbar控制元件show時為何從下往上移出來,看下面這段程式碼就知道呢,如下所示
    • image
  • 為什麼顯示在最下面?
    • 直接找到make方法中的填充佈局,然後去看design_layout_snackbar_include的佈局引數,結果如下:
    • image
  • Snackbar顯示會導致FloatingActionButton上移?
    • 為什麼CoordinatorLayout + FloatingActionButton,當Snackbar顯示的時候FloatingActionButton會上移呢,這個是怎麼實現的?
    • 把CoordinatorLayout替換成FrameLayout確不行。這個問題我們還沒說。其實這個不是在Snackbar裡面處理的,是通過CoordinatorLayout和Behavior來處理的。那具體的處理在哪裡呢。FloatingActionButton類裡面Behavior類。正是Behavior裡面的兩個函式layoutDependsOn()和onDependentViewChanged()函式作用的結果。直接進去看下FloatingActionButton內部類Behavior裡面這兩個函式的程式碼。

10.0.2.2 說一下Snackbar和SnackbarManager類的設計有哪些奧妙的地方,如何處理訊息的顯示順序?

  • Snackbar和SnackbarManager,SnackbarManager內部有兩個SnackbarRecord,一個mCurrentSnackbar,一個mNextSnackbar,SnackbarManager通過這兩個物件實現Snackbar的順序顯示,如果在一個Snackbar顯示之前有Snackbar正在顯示,那麼使用mNextSnackbar儲存第二個Snackbar,然後讓第一個Snackbar消失,然後消失之後再呼叫SnackbarManager顯示下一個Snackbar,如此迴圈,實現了Snackbar的順序顯示。 部落格
  • Snackbar負責顯示和消失,具體來說其實就是新增和移除View的過程。Snackbar和SnackbarManager的設計很巧妙,利用一個SnackbarRecord物件儲存Snackbar的顯示時間以及SnackbarManager.Callback物件,前面說到每一個Snackbar都有一個叫做mManagerCallback的SnackbarManager.Callback物件,下面看一下SnackRecord類的定義:
    • image
  • Snackbar向SnackbarManager傳送訊息主要是呼叫SnackbarManager.getInstace()返回一個單例物件;而SnackManager向Snackbar傳送訊息就是通過show方法傳入的Callback物件。SnackbarManager中的Handler只處理一個MSG_TIMEOUT事件,最後是呼叫Snackbar的hideView消失的;Snackbar的sHandler處理兩個訊息,showView和hideView,而訊息的傳送者是mManagerCallback,控制者是SnackbarManager。

彈窗開源庫地址:github.com/yangchong21…

筆記開源庫地址:github.com/yangchong21…

相關文章