Android的按键分发--遇到问题后结合源码进行分析

前记:

按键分发是android面试的一个重点,大家有必要好好掌握一下。
在手机上,重点考察的是触摸事件的分发,TV上考察的则是对按键分发的掌握情况。

研究的切入点:

客户的需求:开机向导App,在遥控器连接上之后,用户可以按任意键跳转到下一步。


mView 代表整个红色的跟布局,即LinearLayout。
跳转代码如下:只要有按键分发下来,就做跳转的动作。

mView.setOnKeyListener(new View.OnKeyListener() {
            
            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                //跳转到下一个界面
                mHandler.sendEmptyMessageDelayed(MSG_ID_GO_NEXT_STEP, 200);
                return true;
            }
        });

遇到的问题:阿伦反馈OK键不能跳转,但是方向键可以跳转!!
那么我们就带着这个问题,来从源码角度分析为什么OK键不可,上下左右可?
提问时间:大家能看出是什么问题吗?? (5分钟)

开始分析:

屏蔽跳转+加log

mView.setOnKeyListener(new View.OnKeyListener() {
            
            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                CLog.d(TAG, "onKey keyCode:" + keyCode);
                CLog.d(TAG, "event:" + event.getAction());
                //mHandler.sendEmptyMessageDelayed(MSG_ID_GO_NEXT_STEP, 200);
                return true;
            }
        });

Log分析结果
首先按OK键,没有任何打印。
然后按下键,有响应,但是仅仅是收到了下键的Up事件。
再按OK键,可收到OK键的Down+Up事件。
看起来就有点奇怪了!

提问时间:这就是造成OK键无法跳转的原因!!大家能看出是什么问题吗?? (5分钟)

开始看源码
LinearLayout 没有重写dispatchKeyEvent(),故继承父类ViewGroup的分发方法。
ViewGroup.java

// The view contained within this ViewGroup that has or contains focus.
private View mFocused; //子view
int mPrivateFlags //代表“当前ViewGroup”的属性
@Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onKeyEvent(event, 1);
        }
      //只有eng版本有效,可忽略(输入事件一致性校验器)
        if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
                == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
            if (super.dispatchKeyEvent(event)) {
                return true;
            }
        } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
                == PFLAG_HAS_BOUNDS) {
            if (mFocused.dispatchKeyEvent(event)) {
                return true;
            }
        }

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);
        }//只有eng版本有效,可忽略(输入事件一致性校验器)
        return false;
    }

时间很充裕,我们来简单讲一下这个输入事件一致性效验器:mInputEventConsistencyVerifier
下面结合源代码来看
有几个要点:
1.只有eng版本有效,user版本直接跳过
2.eng版本它能用来干啥?只讲一个点:up事件弹起的时候,会检测上一个键是不是同样的keycode。
因为正常情况下,肯定是先有down再有up,如果up的前一个不是down那么就是异常。
异常会最终通过finishEvent()方法通过log打印出来!!

更多的细节暂时没研究了,大家自己去发现!

由于user版本,一致性效验器不生效,那么我们简化下代码。

// The view contained within this ViewGroup that has or contains focus.
//子view有焦点或者子view包含有焦点的子view
private View mFocused; 
//代表“当前ViewGroup”的属性
int mPrivateFlags 
@Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        //...
        if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
                == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
            if (super.dispatchKeyEvent(event)) {
                return true;
            }
        } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
                == PFLAG_HAS_BOUNDS) {
            if (mFocused.dispatchKeyEvent(event)) {
                return true;
            }
        }
        //...
        return false;
    }

现在来分析这个分发方法

  1. 属性PFLAG_FOCUSED 表示有焦点
  2. 属性PFLAG_HAS_BOUNDS 表示有边界(一个view绘制完成,就肯定有边界的!)

如果当前的ViewGroup有焦点+有边界,则进入super.dispatchKeyEvent(event) 方法!
ViewGroup的Super是View.java
//View.java的按键分发代码:

public boolean dispatchKeyEvent(KeyEvent event) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onKeyEvent(event, 0);
        }//一致性校验,跳过

        // Give any attached key listener a first crack at the event.
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
            return true;
        }

        if (event.dispatch(this, mAttachInfo != null
                ? mAttachInfo.mKeyDispatchState : null, this)) {
            return true;
        }

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }//一致性校验,跳过
        return false;
    }

