--- title: 'WindowManager 和 Dialog' disqus: kyleAlien --- WindowManager 和 Dialog === ## OverView of Content > 可配合 [**Builder 模式**](https://hackmd.io/rLN00StPRmqok3zPzIsxjA?view#Android-Source-Builder-%E7%A0%94%E7%A9%B6)對 AlertDialog 的創建一起看,研究 Dialog 的 show 函數如何新增畫面在界面上 [TOC] ## WindowManager 概述 不只 Dialog 的畫面、Activity 的畫面也是透過 WindowManager 來加載到螢幕上 ### WindowManager 代理 * WindowManager 是 APP 應用端 對系統服務 WMS 的代理,如同在 [**LayoutInflater**](https://hackmd.io/EPfgb14TS-Kcd-Pv7tDZ-w?view#LayoutInflate-%E6%A6%82%E8%BF%B0) 中所提到的,**WMS 服務也是透過 SystemServiceRegistry 加載**,**==WINDOW_SERVICE==** * 傳入 Context 建立 WindowManagerImpl 物件 (下面會看到不同的差異) ```java= // SystemServiceRegistry.java static { ... registerService(Context.WINDOW_SERVICE, WindowManager.class, new CachedServiceFetcher<WindowManager>() { @Override public WindowManager createService(ContextImpl ctx) { return new WindowManagerImpl(ctx); }}); ... } // -------------------------------------------------------------------- // WindowManagerImpl.java public WindowManagerImpl(Context context) { this(context, null); } private WindowManagerImpl(Context context, Window parentWindow) { mContext = context; mParentWindow = parentWindow; } ``` ## Dialog 視窗 我們知道 Context 的實現類是 [**ContextImpl**](https://hackmd.io/qhIoWcSZQ-GzL3G_Sgqb0A#Context-amp-ContextImpl) 類,可以**透過 Context 的 getSystemService 來獲得具體服務 (系統服務進程) 的代理** Dialog 就在建構函數時獲取 WindowManager 服務 ### Dialog 取得 - WindowManager 服務 * Dialog constructor 建構函數:會準備 WindowManager、PhoneWindow 對象 | 類 | 功能概述 | | -------- | -------- | | WindowManager | 添加 Dialog 視窗 | | PhoneWindow | 加載 xml 布局 | ```java= /** * Dialog.java */ public class Dialog implements DialogInterface, Window.Callback, KeyEvent.Callback, OnCreateContextMenuListener, Window.OnWindowDismissedCallback { private final WindowManager mWindowManager; final Window mWindow; // 實作是 PhoneWindow 類 // Dialog construct Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) { ... 準備 context // 發現 Dialog 建構函數在獲取服務 mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); // 自己建構 PhoneWindow 類 final Window w = new PhoneWindow(mContext); mWindow = w; w.setCallback(this); w.setOnWindowDismissedCallback(this); // @ 追蹤 setWindowManager 方法 // 讓 Window & WindowManager 產生關係!!! w.setWindowManager(mWindowManager, null, null); w.setGravity(Gravity.CENTER); mListenersHandler = new ListenersHandler(this); } } ``` > ![](https://i.imgur.com/56rwhRo.png) * **透過 [Window](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/Window.java)#setWindowManager 方法:讓 Window (實作類是 PhoneWindow) & WindowManager 產生了關聯** ```java= /** * Window.java (抽象類) */ private WindowManager mWindowManager; public void setWindowManager(WindowManager wm, IBinder appToken, // null String appName) { // null setWindowManager(wm, appToken, appName, false); } public void setWindowManager(WindowManager wm, IBinder appToken, // null String appName, // null boolean hardwareAccelerated) { mAppToken = appToken; mAppName = appName; mHardwareAccelerated = hardwareAccelerated || SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false); // 如果沒有則再次獲取 Window 服務 if (wm == null) { wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE); } // @ 追蹤 createLocalWindowManager 方法 // 建立 WindowManagerImpl mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this); } ``` * [**WindowManagerImpl**](**https://**)#**createLocalWindowManager** 這個方法中與 `WINDOW_SERVICE 服務` 都是創建 WindowManagerImpl 對象,差別是 **createLocalWindowManager 將傳入的 Window 設定為 parentWindow** > 這裡的 parentWindow 就是 Dialog 的 PhoneWindow 類 > 並可以看出 Window & WindowManagerImpl 相互持有 > ![](https://i.imgur.com/rKq49pU.png) :::info * 與 Context 建立的 WindowManagerImpl 比起來,Context 的 WindowManagerImpl 尚未與 Window 產生關聯 ::: ```java= // WindowManagerImpl.java public final class WindowManagerImpl implements WindowManager { private final Window mParentWindow; // 子系統服務的呼叫,並沒有 parentView public WindowManagerImpl(Context context) { this(context, null /* parentWindow */, null /* clientToken */); } public WindowManagerImpl createLocalWindowManager(Window parentWindow) { return new WindowManagerImpl(mContext, parentWindow); } private WindowManagerImpl(Context context, Window parentWindow, @Nullable IBinder windowContextToken) { mContext = context; mParentWindow = parentWindow; mWindowContextToken = windowContextToken; } } ``` > ![](https://i.imgur.com/JRVGCea.png) ### WindowManager addView - 連接 [**WindowManagerGlobal**](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/WindowManagerGlobal.java) * 在 Dialog 中的 show 方法最後會使用 **WindowManager#addView**,從上面我們會知道它調用到 WindowManagerImpl#**addView** ```java= // Dialog.java public void show() { ... 省略部份 // @ mWindow 就是 PhoneWindow mDecor = mWindow.getDecorView(); ... 省略部份 WindowManager.LayoutParams l = mWindow.getAttributes(); ... // @ 追蹤 addView 方法 mWindowManager.addView(mDecor, l); ... } ``` * WindowManagerImpl#addView 其實會調用到 [**WindowManagerGlobal**](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/WindowManagerGlobal.java)#addView 方法 ```java= /** * WindowManagerImpl,java */ public final class WindowManagerImpl implements WindowManager { // 進程單例 private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance(); private final Context mContext; // 目前是 Dialog#PhoneWindow private final Window mParentWindow; @Override public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) { applyTokens(params); mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow); } private void applyTokens(@NonNull ViewGroup.LayoutParams params) { // token 其實就是 WindowManager.LayoutParams if (!(params instanceof WindowManager.LayoutParams)) { throw new IllegalArgumentException("Params must be WindowManager.LayoutParams"); } final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params; assertWindowContextTypeMatches(wparams.type); // Only use the default token if we don't have a parent window and a token. if (mDefaultToken != null && mParentWindow == null && wparams.token == null) { wparams.token = mDefaultToken; } wparams.mWindowContextToken = mWindowContextToken; } } ``` 從關係圖上可以看出 WindowManagerImpl 是一個橋接類,它聯繫著 Window & WindowManagerGlobal 類 > ![](https://i.imgur.com/DdSVLsN.png) ### WindowManagerGlobal - 創建 ViewRootImpl * WindowManagerGlobal#addView 方法會創建一個 ViewRootImpl 對象 ```java= /** * WindowManagerGlobal,java */ public final class WindowManagerGlobal { ... private static WindowManagerGlobal sDefaultWindowManager; private final ArrayList<View> mViews = new ArrayList<View>(); private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>(); private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>(); private WindowManagerGlobal() { } // 單例 public static WindowManagerGlobal getInstance() { // 使用 - 類鎖 同步機制 synchronized (WindowManagerGlobal.class) { if (sDefaultWindowManager == null) { sDefaultWindowManager = new WindowManagerGlobal(); } return sDefaultWindowManager; } } public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { // 忽略判空、檢查... if (!(params instanceof WindowManager.LayoutParams)) { throw new IllegalArgumentException("Params must be WindowManager.LayoutParams"); } // Window 布局參數 final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params; if (parentWindow != null) { // 目前有 parentWindow parentWindow.adjustLayoutParamsForSubWindow(wparams); } else { // 沒有 parentWindow 設定硬體加速 final Context context = view.getContext(); if (context != null && (context.getApplicationInfo().flags & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) { wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; } } ViewRootImpl root; View panelParentView = null; // mLock 普通對象鎖 synchronized (mLock) { // Start watching for system property changes. ... // @ 建立 ViewRootImpl 對象 root = new ViewRootImpl(view.getContext(), display); // 設定布局參數 view.setLayoutParams(wparams); // @ 以下都是 ArrayList 對象 mViews.add(view); // View mRoots.add(root); // ViewRootImpl mParams.add(wparams); // WindowManager.LayoutParams 視窗參數 } //最後執行此操作,因為它會觸發傳遞消息 try { root.setView(view, wparams, panelParentView); } catch (RuntimeException e) { ... } } ``` 這邊先跳過 Binder 與 WMS 的進程通訊,先知道 WindowManagerGlobal#addView 行為 **會觸發到 ++[ViewRootImpl](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/ViewRootImpl.java)#setView++ 方法** * 分析完畢 View 布局後,在 WindowManagerGlobal 中的 addView 方法主要做了下面 4 件事情 1. **新建 ViewRootImpl 對象** 2. 將 Window 布局參數 Params 設定給 View 3. 儲存 View、ViewRootImpl、Params 到 WindowManagerGlobal#ArrayList 中 4. **透過 ViewRootImpl 的 setView 方法設定 View 顯示到視窗上** * WindowManagerGlobal & WindowManagerImpl 關係圖 > ![](https://i.imgur.com/H5JTp2s.png) ## ViewRootImpl ViewRootImpl 並不是 View 類,負責與 Native 通訊,取得繪畫範圍,並驅動 App 進程繪圖 ### [ViewRootImpl](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/ViewRootImpl.java) 類 - 取得 WindowSession 服務 * 從上面 WindowManagerGlobal#addView 會發現 **++每一個新的 View 都會建立一個 ViewRootImpl 對象++**,**其實它本身並不是 View 類,它是 ==Framework 層與 native 層的交接==** ```java= // ViewRootImpl.java public final class ViewRootImpl implements ViewParent, View.AttachInfo.Callbacks, ThreadedRenderer.HardwareDrawCallbacks { ... public ViewRootImpl(Context context, Display display) { mContext = context; // @ 追蹤 getWindowSession 方法 // 獲取 WindowSession,與 WMS 建立連接,往下追 getWindowSession mWindowSession = WindowManagerGlobal.getWindowSession(); mDisplay = display; mBasePackageName = context.getBasePackageName(); // "1. " // 儲存目前的 Thread mThread = Thread.currentThread(); ... } } ``` :::success * 更新 View 的 Thead,**在 ViewRootImpl 有紀錄 Thread** 在 Anroid 中如果 Work Thread 更新 UI 會導致異常拋出,並不是因為 UI Thread 才能更新 UI,而是因為 ViewRootImpl 紀錄時,**就是紀錄 UI Thread**,**==非 ViewRootImpl 紀錄的 Thread 是不能更新 ViewRootImpl==** ::: * [**WindowManagerGlobal**](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/WindowManagerGlobal.java)#getWindowSession 方法:是 Framework 與 Native Binder 通訊的入口,並且從這裡可以看到 WindowSession 服務 > WindowSession 是一個隱密服務,ServiceManager 也不會記錄,必須透過 WMS 間接得到 ```java= // WindowManagerGlobal.java private static IWindowManager sWindowManagerService; public static IWindowSession getWindowSession() { // 類鎖 synchronized (WindowManagerGlobal.class) { if (sWindowSession == null) { try { InputMethodManager imm = InputMethodManager.getInstance(); // @ getWindowManagerService // 獲取 WMS 服務,往下追 IWindowManager windowManager = getWindowManagerService(); // @ 與 WMS 建立一個 Session sWindowSession = windowManager.openSession(...); } /* 省略 catch*/ return sWindowSession; } } public static IWindowManager getWindowManagerService() { // 類鎖 synchronized (WindowManagerGlobal.class) { // 檢查緩存 if (sWindowManagerService == null) { sWindowManagerService = IWindowManager.Stub.asInterface( ServiceManager.getService("window")); ... 省略部份 } return sWindowManagerService; } } // ------------------------------------------------------------------ /** * ServiceManager.java */ public static IBinder getService(String name) { try { IBinder service = sCache.get(name); // 檢查本地是否有緩存 if (service != null) { return service; } else { // 沒有緩存,則去 ServiceManager 進程取得 Service IBinder 對象 return getIServiceManager().getService(name); } } catch (RemoteException e) { Log.e(TAG, "error in getService", e); } return null; } ``` 1. ViewRootImpl 物件的建立,會儲存當前的 Thread > 這也就是我們 **如果使用 WorkThread 更新 UI 時為何會拋出異常** 2. [**Binder 機制**](https://hackmd.io/yNGrVdN-RtelUqYQgn9avg#%E7%B5%90%E8%AB%96):App 應用端透過 **asInterface 獲取遠端服務的代理,也就是獲取 WMS (IWindowManager) 服務的 ++代理++** > IWindowManager 是 `.aidl` 檔案,它的實做類(真正服務端)就是 WindowManagerService ```java= /** * WindowManagerService.java */ public class WindowManagerService extends IWindowManager.Stub implements Watchdog.Monitor, WindowManagerPolicy.WindowManagerFuncs { ... 省略 @Override public IWindowSession openSession(IWindowSessionCallback callback, IInputMethodClient client, IInputContext inputContext) { if (client == null) throw new IllegalArgumentException("null client"); if (inputContext == null) throw new IllegalArgumentException("null inputContext"); // 在 WMS 創建一個 Session 對象 Session session = new Session(this, callback, client, inputContext); return session; } } ``` * 以上建立好 Session 後就可以 **使用 Session 來交換資訊** :::info * 當 ViewRootImpl & WindowManagerService 產生關連後,**還不能讓 View 顯示在螢幕上,==WMS 是管理 View 的 Z 軸==,++WMS 管理當前狀態下哪個 View 應該在最上層顯示++** * **其是 WMS 並不是管理 Window,而是==管理特定 Window 中的 View==** ::: > ![](https://i.imgur.com/sbLEnOE.png) :::info * 從這邊也可以知道每次呼叫 WindowGlobalManager#addView 都會創建一個 ViewRootImpl,並對應創建一個 WindowSession > ![](https://i.imgur.com/dCtOJk6.png) ::: ### WindowSession 添加 ViewRootImpl * 與 WMS 建立 Session 後會呼叫 ViewRootImpl 的 `setView` 方法:**該方法會向 WMS 發起顯示 `Dialog` or `Activity` 中的 ++DecorView 請求++** > 回到 WindowManagerGlobal 中的 addView 方法的最後一個步驟 "setView" (起初是 Dialog 調用) ```java= /** * WindowManagerGlobal.java */ public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { ... ViewRootImpl root; View panelParentView = null; // mLock 普通對象鎖 synchronized (mLock) { // Start watching for system property changes. ... // 建立 ViewRootImpl 對象 root = new ViewRootImpl(view.getContext(), display); // 設定布局參數 view.setLayoutParams(wparams); // 以下都是 ArrayList mViews.add(view); // View mRoots.add(root); // ViewRootImpl mParams.add(wparams); // WindowManager.LayoutParams 視窗參數 } //最後執行此操作,因為它會觸發傳遞消息 try { // @ 追蹤 ViewRootImpl#setView 方法 root.setView(view, wparams, panelParentView); } /* 省略 catch */ } // --------------------------------------------------------------------- /** * ViewRootImpl.java */ final W mWindow; public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session, boolean useSfChoreographer) { ... 省略部份 mWindow = new W(this); } public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { synchronized (this) { // 由於過於複雜,這裡顯示關鍵函數 requestLayout(); try { ... // 向 WMS 發起請求 // @ 查看 addToDisplay res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mAttachInfo.mOutsets, mInputChannel); } // 省略 catch、finally } ``` * WindowSession#addToDisplay 是添加對 Binder 傳送一個 [**IWindow**](https://cs.android.com/android/platform/superproject/+/master:out/soong/.intermediates/frameworks/base/framework-minus-apex-intdefs/android_common/xref33/srcjars.xref/android/view/IWindow.java) 對象,如果 WMS 需要調整該 View 的話,就會通過 IWindow 來通知該 view ```java= // IWindow.java public interface IWindow extends android.os.IInterface { @Override public void executeCommand(java.lang.String command, java.lang.String parameters, android.os.ParcelFileDescriptor descriptor) throws android.os.RemoteException { } @Override public void resized(android.window.ClientWindowFrames frames, boolean reportDraw, android.util.MergedConfiguration newMergedConfiguration, boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId) throws android.os.RemoteException { } ... 省略部份方法 } ``` :::info * 其實也就是把 View (管理者為 ViewRootImpl) 添加進 WindowSession ::: ### [ViewRootImpl](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/ViewRootImpl.java) - setView 本地繪製 * ViewRootImpl#setView 的內容十分的多,所以我這先提出呼叫 `setView` 時較為關鍵的函數,分別是 1. **requestLayout(要求布局)** 2. **checkThread(檢查是否是主線程更新)** :::warning 這裡就會檢查是否是當初呼叫 ViewRootImpl 的 Thread ::: 3. **scheduleTraversals(準備測量、繪製)** > 之後會觸發 **performTraversals** 方法 ```java= /** * ViewRootImpl.java * * #setView 函數呼叫 requestLayout 函數 */ @Override public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) // 檢查線程是否切換 checkThread(); mLayoutRequested = true; // 安排繪製 scheduleTraversals(); } } void checkThread() { // @ 只有創建 ViewRootImpl 對象的 Thread 可以往下操作執行 if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( "Only the original thread that created a view hierarchy can touch its views."); } } void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); // "1. " 注意第二個參數 mTraversalRunnable mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null ); if (!mUnbufferedInputDispatch) { scheduleConsumeBatchedInput(); } notifyRendererOfFramePending(); pokeDrawLockIfNeeded(); } } // 沒規定 Looper,預設當前的 Thread 的 Looper (Main Thread) final ViewRootHandler mHandler = new ViewRootHandler(); ``` * mTraversalRunnable:它是一個 **Runnable 任務**,,該任務執行 **doTraversal** 方法 ```java= // ViewRootImpl.java final class TraversalRunnable implements Runnable { @Override public void run() { doTraversal(); } } final TraversalRunnable mTraversalRunnable = new TraversalRunnable(); ... void doTraversal() { if (mTraversalScheduled) { mTraversalScheduled = false; mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); if (mProfile) { Debug.startMethodTracing("ViewAncestor"); } performTraversals(); if (mProfile) { Debug.stopMethodTracing(); mProfile = false; } } } // performTraversals 函數也相當的攏長 private void performTraversals() { ... } ``` :::success * performTraversals 主要做以下 4 個任務 1. 獲取 `Surface` 物件,用於圖形繪製 2. 側兩整個檢視樹的各個 View 的大小,**performMeasure** 函數 > 觸發 onMeasure 3. 布局整個 View 樹,**performLayout** 函數 > 觸發 onLayout 4. 繪製整個 View 樹,**performDraw** 函數 > 觸發 onDraw ::: ### ViewRootImpl 取得 Canvas - onDraw 繪製 * ViewRootImpl#performTraversals 函數,其中第 4 個要點繪製 View 樹,Framwork 會獲取到圖形繪製表面 `Surface` (也就是 `Canvas` 對象),View 就會繪製在 Canvas 上 ```java= // ViewRootImpl.java private void performDraw() { if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) { return; } final boolean fullRedrawNeeded = mFullRedrawNeeded; mFullRedrawNeeded = false; mIsDrawing = true; Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw"); try { // 呼叫繪製函數 draw(fullRedrawNeeded); } finally { mIsDrawing = false; Trace.traceEnd(Trace.TRACE_TAG_VIEW); } ... } private void draw(boolean fullRedrawNeeded) { // 1. 繪製表面 Surface surface = mSurface; if (!surface.isValid()) { return; } ... // 2. 繪圖表面更新 if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) { // 使用 GPU 繪製,也就是硬體加速 if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) { ... if (updated) { requestDrawWindow(); } // 3. 硬體加速 ! 使用硬體渲染 mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this); } else { // 4. 使用 CPU 繪製圖形 if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) { return; } } } if (animating) { mFullRedrawNeeded = true; scheduleTraversals(); } } ``` * 在 ViewRootImpl#performDraw 的過程中主要有分為:^1.^ CPU 繪製、^2.^ GPU 繪製,大部分狀況下我們都是使用 CPU 繪製 :::success * Surface 中的可繪製區域就是 Canvas,Surface 就是一個巨大畫布,Canvas 就是該畫布要繪製的區塊 ::: ```java= // ViewRootImpl.java private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty) { // Draw with software renderer. final Canvas canvas; try { final int left = dirty.left; final int top = dirty.top; final int right = dirty.right; final int bottom = dirty.bottom; // 1. 獲取指定區域的 Cavans 物件 canvas = mSurface.lockCanvas(dirty); ... // TODO: Do this in native canvas.setDensity(mDensity); } /* 省略 catch */ try { ... try { // 2. 從 DecorView 開始繪製,也就是整個 Window 的根檢視,這會引起整個 View 樹的重繪操作 mView.draw(canvas); drawAccessibilityFocusedDrawableIfNeeded(canvas); } finally { if (!attachInfo.mSetIgnoreDirtyState) { // Only clear the flag if it was not set during the mView.draw() call attachInfo.mIgnoreDirtyState = false; } } } finally { try { // 3. 釋放 Canvas 鎖,並通知 SurfaceFlinger 更新這塊區域 surface.unlockCanvasAndPost(canvas); } ... } return true; } ``` ## 結論 * Dialog View 創建的概念圖 > ![](https://i.imgur.com/SLHWNly.png) ## Appendix & FAQ :::info ::: ###### tags: `Android Framework`