Android應用Activity、Dialog、PopWindow、Toast窗口添加機制及源碼分析


1  背景

之所以寫這一篇博客的原因是因為之前有寫過一篇《Android應用setContentView與LayoutInflater加載解析機制源碼分析》, 然后有人在文章下面評論和微博私信中問我關於Android應用Activity、Dialog、PopWindow加載顯示機制是咋回事,所以我就寫一 篇文章來分析分析吧(本文以Android5.1.1 (API 22)源碼為基礎分析),以便大家在應用層開發時不再迷糊。

PS一句:不僅有人微博私信我這個問題,還有人問博客插圖這些是用啥畫的,這里告訴大家。就是我,快來猛戳我

還記得之前《Android應用setContentView與LayoutInflater加載解析機制源碼分析》這篇文章的最后分析結果嗎?就是如下這幅圖:

20150604144532934.png

在那篇文章里我們當時重點是Activity的View加載解析xml機制分析,當時說到了Window的東西,但只是皮毛的分析了Activity相關的一些邏輯。(PS:看到這不清楚上面啥意思的建議先移步到《Android應用setContentView與LayoutInflater加載解析機制源碼分析》,完事再回頭繼續看這篇文章。)當時給大家承諾過我們要從應用控件一點一點往下慢慢深入分析,所以現在開始深入,但是本篇的深入也只是僅限Window相關的東東,之后文章還會繼續慢慢深入。

2  淺析Window與WindowManager相關關系及源碼

通過上面那幅圖可以很直觀的看見,Android屏幕顯示的就是Window和各種View,Activity在其中的作用主要是管理生命周期、建 立窗口等。也就是說Window相關的東西對於Android屏幕來說是至關重要的(雖然前面分析Activity的setContentView等原理 時說過一點Window,但那只是皮毛。),所以有必要在分析Android應用Activity、Dialog、PopWindow加載顯示機制前再看 看Window相關的一些東西。

2-1  Window與WindowManager基礎關系

在分析Window與WindowManager之前我們先看一張圖:

20150605171231242.png

接下來看一點代碼,如下:

1
2
3
4
5
6
7
8
9
/** Interface to let you add and remove child views to an Activity. To get an instance
   * of this class, call {@link android.content.Context#getSystemService(java.lang.String) Context.getSystemService()}.
   */
public interface ViewManager
{
     public void addView(View view, ViewGroup.LayoutParams params);
     public void updateViewLayout(View view, ViewGroup.LayoutParams params);
     public void removeView(View view);
}

可以看見,ViewManager接口定義了一組規則,也就是add、update、remove的操作View接口。也就是說ViewManager是用來添加和移除activity中View的接口。繼續往下看:

1
2
3
4
5
6
7
8
9
10
public interface WindowManager extends ViewManager {
     ......
     public Display getDefaultDisplay();
     public void removeViewImmediate(View view);
     ......
     public static class LayoutParams extends ViewGroup.LayoutParams
             implements Parcelable {
         ......
     }
}

看見沒有,WindowManager繼承自ViewManager,然后自己還是一個接口,同時又定義了一個靜態內部類LayoutParams(這個 類比較重要,后面會分析。提前透漏下,如果你在APP做過類似360助手屏幕的那個懸浮窗或者做過那種類似IOS的小白圓點,點擊展開菜單功能,你或多或 少就能猜到這個類的重要性。)。WindowManager用來在應用與Window之間的接口、窗口順序、消息等的管理。繼續看下 ViewManager的另一個實現子類ViewGroup,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
     //protected ViewParent mParent;
     //這個成員是View定義的,ViewGroup繼承自View,所以也可以擁有。
     //這個變量就是前面我們一系列文章分析View向上傳遞的父節點,類似於一個鏈表Node的next一樣
     //最終指向了ViewRoot
     ......
     public void addView(View child, LayoutParams params) {
         addView(child, -1, params);
     }
 
     ......
 
     public void addView(View child, int index, LayoutParams params) {
         ......
         // addViewInner() will call child.requestLayout() when setting the new LayoutParams
         // therefore, we call requestLayout() on ourselves before, so that the child's request
         // will be blocked at our level
         requestLayout();
         invalidate( true );
         addViewInner(child, index, params,  false );
     }
     ......
}

這下理解上面那幅圖了吧,所以說View通過ViewGroup的addView方法添加到ViewGroup中,而ViewGroup層層嵌套到最頂級都會顯示在在一個窗口Window中(正如上面背景介紹中《Android應用setContentView與LayoutInflater加載解析機制源碼分析》的示意圖一樣),其中每個View都有一個ViewParent類型的父節點mParent,最頂上的節點也是一個viewGroup,也即前面文章分析的Window的內部類DecorView(從《Android應用setContentView與LayoutInflater加載解析機制源碼分析》的總結部分或者《Android應用層View繪制流程與源碼分析》的5-1小節都可以驗證這個結論)對象。同時通過上面背景中那幅圖可以看出來,對於一個Activity只有一個DecorView(ViewRoot),也只有一個Window。

2-2  Activity窗口添加流程拓展

前面文章說過,ActivityThread類的performLaunchActivity方法中調運了activity.attach(…)方法進行初始化。如下是Activity的attach方法源碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
     final void attach(Context context, ActivityThread aThread,
             Instrumentation instr, IBinder token, int ident,
             Application application, Intent intent, ActivityInfo info,
             CharSequence title, Activity parent, String id,
             NonConfigurationInstances lastNonConfigurationInstances,
             Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
         ......
         //創建Window類型的mWindow對象,實際為PhoneWindow類實現了抽象Window類
         mWindow = PolicyManager.makeNewWindow( this );
         ......
         //通過抽象Window類的setWindowManager方法給Window類的成員變量WindowManager賦值實例化
         mWindow.setWindowManager(
                 (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                 mToken, mComponent.flattenToString(),
                 (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
         ......
         //把抽象Window類相關的WindowManager對象拿出來關聯到Activity的WindowManager類型成員變量mWindowManager
         mWindowManager = mWindow.getWindowManager();
         ......
     }

看見沒有,Activity類中的attach方法又創建了Window類型的新成員變量mWindow(PhoneWindow實現類)與 Activity相關聯,接着在Activity類的attach方法最后又通過mWindow.setWindowManager(…)方法創建了與 Window相關聯的WindowManager對象,最后又通過mWindow.getWindowManager()將Window的 WindowManager成員變量賦值給Activity的WindowManager成員變量mWindowManager。

接下來我們看下上面代碼中的mWindow.setWindowManager(…)方法源碼(PhoneWindow沒有重寫抽象Window的setWindowManager方法,所以直接看Window類的該方法源碼),如下:

1
2
3
4
5
6
7
8
9
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
         boolean hardwareAccelerated) {
     ......
     if  (wm ==  null ) {
         wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
     }
     //實例化Window類的WindowManager類型成員mWindowManager
     mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager( this );
}

可以看見,Window的setWindowManager方法中通過WindowManagerImpl實例的createLocalWindowManager方法獲取了WindowManager實例,如下:

1
2
3
4
5
6
7
8
9
10
11
12
public final class WindowManagerImpl implements WindowManager {
     ......
     private WindowManagerImpl(Display display, Window parentWindow) {
         mDisplay = display;
         mParentWindow = parentWindow;
     }
     ......
     public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
         return  new  WindowManagerImpl(mDisplay, parentWindow);
     }
     ......
}

看見沒有?這樣就把Activity的Window與WindowManager關聯起來了。Activity類的Window類型成員變量mWindow及WindowManager類型成員變量mWindowManager就是這么來的。

回過頭繼續看上面剛剛貼的Activity的attach方法代碼,看見mWindow.setWindowManager方法傳遞的第一個參數 沒?有人會想(WindowManager)context.getSystemService(Context.WINDOW_SERVICE)這行代 碼是什么意思,現在告訴你。

《Android應用Context詳解及源碼解析》一 文中第三部分曾經說過ActivityThread中創建了Acitivty(執行attach等方法)等東東,在創建這個Activity之前得到了 Context的實例。記不記得當時說Context的實現類就是ContextImpl嗎?下面我們看下ContextImpl類的靜態方法塊,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class ContextImpl extends Context {
     ......
     //靜態代碼塊,類加載時執行一次
     static {
         ......
         //這里有一堆類似的XXX_SERVICE的注冊
         ......
         registerService(WINDOW_SERVICE,  new  ServiceFetcher() {
                 Display mDefaultDisplay;
                 public Object getService(ContextImpl ctx) {
                     //搞一個Display實例
                     Display display = ctx.mDisplay;
                     if  (display ==  null ) {
                         if  (mDefaultDisplay ==  null ) {
                             DisplayManager dm = (DisplayManager)ctx.getOuterContext().
                                     getSystemService(Context.DISPLAY_SERVICE);
                             mDefaultDisplay = dm.getDisplay(Display.DEFAULT_DISPLAY);
                         }
                         display = mDefaultDisplay;
                     }
                     //返回一個WindowManagerImpl實例
                     return  new  WindowManagerImpl(display);
                 }});
         ......
     }
     //這就是你在外面調運Context的getSystemService獲取到的WindowManagerImpl實例
     @Override
     public Object getSystemService(String name) {
         ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);
         return  fetcher ==  null  null  : fetcher.getService( this );
     }
     //上面static代碼塊創建WindowManagerImpl實例用到的方法
     private static void registerService(String serviceName, ServiceFetcher fetcher) {
         if  (!(fetcher  instanceof  StaticServiceFetcher)) {
             fetcher.mContextCacheIndex = sNextPerContextServiceCacheIndex++;
         }
         SYSTEM_SERVICE_MAP.put(serviceName, fetcher);
     }
}