继续分析:
app层调用了setOnKeyListener()方法。
View.java 执行getListenerInfo().mOnKeyListener = l;
执行之后,mListenerInfo不为空了。mListenerInfo.mOnKeyListener 也不为空。
(mViewFlags & ENABLED_MASK) == ENABLED 成立,正常的View肯定是enable状态的!
接下来,就进入 li.mOnKeyListener.onKey(this, event.getKeyCode(), event) 完成app的回调!

mView.setOnKeyListener(new View.OnKeyListener() {
            
            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                CLog.d(TAG, "onKey keyCode:" + keyCode);
                CLog.d(TAG, "event:" + event.getAction());
                //mHandler.sendEmptyMessageDelayed(MSG_ID_GO_NEXT_STEP, 200);
                return true;
            }
        });

不做上层开发的,可能觉得回调不好理解,这里就是实现的方法!好像以前凯荣疑惑过这个问题。

这里大家可以一起沟通(5分钟),最好是能问懵的那种~··

整个回调过程已经看清楚了,并没有对OK键跟方向键做差异,那么问题在哪呢??

继续排查
首先从ViewGroup开始加log:

// The view contained within this ViewGroup that has or contains focus.
//子view有焦点或者子view包含有焦点的子view
private View mFocused; 
//代表“当前ViewGroup”的属性
int mPrivateFlags 
@Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        //...
        Log.d(TAG, "mPrivateFlags :" + mPrivateFlags);
        if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
                == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
            if (super.dispatchKeyEvent(event)) {
                return true;
            }
        } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
                == PFLAG_HAS_BOUNDS) {
            if (mFocused.dispatchKeyEvent(event)) {
                return true;
            }
        }
        //...
        return false;
    }

通过打印发现,原来是mPrivateFlags 没有焦点!
那么基本可以推测:OK键之所以没有分发下来,是因为无焦点造成的。而方向键会赋予ViewGroup焦点。所以方向键可实现跳转!!

尝试解决:

mView.requestFocus();

测试验证OK!


上面讲的是解决+分析问题的过程,现在我们稍微延伸一下。

View焦点获取的过程

方向键Down事件,使该ViewGroup获取的焦点,调用堆栈:

I ViewGroup: android.view.ViewGroup.requestChildFocus
I ViewGroup: android.view.View.handleFocusGainInternal
I ViewGroup: android.view.ViewGroup.handleFocusGainInternal
I ViewGroup: android.view.View.requestFocusNoSearch
I ViewGroup: android.view.View.requestFocus
I ViewGroup: android.view.ViewGroup.requestFocus
I ViewGroup: android.view.View.requestFocus
I ViewGroup: android.view.ViewRootImpl$ViewPostImeInputStage.processKeyEvent

下面看一下代码:
代码位于 ViewRootImpl.java
看下source的官网说明:

/*
 * The top of a view hierarchy, implementing the needed protocol between View
 * and the WindowManager.  This is for the most part an internal implementation
 * detail of {@link WindowManagerGlobal}.
 * {@hide}
 */

view层级的顶部,JAVA层的按键事件、触摸事件、View的绘制流程,都是由改类分发发起。
但是ViewRootImpl是不是View,更像是View图层的大脑!!

讨论5分钟:假如我们不看代码,大致猜猜焦点切换逻辑,越详细越好
...
...
...

下面我们一起看下代码层,看看焦点是如何完成切换的:
计算机术语中modifierkey 是“辅助按键”的意思。

