美文网首页
Android的按键分发--遇到问题后结合源码进行分析

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

作者: _水蓝 | 来源:发表于2020-06-08 13:59 被阅读0次

    前记:

    按键分发是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时,才可以传递到执行

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

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/ypxnzhtx.html