看見沒有,我們都知道Java的靜態代碼塊是類加載是執行一次的,也就相當於一個全局的,這樣就相當於每個Application只有一個WindowManagerImpl(display)實例。

還記不記得《Android應用setContentView與LayoutInflater加載解析機制源碼分析》一文2-6小節中說的,setContentView的實質顯示是觸發了Activity的resume狀態,也就是觸發了makeVisible方法,那我們再來看下這個方法,如下:

1
2
3
4
5
6
7
8
9
10
11
void makeVisible() {
     if  (!mWindowAdded) {
         //也就是獲取Activity的mWindowManager
         //這個mWindowManager是在Activity的attach中通過mWindow.getWindowManager()獲得
         ViewManager wm = getWindowManager();
         //調運的實質就是ViewManager接口的addView方法,傳入的是mDecorView
         wm.addView(mDecor, getWindow().getAttributes());
         mWindowAdded =  true ;
     }
     mDecor.setVisibility(View.VISIBLE);
}

特別注意,看見makeVisible方法的wm變量沒,這個變量就是Window類中通過調運WindowManagerImpl的 createLocalWindowManager創建的實例,也就是說每一個Activity都會新創建這么一個WindowManager實例來顯示 Activity的界面的,有點和上面分析的ContextImpl中static塊創建的WindowManager不太一樣的地方就在於 Context的WindowManager對每個APP來說是一個全局單例的,而Activity的WindowManager是每個Activity 都會新創建一個的(其實你從上面分析的兩個實例化WindowManagerImpl的構造函數參數傳遞就可以看出來,Activity中Window的 WindowManager成員在構造實例化時傳入給WindowManagerImpl中mParentWindow成員的是當前Window對象,而 ContextImpl的static塊中單例實例化WindowManagerImpl時傳入給WindowManagerImpl中 mParentWindow成員的是null值)。

繼續看makeVisible中調運的WindowManagerImpl的addView方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
public final class WindowManagerImpl implements WindowManager {
     //繼承自Object的單例類
     private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
     ......
     public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
         applyDefaultToken(params);
         //mParentWindow是上面分析的在Activity中獲取WindowManagerImpl實例化時傳入的當前Window
         //view是Activity中最頂層的mDecor
         mGlobal.addView(view, params, mDisplay, mParentWindow);
     }
     ......
}

這里當前傳入的view是mDecor,LayoutParams呢?可以看見是getWindow().getAttributes(),那我們進去看看Window類的這個屬性,如下:

1
2
// The current window attributes.
     private final WindowManager.LayoutParams mWindowAttributes =  new  WindowManager.LayoutParams();

原來是WindowManager的靜態內部類LayoutParams的默認構造函數:

1
2
3
4
5
public LayoutParams() {
     super (LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
     type = TYPE_APPLICATION;
     format = PixelFormat.OPAQUE;
}

看見沒有,Activity窗體的WindowManager.LayoutParams類型是TYPE_APPLICATION的。

繼續回到WindowManagerImpl的addView方法,分析可以看見WindowManagerImpl中有一個單例模式的 WindowManagerGlobal成員mGlobal,addView最終調運了WindowManagerGlobal的addView,源碼如 下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public final class WindowManagerGlobal {
     ......
     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 final ArraySet<View> mDyingViews =  new  ArraySet<View>();
 
     ......
     public void addView(View view, ViewGroup.LayoutParams params,
             Display display, Window parentWindow) {
         ......
         //獲取Activity的Window的getWindow().getAttributes()的LayoutParams 
         final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
         //如果是Activity中調運的,parentWindow=Window,如果不是Activity的,譬如是Context的靜態代碼塊的實例化則parentWindow為null
         if  (parentWindow !=  null ) {
             //依據當前Activity的Window調節sub Window的LayoutParams
             parentWindow.adjustLayoutParamsForSubWindow(wparams);
         else  {
             ......
         }
 
         ViewRootImpl root;
         ......
         synchronized (mLock) {
             ......
             //為當前Window創建ViewRoot
             root =  new  ViewRootImpl(view.getContext(), display);
             view.setLayoutParams(wparams);
             //把當前Window相關的東西存入各自的List中,在remove中會刪掉
             mViews.add(view);
             mRoots.add(root);
             mParams.add(wparams);
         }
 
         // do this last because it fires off messages to start doing things
         try  {
             //把View和ViewRoot關聯起來,很重要!!!
             root.setView(view, wparams, panelParentView);
         catch  (RuntimeException e) {
             ......
         }
     }
     ......
}

可以看見,在addView方法中會利用LayoutParams獲得Window的屬性,然后為每個Window創建ViewRootImpl, 最后通過ViewRootImpl的setView方法通過mSession向WindowManagerService發送添加窗口請求把窗口添加到 WindowManager中,並且由WindowManager來管理窗口的view、事件、消息收集處理等(ViewRootImpl的這一添加過程 后面會寫文章分析,這里先記住這個概念即可)。

至此我們對上面背景中那幅圖,也就是《Android應用setContentView與LayoutInflater加載解析機制源碼分析》這篇文章總結部分的那幅圖又進行了更深入的一點分析,其目的也就是為了下面分析Android應用Dialog、PopWindow、Toast加載顯示機制做鋪墊准備。

2-3  繼續順藤摸瓜WindowManager.LayoutParams類的源碼

上面2-1分析Window與WindowManager基礎關系時提到了WindowManager有一個靜態內部類LayoutParams, 它繼承於ViewGroup.LayoutParams,用於向WindowManager描述Window的管理策略。現在我們來看下這個類(PS:在 AD上也可以看見,自備梯子點我看AD的),如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
     public static class LayoutParams extends ViewGroup.LayoutParams
             implements Parcelable {
         //窗口的絕對XY位置,需要考慮gravity屬性
         public int x;
         public int y;
         //在橫縱方向上為相關的View預留多少擴展像素,如果是0則此view不能被拉伸,其他情況下擴展像素被widget均分
         public float horizontalWeight;
         public float verticalWeight;
         //窗口類型
         //有3種主要類型如下:
         //ApplicationWindows取值在FIRST_APPLICATION_WINDOW與LAST_APPLICATION_WINDOW之間,是常用的頂層應用程序窗口,須將token設置成Activity的token;
         //SubWindows取值在FIRST_SUB_WINDOW和LAST_SUB_WINDOW之間,與頂層窗口相關聯,需將token設置成它所附着宿主窗口的token;
         //SystemWindows取值在FIRST_SYSTEM_WINDOW和LAST_SYSTEM_WINDOW之間,不能用於應用程序,使用時需要有特殊權限,它是特定的系統功能才能使用;
         public int type;
 
         //WindowType:開始應用程序窗口
         public static final int FIRST_APPLICATION_WINDOW = 1;
         //WindowType:所有程序窗口的base窗口,其他應用程序窗口都顯示在它上面
         public static final int TYPE_BASE_APPLICATION   = 1;
         //WindowType:普通應用程序窗口,token必須設置為Activity的token來指定窗口屬於誰
         public static final int TYPE_APPLICATION        = 2;
         //WindowType:應用程序啟動時所顯示的窗口,應用自己不要使用這種類型,它被系統用來顯示一些信息,直到應用程序可以開啟自己的窗口為止
         public static final int TYPE_APPLICATION_STARTING = 3;
         //WindowType:結束應用程序窗口
         public static final int LAST_APPLICATION_WINDOW = 99;
 
         //WindowType:SubWindows子窗口,子窗口的Z序和坐標空間都依賴於他們的宿主窗口
         public static final int FIRST_SUB_WINDOW        = 1000;
         //WindowType: 面板窗口,顯示於宿主窗口的上層
         public static final int TYPE_APPLICATION_PANEL  = FIRST_SUB_WINDOW;
         //WindowType:媒體窗口(例如視頻),顯示於宿主窗口下層
         public static final int TYPE_APPLICATION_MEDIA  = FIRST_SUB_WINDOW+1;
         //WindowType:應用程序窗口的子面板,顯示於所有面板窗口的上層
         public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW+2;
         //WindowType:對話框,類似於面板窗口,繪制類似於頂層窗口,而不是宿主的子窗口
         public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW+3;
         //WindowType:媒體信息,顯示在媒體層和程序窗口之間,需要實現半透明效果
         public static final int TYPE_APPLICATION_MEDIA_OVERLAY  = FIRST_SUB_WINDOW+4;
         //WindowType:子窗口結束
         public static final int LAST_SUB_WINDOW         = 1999;
 
         //WindowType:系統窗口,非應用程序創建
         public static final int FIRST_SYSTEM_WINDOW     = 2000;
         //WindowType:狀態欄,只能有一個狀態欄,位於屏幕頂端,其他窗口都位於它下方
         public static final int TYPE_STATUS_BAR         = FIRST_SYSTEM_WINDOW;
         //WindowType:搜索欄,只能有一個搜索欄,位於屏幕上方
         public static final int TYPE_SEARCH_BAR         = FIRST_SYSTEM_WINDOW+1;
         //WindowType:電話窗口,它用於電話交互(特別是呼入),置於所有應用程序之上,狀態欄之下
         public static final int TYPE_PHONE              = FIRST_SYSTEM_WINDOW+2;
         //WindowType:系統提示,出現在應用程序窗口之上
         public static final int TYPE_SYSTEM_ALERT       = FIRST_SYSTEM_WINDOW+3;
         //WindowType:鎖屏窗口
         public static final int TYPE_KEYGUARD           = FIRST_SYSTEM_WINDOW+4;
         //WindowType:信息窗口,用於顯示Toast
         public static final int TYPE_TOAST              = FIRST_SYSTEM_WINDOW+5;
         //WindowType:系統頂層窗口,顯示在其他一切內容之上,此窗口不能獲得輸入焦點,否則影響鎖屏
         public static final int TYPE_SYSTEM_OVERLAY     = FIRST_SYSTEM_WINDOW+6;
         //WindowType:電話優先,當鎖屏時顯示,此窗口不能獲得輸入焦點,否則影響鎖屏
         public static final int TYPE_PRIORITY_PHONE     = FIRST_SYSTEM_WINDOW+7;
         //WindowType:系統對話框
         public static final int TYPE_SYSTEM_DIALOG      = FIRST_SYSTEM_WINDOW+8;
         //WindowType:鎖屏時顯示的對話框
         public static final int TYPE_KEYGUARD_DIALOG    = FIRST_SYSTEM_WINDOW+9;
         //WindowType:系統內部錯誤提示,顯示於所有內容之上
         public static final int TYPE_SYSTEM_ERROR       = FIRST_SYSTEM_WINDOW+10;
         //WindowType:內部輸入法窗口,顯示於普通UI之上,應用程序可重新布局以免被此窗口覆蓋
         public static final int TYPE_INPUT_METHOD       = FIRST_SYSTEM_WINDOW+11;
         //WindowType:內部輸入法對話框,顯示於當前輸入法窗口之上
         public static final int TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12;
         //WindowType:牆紙窗口
         public static final int TYPE_WALLPAPER          = FIRST_SYSTEM_WINDOW+13;
         //WindowType:狀態欄的滑動面板
         public static final int TYPE_STATUS_BAR_PANEL   = FIRST_SYSTEM_WINDOW+14;
         //WindowType:安全系統覆蓋窗口,這些窗戶必須不帶輸入焦點,否則會干擾鍵盤
         public static final int TYPE_SECURE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+15;
         //WindowType:拖放偽窗口,只有一個阻力層(最多),它被放置在所有其他窗口上面
         public static final int TYPE_DRAG               = FIRST_SYSTEM_WINDOW+16;
         //WindowType:狀態欄下拉面板
         public static final int TYPE_STATUS_BAR_SUB_PANEL = FIRST_SYSTEM_WINDOW+17;
         //WindowType:鼠標指針
         public static final int TYPE_POINTER = FIRST_SYSTEM_WINDOW+18;
         //WindowType:導航欄(有別於狀態欄時)
         public static final int TYPE_NAVIGATION_BAR = FIRST_SYSTEM_WINDOW+19;
         //WindowType:音量級別的覆蓋對話框,顯示當用戶更改系統音量大小
         public static final int TYPE_VOLUME_OVERLAY = FIRST_SYSTEM_WINDOW+20;
         //WindowType:起機進度框,在一切之上
         public static final int TYPE_BOOT_PROGRESS = FIRST_SYSTEM_WINDOW+21;
         //WindowType:假窗,消費導航欄隱藏時觸摸事件
         public static final int TYPE_HIDDEN_NAV_CONSUMER = FIRST_SYSTEM_WINDOW+22;
         //WindowType:夢想(屏保)窗口,略高於鍵盤
         public static final int TYPE_DREAM = FIRST_SYSTEM_WINDOW+23;
         //WindowType:導航欄面板(不同於狀態欄的導航欄)
         public static final int TYPE_NAVIGATION_BAR_PANEL = FIRST_SYSTEM_WINDOW+24;
         //WindowType:universe背后真正的窗戶
         public static final int TYPE_UNIVERSE_BACKGROUND = FIRST_SYSTEM_WINDOW+25;
         //WindowType:顯示窗口覆蓋,用於模擬輔助顯示設備
         public static final int TYPE_DISPLAY_OVERLAY = FIRST_SYSTEM_WINDOW+26;
         //WindowType:放大窗口覆蓋,用於突出顯示的放大部分可訪問性放大時啟用
         public static final int TYPE_MAGNIFICATION_OVERLAY = FIRST_SYSTEM_WINDOW+27;
         //WindowType:......
         public static final int TYPE_KEYGUARD_SCRIM           = FIRST_SYSTEM_WINDOW+29;
         public static final int TYPE_PRIVATE_PRESENTATION = FIRST_SYSTEM_WINDOW+30;
         public static final int TYPE_VOICE_INTERACTION = FIRST_SYSTEM_WINDOW+31;
         public static final int TYPE_ACCESSIBILITY_OVERLAY = FIRST_SYSTEM_WINDOW+32;
         //WindowType:系統窗口結束
         public static final int LAST_SYSTEM_WINDOW      = 2999;
 
         //MemoryType:窗口緩沖位於主內存
         public static final int MEMORY_TYPE_NORMAL = 0;
         //MemoryType:窗口緩沖位於可以被DMA訪問,或者硬件加速的內存區域
         public static final int MEMORY_TYPE_HARDWARE = 1;
         //MemoryType:窗口緩沖位於可被圖形加速器訪問的區域
         public static final int MEMORY_TYPE_GPU = 2;
         //MemoryType:窗口緩沖不擁有自己的緩沖區,不能被鎖定,緩沖區由本地方法提供
         public static final int MEMORY_TYPE_PUSH_BUFFERS = 3;
 
         //指出窗口所使用的內存緩沖類型,默認為NORMAL 
         public int memoryType;
 
         //Flag:當該window對用戶可見的時候,允許鎖屏
         public static final int FLAG_ALLOW_LOCK_WHILE_SCREEN_ON     = 0x00000001;
         //Flag:讓該window后所有的東西都成暗淡
         public static final int FLAG_DIM_BEHIND        = 0x00000002;
         //Flag:讓該window后所有東西都模糊(4.0以上已經放棄這種毛玻璃效果)
         public static final int FLAG_BLUR_BEHIND        = 0x00000004;
         //Flag:讓window不能獲得焦點,這樣用戶快就不能向該window發送按鍵事
         public static final int FLAG_NOT_FOCUSABLE      = 0x00000008;
         //Flag:讓該window不接受觸摸屏事件
         public static final int FLAG_NOT_TOUCHABLE      = 0x00000010;
         //Flag:即使在該window在可獲得焦點情況下,依舊把該window之外的任何event發送到該window之后的其他window
         public static final int FLAG_NOT_TOUCH_MODAL    = 0x00000020;
         //Flag:當手機處於睡眠狀態時,如果屏幕被按下,那么該window將第一個收到
         public static final int FLAG_TOUCHABLE_WHEN_WAKING = 0x00000040;
         //Flag:當該window對用戶可見時,讓設備屏幕處於高亮(bright)狀態
         public static final int FLAG_KEEP_SCREEN_ON     = 0x00000080;
         //Flag:讓window占滿整個手機屏幕,不留任何邊界
         public static final int FLAG_LAYOUT_IN_SCREEN   = 0x00000100;
         //Flag:window大小不再不受手機屏幕大小限制,即window可能超出屏幕之外
         public static final int FLAG_LAYOUT_NO_LIMITS   = 0x00000200;
         //Flag:window全屏顯示
         public static final int FLAG_FULLSCREEN      = 0x00000400;
         //Flag:恢復window非全屏顯示
         public static final int FLAG_FORCE_NOT_FULLSCREEN   = 0x00000800;
         //Flag:開啟抖動(dithering)
         public static final int FLAG_DITHER             = 0x00001000;
         //Flag:當該window在進行顯示的時候,不允許截屏
         public static final int FLAG_SECURE             = 0x00002000;
         //Flag:一個特殊模式的布局參數用於執行擴展表面合成時到屏幕上
         public static final int FLAG_SCALED             = 0x00004000;
         //Flag:用於windows時,經常會使用屏幕用戶持有反對他們的臉,它將積極過濾事件流,以防止意外按在這種情況下,可能不需要為特定的窗口,在檢測到這樣一個事件流時,應用程序將接收取消運動事件表明,這樣應用程序可以處理這相應地采取任何行動的事件,直到手指釋放
         public static final int FLAG_IGNORE_CHEEK_PRESSES    = 0x00008000;
         //Flag:一個特殊的選項只用於結合FLAG_LAYOUT_IN_SC
         public static final int FLAG_LAYOUT_INSET_DECOR = 0x00010000;
         //Flag:轉化的狀態FLAG_NOT_FOCUSABLE對這個窗口當前如何進行交互的方法
         public static final int FLAG_ALT_FOCUSABLE_IM = 0x00020000;
         //Flag:如果你設置了該flag,那么在你FLAG_NOT_TOUNCH_MODAL的情況下,即使觸摸屏事件發送在該window之外,其事件被發送到了后面的window,那么該window仍然將以MotionEvent.ACTION_OUTSIDE形式收到該觸摸屏事件
         public static final int FLAG_WATCH_OUTSIDE_TOUCH = 0x00040000;
         //Flag:當鎖屏的時候,顯示該window
         public static final int FLAG_SHOW_WHEN_LOCKED = 0x00080000;
         //Flag:在該window后顯示系統的牆紙
         public static final int FLAG_SHOW_WALLPAPER = 0x00100000;
         //Flag:當window被顯示的時候,系統將把它當做一個用戶活動事件,以點亮手機屏幕
         public static final int FLAG_TURN_SCREEN_ON = 0x00200000;
         //Flag:消失鍵盤
         public static final int FLAG_DISMISS_KEYGUARD = 0x00400000;
         //Flag:當該window在可以接受觸摸屏情況下,讓因在該window之外,而發送到后面的window的觸摸屏可以支持split touch
         public static final int FLAG_SPLIT_TOUCH = 0x00800000;
         //Flag:對該window進行硬件加速,該flag必須在Activity或Dialog的Content View之前進行設置
         public static final int FLAG_HARDWARE_ACCELERATED = 0x01000000;
         //Flag:讓window占滿整個手機屏幕,不留任何邊界
         public static final int FLAG_LAYOUT_IN_OVERSCAN = 0x02000000;
         //Flag:請求一個半透明的狀態欄背景以最小的系統提供保護
         public static final int FLAG_TRANSLUCENT_STATUS = 0x04000000;
         //Flag:請求一個半透明的導航欄背景以最小的系統提供保護
         public static final int FLAG_TRANSLUCENT_NAVIGATION = 0x08000000;
         //Flag:......
         public static final int FLAG_LOCAL_FOCUS_MODE = 0x10000000;
         public static final int FLAG_SLIPPERY = 0x20000000;
         public static final int FLAG_LAYOUT_ATTACHED_IN_DECOR = 0x40000000;
         public static final int FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS = 0x80000000;
 
         //行為選項標記
         public int flags;
 
         //PrivateFlags:......
         public static final int PRIVATE_FLAG_FAKE_HARDWARE_ACCELERATED = 0x00000001;
         public static final int PRIVATE_FLAG_FORCE_HARDWARE_ACCELERATED = 0x00000002;
         public static final int PRIVATE_FLAG_WANTS_OFFSET_NOTIFICATIONS = 0x00000004;
         public static final int PRIVATE_FLAG_SHOW_FOR_ALL_USERS = 0x00000010;
         public static final int PRIVATE_FLAG_NO_MOVE_ANIMATION = 0x00000040;
         public static final int PRIVATE_FLAG_COMPATIBLE_WINDOW = 0x00000080;
         public static final int PRIVATE_FLAG_SYSTEM_ERROR = 0x00000100;
         public static final int PRIVATE_FLAG_INHERIT_TRANSLUCENT_DECOR = 0x00000200;
         public static final int PRIVATE_FLAG_KEYGUARD = 0x00000400;
         public static final int PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS = 0x00000800;
 
         //私有的行為選項標記
         public int privateFlags;
 
         public static final int NEEDS_MENU_UNSET = 0;
         public static final int NEEDS_MENU_SET_TRUE = 1;
         public static final int NEEDS_MENU_SET_FALSE = 2;
         public int needsMenuKey = NEEDS_MENU_UNSET;
 
         public static boolean mayUseInputMethod(int flags) {
             ......
         }
 
         //SOFT_INPUT:用於描述軟鍵盤顯示規則的bite的mask
         public static final int SOFT_INPUT_MASK_STATE = 0x0f;
         //SOFT_INPUT:沒有軟鍵盤顯示的約定規則
         public static final int SOFT_INPUT_STATE_UNSPECIFIED = 0;
         //SOFT_INPUT:可見性狀態softInputMode,請不要改變軟輸入區域的狀態
         public static final int SOFT_INPUT_STATE_UNCHANGED = 1;
         //SOFT_INPUT:用戶導航(navigate)到你的窗口時隱藏軟鍵盤
         public static final int SOFT_INPUT_STATE_HIDDEN = 2;
         //SOFT_INPUT:總是隱藏軟鍵盤
         public static final int SOFT_INPUT_STATE_ALWAYS_HIDDEN = 3;
         //SOFT_INPUT:用戶導航(navigate)到你的窗口時顯示軟鍵盤
         public static final int SOFT_INPUT_STATE_VISIBLE = 4;
         //SOFT_INPUT:總是顯示軟鍵盤
         public static final int SOFT_INPUT_STATE_ALWAYS_VISIBLE = 5;
         //SOFT_INPUT:顯示軟鍵盤時用於表示window調整方式的bite的mask
         public static final int SOFT_INPUT_MASK_ADJUST = 0xf0;
         //SOFT_INPUT:不指定顯示軟件盤時,window的調整方式
         public static final int SOFT_INPUT_ADJUST_UNSPECIFIED = 0x00;
         //SOFT_INPUT:當顯示軟鍵盤時,調整window內的控件大小以便顯示軟鍵盤
         public static final int SOFT_INPUT_ADJUST_RESIZE = 0x10;
         //SOFT_INPUT:當顯示軟鍵盤時,調整window的空白區域來顯示軟鍵盤,即使調整空白區域,軟鍵盤還是有可能遮擋一些有內容區域,這時用戶就只有退出軟鍵盤才能看到這些被遮擋區域並進行
         public static final int SOFT_INPUT_ADJUST_PAN = 0x20;
         //SOFT_INPUT:當顯示軟鍵盤時,不調整window的布局
         public static final int SOFT_INPUT_ADJUST_NOTHING = 0x30;
         //SOFT_INPUT:用戶導航(navigate)到了你的window
         public static final int SOFT_INPUT_IS_FORWARD_NAVIGATION = 0x100;
 
         //軟輸入法模式選項
         public int softInputMode;
 
         //窗口如何停靠
         public int gravity;
         //水平邊距,容器與widget之間的距離,占容器寬度的百分率
         public float horizontalMargin;
         //縱向邊距
         public float verticalMargin;
         //積極的insets繪圖表面和窗口之間的內容
         public final Rect surfaceInsets =  new  Rect();
         //期望的位圖格式,默認為不透明,參考android.graphics.PixelFormat
         public int format;
         //窗口所使用的動畫設置,它必須是一個系統資源而不是應用程序資源,因為窗口管理器不能訪問應用程序
         public int windowAnimations;
         //整個窗口的半透明值,1.0表示不透明,0.0表示全透明
         public float alpha = 1.0f;
         //當FLAG_DIM_BEHIND設置后生效,該變量指示后面的窗口變暗的程度,1.0表示完全不透明,0.0表示沒有變暗
         public float dimAmount = 1.0f;
 
         public static final float BRIGHTNESS_OVERRIDE_NONE = -1.0f;
         public static final float BRIGHTNESS_OVERRIDE_OFF = 0.0f;
         public static final float BRIGHTNESS_OVERRIDE_FULL = 1.0f;
         public float screenBrightness = BRIGHTNESS_OVERRIDE_NONE;
         //用來覆蓋用戶設置的屏幕亮度,表示應用用戶設置的屏幕亮度,從0到1調整亮度從暗到最亮發生變化
         public float buttonBrightness = BRIGHTNESS_OVERRIDE_NONE;
 
         public static final int ROTATION_ANIMATION_ROTATE = 0;
         public static final int ROTATION_ANIMATION_CROSSFADE = 1;
         public static final int ROTATION_ANIMATION_JUMPCUT = 2;
         //定義出入境動畫在這個窗口旋轉設備時使用
         public int rotationAnimation = ROTATION_ANIMATION_ROTATE;
 
         //窗口的標示符
         public IBinder token =  null ;
         //此窗口所在的包名
         public String packageName =  null ;
         //屏幕方向
         public int screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
         //首選的刷新率的窗口
         public float preferredRefreshRate;
         //控制status bar是否顯示
         public int systemUiVisibility;
         //ui能見度所請求的視圖層次結構
         public int subtreeSystemUiVisibility;
         //得到關於系統ui能見度變化的回調
         public boolean hasSystemUiListeners;
 
         public static final int INPUT_FEATURE_DISABLE_POINTER_GESTURES = 0x00000001;
         public static final int INPUT_FEATURE_NO_INPUT_CHANNEL = 0x00000002;
         public static final int INPUT_FEATURE_DISABLE_USER_ACTIVITY = 0x00000004;
         public int inputFeatures;
         public long userActivityTimeout = -1;
 
         ......
         public final int copyFrom(LayoutParams o) {
             ......
         }
 
         ......
         public void scale(float scale) {
             ......
         }
 
         ......
     }

看見沒有,從上面類可以看出,Android窗口類型主要分成了三大類:

  1. 應用程序窗口。一般應用程序的窗口,比如我們應用程序的Activity的窗口。

  2. 子窗口。一般在Activity里面的窗口,比如對話框等。

  3. 系統窗口。系統的窗口,比如輸入法,Toast,牆紙等。

同時還可以看見,WindowManager.LayoutParams里面窗口的type類型值定義是一個遞增保留的連續增大數值,從注釋可以看 出來其實就是窗口的Z-ORDER序列(值越大顯示的位置越在上面,你需要將屏幕想成三維坐標模式)。創建不同類型的窗口需要設置不同的type值,譬如 上面拓展Activity窗口加載時分析的makeVisible方法中的Window默認屬性的type=TYPE_APPLICATION。

既然說這個類很重要,那總得感性的體驗一下重要性吧,所以我們先來看幾個實例。

2-4  通過上面WindowManager.LayoutParams分析引出的應用層開發常用經典實例

有了上面分析相信你一定覺得WindowManager.LayoutParams還是蠻熟悉的,不信我們來看下。

Part1:開發APP時設置Activity全屏常亮的一種辦法(設置Activity也就是Activity的Window):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MainActivity extends ActionBarActivity {
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super .onCreate(savedInstanceState);
         //設置Activity的Window為全屏,當然也可以在xml中設置
         Window window = getWindow();
         WindowManager.LayoutParams windowAttributes = window.getAttributes();
         windowAttributes.flags = WindowManager.LayoutParams.FLAG_FULLSCREEN | windowAttributes.flags;
         window.setAttributes(windowAttributes);
         //設置Activity的Window為保持屏幕亮
         window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
 
         setContentView(R.layout.activity_main);
     }
}

這是運行結果:

20150607143240960.png

Part2:App開發中彈出軟鍵盤時下面的輸入框被軟件盤擋住問題的解決辦法:

在Activity中的onCreate中setContentView之前寫如下代碼:

1
2
//你也可以在xml文件中設置,一樣的效果
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE|WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);

Part3:創建懸浮窗口(仿IPhone的小圓點或者魅族的小白點或者360手機衛士的小浮標),退出當前Activity依舊可見的一種實現方法:

省略了Activity的start與stop Service的按鈕代碼,直接給出了核心代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
  * Author       : yanbo
  * Time         : 14:47
  * Description  : 手機屏幕懸浮窗,仿IPhone小圓點
  *               (未完全實現,只提供思路,如需請自行實現)
  * Notice       : <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
  */
public class WindowService extends Service {
     private WindowManager mWindowManager;
     private ImageView mImageView;
 
     @Override
     public IBinder onBind(Intent intent) {
         return  null ;
     }
 
     @Override
     public void onCreate() {
         super .onCreate();
         //創建懸浮窗
         createFloatWindow();
     }
 
     private void createFloatWindow() {
         //這里的參數設置上面剛剛講過,不再說明
         WindowManager.LayoutParams layoutParams =  new  WindowManager.LayoutParams();
         mWindowManager = (WindowManager) getApplication().getSystemService(Context.WINDOW_SERVICE);
         //設置window的type
         layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
         //設置效果為背景透明
         layoutParams.format = PixelFormat.RGBA_8888;
         //設置浮動窗口不可聚焦
         layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
         layoutParams.gravity = Gravity.BOTTOM | Gravity.RIGHT;
         layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
         layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
         layoutParams.x = -50;
         layoutParams.y = -50;
 
         mImageView =  new  ImageView( this );
         mImageView.setImageResource(android.R.drawable.ic_menu_add);
         //添加到Window
         mWindowManager.addView(mImageView, layoutParams);
         //設置監聽
         mImageView.setOnTouchListener(touchListener);
     }
 
     @Override
     public void onDestroy() {
         super .onDestroy();
         if  (mImageView !=  null ) {
             //講WindowManager時說過,add,remove成對出現,所以需要remove
             mWindowManager.removeView(mImageView);
         }
     }
 
     private View.OnTouchListener touchListener =  new  View.OnTouchListener() {
         @Override
         public boolean onTouch(View v, MotionEvent event) {
             //模擬觸摸觸發的事件
             Intent intent =  new  Intent(Intent.ACTION_VIEW);
             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
             startActivity(intent);
             return  false ;
         }
     };
}

如下是運行過程模擬,特別留意屏幕右下角的變化:

20150607153556977.gif

怎么樣,通過最后這個例子你是不是就能體會到WindowManager.LayoutParams的Z-ORDER序列類型,值越大顯示的位置越在上面。

2-5  總結Activity的窗口添加機制

有了上面這么多分析和前幾篇的分析,我們對Activity的窗口加載再次深入分析總結如下:

20150608150129190.png

可以看見Context的WindowManager對每個APP來說是一個全局單例的,而Activity的WindowManager是每個 Activity都會新創建一個的(其實你從上面分析的兩個實例化WindowManagerImpl的構造函數參數傳遞就可以看出來,Activity 中Window的WindowManager成員在構造實例化時傳入給WindowManagerImpl中mParentWindow成員的是當前 Window對象,而ContextImpl的static塊中單例實例化WindowManagerImpl時傳入給 WindowManagerImpl中mParentWindow成員的是null值),所以上面模擬蘋果浮動小圖標使用了Application的 WindowManager而不是Activity的,原因就在於這里;使用Activity的WindowManager時當Activity結束時 WindowManager就無效了,所以使用Activity的getSysytemService(WINDOW_SERVICE)獲取的是 Local的WindowManager。同時可以看出來Activity中的WindowManager.LayoutParams的type為 TYPE_APPLICATION。

好了,上面也說了不少了,有了上面這些知識點以后我們就來開始分析Android應用Activity、Dialog、PopWindow窗口顯示機制。

3  Android應用Dialog窗口添加顯示機制源碼

3-1  Dialog窗口源碼分析

寫過APP都知道,Dialog是一系列XXXDialog的基類,我們可以new任意Dialog或者通過Activity提供的 onCreateDialog(……)、onPrepareDialog(……)和showDialog(……)等方法來管理我們的Dialog,但是究 其實質都是來源於Dialog基類,所以我們對於各種XXXDialog來說只用分析Dialog的窗口加載就可以了。

如下從Dialog的構造函數開始分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Dialog implements DialogInterface, Window.Callback,
         KeyEvent.Callback, OnCreateContextMenuListener, Window.OnWindowDismissedCallback {
     ......
     public Dialog(Context context) {
         this (context, 0,  true );
     }
     //構造函數最終都調運了這個默認的構造函數
     Dialog(Context context, int theme, boolean createContextThemeWrapper) {
         //默認構造函數的createContextThemeWrapper為true
         if  (createContextThemeWrapper) {
             //默認構造函數的theme為0
             if  (theme == 0) {
                 TypedValue outValue =  new  TypedValue();
                 context.getTheme().resolveAttribute(com.android.internal.R.attr.dialogTheme,
                         outValue,  true );
                 theme = outValue.resourceId;
             }
             mContext =  new  ContextThemeWrapper(context, theme);
         else  {
             mContext = context;
         }
         //mContext已經從外部傳入的context對象獲得值(一般是個Activity)!!!非常重要,先記住!!!
 
         //獲取WindowManager對象
         mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
         //為Dialog創建新的Window
         Window w = PolicyManager.makeNewWindow(mContext);
         mWindow = w;
         //Dialog能夠接受到按鍵事件的原因
         w.setCallback( this );
         w.setOnWindowDismissedCallback( this );
         //關聯WindowManager與新Window,特別注意第二個參數token為null,也就是說Dialog沒有自己的token
         //一個Window屬於Dialog的話,那么該Window的mAppToken對象是null
         w.setWindowManager(mWindowManager,  null null );
         w.setGravity(Gravity.CENTER);
         mListenersHandler =  new  ListenersHandler( this );
     }
     ......
}

可以看到,Dialog構造函數首先把外部傳入的參數context對象賦值給了當前類的成員(我們的Dialog一般都是在Activity中啟動的, 所以這個context一般是個Activity),然后調用 context.getSystemService(Context.WINDOW_SERVICE)獲取WindowManager,這個 WindowManager是哪來的呢?先按照上面說的context一般是個Activity來看待,可以發現這句實質就是Activity的 getSystemService方法,我們看下源碼,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
     @Override
     public Object getSystemService(@ServiceName @NonNull String name) {
         if  (getBaseContext() ==  null ) {
             throw  new  IllegalStateException(
                     "System services not available to Activities before onCreate()" );
         }
         //我們Dialog中獲得的WindowManager對象就是這個分支
         if  (WINDOW_SERVICE.equals(name)) {
             //Activity的WindowManager
             return  mWindowManager;
         else  if  (SEARCH_SERVICE.equals(name)) {
             ensureSearchManager();
             return  mSearchManager;
         }
         return  super .getSystemService(name);
     }

看見沒有,Dialog中的WindowManager成員實質和Activity里面是一樣的,也就是共用了一個WindowManager。

回到Dialog的構造函數繼續分析,在得到了WindowManager之后,程序又新建了一個Window對象(類型是PhoneWindow 類型,和Activity的Window新建過程類似);接着通過w.setCallback(this)設置Dialog為當前window的回調接 口,這樣Dialog就能夠接收事件處理了;接着把從Activity拿到的WindowManager對象關聯到新創建的Window中。

至此Dialog的創建過程Window處理已經完畢,很簡單,所以接下來我們繼續看看Dialog的show與cancel方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
     public void show() {
         ......
         if  (!mCreated) {
             //回調Dialog的onCreate方法
             dispatchOnCreate( null );
         }
         //回調Dialog的onStart方法
         onStart();
         //類似於Activity,獲取當前新Window的DecorView對象,所以有一種自定義Dialog布局的方式就是重寫Dialog的onCreate方法,使用setContentView傳入布局,就像前面文章分析Activity類似
         mDecor = mWindow.getDecorView();
         ......
         //獲取新Window的WindowManager.LayoutParams參數,和上面分析的Activity一樣type為TYPE_APPLICATION
         WindowManager.LayoutParams l = mWindow.getAttributes();
         ......
         try  {
             //把一個View添加到Activity共用的windowManager里面去
             mWindowManager.addView(mDecor, l);
             ......
         } finally {
         }
     }

可以看見Dialog的新Window與Activity的Window的type同樣都為TYPE_APPLICATION,上面介紹 WindowManager.LayoutParams時TYPE_APPLICATION的注釋明確說過,普通應用程序窗口 TYPE_APPLICATION的token必須設置為Activity的token來指定窗口屬於誰。所以可以看見,既然Dialog和 Activity共享同一個WindowManager(也就是上面分析的WindowManagerImpl),而WindowManagerImpl 里面有個Window類型的mParentWindow變量,這個變量在Activity的attach中創建WindowManagerImpl時傳入 的為當前Activity的Window,而當前Activity的Window里面的mAppToken值又為當前Activity的token,所以 Activity與Dialog共享了同一個mAppToken值,只是Dialog和Activity的Window對象不同。

3-2  Dialog窗口加載總結

通過上面分析Dialog的窗口加載原理,我們總結如下圖:

20150608151434814.png

從圖中可以看出,Activity和Dialog共用了一個Token對象,Dialog必須依賴於Activity而顯示(通過別的 context搞完之后token都為null,最終會在ViewRootImpl的setView方法中加載時因為token為null拋出異常),所 以Dialog的Context傳入參數一般是一個存在的Activity,如果Dialog彈出來之前Activity已經被銷毀了,則這個 Dialog在彈出的時候就會拋出異常,因為token不可用了。在Dialog的構造函數中我們關聯了新Window的callback事件監聽處理, 所以當Dialog顯示時Activity無法消費當前的事件。

到此Dialog的窗口加載機制就分析完畢了,接下來我們說說應用開發中常見的一個詭異問題。

3-3  從Dialog窗口加載分析引出的應用開發問題

有了上面的分析我們接下來看下平時開發App初學者容易犯的幾個錯誤。

實現在一個Activity中顯示一個Dialog,如下代碼:

1
2
3
4
5
6
7
8
9
10
11
public class MainActivity extends AppCompatActivity {
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         setContentView(R.layout.activity_main);
 
         //重點關注構造函數的參數,創建一個Dialog然后顯示出來
         Dialog dialog =  new  ProgressDialog( this );
         dialog.setTitle( "TestDialogContext" );
         dialog.show();
     }
}

分析:使用了Activity為context,也即和Activity共用token,符合上面的分析,所以不會報錯,正常執行。

實現在一個Activity中顯示一個Dialog,如下代碼:

1
2
3
4
5
6
7
8
9
10
11
public class MainActivity extends AppCompatActivity {
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         setContentView(R.layout.activity_main);
 
         //重點關注構造函數的參數,創建一個Dialog然后顯示出來
         Dialog dialog =  new  ProgressDialog(getApplicationContext());
         dialog.setTitle( "TestDialogContext" );
         dialog.show();
     }
}

分析:傳入的是Application的Context,導致TYPE_APPLICATION類型Dialog的token為null,所以拋出如下異常,無法顯示對話框。

1
2
3
4
5
Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token  null  is not  for  an application
             at android.view.ViewRootImpl.setView(ViewRootImpl.java:566)
             at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:272)
             at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)
             at android.app.Dialog.show(Dialog.java:298)

