zoukankan      html  css  js  c++  java
  • Android FrameWork——Touch事件派发过程详解

    对于android的窗口window管理,一直感觉很混乱,总想找个时间好好研究,却不知如何入手,现在写的Touch事件派发过程详解,其实跟 android的窗口window管理服务WindowManagerService存在紧密联系,所以从这里入手切入到 WindowManagerService的研究,本blog主要讲述一个touch事件如何从用户消息的采集,到 WindowManagerService对Touch事件的派发,再到一个Activity窗口touch事件的派发,并着重讲了Activity窗口 touch事件的派发,因为这个的理解对我们写应用很好地处理touch事件很重要

    一.用户事件采集到WindowManagerService和派发

    --1.WindowManagerService,顾名思义,它是是一个窗口管理系统服务,它的主要功能包含如下:
            --窗口管理,绘制
            --转场动画--Activity切换动画
            --Z-ordered的维护,Activity窗口显示前后顺序
            --输入法管理
            --Token管理
            --系统消息收集线程
            --系统消息分发线程
    这里,我关注的是系统消息的收集和系统消息的分发,其他功能,当我对WindowManagerService有一个完整的研究后在发blog

    --2.系统消息收集和分发线程的创建
    这个的从WindowManagerService服务的创建说起,与其他系统服务一样,WindowManagerService在systemServer中创建的:
    ServerThread.run
    -->WindowManagerService.main
       -->WindowManagerService.WMThread.run(构建一个专门线程负责WindowManagerService)
          -->WindowManagerService s = new WindowManagerService(mContext, mPM,mHaveInputMethods);
             --mQueue = new KeyQ();//消息队列,在构造KeyQ中会创建一个InputDeviceReader线程去读取用户输入消息
             --mInputThread = new InputDispatcherThread();//创建一个消息分发线程,读取并处理mQueue中消息

    整个过程处理原理很简单,典型的生产者消费者模型,我先画个图,后面针对代码进一步说明

    --3.InputDeviceReader线程,KeyQ构建时,会启动一个线程去读取用户消息,具体代码在KeyInputQueue.mThread,在构造函数中,mThread会start,接下来,接研究一下mThread.run:
        //用户输入事件消息读取线程
        Thread mThread = new Thread("InputDeviceReader") {
            public void run() {
                RawInputEvent ev = new RawInputEvent();
                while (true) {//开始消息读取循环
                    try {
                        InputDevice di;
                        //本地方法实现,读取用户输入事件
                        readEvent(ev);
                        //根据ev事件进行相关处理
                        ...
                        synchronized (mFirst) {//mFirst是keyQ队列头指针
                        ...
                        addLocked(di, curTimeNano, ev.flags,RawInputEvent.CLASS_TOUCHSCREEN, me);
                        ...
                        }
                    }
            }
           }
    函数我也没有看大明白:首先调用本地方法readEvent(ev);去读取用户消息,这个消息包括按键,触摸,滚轮等所有用户输入事件,后面不同的事件类型会有不同的处理,不过最后事件都要添加到keyQ的队列中,通过addLocked函数

    --4队列添加和读取函数addLocked,getEvent
    addLocked函数比较简单,就分析一下,有助于对消息队列KeyQ的数据结构进行理解:
        //event加入inputQueue队列
        private void addLocked(InputDevice device, long whenNano, int flags,
                int classType, Object event) {
            boolean poke = mFirst.next == mLast;//poke为true表示消息队列为空
            //从QueuedEvent缓存QueuedEvent获取一个QueuedEvent对象,并填入用户事件数据,包装成一个QueuedEvent
            QueuedEvent ev = obtainLocked(device, whenNano, flags, classType, event);
            QueuedEvent p = mLast.prev;//队列尾节点为mLast,把ev添加到mlast前
            while (p != mFirst && ev.whenNano < p.whenNano) {
                p = p.prev;
            }
            ev.next = p.next;
            ev.prev = p;
            p.next = ev;
            ev.next.prev = ev;
            ev.inQueue = true;

            if (poke) {//poke为true,意味着在空队列中添加了一个QueuedEvent,这时系统消息分发线程可能在wait,需要notify一下
                long time;
                if (MEASURE_LATENCY) {
                    time = System.nanoTime();
                }
                mFirst.notify();//唤醒在 mFirst上等待的线程
                mWakeLock.acquire();
                if (MEASURE_LATENCY) {
                    lt.sample("1 addLocked-queued event ", System.nanoTime() - time);
                }
            }
        }
    很简单,使用mFirst,mLast实现的指针队列,addLocked是QueuedEvent对象添加函数,对应在系统消息分发线程中会有一个getEvent函数来读取inputQueue队列的消息,我在这里也先讲一下:
        QueuedEvent getEvent(long timeoutMS) {
            long begin = SystemClock.uptimeMillis();
            final long end = begin+timeoutMS;
            long now = begin;
            synchronized (mFirst) {//获取mFirst上同步锁
                while (mFirst.next == mLast && end > now) {
                    try {//mFirst.next == mLast意味队列为空,同步等待mFirst锁对象
                        mWakeLock.release();
                        mFirst.wait(end-now);
                    }
                    catch (InterruptedException e) {
                    }
                    now = SystemClock.uptimeMillis();
                    if (begin > now) {
                        begin = now;
                    }
                }
                if (mFirst.next == mLast) {
                    return null;
                }
                QueuedEvent p = mFirst.next;//返回mFirst的下一个节点为处理的QueuedEvent
                mFirst.next = p.next;
                mFirst.next.prev = mFirst;
                p.inQueue = false;
                return p;
            }
        }

    通过上面两个函数得知,消息队列是通过mFirst,mLast实现的生产者消费模型的同步链表队列

    --5.InputDispatcherThread线程
    InputDispatcherThread处理InputDeviceReader线程存放在KeyInputQueue队列中的消息,分发到具体的一个客户端的IWindow
    InputDispatcherThread.run
    -->windowManagerService.process{               
                ...
                while (true) {               
                    // 从mQueue(KeyQ)获取一个用户输入事件,正上调用我上面提到的getEvent方法,若队列为空,线程阻塞挂起
                    QueuedEvent ev = mQueue.getEvent(
                        (int)((!configChanged && curTime < nextKeyTime)
                                ? (nextKeyTime-curTime) : 0));
                    ...
                    try {
                        if (ev != null) {
                            ...
                            if (ev.classType == RawInputEvent.CLASS_TOUCHSCREEN) {//touch事件
                                eventType = eventType((MotionEvent)ev.event);
                            } else if (ev.classType == RawInputEvent.CLASS_KEYBOARD ||
                                        ev.classType == RawInputEvent.CLASS_TRACKBALL) {//键盘输入事件
                                eventType = LocalPowerManager.BUTTON_EVENT;
                            } else {
                                eventType = LocalPowerManager.OTHER_EVENT;//其他事件
                            }
                            ...
                            switch (ev.classType) {
                                case RawInputEvent.CLASS_KEYBOARD:
                                    ...
                                    dispatchKey((KeyEvent)ev.event, 0, 0);//键盘输入,派发key事件
                                    mQueue.recycleEvent(ev);
                                    break;
                                case RawInputEvent.CLASS_TOUCHSCREEN:
                                    dispatchPointer(ev, (MotionEvent)ev.event, 0, 0);//touch事件,派发touch事件
                                    break;
                                case RawInputEvent.CLASS_TRACKBALL:
                                    dispatchTrackball(ev, (MotionEvent)ev.event, 0, 0);//滚轮事件,派发Trackball事件
                                    break;
                                case RawInputEvent.CLASS_CONFIGURATION_CHANGED:
                                    configChanged = true;
                                    break;
                                default:
                                    mQueue.recycleEvent(ev);//销毁事件
                                break;
                            }

                        }
                    } catch (Exception e) {
                        Slog.e(TAG,
                            "Input thread received uncaught exception: " + e, e);
                    }
                }       
       }

    WindowManagerService.dispatchPointer,一旦判断QueuedEvent为屏幕点击事件,就调用函数WindowManagerService.dispatchPointer进行处理:
    WindowManagerService.dispatchPointer
    -->WindowManagerService.KeyWaiter.waitForNextEventTarget(获取touch事件要派发的目标windowSate)
       -->WindowManagerService.KeyWaiter.findTargetWindow(从一个一个WindowSate的z-order顺序列表mWindow中获取一个能够接收当前touch事件的WindowSate)
    -->WindowSate target = waitForNextEventTarget返回的WindowSate对象
    -->target.mClient.dispatchPointer(ev, eventTime, true);(往目标window派发touch消息
    target.mClient是一个IWindow代理对象IWindow.Proxy,它对应的代理类是ViewRoot.W,通过远程代理调用,WindowManagerService把touch消息派发到了对应的Activity的PhoneWindow
    之后进一步WindowManagerService到Activity消息的派发在下文中说明

    二WindowManagerService派发Touch事件到当前top Activity

    --1.先我们看一个system_process的touch事件消息调用堆栈,在WindowManagerService中的函数 dispatchPointer,通过一个IWindow的客户端代理对象把消息发送到相应的IWindow服务端,也就是一个IWindow.Stub 子类。
    Thread [<21> InputDispatcher] (Suspended (breakpoint at line 321 in IWindow$Stub$Proxy))       
            IWindow$Stub$Proxy.dispatchPointer(MotionEvent, long, boolean) line: 321       
            WindowManagerService.dispatchPointer(KeyInputQueue$QueuedEvent, MotionEvent, int, int) line: 5270              
            WindowManagerService$InputDispatcherThread.process() line: 6602       
            WindowManagerService$InputDispatcherThread.run() line: 6482  

    --2.通过IWindow.Stub.Proxy代理对象把消息传递给IWindow.Stub对象。 code=TRANSACTION_dispatchPointer,IWindow.Stub对象被ViewRoot拥有(成员mWindow,它是一 个ViewRoot.W类对象)

    --3.在case TRANSACTION_dispatchPointer会调用IWindow.Stub子类的实现方法dispatchPointer

    --4.IWindow.Stub.dispatchPointer
            -->ViewRoot.W.dispatchPointer
                    -->ViewRoot.dispatchPointer
        public void dispatchPointer(MotionEvent event, long eventTime,
                boolean callWhenDone) {
            Message msg = obtainMessage(DISPATCH_POINTER);
            msg.obj = event;
            msg.arg1 = callWhenDone ? 1 : 0;
            sendMessageAtTime(msg, eventTime);
        }

    --5.ViewRoot继承自handle,在handleMessage函数的case-DISPATCH_POINTER会调用mView.dispatchTouchEvent(event),
    mView是一个PhoneWindow.DecorView对象,在PhoneWindow.openPanel方法会创建一个ViewRoot对象, 并设置ViewRoot对象的mView为一个PhoneWindow.decorView成员,PhoneWindow.DecorView是真正的 root view,它继承自FrameLayout,这样调用mView.dispatchTouchEvent(event)
    其实就是调用PhoneWindow.decorView的dispatchTouchEvent方法:
            @Override
            public boolean dispatchTouchEvent(MotionEvent ev) {
                final Callback cb = getCallback();
                return cb != null && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super
                        .dispatchTouchEvent(ev);
            } 

    --6.分析上面一段红色代码,可以写成return (cb != null) && (mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev)).当cb不为null执行后面,如果mFeatureId<0,执行 cb.dispatchTouchEvent(ev),否则执行super.dispatchTouchEvent(ev),也就是 FrameLayout.dispatchTouchEvent(ev),那么callback cb是什么呢?是Window类的一个成员mCallback,我下面给一个图你可以看到何时被赋值的:
    setCallback(Callback) : void - android.view.Window
            -->attach(Context, ActivityThread, Instrumentation, IBinder, int, Application, Intent, ActivityInfo, CharSequence, Activity, String, Object, HashMap<String, Object>, Configuration) : void - android.app.Activity
                   --> performLaunchActivity(ActivityRecord, Intent) : Activity - android.app.ActivityThread
    performLaunchActivity我们很熟识,因为我前面在讲Activity启动过程详解时候讲过,在启动一个新的Activity会执行该方法,在该方法里面会执行attach方法,找到attach方法对应代码可以看到:
            mWindow = PolicyManager.makeNewWindow(this);
            mWindow.setCallback(this);
    mWindow就是一个PhoneWindow,它是Activity的一个内部成员,通过调用mWindow的setCallback(this),把新建立的Activity设置为PhoneWindow一个mCallback成员,这样我们就清楚了,前面的cb就是拥有这个PhoneWindow的Activity,cb.dispatchTouchEvent(ev)也就是执行:Activity.dispatchTouchEvent
        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                onUserInteraction();
            }
            //getWindow()返回的就是PhoneWindow对象,执行superDispatchTouchEvent,就是执行PhoneWindow.superDispatchTouchEvent
            if (getWindow().superDispatchTouchEvent(ev)) {
                return true;
            }
            //执行Activity.onTouchEvent方法
            return onTouchEvent(ev);
        }

    --7.再看PhoneWindow.superDispatchTouchEvent:
        @Override
        public boolean superDispatchTouchEvent(MotionEvent event) {
            return mDecor.superDispatchTouchEvent(event);
                    -->        public boolean superDispatchTouchEvent(MotionEvent event) {
                                        return super.dispatchTouchEvent(event);//FrameLayout.dispatchTouchEvent
            }
        }
    superDispatchTouchEvent调用super.dispatchTouchEvent,我前面讲过mDector是一个 PhoneWindow.DecorView,它是一个真正Activity的root view,它继承了FrameLayout,通过super.dispatchTouchEvent他会把touchevent派发给各个 activity的子view,也就是我们再Activity.onCreat方法中setContentView时设置的view,touch event时间如何在Activity各个view中进行派发的我后面再作详细说明,但是从上面我们可以看出一点若Activity下面的子view拦截了touchevent事件(返回true),Activity.onTouchEvent就不会执行。

    --8.这部分,我再画一个静态类结构图把前面讲到的一些类串起来看一下:

    我用红色箭头线把整个消息派发过程过程给串起来,然后system_process进程和ap进程分别用虚线椭圆圈起,这样以后相信你更理解各个类之间关系。

    对应的对象空间图如下,与上面图是对应的,只是从不同角度去看:

    --9.其实上面所讲的大部分已经是在客户端ap中执行了,也就是在ap进程中,只是执行逻辑基本是框架代码中,还没有到达我们使用 layout.xml布局的view中来,这里我先在我们的一个view中onTouchEvent插入一个断点看一看消息从 WindowManagerService到达Activity.PhoneWindow后执行堆栈情况(我插入的断点在Launcher2的 HandleView中),后面继续讲解:
    Thread [<1> main] (Suspended (breakpoint at line 4280 in View))       
            HandleView(View).onTouchEvent(MotionEvent) line: 4280       
            HandleView.onTouchEvent(MotionEvent) line: 71       
            HandleView(View).dispatchTouchEvent(MotionEvent) line: 3766       
            RelativeLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 863       
            DragLayer(ViewGroup).dispatchTouchEvent(MotionEvent) line: 863       
            FrameLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 863       
            PhoneWindow$DecorView(ViewGroup).dispatchTouchEvent(MotionEvent) line: 863       
            PhoneWindow$DecorView.superDispatchTouchEvent(MotionEvent) line: 1671       
            PhoneWindow.superDispatchTouchEvent(MotionEvent) line: 1107       
            ForyouLauncher(Activity).dispatchTouchEvent(MotionEvent) line: 2086       
            PhoneWindow$DecorView.dispatchTouchEvent(MotionEvent) line: 1655       
            ViewRoot.handleMessage(Message) line: 1785       
            ViewRoot(Handler).dispatchMessage(Message) line: 99       
            Looper.loop() line: 123       
            ActivityThread.main(String[]) line: 4634

    三.Activity中View中的Touch事件派发

    --1.首先我画一个Activity中的view层次结构图:

    前面我讲过,来自windowManagerService的touch消息最终会派发到到Decorview,Decorview继承子 FrameLayout,它只有一个子view就是mContentParent,我们写ap的view全部添加到到mContentParent。

    --2.了解了Activity中的view的层次结构,那先从DecorView开始看touch事件是如何被派发的,前面讲过最终消息会派发到 FrameLayout.dispatchTouchEvent也就是 ViewGroup.dispatchTouchEvent(FrameLayout也没有覆盖该方法),
    同样mContentParent也是执行ViewGroup.dispatchTouchEvent来派发touch消息,那我们就详细看一下ViewGroup.dispatchTouchEvent(若要很好掌握应用程序touch事件处理,这部分要重点看):
        public boolean dispatchTouchEvent(MotionEvent ev) {
            ......
            boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//计算是否禁止touch Intercept
            if (action == MotionEvent.ACTION_DOWN) {//按下事件,也就是touch开始
                if (mMotionTarget != null) {
                    mMotionTarget = null;//清除mMotionTarget,也就是说每次touch开始,mMotionTarget要被重新设置
                }
                if (disallowIntercept || !onInterceptTouchEvent(ev)) {//判断消息是否需要被viewGroup拦截
                    // 消息不被viewGroup拦截,找到相应的子view进行touch事件派发
                    ev.setAction(MotionEvent.ACTION_DOWN);//重新设置event 为action_down
                  
                    final int scrolledXInt = (int) scrolledXFloat;
                    final int scrolledYInt = (int) scrolledYFloat;
                    final View[] children = mChildren;//获取viewgroup所有的子view
                    final int count = mChildrenCount;
                    for (int i = count - 1; i >= 0; i--) {
                        final View child = children[i];
                        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                                || child.getAnimation() != null) {//若子view可见或者有动画在执行的,才能够接收touch事件
                            child.getHitRect(frame);//获取子view的布局坐标区域
                            if (frame.contains(scrolledXInt, scrolledYInt)) {//若子view 区域包含当前touch点击区域
                                // offset the event to the view's coordinate system
                                final float xc = scrolledXFloat - child.mLeft;
                                final float yc = scrolledYFloat - child.mTop;
                                ev.setLocation(xc, yc);
                                child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                                if (child.dispatchTouchEvent(ev))  {//派发TouchEvent给包含这个touch区域的子view
                                    // 若该子view消费了对应的touch事件
                                    mMotionTarget = child;//设置viewgroup消息派发的目标子view
                                    return true;//返回true,该touch事件被消费掉
                                }
                            }
                        }
                    }
                }
              //若touch事件被拦截,mMotionTarget = null,后面touch消息不再派发给子view
            }

            boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||//计算是up或者cancel
                    (action == MotionEvent.ACTION_CANCEL);

            if (isUpOrCancel) {
                mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
            }

          
            final View target = mMotionTarget;
            if (target == null) {
                //target为null,意味着在ACTION_DOWN时没有找到能消费touch消息的子view或者在ACTION_DOWN时消息被拦截了,这个时候
                //调用父类view的dispatchTouchEvent消息进行派发,也就是说,此时viewgroup处理touch消息跟普通view一致。
                ev.setLocation(xf, yf);
                if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
                    ev.setAction(MotionEvent.ACTION_CANCEL);
                    mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                }
                return super.dispatchTouchEvent(ev);
            }

            //target!=null,意味在ACTION_DOWN时touch消息没有被拦截,而且子view target消费了ACTION_DOWN消息,需要再判断消息是否被拦截
            if (!disallowIntercept && onInterceptTouchEvent(ev)) {
                //消息被拦截,而前面ACTION_DOWN时touch消息没有被拦截,所以需要发送ACTION_CANCEL通知子view target
                final float xc = scrolledXFloat - (float) target.mLeft;
                final float yc = scrolledYFloat - (float) target.mTop;
                mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                ev.setAction(MotionEvent.ACTION_CANCEL);
                ev.setLocation(xc, yc);
                if (!target.dispatchTouchEvent(ev)) {
                    // 派发消息ACTION_CANCEL给子view target
                }
                // mMotionTarget=null,后面消息不再派发给子view
                mMotionTarget = null;
                return true;
            }

            if (isUpOrCancel) {
                //isUpOrCancel,设置mMotionTarget=null,后面消息不再派发给子view
                mMotionTarget = null;
            }

            ......
            //没有被拦截继续派发消息给子view target
            return target.dispatchTouchEvent(ev);
        }

    --3.ViewGroup.dispatchTouchEvent我查看了一下所有子类,只有PhoneWindow.DecorView覆盖了 该方法,该方法前面讲DecorView消息派发时提过,它会找到对应包含这个PhoneWindow.DecorView对象的Activity把消息 交给Activity去处理,其它所有viewGroup的子类均没有覆盖dispatchTouchEvent,也就是说所有包含子view的父 view对于touch消息派发均采用上面的逻辑,当然,必要的时候我们可以覆盖该方法实现自己的touch消息派发逻辑,如Launcher2中的 workspace类就是重新实现的该dispatchTouchEvent方法,从上面的dispatchTouchEvent函数逻辑其实我们也可以 总结几条touch消息派发逻辑:
    (1).onInterceptTouchEvent用来定义是否截取touch消息逻辑,若在groupview中想截取touch消息,必须覆盖viewgroup中该方法
    (2).消息在整个dispatchTouchEvent过程中,若子 view.dispatchTouchEvent返回true,父view中将不再处理该消息,但前提是该消息没有被父view截取,在整个touch消 息处理过程中,若处理函数返回true,我们称之为消费了该touch事件,并且后面的父view将不再处理该消息。
    (3).在整个touch事件过程中,从action_down到action_up,若父ViewGroup的函数onInterceptTouchEvent一旦返回true,消息将不再派发给子view,细分可为两种情况,若是在action_down时onInterceptTouchEvent返回true,不会派发任何消息给子view,并且后面onInterceptTouchEvent函数将不再会被执行若是action_down时onInterceptTouchEvent返回false ,而后面touch过程中onInterceptTouchEvent==true,父viewGroup会把action_cancel派发给子view,也之后不再派发消息给子view,并且onInterceptTouchEvent函数后面将不再被执行。

    --4.为了更清楚的理解viewGroup消息的派发流程,我画一个流程图如下:

    --5.上面我只是讲了父view与子view之间当有touch事件的消息派发流程,对于view的消息是怎么派发的(也包裹viewGroup 没有子view或者有子view但是不消费该touch消息情况),因为从继承结构上看viewgroup继承了view,viewgroup覆盖了 view的dispatchTouchEvent方法,不过从上面流程图也可以看到当mMotionTarget为Null它会执行父类 view.dispatchTouchEvent,其他view的子类都是执行view.dispatchTouchEvent派发touch事件,不过 若我们自定义view是可以覆盖该方法的。下面就仔细研究一下view.dispatchTouchEvent方法的代码:
        public final boolean dispatchTouchEvent(MotionEvent event) {
            //mOnTouchListener是被View.setOnTouchListener设置的,(mViewFlags & ENABLED_MASK)计算view是否可被点击
            //当view可被点击并且mOnTouchListener被设置,执行mOnTouchListener.onTouch
            if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
                    mOnTouchListener.onTouch(this, event)) {
                return true;//若mOnTouchListener.onTouch返回true,函数返回true
            }
            return onTouchEvent(event);//若mOnTouchListener.onTouch返回false,调用onToucheEvent
        }
    函数逻辑很简单,前面的viewGroup touch事件流程图中我已经画出的,为区别我把它着色成青绿色,总结一句话若mOnTouchListener处理了touch消息,不执行onTouchEvent,否则交给onTouchEvent进行处理。
    不知道是否讲清楚的,要清楚掌握估计还得写些例子测试一下是否是我上面所说的流程,不过我想了解事件的派发流程,对写应用的事件处理相信很有用,比如我以 前碰到一个问题是手指点击屏幕到底是子view执行onclick还是执行父view的view移动,这个时候就需要深入了解viewde touch事件派发流程,该响应点击的时候响应子view的点击,该父view移动的时候拦截touch事件交给父view进行处理。

    原文:http://blog.csdn.net/stonecao/article/details/6759189

  • 相关阅读:
    阈值处理——实例分析
    阈值处理
    split()函数+merge()函数
    imread函数+cvtColor()函数
    OpenCV3.2.0+VS2015开发环境配置
    Javascript中的async await
    React Native 系列(一)
    React Native 系列(三)
    React Native 系列(六)
    React Native 系列(七)
  • 原文地址:https://www.cnblogs.com/xiaoxiaoboke/p/2342897.html
Copyright © 2011-2022 走看看