Android 42 由Context引发的思考.docx
《Android 42 由Context引发的思考.docx》由会员分享,可在线阅读,更多相关《Android 42 由Context引发的思考.docx(19页珍藏版)》请在冰豆网上搜索。
Android42由Context引发的思考
Android4.2由Context引发的思考
目录(?
)[-]
1问题描述
2复现步骤
3期望结果
4实际结果
5问题分析
6Dialog显示流程图
7重要方法
71addWindowToListInOrderLocked
72updateFocusedWindowLocked
8解决方案
9总结
10知识点
11注
最近在做类似于三星S4的那种皮套(后面简称SmartCover),具有可操作的窗口,一方面用户可以保护手机屏幕,另一方面用户可以直接在SmartCover上接听电话,非常方便。
在开发过程中发现一个问题,虽然最终解决但还是记录一下,好记性不如烂笔头啊。
转载请务必注明出处:
问题描述:
SmartCover滑动控件失效
复现步骤:
1.当前正在通话,长按Power键弹出关机对话框
2.关闭SmartCover
3.滑动挂断按钮,挂断当前通话
期望结果:
正常滑动挂断按钮并能挂断当前通话
实际结果:
无法滑动挂断按钮
问题分析:
在关机对话框(GlobalActions)弹出之后,SmartCover就无法接收点击事件了,同时打开SmartCover后会发现之前的点击事件传到了关机对话框上。
SmartCover的显示是基于Dialog的,是一个自定Dialog。
虽然能够正常显示,却没法获取点击事件,那这就得从Android的事件派发来分析问题了!
我们知道在Android中事件的派发可以分为两类:
按键事件和触摸事件
无论是哪种事件,这些消息首先都会被硬件设备所检测到,比如我们点击Home键,对硬件来说只会检测到Down/Up,这些硬件消息会被转换成统一的系统消息,并向上逐层传递。
接下来,Android的窗口管理系统(WmS)会根据窗口的状态,判断用户正在与哪个窗口进行交互,然后把消息派发给该窗口。
在Android系统中,所有的窗口都是有WmS创建的,因此WmS可以查出所有窗口的状态,包括窗口大小、位置、是否具有焦点等等。
当底层消息传递上来时,如果是按键消息,则直接派发给当前窗口;如果是触摸消息,则WmS会根据触摸消息判断用户触摸区域属于哪个窗口,并将消息传递给该窗口。
最后这些消息如何处理就是窗口自己的事儿了,也就是View的处理逻辑了,dispatchEvent,onTouchEvent等等。
当问题发生时,我们可以点击屏幕,但消息没有传递给SmartCover,反而传递给了关机对话框,因此问题的大致原因可能是WmS在派发消息时出现了问题。
既然问题可能出现在WmS中,自然需要查看WmS的Log,因为WmS的Log属于systemlog,而Android默认打印的log也就是adblogcat直接输出的log是mainlog,所以需要加-b指定buffer,同时我们需要过滤WindowManager这个TAG(WmS的LogTAG是WindowManager),所以在终端中输入:
adblogcat-bsystem-s"WindowManager"
然后按照复现步骤操作一遍并查看log。
当在通话界面长按Power键之后会出现以下Log:
[plain]viewplaincopy
12V/WindowManager(536):
ChangingfocusfromWindow{424ce3f0u0com.android.phone
13/com.android.phone.InCallScreen}toWindow{4232b120u0GlobalActions}
也就是说这时候的焦点从InCallScreen变到了GlobalActions上,GlobalActions就是关机对话框,这里没有问题。
但当我们关闭SmartCover时,WmS没有焦点变化的Log输出。
对比以没有关机对话框的Log:
[plain]viewplaincopy
14V/WindowManager(536):
ChangingfocusfromWindow{424ce3f0u0com.android.phone
15/com.android.phone.InCallScreen}toWindow{4232b120u0com.android.phone/com.and
16roid.phone.InCallScreen}
我们可以看到如果没有关机对话框,在通话界面关闭SmartCover时会有焦点改变,只是焦点还在InCallScreen上,毕竟SmartCover弹出的Dialog是基于InCallScreen的(Context是InCallScreen),因此焦点还是属于InCallScreen这个Activity。
仔细想一下,这个逻辑也是对的,比如我们的一个Activity要弹出一个Dialog,但当我们Pause它时,它虽然会照常弹出Dialog但此时的Dialog是不具有焦点的,也不能接收任何点击事件。
为什么SmartCover的Dialog能够正常显示呢?
那是因为SmartCover的WindowManager.LayoutParams对象type属性被我们自定义了,定义了一个比关机对话框还高的等级,因此SmartCover能够显示却不能获取焦点,从而无法接收WmS派发的点击事件。
也就是说SmartCover并没有添加到最顶层,系统认为当前最顶层的Window任然是GlobalActions(关机对话框)。
将WmS中Debug值DEBUG_FOCUS修改为true,查看焦点切换的Log信息,如下:
先接通电话,后弹出关机对话框
[plain]viewplaincopy
17V/WindowManager(536):
AddingwindowWindow{4211a128u0GlobalActions}at3of
184
19V/WindowManager(536):
Lookingforfocus:
4=Window{4242da68u0StatusBar},fl
20ags=25165896,canReceive=false
21V/WindowManager(536):
Lookingforfocus:
3=Window{4211a128u0GlobalActions}
22,flags=8519682,canReceive=true
我们看到先Add了一个Window(即GlobalActions关机对话框),然后寻找到焦点在GlobalActions上(canReceive=true),此时我们关闭SmartCover再次查看Log输出如下:
[java]viewplaincopy
23V/WindowManager(536):
AddingwindowWindow{42b22850u0com.android.phone/com.a
24ndroid.phone.InCallScreen}at3of5
25V/WindowManager(536):
Lookingforfocus:
5=Window{4242da68u0StatusBar},fl
26ags=25165896,canReceive=false
27V/WindowManager(536):
Lookingforfocus:
4=Window{423e1f78u0GlobalActions}
28,flags=8519682,canReceive=true
29V/WindowManager(536):
Foundfocus@4=Window{423e1f78u0GlobalActions}
可以看到关闭SmartCover后的确也弹出了显示界面,即这里的Addwindow(InCallScreen),只是在Lookingforfocus的时候还是找到的GlobalActions(关机对话框),即这里的4,而没有继续找到我们想要的3。
如果不弹出关机对话框则显示的Log如下:
[java]viewplaincopy
30V/WindowManager(536):
AddingwindowWindow{41f756d0u0com.android.phone/com.a
31ndroid.phone.InCallScreen}at3of4
32V/WindowManager(536):
Lookingforfocus:
4=Window{4242da68u0StatusBar},fl
33ags=25165896,canReceive=false
34V/WindowManager(536):
Lookingforfocus:
3=Window{41f756d0u0com.android.ph
35one/com.android.phone.InCallScreen},flags=23592960,canReceive=true
36V/WindowManager(536):
Foundfocus@3=Window{41f756d0u0com.android.phone/c
37om.android.phone.InCallScreen}
38V/WindowManager(536):
ChangingfocusfromWindow{424ce3f0u0com.android.phone
39/com.android.phone.InCallScreen}toWindow{41f756d0u0com.android.phone/com.and
40roid.phone.InCallScreen}
从这里我们可以看到,如果没有关机GlobalActions(关机对话框)则焦点的获取是正常的,关机对话实际上也是一个Dialog,那么为什么它能获取到焦点呢?
Dialog显示流程图:
这里大致画一下Dialog显示流程,如图1:
图1
最终可以看到Dialog是在WmS中进行的显示添加,看一下其中几个重要的方法。
重要方法:
(1).addWindowToListInOrderLocked
该方法会根据appWindowToken的值不同,从而添加到不同的layer。
[java]viewplaincopy
41privatevoidaddWindowToListInOrderLocked(WindowStatewin,booleanaddToToken){
42finalIWindowclient=win.mClient;
43finalWindowTokentoken=win.mToken;
44finalDisplayContentdisplayContent=win.mDisplayContent;
45
46finalWindowListwindows=win.getWindowList();
47finalintN=windows.size();
48finalWindowStateattached=win.mAttachedWindow;
49inti;
50WindowListtokenWindowList=getTokenWindowsOnDisplay(token,displayContent);
51if(attached==null){
52inttokenWindowsPos=0;
53intwindowListPos=tokenWindowList.size();
54if(token.appWindowToken!
=null){//如果appWindowToken不为null
55intindex=windowListPos-1;
56if(index>=0){
57//......省略
58}else{
59//......省略
60}else{
61intnewIdx=findIdxBasedOnAppTokens(win);
62//thereisawindowabovethisoneassociatedwiththesame
63//apptokennotethatthewindowcouldbeafloatingwindow
64//thatwascreatedlaterorawindowatthetopofthelistof
65//windowsassociatedwiththistoken.
66if(DEBUG_FOCUS||DEBUG_WINDOW_MOVEMENT||DEBUG_ADD_REMOVE){
67Slog.v(TAG,"Addingwindow"+win+"at"
68+(newIdx+1)+"of"+N);
69}
70windows.add(newIdx+1,win);
71if(newIdx<0){
72//Nowindowfromtokenfoundonwin'sdisplay.
73tokenWindowsPos=0;
74}else{
75tokenWindowsPos=indexOfWinInWindowList(
76windows.get(newIdx),token.windows)+1;
77}
78mWindowsChanged=true;
79}
80}
81//......省略
82}else{微软雅黑">//如果appWindowToken为null
83//Figureoutwherewindowshouldgo,basedonlayer.
84///M:
Ignorethewallpaperwindowwhenaddingwindow@{
85finalintmyLayer=win.mBaseLayer;
86WindowStatelocalWindow;
87for(i=N-1;i>=0;i--){
88localWindow=windows.get(i);
89if(localWindow.mBaseLayer<=myLayer
90&&localWindow.mAttrs.type!
=TYPE_WALLPAPER){
91///@}
92break;
93}
94}
95i++;
96if(DEBUG_FOCUS||DEBUG_WINDOW_MOVEMENT||DEBUG_ADD_REMOVE)Slog.v(
97TAG,"Addingwindow"+win+"at"
98+i+"of"+N);
99windows.add(i,win);
100mWindowsChanged=true;
101}
102
103//......省略
104}
105}
在该方法中,会根据appWindowToken值是否为null采用不同的添加window策略,如果appWindowToken!
=null则最终会执行:
[java]viewplaincopy
106intnewIdx=findIdxBasedOnAppTokens(win);
107if(DEBUG_FOCUS||DEBUG_WINDOW_MOVEMENT||DEBUG_ADD_REMOVE){
108Slog.v(TAG,"Addingwindow"+win+"at"
109+(newIdx+1)+"of"+N);
110}
111windows.add(newIdx+1,win);
文章前面的Log信息的确也是这样输出的“xxxat3of4”“xxxat3of5”,继续查看这里的findIdxBaseOnAppTokens方法:
[java]viewplaincopy
112privateintfindIdxBasedOnAppTokens(WindowStatewin){
113WindowListwindows=win.getWindowList();
114for(intj=windows.size()-1;j>=0;j--){
115WindowStatewentry=windows.get(j);
116if(wentry.mAppToken==win.mAppToken){
117returnj;
118}
119}
120return-1;
121}
该方法中会去获取所有的WindowList,然后用一个for循环找到每一个Window的mAppToken,与当前需要添加的窗口的mAppToken进行比较。
这简单的说明一下,Android添加Window的顺序有点类似于做奶油蛋糕,一层一层的添加,最顶层永远是当前具有焦点的窗口。
通过该for循环,可以找到当前系统中是否具有和需要添加的Window相同mAppToken的窗口,如果有则返回对应的层级给它。
有点绕,可以简单的这么说,当前系统的窗口我们可以用一个5层的奶油蛋糕来表示,顺序从底向上依次是:
红->白->绿->黄->蓝,现在我想给这个蛋糕加一个绿色的巧克力上去(这里颜色相当于系统中的mAppToken),我们看到(for循环查找)当前已经有一层绿色了,那么我们就把这个绿色的巧克力放到(Addwindow)绿色这一层。
如果appWindowToken==null呢,那执行以下代码:
[java]viewplaincopy
122finalintmyLayer=win.mBaseLayer;
123WindowStatelocalWindow;
124for(i=N-1;i>=0;i--){
125localWindow=windows.get(i);
126if(localWindow.mBaseLayer<=myLayer
127&&localWindow.mAttrs.type!
=TYPE_WALLPAPER){
128///@}
129break;
130}
131}
132i++;
133if(DEBUG_FOCUS||DEBUG_WINDOW_MOVEMENT||DEBUG_ADD_REMOVE)Slog.v(
134TAG,"Addingwindow"+win+"at"
135+i+"of"+N);
136windows.add(i,win);
这里的N实际为windows.size(),而windows就是WindowList对象,相当于还是得出当前的Window数量,然后判断最顶层Window的下一层Window是否是BaseLayer以及属性是否是TYPE_WALLPAPER,如果不是则break掉。
然后将需要增加的Window(需要显示的View)添加到最顶层。
还是用前面奶油蛋糕的例子吧,比如现在依旧是5层,从底向上依次是:
红->白->绿->黄->蓝,此时我们需要添加一个黑色的巧克力(这里颜色相当于系统中的mAppToken)在这个蛋糕上,那么我们先判断已有的每一层蛋糕,是否是蛋糕的托盘并具有图案(这里判断我们是否是想给蛋糕加上一个托盘,如果是托盘我们需要放在蛋糕的最下面),首先判断黄色这层然后依次向下。
如果都不满足则把我们需要添加的黑色巧克力放在最上面,即蓝色那层。
通过以上分析我们可以很清楚的知道,如果appWindowToken的值不同,则会将我们的Dialog添加到不同的Layer上,如果该Layer此时已经没有Focus了,那么我们先添加的Dialog也不会具有Focus,系统也不会执行Focuschange。
那么在什么情况下appWindowToken==null,什么情况下appWindowToken!
=null。
到这里终于可以请出本文的主角Context了......o(╯□╰)o......
Context在开发中经常被用到,Context一般理解为上下文,也可以理解为场景,比如当收到一条短信时,Context就包括当前的界面以及后台的数据。
Context是一个abstract类,Activity以及Service都是它的子类,为了便于使用又定义了一个包装类ContextWrapper,而真正实现了Context的是ContextImpl类,应用程序中调用的各种Context方法其实都来自它。
我们知道获取Context的方法有Activity.this、Service.this、getApplicationContext方法。
那么它们有什么不同呢?
Activity启动时会调用到ActivityThread中的performLaunchActivity方法,并最终调用createBaseContextForActivity方法创建Context对象:
[java]viewplaincopy
137ContextImplappContext=newContextImpl();
138appContext.init(r.packageInfo,r.token,this);
139appContext.setOuterContext(activity);
Service启动时会调用到ActivityThread中的handleCreateService方法,并最终创建Context对象:
[java]viewplaincopy
140ContextImplcontext=newContextImpl();
141context.init(packageInfo,null,this);
142Applicationap