實現在一個Service中顯示一個Dialog,如下代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class WindowService extends Service {
     @Override
     public IBinder onBind(Intent intent) {
         return  null ;
     }
 
     @Override
     public void onCreate() {
         super .onCreate();
         //重點關注構造函數的參數
         Dialog dialog =  new  ProgressDialog( this );
         dialog.setTitle( "TestDialogContext" );
         dialog.show();
     }
}

分析:傳入的Context是一個Service,類似上面傳入ApplicationContext一樣的后果,一樣的原因,拋出如下異常:

1
2
3
4
5
Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token  null  is not  for  an application
             at android.view.ViewRootImpl.setView(ViewRootImpl.java:566)
             at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:272)
             at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)
             at android.app.Dialog.show(Dialog.java:298)

至此通過我們平時使用最多的Dialog也驗證了Dialog成功顯示的必要條件,同時也讓大家避免了再次使用Dialog不當出現異常的情況,或者出現類似異常后知道真實的背后原因是什么的問題。

可以看見,Dialog的實質無非也是使用WindowManager的addView、updateViewLayout、removeView進行一些操作展示。

4  Android應用PopWindow窗口添加顯示機制源碼

PopWindow實質就是彈出式菜單,它與Dialag不同的地方是不會使依賴的Activity組件失去焦點(PopupWindow彈出后可 以繼續與依賴的Activity進行交互),Dialog卻不能這樣。同時PopupWindow與Dialog另一個不同點是PopupWindow是 一個阻塞的對話框,如果你直接在Activity的onCreate等方法中顯示它則會報錯,所以PopupWindow必須在某個事件中顯示地或者是開 啟一個新線程去調用。