private int processKeyEvent(QueuedInputEvent q) {
            final KeyEvent event = (KeyEvent)q.mEvent;
            //... 省略不相关的代码
            // Handle automatic focus changes.
            //下面的代码块,完成的方向判定,即焦点切换方向(上、下、左、右)。
            if (event.getAction() == KeyEvent.ACTION_DOWN) {
                int direction = 0;
                switch (event.getKeyCode()) {
                    case KeyEvent.KEYCODE_DPAD_LEFT:
                        if (event.hasNoModifiers()) {
                            direction = View.FOCUS_LEFT;
                        }
                        break;
                    case KeyEvent.KEYCODE_DPAD_RIGHT:
                        if (event.hasNoModifiers()) {
                            direction = View.FOCUS_RIGHT;
                        }
                        break;
                    case KeyEvent.KEYCODE_DPAD_UP:
                        if (event.hasNoModifiers()) {
                            direction = View.FOCUS_UP;
                        }
                        break;
                    case KeyEvent.KEYCODE_DPAD_DOWN:
                        if (event.hasNoModifiers()) {
                            direction = View.FOCUS_DOWN;
                        }
                        break;
                    case KeyEvent.KEYCODE_TAB:
                        if (event.hasNoModifiers()) {
                            direction = View.FOCUS_FORWARD;
                        } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
                            direction = View.FOCUS_BACKWARD;
                        }
                        break;
                }
                //焦点切换方向已经确定
                if (direction != 0) {
                    //找到当前获取焦点的View(因为要以当前焦点View为基点,然后才能计算出下一个焦点是哪个小伙~)
                    View focused = mView.findFocus();
                    if (focused != null) {
                        //根据方向direction,计算到下一个Foucus View,如果我们要该修改聚焦逻辑,那么可以从这里着手。
                        View v = focused.focusSearch(direction);
                        if (v != null && v != focused) {
                            // do the math the get the interesting rect
                            // of previous focused into the coord system of
                            // newly focused view
                            focused.getFocusedRect(mTempRect);
                            if (mView instanceof ViewGroup) {
                                ((ViewGroup) mView).offsetDescendantRectToMyCoords(
                                        focused, mTempRect);
                                ((ViewGroup) mView).offsetRectIntoDescendantCoords(
                                        v, mTempRect);
                            }
                            //自此下一个View已经获得了焦点(对应的UI把焦点画上)
                            if (v.requestFocus(direction, mTempRect)) {
                                //咔一下,播放下焦点改变的声音。
                                //这里可以加log,下面再讲做啥
                                playSoundEffect(SoundEffectConstants
                                        .getContantForFocusDirection(direction));
                                return FINISH_HANDLED;
                            }
                        }

                        // Give the focused view a last chance to handle the dpad key.
                        if (mView.dispatchUnhandledMove(focused, direction)) {
                            return FINISH_HANDLED;
                        }
                    } else {
                        // find the best view to give focus to in this non-touch-mode with no-focus
                        View v = focusSearch(null, direction);
                        if (v != null && v.requestFocus(direction)) {
                            return FINISH_HANDLED;
                        }
                    }
                }
            }
            return FORWARD;
        }

上面的代码,就是焦点如何完成切换的代码。
大致过程是:首先找点找点当前焦点View,然后根据位置关系+dirrection,计算出下一个焦点View.
聚焦逻辑的代码细节,暂时不仔细分析,如果你非要问,那只能说:


泽宝反馈的嘟嘟嘟bug:

大家一致的总结是两个方向:
1.按键事件异常
2.audio播放异常

想法:
我们可以在上面的播放按键声音的地方,添加上log,然后压测。
如果异常复现的时候,没有反复调用这个播放代码,则不是按键异常的问题。
基本定位在Audio上面了,也许是audio模块的一个概率性播放异常!!!
这个需要大家下来好好分析了!!!

下面我们继续聚焦在按键分发上面~~

安卓View层级的按键分发过程:

最后再根据Listener的不同返回值,来看下代码调用过程:

APP响应代码

mView.setOnKeyListener(new View.OnKeyListener() {
            //onKey( )方
              @Override
              public boolean onKey(View v, int keyCode, KeyEvent event) {
                // TODO Auto-generated method stub
                     return false; //case 1
                     return true; //case 2
            }
        });

ViewGroup.java 分发流程

@Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))  //自己有焦点
                == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
            //VG -1
            if (super.dispatchKeyEvent(event)) {//VG -2
                return true;//VG -3
            }
          //VG -4
        } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS) //包含焦点View
                == PFLAG_HAS_BOUNDS) {
             //VG -5
            if (mFocused.dispatchKeyEvent(event)) { //VG -6
                return true; //VG -7
            }
        }
        return false; //VG -8
    }

View.java 分发流程

public boolean dispatchKeyEvent(KeyEvent event) {
        // Give any attached key listener a first crack at the event.
        //noinspection SimplifiableIfStatement
        //V-1
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) { //V -2
            return true; //V -3
        }
        if (event.dispatch(this, mAttachInfo != null
                ? mAttachInfo.mKeyDispatchState : null, this)) {
            return true;
        }
        return false; //V -4
    }

要点提要

