目錄介紹
- 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類的設計有哪些奧妙的地方,如何處理消息的顯示順序?
好消息
- 部落格筆記大彙總【15年10月到至今】,包括Java基礎及深入知識點,Android技術部落格,Python學習筆記等等,還包括平時開發中遇到的bug彙總,當然也在工作之餘收集了大量的面試題,長期更新維護并且修正,持續完善……開源的檔案是markdown格式的!同時也開源了生活部落格,從12年起,積累共計500篇[近100萬字],将會陸續發表到網上,轉載請注明出處,謝謝!
- 連結位址: https://github.com/yangchong211/YCBlogs
- 如果覺得好,可以star一下,謝謝!當然也歡迎提出建議,萬事起于忽微,量變引起質變!
彈窗部落格筆記彙總
- 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模式優缺點分析。
- 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 }
- 添加、更新、删除View
- 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
- 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。
- 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 關系圖
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的視圖加載的源碼分析
- Dialog的Window建立過程
- 建立WindowDialog。和Activity類似,同樣是通過PolicyManager.makeNewWindow() 來實作。
- 初始化DecorView并将Dialog的視圖添加到DecorView中去。和Activity類似,同樣是通過Window.setContentView() 來實作。
- 将DecorView添加到Window中顯示。和Activity一樣,都是在自身要出現在前台時才會将添加Window。
- Dialog.show() 方法:完成DecorView的顯示。
- WindowManager.remoteViewImmediate() 方法:當Dialog被dismiss時移除DecorView。
- 使用中遇到的問題
- 解決的辦法
建立工具類: /** * 吐司工具類 避免點選多次導緻吐司多次,最後導緻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對象,是以剛才我們遇到的問題在這裡就不會出現
- DecorView何時才被WindowManager真正添加到Window中?
- 即使Activity的布局已經成功添加到DecorView中,DecorView此時還沒有添加到Window中
- ActivityThread的handleResumeActivity方法中,首先會調用Activity的onResume方法,接着調用Activity的makeVisible()方法
- makeVisible()中完成了DecorView的添加和顯示兩個過程
- 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移除監聽器
- 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進而導緻失敗。
- 什麼是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介紹
- 通過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便可見,顯示在螢幕中。但是在這其中,
起到了重要的作用,因為其内部建立了一個ViewRootImpl對象,負責繪制顯示各個子View。wm.addView(mDecor, getWindow().getAttributes());
- 到此DecorView便可見,顯示在螢幕中。但是在這其中,
- 最後通過WindowManagerImpl的addView方法将DecorView加載出來
- 什麼是ViewRoot
- ViewRoot可能比較陌生,但是其作用非常重大。所有View的繪制以及事件分發等互動都是通過它來執行或傳遞的。
- ViewRoot對應ViewRootImpl類,它是連接配接WindowManagerService和DecorView的紐帶,View的三大流程(測量(measure),布局(layout),繪制(draw))均通過ViewRoot來完成。
- ViewRoot屬于View樹的一份子嗎?
- 下面結構圖可以清晰的揭示四者之間的關系:
- 吐司為何會出現記憶體洩漏
- 原因在于:如果在 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); }
- 在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隊列中,不會被黑名單阻止。
- 連續吐司是如何确定吐司的先後順序?
- 主要是說一下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添加進來,同時在該方法中也設定了大量的布局屬性。
- 主要是說一下showNextToastLocked()方法中的源代碼
- 為什麼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; } }
- 如何了解普通應用的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
- 為何Activity銷毀後Toast仍會顯示
- 記得以前昊哥問我,為何toast在activity銷毀後仍然會彈出呢,我毫不思索地說,因為toast是系統級别的呀。那麼是如何實作的呢,我就無言以對呢……今天終于可以回答呢!
- 還是回到NotificationManagerService類中的enqueueToast方法中,直接檢視keepProcessAliveIfNeededLocked(callingPid)方法。這段代碼的意思是将目前Toast所在程序設定為前台程序,這裡的mAm = ActivityManager.getService(),調用了setProcessImportant方法将目前pid的程序置為前台程序,保證不會系統殺死。這也就解釋了為什麼當我們finish目前Activity時,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中删除,并且判斷吐司消息隊列中是否還有消息,如果有,則繼續吐司!
- 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顯示的時候,原因是因為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 的逾時時間,還是會出現問題
- 報錯日志,是不是有點眼熟呀?更多可以看我的開源項目: https://github.com/yangchong211
- Toast運作在子線程問題
- 先來看看問題代碼,會出現什麼問題呢?
new Thread(new Runnable() { @Override public void run() { ToastUtils.showRoundRectToast("潇湘劍雨-楊充"); } }).start();
- 報錯日志如下所示:
- 子線程中吐司的正确做法,代碼如下所示
new Thread(new Runnable() { @Override public void run() { Looper.prepare(); ToastUtils.showRoundRectToast("潇湘劍雨-楊充"); Looper.loop(); } }).start();
- 得出的結論
- 為什麼建議用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來滿足。
- Dialog的Window建立過程是怎樣的?
- 為什麼Dialog不能用Application的Context,說一下原因?
- 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); } }
- 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消失的效果
- Snackbar與吐司有何差別
- 與Toast進行比較,SnackBar有優勢:
- 1.SnackBar可以自動消失,也可以手動取消(側滑取消,但是需要在特殊的布局中,後面會仔細說)
- 2.SnackBar可以通過setAction()來與使用者進行互動
- 3.通過CallBack我們可以擷取SnackBar的狀态
- Snackbar控件show時為何從下往上移出來?
- 至于說Snackbar控件show時為何從下往上移出來,看下面這段代碼就知道呢,如下所示
- 為什麼顯示在最下面?
- 直接找到make方法中的填充布局,然後去看design_layout_snackbar_include的布局參數,結果如下:
- Snackbar顯示會導緻FloatingActionButton上移?
- 為什麼CoordinatorLayout + FloatingActionButton,當Snackbar顯示的時候FloatingActionButton會上移呢,這個是怎麼實作的?
- 把CoordinatorLayout替換成FrameLayout确不行。這個問題我們還沒說。其實這個不是在Snackbar裡面處理的,是通過CoordinatorLayout和Behavior來處理的。那具體的處理在哪裡呢。FloatingActionButton類裡面Behavior類。正是Behavior裡面的兩個函數layoutDependsOn()和onDependentViewChanged()函數作用的結果。直接進去看下FloatingActionButton内部類Behavior裡面這兩個函數的代碼。
- 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類的定義:
- Snackbar向SnackbarManager發送消息主要是調用SnackbarManager.getInstace()傳回一個單例對象;而SnackManager向Snackbar發送消息就是通過show方法傳入的Callback對象。SnackbarManager中的Handler隻處理一個MSG_TIMEOUT事件,最後是調用Snackbar的hideView消失的;Snackbar的sHandler處理兩個消息,showView和hideView,而消息的發送者是mManagerCallback,控制者是SnackbarManager。
其他介紹
01.關于部落格彙總連結
02.關于我的部落格
- github:
- 知乎: https://www.zhihu.com/people/yczbj/activities
- 簡書: 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
- 泡在網上的日子: http://www.jcodecraeer.com/member/content_list.php?channelid=1
- 郵箱:[email protected]
- 阿裡雲部落格: https://yq.aliyun.com/users/article?spm=5176.100- 239.headeruserinfo.3.dT4bcV
- segmentfault頭條: https://segmentfault.com/u/xiangjianyu/articles
- 掘金: https://juejin.im/user/5939433efe88c2006afa0c6e