摘要:所以,從體驗上考慮,這個情況并不屬于問題。一般情況下,這個節點占據了除了通知欄的所有區域。通知給對象的消息,都會被這個內部對象進行處理通過執行處理消息在通知給對象顯示的時候,對象將給對象發送一條消息,并在的函數中執行。
歡迎大家前往云+社區,獲取更多騰訊海量技術實踐干貨哦~
作者:QQ音樂技術團隊題記
Toast 作為 Android 系統中最常用的類之一,由于其方便的api設計和簡潔的交互體驗,被我們所廣泛采用。但是,伴隨著我們開發的深入,Toast 的問題也逐漸暴露出來。
本系列文章將分成兩篇:
第一篇,我們將分析 Toast 所帶來的問題
第二篇,將提供解決 Toast 問題的解決方案
(注:本文源碼基于Android 7.0)
上一篇 [[Android] Toast問題深度剖析(一)] 筆者解釋了:
Toast 系統如何構建窗口(通過系統服務NotificationManager來生成系統窗口)
Toast 異常出現的原因(系統調用 Toast的時序紊亂)
而本篇的重點,在于解決我們第一章所說的 Toast 問題。
2.解決思路基于第一篇的知識,我們知道,Toast 的窗口屬于系統窗口,它的生成和生命周期依賴于系統服務 NotificationManager。一旦 NotificationManager 所管理的窗口生命周期跟我們本地的進程不一致,就會發生異常。那么,我們能不能不使用系統的窗口,而使用自己的窗口,并且由我們自己控制生命周期呢?事實上, SnackBar 就是這樣的方案。不過,如果不使用系統類型的窗口,就意味著你的Toast 界面,無法在其他應用之上顯示。(比如,我們經常看到的一個場景就是你在你的應用出調用了多次 Toast.show函數,然后退回到桌面,結果發現桌面也會彈出 Toast,就是因為系統的 Toast 使用了系統窗口,具有高的層級)不過在某些版本的手機上,你的應用可以申請權限,往系統中添加 TYPE_SYSTEM_ALERT 窗口,這也是一種系統窗口,經常用來作為浮層顯示在所有應用程序之上。不過,這種方式需要申請權限,并不能做到讓所有版本的系統都能正常使用。
如果我們從體驗的角度來看,當用戶離開了該進程,就不應該彈出另外一個進程的 Toast 提示去干擾用戶的。Android 系統似乎也意識到了這一點,在新版本的系統更新中,限制了很多在桌面提示窗口相關的權限。所以,從體驗上考慮,這個情況并不屬于問題。
“那么我們可以選擇哪些窗口的類型呢?”
使用子窗口: 在 Android 進程內,我們可以直接使用類型為子窗口類型的窗口。在 Android 代碼中的直接應用是 PopupWindow 或者是 Dialog 。這當然可以,不過這種窗口依賴于它的宿主窗口,它可用的條件是你的宿主窗口可用
采用 View 系統: 使用 View 系統去模擬一個 Toast 窗口行為,做起來不僅方便,而且能更加快速的實現動畫效果,我們的 SnackBar 就是采用這套方案。這也是我們今天重點講的方案
“如果采用 View 系統方案,那么我要往哪個控件中添加我的 Toast 控件呢?”
在Android進程中,我們所有的可視操作都依賴于一個 Activity 。 Activity 提供上下文(Context)和視圖窗口(Window) 對象。我們通過 Activity.setContentView 方法所傳遞的任何 View對象 都將被視圖窗口( Window) 中的 DecorView 所裝飾。而在 DecorView 的子節點中,有一個 id 為 android.R.id.content 的 FrameLayout 節點(后面簡稱 content 節點) 是用來容納我們所傳遞進去的 View 對象。一般情況下,這個節點占據了除了通知欄的所有區域。這就特別適合用來作為 Toast 的父控件節點。
“我什么時機往這個content節點中添加合適呢?這個 content 節點什么時候被初始化呢?”
根據不同的需求,你可能會關注以下兩個時機:
Content 節點生成
Content 內容顯示
實際我們只需要將我們的 Toast 添加到 Content 節點中,只要滿足第一條即可。如果你是為了完成性能檢測,測量或者其他目的,那么你可能更關心第二條。 那么什么情況下 Content 節點生成呢?剛才我們說了,Content 節點包含在我們的 DecorView 控件中,而 DecorView 是由 Activity 的 Window對象所持有的控件。Window 在 Android 中的實現類是 PhoneWindow,(這部分代碼有興趣可以自行閱讀) 我們來看下源碼:
//code PhoneWindow.java @Override public void setContentView(int layoutResID) { if (mContentParent == null) { //mContentParent就是我們的 content 節點 installDecor();//生成一個DecorView } else { mContentParent.removeAllViews(); } mLayoutInflater.inflate(layoutResID, mContentParent); final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } }
PhoneWindow 對象通過 installDecor 函數生成 DecorView 和 我們所需要的 content 節點(最終會存到 mContentParent) 變量中去。但是, setContentView 函數需要我們主動調用,如果我并沒有調用這個 setContentView 函數,installDecor 方法將不被調用。那么,有沒有某個時刻,content 節點是必然生成的呢?當然有,除了在 setContentView 函數中調用installDecor外,還有一個函數也調用到了這個,那就是:
//code PhoneWindow.java @Override public final View getDecorView() { if (mDecor == null) { installDecor(); } return mDecor; }
而這個函數,將在 Activity.findViewById 的時候調用:
//code Activity.java public View findViewById(@IdRes int id) { return getWindow().findViewById(id); } //code Window.java public View findViewById(@IdRes int id) { return getDecorView().findViewById(id); }
因此,只要我們只要調用了 findViewById 函數,一樣可以保證 content 被正常初始化。這樣我們解釋了第一個”就緒”(Content 節點生成)。我們再來看下第二個”就緒”,也就是 Android 界面什么時候顯示呢?相信你可能迫不及待的回答不是 onResume 回調的時候么?實際上,在 onResume 的時候,根本還沒處理跟界面相關的事情。我們來看下 Android 進程是如何處理 resume 消息的:
(注: AcitivityThread 是 Android 進程的入口類, Android 進程處理 resume 相關消息將會調用到 AcitivityThread.handleResumeActivity 函數)
//code AcitivityThread.java void handleResumeActivity(...) { ... ActivityClientRecord r = performResumeActivity(token, clearHide); // 之后會調用call onResume ... View decor = r.window.getDecorView(); //調用getDecorView 生成 content節點 decor.setVisibility(View.INVISIBLE); .... if (r.activity.mVisibleFromClient) { r.activity.makeVisible();//add to WM 管理 } ... } //code Activity.java void makeVisible() { if (!mWindowAdded) { ViewManager wm = getWindowManager(); wm.addView(mDecor, getWindow().getAttributes()); mWindowAdded = true; } mDecor.setVisibility(View.VISIBLE); }
Android 進程在處理 resume 消息的時候,將走以下的流程:
調用 performResumeActivity 回調 Activity 的 onResume 函數
調用 Window 的 getDecorView 生成 DecorView 對象和 content 節點
將DecorView納入 WindowManager (進程內服務)的管理
調用 Activity.makeVisible 顯示當前 Activity
按照上述的流程,在 Activity.onResume 回調之后,才將控件納入本地服務 WindowManager 的管理中。也就是說, Activity.onResume 根本沒有顯示任何東西。我們不妨寫個代碼驗證一下:
//code DemoActivity.java public DemoActivity extends Activity { private View view ; @Override protected void onCreate( Bundle savedInstanceState) { super.onCreate(savedInstanceState); view = new View(this); this.setContentView(view); } @Override protected void onResume() { super.onResume(); Log.d("cdw","onResume :" +view.getHeight());// 有高度是顯示的必要條件 } }
這里,我們通過在 onResume 中獲取高度的方式驗證界面是否被繪制,最終我們將輸出日志:
D cdw : onResume :0
那么,界面又是在什么時候完成的繪制呢?是不是在 WindowManager.addView 之后呢?我們在 onResume之后會調用Activity.makeVisible,里面會調用 WindowManager.addView。因此我們在onResume 里post一個消息就可以檢測WindowManager.addView 之后的情況:
@Override protected void onResume() { super.onResume(); this.runOnUiThread(new Runnable() { @Override public void run() { Log.d("cdw","onResume :" +view.getHeight()); } }); } //控制臺輸出: 01-02 21:30:27.445 2562 2562 D cdw : onResume :0
從結果上看,我們在 WindowManager.addView 之后,也并沒有繪制界面。那么,Android的繪制是什么時候開始的?又是到什么時候結束?
在 Android 系統中,每一次的繪制都是通過一個 16ms 左右的 VSYNC 信號控制的,這種信號可能來自于硬件也可能來自于軟件模擬。每一次非動畫的繪制,都包含:測量,布局,繪制三個函數。而一般觸發這一事件的的動作有:
View 的某些屬性的變更
View 重新布局Layout
增刪 View 節點
當調用 WindowManager.addView 將空間添加到 WM 服務管理的時候,會調用一次Layout請求,這就觸發了一次 VSYNC 繪制。因此,我們只需要在 onResume 里 post 一個幀回調就可以檢測繪制開始的時間:
@Override protected void onResume() { super.onResume(); Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() { @Override public void doFrame(long frameTimeNanos) { //TODO 繪制開始 } }); }
我們先來看下 View.requestLayout 是怎么觸發界面重新繪制的:
//code View.java public void requestLayout() { .... if (mParent != null) { ... if (!mParent.isLayoutRequested()) { mParent.requestLayout(); } } }
View 對象調用 requestLayout 的時候會委托給自己的父節點處理,這里之所以不稱為父控件而是父節點,是因為除了控件外,還有 ViewRootImpl 這個非控件類型作為父節點,而這個父節點會作為整個控件樹的根節點。按照我們上面說的委托的機制,requestLayout 最終將會調用到 ViewRootImpl.requestLayout。
//code ViewRootImpl.java @Override public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals();//申請繪制請求 } } void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; .... mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);//申請繪制 .... } }
ViewRootImpl 最終會將 mTraversalRunnable 處理命令放到 CALLBACK_TRAVERSAL 繪制隊列中去:
final class TraversalRunnable implements Runnable { @Override public void run() { doTraversal();//執行布局和繪制 } } void doTraversal() { if (mTraversalScheduled) { mTraversalScheduled = false; ... performTraversals(); ... } }
mTraversalRunnable 命令最終會調用到 performTraversals() 函數:
private void performTraversals() { final View host = mView; ... host.dispatchAttachedToWindow(mAttachInfo, 0);//attachWindow ... getRunQueue().executeActions(attachInfo.mHandler);//執行某個指令 ... childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); host.measure(childWidthMeasureSpec, childHeightMeasureSpec);//測量 .... host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());//布局 ... draw(fullRedrawNeeded);//繪制 ... }
performTraversals 函數實現了以下流程:
調用 dispatchAttachedToWindow 通知子控件樹當前控件被 attach 到窗口中
執行一個命令隊列 getRunQueue
執行 meausre 測量指令
執行 layout 布局函數
執行繪制 draw
這里我們看到一句方法調用:
getRunQueue().executeActions(attachInfo.mHandler);
這個函數將執行一個延時的命令隊列,在 View 對象被 attach 到 View樹之前,通過調用 View.post 函數,可以將執行消息命令加入到延時執行隊列中去:
//code View.java public boolean post(Runnable action) { Handler handler; AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { handler = attachInfo.mHandler; } else { // Assume that post will succeed later ViewRootImpl.getRunQueue().post(action); return true; } return handler.post(action); }
getRunQueue().executeActions 函數執行的時候,會將該命令消息延后一個UI線程消息執行,這就保證了執行的這個命令消息發生在我們的繪制之后:
//code RunQueue.java void executeActions(Handler handler) { synchronized (mActions) { ... for (int i = 0; i < count; i++) { final HandlerAction handlerAction = actions.get(i); handler.postDelayed(handlerAction.action, handlerAction.delay);//推遲一個消息 } } }
所以,我們只需要在視圖被 attach 之前通過一個 View 來拋出一個命令消息,就可以檢測視圖繪制結束的時間點:
//code DemoActivity.java @Override protected void onResume() { super.onResume(); Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() { @Override public void doFrame(long frameTimeNanos) { start = SystemClock.uptimeMillis(); log("繪制開始:height = "+view.getHeight()); } }); } @Override protected void onCreate( Bundle savedInstanceState) { super.onCreate(savedInstanceState); view = new View(this); view.post(new Runnable() { @Override public void run() { log("繪制耗時:"+(SystemClock.uptimeMillis()-start)+"ms"); log("繪制結束后:height = "+view.getHeight()); } }); this.setContentView(view); } //控制臺輸出: 01-03 23:39:27.251 27069 27069 D cdw : --->繪制開始:height = 0 01-03 23:39:27.295 27069 27069 D cdw : --->繪制耗時:44ms 01-03 23:39:27.295 27069 27069 D cdw : --->繪制結束后:height = 1232
我們帶著我們上面的知識儲備,來看下SnackBar是如何做的呢:
3.SnackbarSnackBar 系統主要依賴于兩個類:
SnackBar 作為門面,與業務程序交互
SnackBarManager 作為時序管理器, SnackBar 與 SnackBarManager 的交互,通過 Callback 回調對象進行
SnackBarManager 的時序管理跟 NotifycationManager 的很類似不再贅述
SnackBar 通過靜態方法 make 靜態構造一個 SnackBar:
public static Snackbar make(@NonNull View view, @NonNull CharSequence text, @Duration int duration) { Snackbar snackbar = new Snackbar(findSuitableParent(view)); snackbar.setText(text); snackbar.setDuration(duration); return snackbar; }
這里有一個關鍵函數 findSuitableParent ,這個函數的目的就相當于我們上面的 findViewById(R.id.content) 一樣,給 SnackBar 所定義的 Toast 控件找一個合適的容器:
private static ViewGroup findSuitableParent(View view) { ViewGroup fallback = null; do { if (view instanceof CoordinatorLayout) { return (ViewGroup) view; } else if (view instanceof FrameLayout) { if (view.getId() == android.R.id.content) {//把 `Content` 節點作為容器 ... return (ViewGroup) view; } else { // It"s not the content view but we"ll use it as our fallback fallback = (ViewGroup) view; } } ... } while (view != null); // If we reach here then we didn"t find a CoL or a suitable content view so we"ll fallback return fallback; }
我們發現,除了包含 CoordinatorLayout 控件的情況, 默認情況下, SnackBar 也是找的 Content 節點。找到的這個父節點,作為 Snackbar 構造器的形參:
private Snackbar(ViewGroup parent) { mTargetParent = parent; mContext = parent.getContext(); ... LayoutInflater inflater = LayoutInflater.from(mContext); mView = (SnackbarLayout) inflater.inflate( R.layout.design_layout_snackbar, mTargetParent, false); ... }
Snackbar 將生成一個 SnackbarLayout 控件作為 Toast 控件。最后當時序控制器 SnackBarManager 回調返回的時候,通知 SnackBar 顯示,即將 SnackBar.mView 增加到 mTargetParent 控件中去。
這里有人或許會有疑問,這里使用強引用,會不會造成一段時間內的內存泄漏呢?
假如你現在彈了 10 個 Toast ,每個 Toast 的顯示時間是 2s 。也就是說你的最后一個 SnackBar 將被 SnackBarManager 持有至少 20s。而 SnackBar 中又存在有父控件 mTargetParent 的強引用。相當于在這20s內, 你的mTargetParent 和它所持有的 Context (一般是 Activity)無法釋放
這個其實是不會的,原因在于 SnackBarManager 在管理這種回調 callback 的時候,采用了弱引用。
private static class SnackbarRecord { final WeakReferencecallback; .... }
但是,我們從 SnackBar 的設計可以看出,SnackBar無法定制具體的樣式: SnackBar 只能生成 SnackBarLayout 這種控件和布局,可能并不滿足你的業務需求。當然你也可以變更 SnackBarLayout 也能達到目的。不過,有了上面的知識儲備,我們完全可以寫一個自己的 Snackbar。
4.基于Toast的改法從第一篇文章我們知道,我們直接在 Toast.show 函數外增加 try-catch 是沒有意義的。因為 Toast.show 實際上只是發了一條命令給 NotificationManager 服務。真正的顯示需要等 NotificationManager 通知我們的 TN 對象 show 的時候才能觸發。NotificationManager 通知給 TN 對象的消息,都會被 TN.mHandler 這個內部對象進行處理
//code Toast.java private static class TN { final Runnable mHide = new Runnable() {// 通過 mHandler.post(mHide) 執行 @Override public void run() { handleHide(); mNextView = null; } }; final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { IBinder token = (IBinder) msg.obj; handleShow(token);// 處理 show 消息 } }; }
在NotificationManager 通知給 TN 對象顯示的時候,TN 對象將給 mHandler 對象發送一條消息,并在 mHandler 的 handleMessage 函數中執行。 當NotificationManager 通知 TN 對象隱藏的時候,將通過 mHandler.post(mHide) 方法,發送隱藏指令。不論采用哪種方式發送的指令,都將執行 Handler 的 dispatchMessage(Message msg) 函數:
//code Handler.java public void dispatchMessage(Message msg) { if (msg.callback != null) { handleCallback(msg);// 執行 post(Runnable)形式的消息 } else { ... handleMessage(msg);// 執行 sendMessage形式的消息 } } 因此,我們只需要在 dispatchMessage 方法體內加入 try-catch 就可以避免 Toast 崩潰對應用程序的影響: public void dispatchMessage(Message msg) { try { super.dispatchMessage(msg); } catch(Exception e) {} }
因此,我們可以定義一個安全的 Handler 裝飾器:
private static class SafelyHandlerWarpper extends Handler { private Handler impl; public SafelyHandlerWarpper(Handler impl) { this.impl = impl; } @Override public void dispatchMessage(Message msg) { try { super.dispatchMessage(msg); } catch (Exception e) {} } @Override public void handleMessage(Message msg) { impl.handleMessage(msg);//需要委托給原Handler執行 } }
由于 TN.mHandler 對象復寫了 handleMessage 方法,因此,在 Handler 裝飾器里,需要將 handleMessage 方法委托給 TN.mHandler 執行。定義完裝飾器之后,我們就可以通過反射往我們的 Toast 對象中注入了:
public class ToastUtils { private static Field sField_TN ; private static Field sField_TN_Handler ; static { try { sField_TN = Toast.class.getDeclaredField("mTN"); sField_TN.setAccessible(true); sField_TN_Handler = sField_TN.getType().getDeclaredField("mHandler"); sField_TN_Handler.setAccessible(true); } catch (Exception e) {} } private static void hook(Toast toast) { try { Object tn = sField_TN.get(toast); Handler preHandler = (Handler)sField_TN_Handler.get(tn); sField_TN_Handler.set(tn,new SafelyHandlerWarpper(preHandler)); } catch (Exception e) {} } public static void showToast(Context context,CharSequence cs, int length) { Toast toast = Toast.makeText(context,cs,length); hook(toast); toast.show(); } }
我們再用第一章中的代碼測試一下:
public void showToast(View view) { ToastUtils.showToast(this,"hello", Toast.LENGTH_LONG); try { Thread.sleep(10000); } catch (InterruptedException e) {} }
等 10s 之后,進程正常運行,不會因為 Toast 的問題而崩潰。
相關閱讀[Android] Toast問題深度剖析(一)
Android基礎:Fragment,看這篇就夠了
Android圖像處理 - 高斯模糊的原理及實現
此文已由作者授權云加社區發布,轉載請注明文章出處
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/70997.html
摘要:模仿的功能掘金本模仿了的功能。國內曾經出現的團購類網站有多家,到四年多以后的現在,美團已經是成為國內最大的本地生活服務平臺,不管怎餓了么移動的架構演進掘金引言時代演進,技術也隨之發展。 模仿 Smartisan OS 的 BigBang 功能 ??? - Android - 掘金 本 Demo 模仿了 Smartisan OS 的 BigBang 功能。App 打開會從剪切板讀取文字并...
摘要:中和的交互方式在進行交互之前需要我們對進行設置開啟對的支持。定義和相關的交互類和方法,對于方法通過注解進行標注。向添加該,同時為其指定一個名稱,該名稱將會在文件中使用。傳遞的數據中有一個端口號,通過這個端口號作為標示,來調用相應的方法。 隨著H5性能的提升,在我們移動應用開發的過程中,我們會越來越多的在我們的App頁面內嵌入H5頁面,使得App變的更加動態靈活。而H5頁面往往并不是獨立...
摘要:在代碼中的直接應用是或者是。就像一個控制器,統籌視圖的添加與顯示,以及通過其他回調方法,來與以及進行交互。創建需要通過創建,通過將加載其中,并將交給,進行視圖繪制以及其他交互。創建機制分析實例的創建中執行,從而生成了的實例。 目錄介紹 01.Window,View,子Window 02.什么是Activity 03.什么是Window 04.什么是DecorView 05.什么是Vi...
摘要:不努力不奮斗,可能就會在基層一輩子止步不前。不過,只一句,如果你還在做這一行,還是一名程序猿媛,想走上坡路的你,也許我這到手的十幾家一線互聯網公司性能優化項目實戰可能會對你有所幫助。 ...
閱讀 2849·2021-11-22 11:56
閱讀 3553·2021-11-15 11:39
閱讀 898·2021-09-24 09:48
閱讀 759·2021-08-17 10:14
閱讀 1322·2019-08-30 15:55
閱讀 2753·2019-08-30 15:55
閱讀 1310·2019-08-30 15:44
閱讀 2775·2019-08-30 10:59