Android 解決NestedScrollView包裹橫向RecyclerView導(dǎo)致behavior回調(diào)方法沒(méi)有執(zhí)行及源碼分析

前言

如題谊路,現(xiàn)在有一種behavior的使用場(chǎng)景:NestedScrollView下面包裹橫向的RecyclerView析二,behavior的滾動(dòng)回調(diào)方法不執(zhí)行霉涨。詳細(xì)可見(jiàn)demo, 建議最好clone下來(lái)自己試一試茂嗓,因?yàn)槟憧傆幸惶鞎?huì)用到behavior钥勋!
看看問(wèn)題

滾動(dòng)下面bottomView沒(méi)有跟著動(dòng)

  • 先來(lái)看看demo的布局層級(jí)

    main_activity

    CoordinatorLayout包含兩個(gè)子View: Viewpager和View(注入behavior關(guān)聯(lián)滾動(dòng)的view)
  • 再看看viewpager_item


    viewpager_item

    里面是一層NestedScrollView炬转,里面包含幾個(gè)子Linear, Linear里面包裹橫向的RecyclerView

  • 最終層級(jí)圖
效果希望滾動(dòng)里面的nestedscrollView然后顯示和隱藏bottomView

這個(gè)層級(jí)還是簡(jiǎn)化后的demo的,實(shí)際開(kāi)發(fā)中我們遇到的情況比這個(gè)更加復(fù)雜算灸,但是就算層級(jí)再多再?gòu)?fù)雜扼劈,只要符合behavior的使用規(guī)則,那么一切皆可以實(shí)現(xiàn)菲驴。

  • 再看看behavior
public class MyBehavior extends CoordinatorLayout.Behavior<View> {
    private boolean isHide = false;
    public MyBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
        return true;
    }

    @Override
    public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
        Log.e("test", "onNS");
        if(dyConsumed >0 ) {
            if (!isHide) {
                child.offsetTopAndBottom(child.getHeight());
                isHide = true;
            }
        }else {
            if(isHide){
                child.offsetTopAndBottom(-child.getHeight());
                isHide = false;
            }
        }
    }
}

也超級(jí)簡(jiǎn)單就是判斷一下滾動(dòng)方向荐吵,然后顯示和隱藏bottomView而已。

但是

我們這樣簡(jiǎn)單的代碼卻有著問(wèn)題,我們實(shí)際運(yùn)行發(fā)現(xiàn)先煎,貌似滾動(dòng)的關(guān)聯(lián)“不太靈敏”贼涩,打log發(fā)現(xiàn),有時(shí)候onNestedScroll方法不會(huì)調(diào)用薯蝎。這是為什么呢遥倦?

問(wèn)題

于是提出兩個(gè)問(wèn)題:
1、為什么onNestedScroll方法不會(huì)調(diào)用占锯?
2袒哥、為什么讓RecyclerView設(shè)置setNestedScrollingEnable(false)就能夠正常使用?

另外后面會(huì)進(jìn)行更深層次的源碼分析消略,附加幾個(gè)問(wèn)題:
1堡称、對(duì)于如果onIntercept返回true攔截了,交給onTouchEvent去處理艺演,具體體現(xiàn)在何處却紧?
2、判斷子View是否能夠接收事件從哪里體現(xiàn)钞艇?
3啄寡、另外一個(gè)比較重要的方法dispatchTransformedTouchEvent干什么用的?
4哩照、viewGroup和view的dispatch返回false挺物,會(huì)直接回溯到parent的onTouchEvent,這個(gè)又在哪里體現(xiàn)飘弧?
5识藤、viewGroup重寫(xiě)了dispatch但是沒(méi)有調(diào)用super, 那么它在哪里調(diào)用自己的onTouch的呢?

正題

首先我們解決第一個(gè)問(wèn)題次伶,“為什么onNestedScroll沒(méi)有調(diào)用痴昧?”

這需要大家對(duì)behavior有一定的了解,我們都知道coordinatorLayout和behavior聯(lián)合使用可以實(shí)現(xiàn)許多花哨的效果冠王,很牛逼赶撰。

behavior的工作原理就是:
1、coordinatorLayout下面的所有子view(包含子孫view),實(shí)現(xiàn)了滾動(dòng)接口(包括NestedScrollingChild柱彻、NestedScrollingParent等等)的view, 如果有滑動(dòng)事件的消耗豪娜,就會(huì)一層一層向上傳遞,直到coordinatorLayout
2哟楷、然后coordinatorLayout再對(duì)注入了behavior的子View傳遞滾動(dòng)回調(diào)事件瘤载,這樣,behavior就能拿到滾動(dòng)的值卖擅,進(jìn)而進(jìn)行對(duì)View的一些關(guān)聯(lián)滾動(dòng)操作
如果用最通俗的例子來(lái)講就是:
父親是CoordinatorLayout鸣奔,它有兩個(gè)兒子墨技,一個(gè)是NestedScrollView,一個(gè)是BottomView挎狸,behavior綁在BottomView身上(父親比較偏愛(ài)他)扣汪。NestedScrollView發(fā)年終獎(jiǎng)了(滾動(dòng)了),發(fā)了紅包給父親(通知給了父親)伟叛,然后父親又把錢(qián)分給了喜愛(ài)的兒子BottomView(父親又通知了BottomView)
貼點(diǎn)重要代碼
recycler->linear->nestedScroll->coordinator:


傳遞過(guò)程私痹,其他非滾動(dòng)view“透明”

異常中返回false

為什么onNestedScroll沒(méi)有回調(diào)呢?
PS: 這里的源碼是對(duì)應(yīng)26的统刮,support是26.1
通過(guò)在代碼里面打斷點(diǎn)發(fā)現(xiàn):

  • RecyclerView中自己消費(fèi)了consumedY

RecyclerView中自己消費(fèi)了consumedY,uncomsumed = y - consumeY = 0 账千,然后


NestedScrollView中拿到的dyUnConsumed為0

NestedScrollView中拿到的dyUnConsumed為0侥蒙,調(diào)用dispatchNestedScroll方法也就傳入0


NestedScrollingChildHelper中if分支進(jìn)不去

這樣的話(huà),NestedScrollingChildHelper中if分支進(jìn)不去匀奏,就沒(méi)法向上層傳遞消費(fèi)的y值(相當(dāng)于它并沒(méi)有滾動(dòng)),ViewParentCompat.onNestedScroll沒(méi)法調(diào)用,所以沒(méi)能傳遞到頂層的CoordinatorLayout副签,自然behavior里面也不會(huì)收到回調(diào)了奶是。

從源碼上來(lái)看是這樣的,如果從宏觀上來(lái)講聚磺,其實(shí)就是RecyclerView和NestedScrollView的事件處理有沖突坯台,RecyclerView消費(fèi)了事件,從而NestedScrollView沒(méi)能把自己消費(fèi)的事件往上傳遞瘫寝。
按道理蜒蕾,我們都知道,如果豎向的RecyclerView和NestedScrollView或者ScrollView聯(lián)合使用的話(huà)(雖然焕阿,這樣聯(lián)合使用沒(méi)有意義咪啡,也不建議這樣做),會(huì)出現(xiàn)事件沖突暮屡。但是撤摸,橫向的RecyclerView和NestedScrollView一起使用,在事件處理上面是沒(méi)有問(wèn)題的褒纲,沒(méi)有沖突准夷,但是,在使用到behavior外厂,希望nestedScrollView能夠把自己滾動(dòng)消費(fèi)的事件往上傳遞的時(shí)候就會(huì)出問(wèn)題了冕象。
(我們都希望behavior的使用是在沒(méi)有嵌套滾動(dòng)沖突的情況下,兄弟滾動(dòng)汁蝶,然后父親知道渐扮,父親通知另外一個(gè)兄弟做出相應(yīng)的行為论悴,而如果是子孫滾動(dòng),往上傳給父親墓律,這期間出了問(wèn)題膀估,就沒(méi)法正常工作了)

接著第二個(gè)問(wèn)題,“為什么讓RecyclerView設(shè)置setNestedScrollingEnable(false)就能夠正常使用耻讽?”

看看效果


OK,可能轉(zhuǎn)gif幀率不夠有點(diǎn)卡

第二個(gè)問(wèn)題就需要大家對(duì)于事件分發(fā)機(jī)制有一定的了解察纯,這里就大致貼張圖。

事件分發(fā)機(jī)制

另外针肥,貼幾個(gè)認(rèn)為比較不錯(cuò)的鏈接:
1饼记、圖解 Android 事件分發(fā)機(jī)制
2、Android事件分發(fā)機(jī)制詳解:史上最全面慰枕、最易懂
3具则、Android6.0源碼解讀之View點(diǎn)擊事件分發(fā)機(jī)制
4、Android 事件分發(fā)機(jī)制-試著讀懂每一行源碼-View
5具帮、ScrollView與頭+RecycleView嵌套沖突源碼分析

我們看看setNestedScrollingEnabled

// RecyclerView
   @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        getScrollingChildHelper().setNestedScrollingEnabled(enabled);
    }

調(diào)用了輔助類(lèi)

// NestedScrollingChildHelper
   public void setNestedScrollingEnabled(boolean enabled) {
        if (mIsNestedScrollingEnabled) {
            ViewCompat.stopNestedScroll(mView);
        }
        mIsNestedScrollingEnabled = enabled;
    }

輔助類(lèi)設(shè)置mIsNestedScrollingEnabled為false博肋,并且調(diào)用了 ViewCompat.stopNestedScroll(mView);傳入了自己

// NestedScrollingChildHelper
 public boolean isNestedScrollingEnabled() {
        return mIsNestedScrollingEnabled;
    }

這樣isNestedScrollingEnabled返回false了,以后behavior的回調(diào)方法里面的if(isNestedScrollingEnabled())就進(jìn)不去了
接著:

//ViewCompat
 public static void stopNestedScroll(@NonNull View view) {
        IMPL.stopNestedScroll(view);
    }

這里蜂厅,ViewCompat就是一個(gè)兼容類(lèi)匪凡,兼容各個(gè)版本api的使用,因?yàn)橛幸恍┬掳姹镜腶pi掘猿,實(shí)現(xiàn)的是NestedScrollingParent2等方法病游。

//ViewCompat
 public void stopNestedScroll(View view) {
            if (view instanceof NestedScrollingChild) {
                ((NestedScrollingChild) view).stopNestedScroll();
            }
        }

這里是相當(dāng)于調(diào)用view的stopNestedScroll,也就是RecyclerView的术奖。

//RecyclerView
  @Override
    public void stopNestedScroll() {
        getScrollingChildHelper().stopNestedScroll();
    }
//NestedScrollingChildHelper
   public void stopNestedScroll() {
        stopNestedScroll(TYPE_TOUCH);
    }
//NestedScrollingChildHelper
 public void stopNestedScroll(@NestedScrollType int type) {
        ViewParent parent = getNestedScrollingParentForType(type);
        if (parent != null) {
            ViewParentCompat.onStopNestedScroll(parent, mView, type);
            setNestedScrollingParentForType(type, null);
        }
    }

這里就比較重要了礁遵,這里通過(guò)getNestedScrollingParenForType獲得了parent,然后調(diào)用了ViewParentCompat.onStopNestedScroll(parent, mView, type);

//viewParentCompat
  public static void onStopNestedScroll(ViewParent parent, View target, int type) {
        if (parent instanceof NestedScrollingParent2) {
            // First try the NestedScrollingParent2 API
            ((NestedScrollingParent2) parent).onStopNestedScroll(target, type);
        } else if (type == ViewCompat.TYPE_TOUCH) {
            // Else if the type is the default (touch), try the NestedScrollingParent API
            IMPL.onStopNestedScroll(parent, target);
        }
    }

這個(gè)方法會(huì)又調(diào)用IMPL.onStopNestedScroll(parent, target);這樣類(lèi)似的方法其實(shí)就是把事件一層一層往上傳采记,當(dāng)然佣耐,其他onPreNestedScroll、onNestedScroll這些也都是這樣的唧龄。

//NestesScrollView
  @Override
    public void onStopNestedScroll(View target) {
        mParentHelper.onStopNestedScroll(target);
        stopNestedScroll();
    }

我們又看NestedScrollView里面的onStopNestedScroll
stopNestedScroll();是繼續(xù)往上調(diào)用傳遞
mParentHelper.onStopNestedScroll(target);就比較關(guān)鍵了

//NestedScrollingParentHelper
  public void onStopNestedScroll(@NonNull View target) {
        onStopNestedScroll(target, ViewCompat.TYPE_TOUCH);
    }
 public void onStopNestedScroll(@NonNull View target, @NestedScrollType int type) {
        mNestedScrollAxes = 0;
    }

這個(gè)就關(guān)鍵了兼砖,mNestedScrollAxes = 0

        return mNestedScrollAxes;
    }

這個(gè)方法返回0了,看看它在哪被調(diào)用

//NestedScrollVIew#onIntercept#move
    final int yDiff = Math.abs(y - mLastMotionY);
                if (yDiff > mTouchSlop
                        && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) {
                    mIsBeingDragged = true;
                    mLastMotionY = y;
                    initVelocityTrackerIfNotExists();
                    mVelocityTracker.addMovement(ev);
                    mNestedYOffset = 0;
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                }
                break;

在move的時(shí)候既棺,它返回為0讽挟,那么走入分支的話(huà),mIsBeingDragged =true
onInterceptTouchEvent就返回true, 就會(huì)攔截了丸冕。
這就說(shuō)明耽梅,在move的時(shí)候,nestedScrollView就完全攔截了事件胖烛,里面的子孫view(包括橫向的RecyclerView就不會(huì)有事件了眼姐,更不用談什么它自己消費(fèi)掉了consumeY诅迷,NestedScrollView自己全權(quán)處理了),這樣的話(huà)它自己的滾動(dòng)事件就能夠再往上一直傳遞到coordinatorLayout众旗,然后behavior也就肯定能夠執(zhí)行回到方法了罢杉!
啊,原來(lái)如此贡歧,恍然大悟滩租!

5個(gè)小問(wèn)題

前面兩個(gè)大問(wèn)題終于解決了,下面來(lái)搞清楚后面提的那5個(gè)小問(wèn)題利朵。
1律想、對(duì)于如果onIntercept返回true攔截了,交給onTouchEvent去處理绍弟,具體體現(xiàn)在何處蜘欲?
2、判斷子View是否能夠接收事件從哪里體現(xiàn)晌柬?
3、另外一個(gè)比較重要的方法dispatchTransformedTouchEvent干什么用的郭脂?
4年碘、viewGroup和view的dispatch返回false,會(huì)直接回溯到parent的onTouchEvent展鸡,這個(gè)又在哪里體現(xiàn)屿衅?
5、viewGroup重寫(xiě)了dispatch但是沒(méi)有調(diào)用super, 那么它在哪里調(diào)用自己的onTouch的呢莹弊?

這幾個(gè)問(wèn)題全是關(guān)于事件分發(fā)的涤久,大家可以把那幾個(gè)鏈接的文章都看了,如果還不能解決忍弛,那么再往下看响迂。

//ViewGroup#dispatchTouchEvent  代碼有省略
 @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
      boolean handled = false;
            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
    ##### 重點(diǎn)
              // 這里mFirstTouchTarget置為null
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
   ##### 重要 if分支1
            if (!canceled && !intercepted) {
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

    ##### 重點(diǎn)
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            resetCancelNextUpFlag(child);
    ##### 重點(diǎn)
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
    ##### 重點(diǎn)
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }
            }

            // Dispatch to touch targets.
   ##### 重要 if分支2
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
    ##### 重點(diǎn)
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
      ...
        return handled;
    }
//ViewGroup#dispatchTransformedTouchEvent  代碼有省略
 private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

        final MotionEvent transformedEvent;
        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
              handled = child.dispatchTouchEvent(event);
                }
                return handled;
            }
            transformedEvent = MotionEvent.obtain(event);
        } else {
            transformedEvent = event.split(newPointerIdBits);
        }

        // Perform any necessary transformations and dispatch.
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
         handled = child.dispatchTouchEvent(transformedEvent);
        }

        // Done.
        transformedEvent.recycle();
        return handled;
    }

1 、對(duì)于如果onIntercept返回true攔截了细疚,交給onTouchEvent去處理蔗彤,具體體現(xiàn)在何處?

如果onIntercep返回true疯兼,那么interceped變量為true然遏,那么不會(huì)走入【重要 if分支1】(里面分發(fā)事件,設(shè)置mFirstTouchTarget等)吧彪,mFirstTouchTarget依舊為null, 于是走入【重要 if分支2】的dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS) 并且傳入child為null,
在dispatchTransformedTouchEvent中如果child為null,就會(huì)走super.dispatch, super就是view待侵,這樣的話(huà),就會(huì)走view的dispatch(view本身的dispatch會(huì)調(diào)onTouch)姨裸,就會(huì)走到onTouch去了

2秧倾、判斷子View是否能夠接收事件從哪里體現(xiàn)怨酝?

在viewgroup的dispatch中的走入if分支之后,里面有個(gè)判斷