一、mPrivateFlags 代表“当前ViewGroup”的属性

if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
            == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
    //当前ViewGroup有焦点且有边框,条件才成立。
}

二、mFocused

// The view contained within this ViewGroup that has or contains focus.
private View mFocused;
意思是当前的子ViewGroup有焦点,或者当前子ViewGroup包含有焦点。    
是当前ViewGroup的子View。

onKey( )方法返回false,整个调用流程
层级1. DecorView :VG-5 VG-6
层级2. LinearLayout : VG-5 VG-6
层级3. FrameLayout : VG-5 VG-6
层级4. LinearLayout : VG-5 VG-6
层级5. LinearLayout : VG-5 VG-6
层级6. FrameLayout : VG-5 VG-6 ( framelayout_container )
层级7. LinearLayout VG-1 VG-2 V-1 V-2 V-4
接着开始返回了:
VG-8( 层级7返回 ) VG-8( 层级6返回 ) VG-8( 层级5返回 ) VG-8( 层级4返回 ) VG-8( 层级3返回 ) VG-8( 层级2返回 ) VG-8( 层级1返回 )

onKey( )方法返回true,整个调用流程
层级1. DecorView :VG-5 VG-6
层级2. LinearLayout : VG-5 VG-6
层级3. FrameLayout : VG-5 VG-6
层级4. LinearLayout : VG-5 VG-6
层级5. LinearLayout : VG-5 VG-6
层级6. FrameLayout : VG-5 VG-6 ( framelayout_container )
层级7. LinearLayout VG-1 VG-2 V-1 V-2 V-3
接着开始返回了:
VG-3(层级7返回)VG-7( 层级6返回 ) VG-7(层级5返回 ) VG-7(层级 4返回 ) VG-7( 层级3返回 ) VG-7(层级 2返回 ) VG-7( 层级1返回 )

总结:

总体上,按键分发是按View的层级逐层分发,分发的规则是以焦点View为基础,焦点View-> 焦点View -> 焦点View ... 。

彩蛋:

一、KeyEvent传递过程:

主要可以划分为三步:过滤器、View树、Activity。

1.WindowManagerService.java内有两个线程,一个负责分发按键消息,一个负责读取按键消息。在执行processEvent分发事件时,系统有一些过滤器可以进行一些特定的处理操作,这些过滤器的处理既可以在事件分发前也可以在事件分发后。比如home键在分发前进行处理消费,应用无法监听该类消息,而返回键是在事件分发之后,只有当应用不进行处理时,系统才会处理返回键用来退出当前Activity,。

2.processEvent分发事件后,先传递到ViewRootImpl的deliverKeyEvent中,如果mView(即根DecorView)为空或者mAdded为false,则直接返回。若不为空,然后判断IME窗口(输入法)是否存在,若存在则优先传递到IME中,当输入窗口没有处理这个事件,事件则会真正派发到根视图mView.dispatchKeyEvent中,然后回调Activity的dispatchKeyEvent(event)去处理,由于Activity中的dispatchKeyEvent最终调用的是mDecor中的superDispatchKeyEvent(event),之后就是ViewGroup通过递归调用把Event传递到指定的View上。

3.事件传递到VIew之后,先会调用View的dispatchKeyEvent,如果有注册Listener,就直接调用Listener的onKey去响应,如果没有注册Listener,z之后会根据Action的类型调用对应的onXXX(onKeyDown,onKeyup)函数去处理,如果所有的view都没有进行处理,那么最终会回调到activity的onXXX方法中。

二、Activity View的dispatchKeyEvent,onkeyDown,onKeyUp, setListener处理顺序:

  1. Activity.dispatchKeyEvent(down) ----->view.dispatchKeyEvent ------>view.setListener(如setOnKeyListener) ------>view.onkeyDown------->Activity.onkeyDown------>Activity.dispatchKeyEven(up)------>view.dispatchKeyEvent---->view.setListener(如setOnKeyListener) ------>view.onkeyup----->view.onClick(setOnClickListener)---->Activity.onkeyUp

  2. 当一个event事件传递到某个view上时,如果对一些Action(比如Down)进行了消费后,则该View下的子view以及想消费该event的Action的行为都不会执行。默认情况下,ViewGroup控件不会执行onkeyDown和onkeyup,只有当其焦点属性为true时,才可以传递到执行

以后再来进一步分析一下~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345