說這么多還是直接看代碼吧。

4-1  PopWindow窗口源碼分析

依據PopWindow的使用,我們選擇最常用的方式來分析,如下先看其中常用的一種構造函數:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class PopupWindow {
     ......
     //我們只分析最常用的一種構造函數
     public PopupWindow(View contentView, int width, int height, boolean focusable) {
         if  (contentView !=  null ) {
             //獲取mContext,contentView實質是View,View的mContext都是構造函數傳入的,View又層級傳遞,所以最終這個mContext實質是Activity!!!很重要
             mContext = contentView.getContext();
             //獲取Activity的getSystemService的WindowManager
             mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
         }
         //進行一些Window類的成員變量初始化賦值操作
         setContentView(contentView);
         setWidth(width);
         setHeight(height);
         setFocusable(focusable);
     }
     ......
}

可以看見,構造函數只是初始化了一些變量,看完構造函數繼續看下PopWindow的展示函數,如下:

1
2
3
4
5
6
7
8
9
10
11
     public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
         ......
         //anchor是Activity中PopWindow准備依附的View,這個View的token實質也是Activity的Window中的token,也即Activity的token
         //第一步   初始化WindowManager.LayoutParams
         WindowManager.LayoutParams p = createPopupLayout(anchor.getWindowToken());
         //第二步
         preparePopup(p);
         ......
         //第三步
         invokePopup(p);
     }

