• QQ空间
  • 收藏

从小白角度探索Android事件分发机制

| 2019-07-11 阅读 164



今日科技快讯


昨日,小米MIX 3在北京故宫博物院正式发布,该机使用磁动力滑盖设计,前后旗舰双摄,售价3299元起。此外,小米还与故宫博物院推出联名特别版,机身背部嵌有祥瑞神兽獬豸纹样,首次配置10GB超大内存,售价4999元。在全面屏上,雷军一直强调小米是开创者。早在2015年就申请了弹射式设计专利,2018年2月份申请了滑盖式设计。此次小米MIX 3采用了磁力滑盖全面屏,快捷操作“一推即达”。


作者简介


明天就是周六啦,提前祝大家周末愉快!

本篇来自 你缺少想象力 的投稿,分享了对事件分发机制的解析,希望对大家有所帮助。

你缺少想象力 的博客地址:

https://blog.csdn.net/IT_XF


概念


说到事件分发机制,这个知识点主要是在自定义view的时候用到,那么什么是事件分发机制呢。

这里我用大白话概述一下:我们在自定义view,或者在使用某个控件,当给这个view或者控件设置事件的时候,比如有setOnTouchListener、setOnClickListener这些方法的时候,这些方法总有一个执行顺序吧,事件分发机制主要就是了解这些方法执行的先后顺序,或者说执行这些事件的顺序和方法之间的关系,比如点击事件,触摸事件,手指上抬下按等等之类的,主要就是要搞清楚这些事件发生的先后顺序和他们之间的关系。

搞清楚这些东西有什么好处呢,首先,即便假设可能由于这些方法名字太像了,所以你还是没有搞清楚这些方法的执行顺序和相互关系,不过在搞清楚的过程中,至少也搞清楚每个方法,就搞清楚这些方法是干嘛的了吧,知道这些方法是干嘛的又能有什么好处呢,至少不仅仅就只会一个setOnClickListener点击事件了吧,如果你搞清楚了setOnTouchListener方法,也许你就可以实现一个view,手指点击按住后拖动,手指放开后,view又回到原来的位置,这种效果,可不是一个简单的setOnClickListener就能够实现的。


代码追踪


(p.s. 以下代码追踪基于Android 8.0的源码,即API 26)

不知道有哪些方法跟触摸点击有关啊,那就看源码吧!从我们最熟知的setOnClickListener开始,setOnClickListener做了啥,跳进View.class里面,发现这个方法长这样:

反正就是赋值,给这个接口赋值,所以我们来看看mOnClickListener在什么地方使用的,代码一顿追踪,mOnClickListener在这里performClick()被使用了:

先不管这里面具体的一个实现流程是啥,知道mOnClickListener在这里被用到就行了,看这个方法名:performClick翻译过来“执行点击”,嗯~靠谱,继续追踪,于是来到了:

看到这里,我们需要稍微总结一下了。为什么要在这里总结呢,因为不一样了啊,哪里不一样了。首先,前面两个方法setOnClickListener和performClick,概念单一,就一个设置具体实现类接口,还有一个执行点击事件嘛,但是onTouchEvent好像跟以上两种不太一样,因为他有个参数MotionEvent,翻译过来运动事件或者手势事件,这个事件包含了我们很多手势操作,比如手指上抬下按,在屏幕上拖动等等。所以完整的onTouchEvent方法是如下形状:

所以我们可以很清晰的看到,点击事件只是在手指抬起的这个行为后执行的,只是用到了这么多操作行为中的其中一个而已。那么我们是不是就可以得出一个结论,performClick()方法其实只有在手指上抬的时候执行,也就是当手指接触屏幕到离开屏幕的这个过程中,performClick()只执行了一次,而onTouchEvent可能就执行了多次,至少2次吧,即上抬和下按。

接下来我们开始追踪onTouchEvent方法,然后发现了以下源码。

看到此次,有人提问 (问我= =)!为啥这里的源码不写成:

因为有个onTouch()方法,待会要讲,主要是由于view有个setOnTouchListener()方法,先不管这些,我们只看上面那个简单的dispatchTouchEvent()方法,这个方法翻译成中文“分发触摸事件”,好像有点谱了,事件分发机制的源头可能就在此处吧。先不管,继续跟踪,我们来到:

继续跟踪:

妈耶!都跑到私有方法去了,不管,继续跟踪:

不管,反正也不知道这个类是干嘛的,继续跟,跟源码死磕到底!

WTF?都跑道setView设置view那里去了,算了算了,弃坑重练,还是定位到靠谱的dispatchTouchEvent()方法就终止追踪了吧。


触发顺序


现在我们来总结以下有哪些方法,从上到下的顺一遍

根据源码,我们的猜想是按照这样一个顺序,那我们来验证一下,首先写一个类,并且让这个类实现那些事件方法,所以这个类,基本上就长这样了。

鉴于已经知道触摸行为常见的有3种,按下移动抬起,为了使日志最短,所以我们飞快的点击了屏幕,不给手指在屏幕上的移动的机会,得到了如下日志:

为了看的更加清楚,我们改改源码,打印出具体的行为:

日志:

所以这里面的0和1都是啥意思,还有,这些方法长的太像了,我已经蒙圈了,只认识onClick了。

先看看0和1都是啥意思,看源码呗,不是都说源码是最好的老师吗。

好了,知道了,手指按下就是0,手指上抬就是1。上面的日志就被改成下面这副模样:

然后我们根据方法的名字,将方法名字翻译成中文,上面的日志又被改成以下模样:

大家感受一下这个顺序,给你10秒钟。接下来我们进入下一个环节。


详细分析


dispatchTouchEvent

首先我们先来到最初的dispatchTouchEvent方法中去寻觅过程。

(当前源码API 26;不用看这些源码,我就摆摆场面= =)

某些读者表示,这些源码又多又乱,我在看一篇博客,我要怎么看这些源码,里面的变量是啥意思都不知道,还不能进行变量跟踪,我要怎么看,如果是在IDE里面打开这些源码,兴许我还有几分愿意阅读的兴趣。

以上问题就是我平时看博客的时候脑子里面想到的事情,最讨厌贴上一片源码,然后就开始讲道理了,源码看都看不懂,或者说不想看= =

教大家一个小技巧,看老版本的源码,因为Android源码只会越来越多啊,所以老版本的源码肯定比新版本的少。

目前我这里找到的最老的源码只有API 15 的,所以我们来看看API 15里面的事件分发是怎么写的吧,同一个方法dispatchTouchEvent:

是不是觉得少了很多,不过还是挺多的,那我们怎么看呢,就看这里面出现的关键点,源码少了很多,我们就能快速定位我们的关键点在什么地方了。

首先这个方法里面出现了两个很重要的地方,onTouch和onTouchEvent方法,所以把跟这些代码无关的地方,我们就都给筛掉,所以就变成了以下模样:

好像基本就这样了,也不能再怎么筛了,所以趁着源码才这几行的机会,我们好好来看一下,首先是ListenerInfo类,这个类是干啥的,看名字好像是接口监听信息,瞅瞅源码:

果然基本所有的接口都在这里面,当接口很多的时候,用这种方式统一管理接口,真是个不错的方法,学到了,果然看源码还是有很多好处的嘛。

好的,我们继续来看dispatchTouchEvent方法(复制了一遍,免得往上翻)

public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
        && li.mOnTouchListener.onTouch(this, event)) {
        return true;
    }

    if (onTouchEvent(event)) {
        return true;
    }
    ...
}

ListenerInfo我们已经知道是怎么回事了,就来看看第一个if,因为第一个if就包含我们其中一个关注点onTouch,这个if条件还挺多的,一共有4个条件,我们一个一个看:

1. li != null

我想,这个一个不用我说了吧,就判断这个接口管理类是否为null

2. li.mOnTouchListener != null

判断这个接口是否为null,我们来看看这个值是在哪里赋值的:

public void setOnTouchListener(OnTouchListener l) {
    getListenerInfo().mOnTouchListener = l;
}

仿佛看到常见方法了,或者关键方法了,这个方法是view的。

3. (mViewFlags & ENABLED_MASK) == ENABLED

说到这里,我要夸夸Google的程序员,确实厉害(Google程序员:还用你夸?)

这个&用的很传神,在API 26 的View.class里面有一群这样的注释:

    /**
     * Masks for mPrivateFlags2, as generated by dumpFlags():
     *
     * |-------|-------|-------|-------|
     *                                 1 PFLAG2_DRAG_CAN_ACCEPT
     *                                1  PFLAG2_DRAG_HOVERED
     *                              11   PFLAG2_LAYOUT_DIRECTION_MASK
     *                             1     PFLAG2_LAYOUT_DIRECTION_RESOLVED_RTL
     *                            1      PFLAG2_LAYOUT_DIRECTION_RESOLVED
     *                            11     PFLAG2_LAYOUT_DIRECTION_RESOLVED_MASK
     *                           1       PFLAG2_TEXT_DIRECTION_FLAGS[1]
     *                          1        PFLAG2_TEXT_DIRECTION_FLAGS[2]
     *                          11       PFLAG2_TEXT_DIRECTION_FLAGS[3]
     *                         1         PFLAG2_TEXT_DIRECTION_FLAGS[4]
     *                         1 1       PFLAG2_TEXT_DIRECTION_FLAGS[5]
     *                         11        PFLAG2_TEXT_DIRECTION_FLAGS[6]
     *                         111       PFLAG2_TEXT_DIRECTION_FLAGS[7]
     *                         111       PFLAG2_TEXT_DIRECTION_MASK
     *                        1          PFLAG2_TEXT_DIRECTION_RESOLVED
     *                       1           PFLAG2_TEXT_DIRECTION_RESOLVED_DEFAULT
     *                     111           PFLAG2_TEXT_DIRECTION_RESOLVED_MASK
     *                    1              PFLAG2_TEXT_ALIGNMENT_FLAGS[1]
     *                   1               PFLAG2_TEXT_ALIGNMENT_FLAGS[2]
     *                   11              PFLAG2_TEXT_ALIGNMENT_FLAGS[3]
     *                  1                PFLAG2_TEXT_ALIGNMENT_FLAGS[4]
     *                  1 1              PFLAG2_TEXT_ALIGNMENT_FLAGS[5]
     *                  11               PFLAG2_TEXT_ALIGNMENT_FLAGS[6]
     *                  111              PFLAG2_TEXT_ALIGNMENT_MASK
     *                 1                 PFLAG2_TEXT_ALIGNMENT_RESOLVED
     *                1                  PFLAG2_TEXT_ALIGNMENT_RESOLVED_DEFAULT
     *              111                  PFLAG2_TEXT_ALIGNMENT_RESOLVED_MASK
     *           111                     PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_MASK
     *         11                        PFLAG2_ACCESSIBILITY_LIVE_REGION_MASK
     *       1                           PFLAG2_ACCESSIBILITY_FOCUSED
     *      1                            PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED
     *     1                             PFLAG2_VIEW_QUICK_REJECTED
     *    1                              PFLAG2_PADDING_RESOLVED
     *   1                               PFLAG2_DRAWABLE_RESOLVED
     *  1                                PFLAG2_HAS_TRANSIENT_STATE
     * |-------|-------|-------|-------|
     */

与(&),一个符号巧妙的搞定了判断两个值是否等于相同,好了不吹了,偏题了= =

总之这个判断大概就是判断该View是否可用。

4. li.mOnTouchListener.onTouch(this, event))

这里,重点环节,回调了onTouch方法,我们就可用在事件onTouchEvent事件执行之前,先一步窥探有什么事件,甚至拦截接下来的事件。

为什么要先一步呢,因为我们在自定义view的时候,可以很方便的重写onTouchEvent方法,但是如果使用的是系统控件,就不能那么方便的得到这些事件了,如果这时候可以巧妙的使用setOnTouchListener,那么就能先一步得到这些事件了。

第一个if的条件分析完了,为了避免再次你们继续翻上去看那个方法,无形增加后摇时间,所以我重新复制一下:

第一个if我们只看了条件,内容就一个return true,我们来看看第二个if,条件居然直接就是onTouchEvent方法的返回值,内容体也是return true。

那我们就根据这点代码来总结一下吧!

不过还是有不必要的代码,我再简化一下吧:

这样看的是不是就足够清楚了,代码就是这样,剔除了那些非核心代码后,核心代码其实就短短几句。

第一个if,我们可以看到,其实这里的onTouch方法是我们手动实现的,使用setOnTouchListener,就可以在这个设置的接口里面具体实现onTouch的内容了。

然后由于我们还可以把控onTouch的返回值,如果我们将onTouch的返回值设为true,那么第一个if就结束了,dispatchTouchEvent也就直接结束了,那么第二个if就不会执行了,相当于我们可以通过onTouch的返回值,直接拦截view自己实现的onTouchEvent方法。假设有个自定义的DragView,可以想拖哪就拖到哪,如果给这个类setOnTouchListener,那么这个控件的拖动方式就全凭你管了啊,想想都刺激。

所以使用setOnTouchListener可以拦截onTouchEvent方法,默默记住这个知识点。

然后我们接着看第二个if,好像onTouchEvent也可以拦截这个if下面的代码哈,然后其他的就没啥了,反正这个if下面又没有什么触摸事件了,这里这个onTouchEvent的返回值是true是false应该都没啥关系了吧,如果你这样想,那么你就错了。别忘了,onTouchEvent可再也不是我们实现的了,这是系统实现的,那还不赶紧进来看看,里面长啥样。

onTouchEvent

老规矩,把API 15的源码搬上来:

public boolean onTouchEvent(MotionEvent event) {
        final int viewFlags = mViewFlags;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PRESSED) != 0) {
                mPrivateFlags &= ~PRESSED;
                refreshDrawableState();
            }
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn"t respond to them.
            return (((viewFlags & CLICKABLE) == CLICKABLE ||
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
        }

        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
                    if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
                        // take focus if we don"t have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            mPrivateFlags |= PRESSED;
                            refreshDrawableState();
                       }

                        if (!mHasPerformedLongPress) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }
                        removeTapCallback();
                    }
                    break;

                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we"re inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        mPrivateFlags |= PRESSED;
                        refreshDrawableState();
                        checkForLongClick(0);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    mPrivateFlags &= ~PRESSED;
                    refreshDrawableState();
                    removeTapCallback();
                    break;

                case MotionEvent.ACTION_MOVE:
                    final int x = (int) event.getX();
                    final int y = (int) event.getY();

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        removeTapCallback();
                        if ((mPrivateFlags & PRESSED) != 0) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback();

                            // Need to switch from pressed to not pressed
                            mPrivateFlags &= ~PRESSED;
                            refreshDrawableState();
                        }
                    }
                    break;
            }
            return true;
        }

        return false;
}

好了好了,我知道你们直接跳过来了,知道你们不会看,所以我准备了一份终极简化版:

是不是看着眼睛干净多了,去掉的大概都是一些什么view是否可用,能不能点击之类,各种对象的处理和判断啦,大概就这些东西,总之不影响我们研究核心的内容。

可以看到onTouchEvent的返回值直接就是true,也就可以认为是事件分发的终点了。那么我们来看看这个方法里面做了什么事,首先switch区分了触摸事件的类型,上抬下按什么的,然后我们发现手指上抬的时候,执行了一个performClick,关于onTouchEvent方法,其他就没什么好说的了。既然如此,我们就来大概看看performClick里面是些什么东西了,不过我想你们应该猜到了。

performClick

直接上终极简化版吧,一目了然的感觉真好。

没错,就是回调了setOnClickListener里面设置的接口。

讲到这里,那view的事件分发基本就算讲完了,顺便一提,关于

现在我们也就知道了,如果onTouchEvent返回false,会影响的就是点击事件了,也就是说,如果我们在重写onTouchEvent的时候,如果返回值是false,那么就没有点击事件了,不过你要把点击事件设置到手指刚刚触碰到屏幕的那一刻也行。

view 事件分发总结

现在我们已经看完了view的整个事件分发的流程源码,重要方法也差不多了解了,那么现在我们来归纳总结一下。

大概总结一下就是:

  • 事件分发最开始是在dispatchTouchEvent这里,这个方法主要是将触摸事件传给onTouch和onTouchEvent。

  • onTouch是一个接口的方法,所以我们可以通过setOnTouchListener来自主实现onTouch里面的内容。

  • 通过控制onTouch方法的返回值,我们可以决定是否拦截系统实现的onTouchEvent方法。

  • onTouchEvent方法里面的触摸行为分为很多种,比如手指下按上抬什么的,当手指上抬的时候,onTouchEvent里面会执行点击操作。

  • 当我们在自定义view的时候,重写onTouchEvent时,如果onTouchEvent的返回值设为false,将不会执行点击操作,不过既然都在重写onTouchEvent了,内部你要怎么实现你的点击事件都可以= =

  • 所以,View的事件分发可以这样说,主要方法:

    顺序就是这样一个顺序,上面的方法可以拦截下面的方法,这里的拦截是指不让下面的方法运行。不过我们主要了解的还是后面3个方法。


    ViewGroup


    说完了view的事件后,我们来谈谈ViewGroup的触摸事件,ViewGroup的触摸事件跟View的触摸事件大体上都差不多,只是有一个地方不一样,举个例子?假设我们现在写了这样两个类:

    两个类,一个是ViewGroup,一个是View,就打印下日志,其他啥也没有了。将这个View放进ViewGroup中,我们运算一下试试。打印结果是:

    有疑问吗?

    View倒是没有啥问题,但是这个ViewGroup就。。。

    为啥ViewGroup没有调用onTouch、onTouchEvent、onClick这三个方法,根据view的事件分发机制,我们可以猜测肯定是ViewGroup里面某个方法把onTouch、onTouchEvent、onClick这三个方法给拦截了,既然ViewGroup的dispatchTouchEvent打印出来了,其他的方法却没有打印出来,肯定是dispatchTouchEvent里面做了什么有拦截性质的操作,让我们在源码较少的API 15里面去寻找答案。

    看源码也不知道是哪里,算了看注释吧,突然发现注释里面有一个注释是Check for interception,拦截检查?听名字靠谱!认真瞧瞧:

    代码还是有多又看不懂,不过这个intercepted变量肯定是关键,然后看到了在哪里赋值后,这个方法在我眼中已经变成如下模样了:

    onInterceptTouchEvent这个方法翻译过来“拦截触摸事件”,还有返回值?哇,跟View的那些触摸事件很像啊,这个返回值肯定就是控制拦截的,不管三七二十一,我们先看看这个方法的源码:

    是我眼花了吗,还能有这么简单的源码?就返回一个false,根据我们对View的了解,这里返回false,应该是没有拦截才对啊,等等!我们先做个实验。在自定义的ViewGroup里面,重写onInterceptTouchEvent方法,直接返回true,看看效果,用实践出真理。

    然后运行一下再看看:

    哇,全是ViewGroup的东西,原来onInterceptTouchEvent拦截的是View里面的触摸事件啊!

    所以这里的开关就是这个onInterceptTouchEvent的返回值,如果是false就走View的事件,如果是true,就走ViewGroup的事件。

    既然View跟ViewGroup的事件分发机制都摸清楚了,那么我们就来总结一下吧!


    总结


    View 事件分发

    首先说说View的事件分发机制,虽然前面已经总结过一次了,不过在这里再总结一次。

    dispatchTouchEvent(MotionEvent ev)负责处理MotionEvent这些触摸事件,然后按照顺序,这里有3个方法:

    用动画的方式看怎么样?

    正常情况下,程序就跟着这个顺序执行下去了:

    我们可以使用setOnTouchListener来实现onTouch,然后可以通过控制onTouch的返回值,来决定是否继续执行下面的两个方法,返回值为true,则不继续执行,为false,则继续执行。像这样?

    为false,我就不做图了,跟图2类似。

    这里面三个方法,前面执行的方法有决策权,可以决定是否执行他之后的方法,返回值为true,则不执行之后的方法,为false则执行之后的方法。

    ViewGroup事件分发

    老实说,ViewGroup的事件分发机制跟View基本一样,毕竟ViewGroup继承View嘛。跟事件有关的那几个方法也是一样的,都是:

    不过如果执行了ViewGroup默认执行View的这三个方法,不会执行ViewGroup的这三个方法,如果想要执行ViewGroup的这三个方法,我们必须修改ViewGroup的onInterceptTouchEvent方法的返回值,为true则可以执行ViewGroup的触摸事件,为false则执行View的触摸事件。


    欢迎长按下图 -> 识别图中二维码

    或者 扫一扫 关注我的公众号

    2019-07-17
    汽车导购 双十一别再瞎买了!「资深剁手党」将送你一份“摄影器材剁手指南”
    一年一度的“双十一”陆续吹响号角 防剁手?不存在的,我们是要让你们更加高质量地剁手! 吃土这种事情,要一起才比较好吃嘛! ....... 其实早在前几天... <详情>
    2019-07-17
    汽车导购 “被误伤”的王晓翠声明:请立即停止对我伤害!
    点击上方蓝字“车与舆”关注本公众号 2018年丨第 183 期 昨天清晨,在微信群、朋友圈和一些自媒体不断发酵的媒体老师“早餐风波”,让大连市区一位名叫“王晓... <详情>
    2019-07-17
    汽车导购 本周发售一览 | 再次补货的“白斑马”,能圆得了你的 YEEZY 梦吗?
    当全民YEEZY的时代来临,整个11月的球鞋发售都被YEEZY串了起来。本周,红极一时的“白斑马”YEEZY BOOST 350 V2 Zebra将再度迎来发售... <详情>
    2019-07-17
    汽车导购 2018年末创业看什么?新的创富风又是什么?【选择比专业重要!】
    特色餐饮加盟店排行榜每周推荐1个开店项目关注 导读:要明白,思维远比勤奋更重要,选择远比专Y重要,你关注什么,你就会成为什么! 在“万众创新,万众创业”的提倡... <详情>
    2019-07-17
    汽车导购 还记得昔日天津男篮的外援杰特吗?他又回到中国了
    天津男篮的老熟人杰特又回到了大家的视线中。11月2日,福建男篮官方宣布:由于拉斯-史密斯遭遇伤病,球队决定更换外援,而替代史密斯的人选所有人都不陌生。他就是曾在... <详情>
    2019-07-17
    汽车导购 什么叫实力?862km续航的它一到,混动市场就要变天了
    戳“阅读原文”↓↓↓↓ 加入【爱买车/My车轱辘车友群】 <详情>