if (!canViewReceivePointerEvents(child)
                                   || !isTransformedTouchPointInView(x, y, child, null)) {
                               ev.setTargetAccessibilityFocus(false);
                               continue;
                           }

有個(gè)方法canViewReceivePointerEvents中狂,里面主要是判斷是否Visible
isTransformedTouchPointInView主要是判斷事件的位置是否在子VIew的區(qū)域內(nèi)
如果不行凫碌,就continue,后續(xù)的事件分發(fā)就不進(jìn)行

3胃榕、另外一個(gè)比較重要的方法dispatchTransformedTouchEvent干什么用的盛险?

dispatchTransformedTouchEvent主要就是對(duì)于事件分發(fā)的處理,比如什么時(shí)候調(diào)用自己的super.dispatch勋又,什么時(shí)候調(diào)用child.disaptch分發(fā)給子View, 這個(gè)判斷方法的主要根據(jù)就是child是否為null, 而這個(gè)又跟mFirstTouchTarget有關(guān)聯(lián)

4苦掘、viewGroup和view的dispatch返回false,會(huì)直接回溯到parent的onTouchEvent楔壤,這個(gè)又在哪里體現(xiàn)鹤啡?

這個(gè)問(wèn)題也跟第1個(gè)問(wèn)題有點(diǎn)類(lèi)似,如果子View的dispatch返回false蹲嚣,那么dispatchTransformedTouchEvent的handled就會(huì)是false返回递瑰,然后【重要 if分支1】就走不進(jìn)去,addTouchTarget這個(gè)方法也不執(zhí)行(主要是給mFirstTarget賦值)

  private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

前面說(shuō)了隙畜,如果mFirstTarget為null, 【重要 if分支2】就會(huì)進(jìn)入dispatchTransformedTouchEvent的時(shí)候傳入為null的child, 這樣就會(huì)調(diào)用super.dispatch抖部,就是view的dispatch,然后就調(diào)用了onTouch咯

viewGroup重寫(xiě)了dispatch但是沒(méi)有調(diào)用super, 那么它在哪里調(diào)用自己的onTouch的呢议惰?

如果看到這慎颗,希望第5個(gè)問(wèn)題我已經(jīng)不用解釋了,因?yàn)榍懊?個(gè)問(wèn)題已經(jīng)把它囊括在內(nèi)了言询。

最后

為了解決這個(gè)問(wèn)題俯萎,最近一直在源碼的黑洞里遨游,打了N個(gè)斷點(diǎn)來(lái)回跳运杭,梳理邏輯夫啊。最后一句,最終想要深刻地理解事件分發(fā)機(jī)制县习、behavior機(jī)制這些玩意兒涮母,RTFSC。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末躁愿,一起剝皮案震驚了整個(gè)濱河市叛本,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌彤钟,老刑警劉巖来候,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異逸雹,居然都是意外死亡营搅,警方通過(guò)查閱死者的電腦和手機(jī)云挟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)转质,“玉大人园欣,你說(shuō)我怎么就攤上這事⌒菪罚” “怎么了沸枯?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)赂弓。 經(jīng)常有香客問(wèn)我绑榴,道長(zhǎng),這世上最難降的妖魔是什么盈魁? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任翔怎,我火速辦了婚禮,結(jié)果婚禮上杨耙,老公的妹妹穿的比我還像新娘赤套。我一直安慰自己,他們只是感情好珊膜,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布于毙。 她就那樣靜靜地躺著,像睡著了一般辅搬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上脖旱,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天堪遂,我揣著相機(jī)與錄音,去河邊找鬼萌庆。 笑死溶褪,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的践险。 我是一名探鬼主播猿妈,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼巍虫!你這毒婦竟也來(lái)了彭则?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤占遥,失蹤者是張志新(化名)和其女友劉穎俯抖,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體瓦胎,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡芬萍,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年尤揣,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片柬祠。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡北戏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出漫蛔,到底是詐尸還是另有隱情嗜愈,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布惩猫,位于F島的核電站芝硬,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏轧房。R本人自食惡果不足惜拌阴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望奶镶。 院中可真熱鬧迟赃,春花似錦、人聲如沸厂镇。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)捺信。三九已至酌媒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間迄靠,已是汗流浹背秒咨。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留掌挚,地道東北人雨席。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像吠式,于是被迫代替她去往敵國(guó)和親陡厘。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345