可以看見,當我們想將PopWindow展示在anchor的下方向(Z軸是在anchor的上面)旁邊時經理了上面三步,我們一步一步來分析,先看第一步,源碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
     private WindowManager.LayoutParams createPopupLayout(IBinder token) {
         //實例化一個默認的WindowManager.LayoutParams,其中type=TYPE_APPLICATION
         WindowManager.LayoutParams p =  new  WindowManager.LayoutParams();
         //設置Gravity
         p.gravity = Gravity.START | Gravity.TOP;
         //設置寬高
         p.width = mLastWidth = mWidth;
         p.height = mLastHeight = mHeight;
         //依據背景設置format
         if  (mBackground !=  null ) {
             p.format = mBackground.getOpacity();
         else  {
             p.format = PixelFormat.TRANSLUCENT;
         }
         //設置flags
         p.flags = computeFlags(p.flags);
         //修改type=WindowManager.LayoutParams.TYPE_APPLICATION_PANEL,mWindowLayoutType有初始值,type類型為子窗口
         p.type = mWindowLayoutType;
         //設置token為Activity的token
         p.token = token;
         ......
         return  p;
     }

接着回到showAsDropDown方法看看第二步,如下源碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
     private void preparePopup(WindowManager.LayoutParams p) {
         ......
         //有無設置PopWindow的background區別
         if  (mBackground !=  null ) {
             ......
             //如果有背景則創建一個PopupViewContainer對象的ViewGroup
             PopupViewContainer popupViewContainer =  new  PopupViewContainer(mContext);
             PopupViewContainer.LayoutParams listParams =  new  PopupViewContainer.LayoutParams(
                     ViewGroup.LayoutParams.MATCH_PARENT, height
             );
             //把背景設置給PopupViewContainer的ViewGroup
             popupViewContainer.setBackground(mBackground);
             //把我們構造函數傳入的View添加到這個ViewGroup
             popupViewContainer.addView(mContentView, listParams);
             //返回這個ViewGroup
             mPopupView = popupViewContainer;
         else  {
             //如果沒有通過PopWindow的setBackgroundDrawable設置背景則直接賦值當前傳入的View為PopWindow的View
             mPopupView = mContentView;
         }
         ......
     }

可以看見preparePopup方法的作用就是判斷設置View,如果有背景則會在傳入的contentView外面包一層 PopupViewContainer(實質是一個重寫了事件處理的FrameLayout)之后作為mPopupView,如果沒有背景則直接用 contentView作為mPopupView。我們再來看下這里的PopupViewContainer類,如下源碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
     private class PopupViewContainer extends FrameLayout {
         ......
         @Override
         protected int[] onCreateDrawableState(int extraSpace) {
             ......
         }
 
         @Override
         public boolean dispatchKeyEvent(KeyEvent event) {
             ......
         }
 
         @Override
         public boolean dispatchTouchEvent(MotionEvent ev) {
             if  (mTouchInterceptor !=  null  && mTouchInterceptor.onTouch( this , ev)) {
                 return  true ;
             }
             return  super .dispatchTouchEvent(ev);
         }
 
         @Override
         public boolean onTouchEvent(MotionEvent event) {
             ......
             if (xxx) {
                 dismiss();
             }
             ......
         }
 
         @Override
         public void sendAccessibilityEvent(int eventType) {
             ......
         }
     }

可以看見,這個PopupViewContainer是一個PopWindow的內部私有類,它繼承了FrameLayout,在其中重寫了Key 和Touch事件的分發處理邏輯。同時查閱PopupView可以發現,PopupView類自身沒有重寫Key和Touch事件的處理,所以如果沒有將 傳入的View對象放入封裝的ViewGroup中,則點擊Back鍵或者PopWindow以外的區域PopWindow是不會消失的(其實PopWindow中沒有向Activity及Dialog一樣new新的Window,所以不會有新的callback設置,也就沒法處理事件消費了)。

接着繼續回到showAsDropDown方法看看第三步,如下源碼:

1
2
3
4
5
6
7
8
     private void invokePopup(WindowManager.LayoutParams p) {
         if  (mContext !=  null ) {
             p.packageName = mContext.getPackageName();
         }
         mPopupView.setFitsSystemWindows(mLayoutInsetDecor);
         setLayoutDirectionFromAnchor();
         mWindowManager.addView(mPopupView, p);
     }

可以看見,這里使用了Activity的WindowManager將我們的PopWindow進行了顯示。

到此可以發現,PopWindow的實質無非也是使用WindowManager的addView、updateViewLayout、 removeView進行一些操作展示。與Dialog不同的地方是沒有新new Window而已(也就沒法設置callback,無法消費事件,也就是前面說的PopupWindow彈出后可以繼續與依賴的Activity進行交互 的原因)。

到此PopWindw的窗口加載顯示機制就分析完畢了,接下來進行總結與應用開發技巧提示。

4-2  PopWindow窗口源碼分析總結及應用開發技巧提示

通過上面分析可以發現總結如下圖:

20150608201409861.png

可以看見,PopWindow完全使用了Activity的Window與WindowManager,相對來說比較簡單容易記理解。

再來看一個開發技巧:

如果設置了PopupWindow的background,則點擊Back鍵或者點擊PopupWindow以外的區域時PopupWindow就 會dismiss;如果不設置PopupWindow的background,則點擊Back鍵或者點擊PopupWindow以外的區域 PopupWindow不會消失。

5  Android應用Toast窗口添加顯示機制源碼

5-1 基礎知識准備

在開始分析這幾個窗口之前需要腦補一點東東,我們從應用層開發來直觀腦補,這樣下面分析源碼時就不蛋疼了。如下是一個我們寫的兩個應用實現 Service跨進程調用服務ADIL的例子,客戶端調運遠程Service的start與stop方法控制遠程Service的操作。

Android系統中的應用程序都運行在各自的進程中,進程之間是無法直接交換數據的,但是Android為開發者提供了AIDL跨進程調用Service的功能。其實AIDL就相當於雙方約定的一個規則而已。

先看下在Android Studio中AIDL開發的工程目錄結構,如下:

20150609100304948.png

由於AIDL文件中不能出現訪問修飾符(如public),同時AIDL文件在兩個項目中要完全一致而且只支持基本類型,所以我們定義的AIDL文件如下:

ITestService.aidl

1
2
3
4
5
6
package io.github.yanbober.myapplication;
 
interface ITestService {
     void start(int id);
     void stop(int id);
}

再來看下依據aidl文件自動生成的ITestService.java文件吧,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/*
  * This file is auto-generated.  DO NOT MODIFY.
  */
package io.github.yanbober.myapplication;
public interface ITestService extends android.os.IInterface
{
     //Stub類是ITestService接口的內部靜態抽象類,該類繼承了Binder類
     public static abstract class Stub extends android.os.Binder implements io.github.yanbober.myapplication.ITestService
     {
         ......
         //這是抽象靜態Stub類中的asInterface方法,該方法負責將service返回至client的對象轉換為ITestService.Stub
         //把遠程Service的Binder對象傳遞進去,得到的是遠程服務的本地代理
         public static io.github.yanbober.myapplication.ITestService asInterface(android.os.IBinder obj)
         {
             ......
         }
         ......
         //遠程服務的本地代理,也會繼承自ITestService
         private static class Proxy implements io.github.yanbober.myapplication.ITestService
         {
             ......
             @Override
             public void start(int id) throws android.os.RemoteException
             {
                 ......
             }
 
             @Override
             public void stop(int id) throws android.os.RemoteException
             {
                 ......
             }
         }
         ......
     }
     //兩個方法是aidl文件中定義的方法
     public void start(int id) throws android.os.RemoteException;
     public void stop(int id) throws android.os.RemoteException;
}

這就是自動生成的java文件,接下來我們看看服務端的Service源碼,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//記得在AndroidManifet.xml中注冊Service的<action android:name="io.github.yanbober.myapplication.aidl" />
 
public class TestService extends Service {
     private TestBinder mTestBinder;
 
     //該類繼承ITestService.Stub類而不是Binder類,因為ITestService.Stub是Binder的子類
     //進程內的Service定義TestBinder內部類是繼承Binder類
     public class TestBinder extends ITestService.Stub {
 
         @Override
         public void start(int id) throws RemoteException {
             Log.i( null "Server Service is start!" );
         }
 
         @Override
         public void stop(int id) throws RemoteException {
             Log.i( null "Server Service is stop!" );
         }
     }
 
     @Override
     public IBinder onBind(Intent intent) {
         //返回Binder
         return  mTestBinder;
     }
 
     @Override
     public void onCreate() {
         super .onCreate();
         //實例化Binder
         mTestBinder =  new  TestBinder();
     }
}

現在服務端App的代碼已經OK,我們來看下客戶端的代碼。客戶端首先也要像上面的工程結構一樣,把AIDL文件放好,接着在客戶端使用遠程服務端的Service代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class MainActivity extends Activity {
     private static final String REMOT_SERVICE_ACTION =  "io.github.yanbober.myapplication.aidl" ;
 
     private Button mStart, mStop;
 
     private ITestService mBinder;
 
     private ServiceConnection connection =  new  ServiceConnection() {
         @Override
         public void onServiceConnected(ComponentName name, IBinder service) {
             //獲得另一個進程中的Service傳遞過來的IBinder對象
             //用IMyService.Stub.asInterface方法轉換該對象
             mBinder = ITestService.Stub.asInterface(service);
         }
 
         @Override
         public void onServiceDisconnected(ComponentName name) {
         }
     };
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super .onCreate(savedInstanceState);
         setContentView(R.layout.activity_main);
 
         mStart = (Button)  this .findViewById(R.id.start);
         mStop = (Button)  this .findViewById(R.id.stop);
 
         mStart.setOnClickListener(clickListener);
         mStop.setOnClickListener(clickListener);
         //綁定遠程跨進程Service
         bindService( new  Intent(REMOT_SERVICE_ACTION), connection, BIND_AUTO_CREATE);
     }
 
     @Override
     protected void onDestroy() {
         super .onDestroy();
         //取消綁定遠程跨進程Service
         unbindService(connection);
     }
 
     private View.OnClickListener clickListener =  new  View.OnClickListener() {
         @Override
         public void onClick(View v) {
             ////調用遠程Service中的start與stop方法
             switch  (v.getId()) {
                 case  R.id.start:
                     try  {
                         mBinder.start(0x110);
                     catch  (RemoteException e) {
                         e.printStackTrace();
                     }
                     break ;
                 case  R.id.stop:
                     try  {
                         mBinder.stop(0x120);
                     catch  (RemoteException e) {
                         e.printStackTrace();
                     }
                     break ;
             }
         }
     };
}

到此你對應用層通過AIDL使用遠程Service的形式已經很熟悉了,至於實質的通信使用Binder的機制我們后面會寫文章一步一步往下分析。到此的准備知識已經足夠用來理解下面我們的源碼分析了。

5-2 Toast窗口源碼分析

我們常用的Toast窗口其實和前面分析的Activity、Dialog、PopWindow都是不同的,因為它和輸入法、牆紙類似,都是系統窗口。

我們還是按照最常用的方式來分析源碼吧。

我們先看下Toast的靜態makeText方法吧,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
     public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
         //new一個Toast對象
         Toast result =  new  Toast(context);
         //獲取前面有篇文章分析的LayoutInflater
         LayoutInflater inflate = (LayoutInflater)
                 context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
         //加載解析Toast的布局,實質transient_notification.xml是一個LinearLayout中套了一個@android:id/message的TextView而已
         View v = inflate.inflate(com.android.internal.R.layout.transient_notification,  null );
         //取出布局中的TextView
         TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
         //把我們的文字設置到TextView上
         tv.setText(text);
         //設置一些屬性
         result.mNextView = v;
         result.mDuration = duration;
         //返回新建的Toast
         return  result;
     }

可以看見,這個方法構造了一個Toast,然后把要顯示的文本放到這個View的TextView中,然后初始化相關屬性后返回這個新的Toast對象。

當我們有了這個Toast對象之后,

可以通過show方法來顯示出來,如下看下show方法源碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
     public void show() {
         ......
         //通過AIDL(Binder)通信拿到NotificationManagerService的服務訪問接口,當前Toast類相當於上面例子的客戶端!!!相當重要!!!
         INotificationManager service = getService();
         String pkg = mContext.getOpPackageName();
         TN tn = mTN;
         tn.mNextView = mNextView;
 
         try  {
             //把TN對象和一些參數傳遞到遠程NotificationManagerService中去
             service.enqueueToast(pkg, tn, mDuration);
         catch  (RemoteException e) {
             // Empty
         }
     }

我們看看show方法中調運的getService方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
     //遠程NotificationManagerService的服務訪問接口
     private static INotificationManager sService;
 
     static private INotificationManager getService() {
         //單例模式
         if  (sService !=  null ) {
             return  sService;
         }
         //通過AIDL(Binder)通信拿到NotificationManagerService的服務訪問接口
         sService = INotificationManager.Stub.asInterface(ServiceManager.getService( "notification" ));
         return  sService;
     }

通過上面我們的基礎腦補實例你也能看懂這個getService方法了吧。那接着我們來看mTN吧,好像mTN在Toast的構造函數里見過一眼,我們來看看,如下:

1
2
3
4
5
6
7
8
     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);
     }

可以看見mTN確實是在構造函數中實例化的,那我們就來看看這個TN類,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
     //類似於上面例子的服務端實例化的Service內部類Binder
     private static class TN extends ITransientNotification.Stub {
         ......
         //實現了AIDL的show與hide方法
         @Override
         public void show() {
             if  (localLOGV) Log.v(TAG,  "SHOW: "  this );
             mHandler.post(mShow);
         }
 
         @Override
         public void hide() {
             if  (localLOGV) Log.v(TAG,  "HIDE: "  this );
             mHandler.post(mHide);
         }
         ......
     }

看見沒有,TN是Toast內部的一個私有靜態類,繼承自ITransientNotification.Stub。你這時指定好奇 ITransientNotification.Stub是個啥玩意,對吧?其實你在上面的腦補實例中見過它的,他出現在服務端實現的Service中, 就是一個Binder對象,也就是對一個aidl文件的實現而已,我們看下這個ITransientNotification.aidl文件,如下:

1
2
3
4
5
6
7
package android.app;
 
/** @hide */
oneway interface ITransientNotification {
     void show();
     void hide();
}

看見沒有,和我們上面的例子很類似吧。

再回到上面分析的show()方法中可以看到,我們的Toast是傳給遠程的NotificationManagerService管理的,為了 NotificationManagerService回到我們的應用程序(回調),我們需要告訴NotificationManagerService 我們當前程序的Binder引用是什么(也就是TN)。是不是覺得和上面例子有些不同,這里感覺Toast又充當客戶端,又充當服務端的樣子,實質就是一 個回調過程而已。

繼續來看Toast中的show方法的service.enqueueToast(pkg, tn, mDuration);語句,service實質是遠程的NotificationManagerService,所以enqueueToast方法就是 NotificationManagerService類的,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
     private final IBinder mService =  new  INotificationManager.Stub() {
         // Toasts
         // ============================================================================
 
         @Override
         public void enqueueToast(String pkg, ITransientNotification callback, int duration)
         {
             ......
             synchronized (mToastQueue) {
                 int callingPid = Binder.getCallingPid();
                 long callingId = Binder.clearCallingIdentity();
                 try  {
                     ToastRecord record;
                     //查看該Toast是否已經在隊列當中
                     int index = indexOfToastLocked(pkg, callback);
                     // If it's already in the queue, we update it in place, we don't
                     // move it to the end of the queue.
                     //注釋說了,已經存在則直接取出update
                     if  (index >= 0) {
                         record = mToastQueue.get(index);
                         record.update(duration);
                     else  {
                         // Limit the number of toasts that any given package except the android
                         // package can enqueue.  Prevents DOS attacks and deals with leaks.
                         ......
                         //將Toast封裝成ToastRecord對象,放入mToastQueue中
                         record =  new  ToastRecord(callingPid, pkg, callback, duration);
                         //把他添加到ToastQueue隊列中
                         mToastQueue.add(record);
                         index = mToastQueue.size() - 1;
                         //將當前Toast所在的進程設置為前台進程
                         keepProcessAliveLocked(callingPid);
                     }
                     //如果index為0,說明當前入隊的Toast在隊頭,需要調用showNextToastLocked方法直接顯示
                     if  (index == 0) {
                         showNextToastLocked();
                     }
                 } finally {
                     Binder.restoreCallingIdentity(callingId);
                 }
             }
         }
    }

繼續看下該方法中調運的showNextToastLocked方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
     void showNextToastLocked() {
         //取出ToastQueue中隊列最前面的ToastRecord
         ToastRecord record = mToastQueue.get(0);
         while  (record !=  null ) {
             try  {
                 //Toast類中實現的ITransientNotification.Stub的Binder接口TN,調運了那個類的show方法
                 record.callback.show();
                 scheduleTimeoutLocked(record);
                 return ;
             catch  (RemoteException e) {
                 ......
             }
         }
     }

繼續先看下該方法中調運的scheduleTimeoutLocked方法,如下:

1
2
3
4
5
6
7
8
9
10
     private void scheduleTimeoutLocked(ToastRecord r)
     {
         //移除上一條消息
         mHandler.removeCallbacksAndMessages(r);
         //依據Toast傳入的duration參數LENGTH_LONG=1來判斷決定多久發送消息
         Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
         long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
         //依據設置的MESSAGE_TIMEOUT后發送消息
         mHandler.sendMessageDelayed(m, delay);
     }

可以看見這里先回調了Toast的TN的show,下面timeout可能就是hide了。接着還在該類的mHandler處理了這條消息,然后調運了如下處理方法:

1
2
3
4
5
6
7
8
9
10
     private void handleTimeout(ToastRecord record)
     {
         ......
         synchronized (mToastQueue) {
             int index = indexOfToastLocked(record.pkg, record.callback);
             if  (index >= 0) {
                 cancelToastLocked(index);
             }
         }
     }

我們繼續看cancelToastLocked方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
     void cancelToastLocked(int index) {
         ToastRecord record = mToastQueue.get(index);
         try  {
             //回調Toast的TN中實現的hide方法
             record.callback.hide();
         catch  (RemoteException e) {
             ......
         }
         //從隊列移除當前顯示的Toast
         mToastQueue.remove(index);
         keepProcessAliveLocked(record.pid);
         if  (mToastQueue.size() > 0) {
             //如果當前的Toast顯示完畢隊列里還有其他的Toast則顯示其他的Toast
             showNextToastLocked();
         }
     }

到此可以發現,Toast的遠程管理NotificationManagerService類的處理實質是通過Handler發送延時消息顯示取消 Toast的,而且在遠程NotificationManagerService類中又遠程回調了Toast的TN類實現的show與hide方法。

現在我們就回到Toast的TN類再看看這個show與hide方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
```java
     private static class TN extends ITransientNotification.Stub {
         ......
         //僅僅是實例化了一個Handler,非常重要!!!!!!!!
         final Handler mHandler =  new  Handler(); 
         ......
         final Runnable mShow =  new  Runnable() {
             @Override
             public void run() {
                 handleShow();
             }
         };
 
         final Runnable mHide =  new  Runnable() {
             @Override
             public void run() {
                 handleHide();
                 // Don't do this in handleHide() because it is also invoked by handleShow()
                 mNextView =  null ;
             }
         };
         ......
         //實現了AIDL的show與hide方法
         @Override
         public void show() {
             if  (localLOGV) Log.v(TAG,  "SHOW: "  this );
             mHandler.post(mShow);
         }
 
         @Override
         public void hide() {
             if  (localLOGV) Log.v(TAG,  "HIDE: "  this );
             mHandler.post(mHide);
         }
         ......
     }

可以看見,這里實現aidl接口的方法實質是通過handler的post來執行的一個方法,而這個Handler僅僅只是new了一下,也就是 說,如果我們寫APP時使用Toast在子線程中則需要自行准備Looper對象,只有主線程Activity創建時幫忙准備了Looper(關於 Handler與Looper如果整不明白請閱讀《Android異步消息處理機制詳解及源碼分析》)。

那我們重點關注一下handleShow與handleHide方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
         public void handleShow() {
             if  (localLOGV) Log.v(TAG,  "HANDLE SHOW: "  this  " mView="  + mView
                     " mNextView="  + mNextView);
             if  (mView != mNextView) {
                 // remove the old view if necessary
                 //如果有必要就通過WindowManager的remove刪掉舊的
                 handleHide();
                 mView = mNextView;
                 Context context = mView.getContext().getApplicationContext();
                 String packageName = mView.getContext().getOpPackageName();
                 if  (context ==  null ) {
                     context = mView.getContext();
                 }
                 //通過得到的context(一般是ContextImpl的context)獲取WindowManager對象(上一篇文章分析的單例的WindowManager)
                 mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                 ......
                 //在把Toast的View添加之前發現Toast的View已經被添加過(有partent)則刪掉
                 if  (mView.getParent() !=  null ) {
                     ......
                     mWM.removeView(mView);
                 }
                 ......
                 //把Toast的View添加到窗口,其中mParams.type在構造函數中賦值為TYPE_TOAST!!!!!!特別重要
                 mWM.addView(mView, mParams);
                 ......
             }
         }
1
2
3
4
5
6
7
8
9
10
11
12
         public void handleHide() {
             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.
                 //注釋說得很清楚了,不解釋,就是remove
                 if  (mView.getParent() !=  null ) {
                     mWM.removeView(mView);
                 }
                 mView =  null ;
             }
         }

到此Toast的窗口添加原理就分析完畢了,接下來我們進行總結。

5-3 Toast窗口源碼分析總結及應用開發技巧

經過上面的分析我們總結如下:

20150609150625943.png

通過上面分析及上圖直觀描述可以發現,之所以Toast的顯示交由遠程的NotificationManagerService管理是因為 Toast是每個應用程序都會彈出的,而且位置和UI風格都差不多,所以如果我們不統一管理就會出現覆蓋疊加現象,同時導致不好控制,所以Google把 Toast設計成為了系統級的窗口類型,由NotificationManagerService統一隊列管理。

在我們開發應用程序時使用Toast注意事項:

  1. 通過分析TN類的handler可以發現,如果想在非UI線程使用Toast需要自行聲明Looper,否則運行會拋出Looper相關的異常;UI線程不需要,因為系統已經幫忙聲明。

  2. 在使用Toast時context參數盡量使用getApplicationContext(),可以有效的防止靜態引用導致的內存泄漏。

  3. 有時候我們會發現Toast彈出過多就會延遲顯示,因為上面源碼分析可以看見Toast.makeText是一個靜態工廠方法,每次調用這 個方法都會產生一個新的Toast對象,當我們在這個新new的對象上調用show方法就會使這個對象加入到 NotificationManagerService管理的mToastQueue消息顯示隊列里排隊等候顯示;所以如果我們不每次都產生一個新的 Toast對象(使用單例來處理)就不需要排隊,也就能及時更新了。

6  Android應用Activity、Dialog、PopWindow、Toast窗口顯示機制總結

可以看見上面無論Acitivty、Dialog、PopWindow、Toast的實質其實都是如下接口提供的方法操作:

1
2
3
4
5
6
public interface ViewManager
{
     public void addView(View view, ViewGroup.LayoutParams params);
     public void updateViewLayout(View view, ViewGroup.LayoutParams params);
     public void removeView(View view);
}

整個應用各種窗口的顯示都離不開這三個方法而已,只是token及type與Window是否共用的問題。

 

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0615/3044.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM