关于Android的Touch事件传递机制,只是知道事件传入Activity后的流程,但是这些事件是如何传递给Activity的一直模糊不清。现在再来好好回顾一遍,顺道整理一点儿东西出来,同时分享给大家。

前面用源码带着大家分享过View的创建和加载,以及View的绘制流程。

手把手教你读懂源码,View的加载流程详细剖析

手把手教你读懂源码,View的绘制流程详细剖析

今天就随着Android源码一起来探寻一番Android中各View的Touch事件到底是怎么注册和接收的,虽然有一些大神做过分享,但是源码比较老旧,而且通过自己研究会掌握的更透彻一些。本文章主要分析Java部分,底层C++部分暂不分析。


Android输入系统的主要工作是读取设备节点中的原始事件,将其加工封装,然后派发给一个特定的窗口以及窗口中的控件,如一个活动状态的Activity界面中的Button。

同时我们知道Android系统是Linux内核的,它的事件处理也是在Linux的基础上完成的,因此我们从Linux 内核往应用这个方向慢慢理清它的大致处理过程。首先来看事件是如何注册的。

1、事件注册

从之前的View加载流程源码分析知道(如果不清楚,建议返回先回顾View的加载流程),在ActivityThread中的handleResumeActivity方法会调用wm.addView()方法,将View添加到mWindowManger上,即WindowManagerImpl类的addView方法,而其又是由WindowManagerGlobal代理的。在WindowManagerGlobal的addView方法中会先创建了一个ViewRootImpl对象,然后调用ViewRootImpl.setView()方法。

setView方法调用时序图

在setView方法中会找到如下代码:

    /*** We have one child*/public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {synchronized (this) {if (mView == null) {mView = view;...// Schedule the first layout -before- adding to the window// manager, to make sure we do the relayout before receiving// any other events from the system.requestLayout();if ((mWindowAttributes.inputFeatures& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {mInputChannel = new InputChannel();}mForceDecorViewVisibility = (mWindowAttributes.privateFlags& PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY) != 0;try {mOrigWindowType = mWindowAttributes.type;mAttachInfo.mRecomputeGlobalAttributes = true;collectViewAttributes();res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,getHostVisibility(), mDisplay.getDisplayId(),mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,mAttachInfo.mOutsets, mInputChannel);} catch (RemoteException e) {mAdded = false;mView = null;mAttachInfo.mRootView = null;mInputChannel = null;mFallbackEventHandler.setView(null);unscheduleTraversals();setAccessibilityFocus(null, null);throw new RuntimeException("Adding window failed", e);} finally {if (restore) {attrs.restore();}}...if (view instanceof RootViewSurfaceTaker) {mInputQueueCallback =((RootViewSurfaceTaker)view).willYouTakeTheInputQueue();}if (mInputChannel != null) {if (mInputQueueCallback != null) {mInputQueue = new InputQueue();mInputQueueCallback.onInputQueueCreated(mInputQueue);}mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,Looper.myLooper());}view.assignParent(this); mAddedTouchMode = (res & WindowManagerGlobal.ADD_FLAG_IN_TOUCH_MODE) != 0;mAppVisible = (res & WindowManagerGlobal.ADD_FLAG_APP_VISIBLE) != 0;if (mAccessibilityManager.isEnabled()) {mAccessibilityInteractionConnectionManager.ensureConnection();}if (view.getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);}// Set up the input pipeline.CharSequence counterSuffix = attrs.getTitle();mSyntheticInputStage = new SyntheticInputStage();InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,"aq:native-post-ime:" + counterSuffix);InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);InputStage imeStage = new ImeInputStage(earlyPostImeStage,"aq:ime:" + counterSuffix);InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,"aq:native-pre-ime:" + counterSuffix);mFirstInputStage = nativePreImeStage;mFirstPostImeInputStage = earlyPostImeStage;mPendingInputEventQueueLengthCounterName = "aq:pending:" + counterSuffix;}}}

会发现在requestLayout后创建一个InputChannel对象,然后会调用addToDisplay方法,继续跟踪分析:

    @Overridepublic int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,Rect outOutsets, InputChannel outInputChannel) {return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,outContentInsets, outStableInsets, outOutsets, outInputChannel);}

该方法继续调用了WindowManagerService的addWindow方法:

    public int addWindow(Session session, IWindow client, int seq,WindowManager.LayoutParams attrs, int viewVisibility, int displayId,Rect outContentInsets, Rect outStableInsets, Rect outOutsets,InputChannel outInputChannel) {int[] appOp = new int[1];int res = mPolicy.checkAddPermission(attrs, appOp);if (res != WindowManagerGlobal.ADD_OKAY) {return res;}boolean reportNewConfig = false;WindowState attachedWindow = null;long origId;final int callingUid = Binder.getCallingUid();final int type = attrs.type;synchronized(mWindowMap) {...WindowState win = new WindowState(this, session, client, token,attachedWindow, appOp[0], seq, attrs, viewVisibility, displayContent);if (win.mDeathRecipient == null) {// Client has apparently died, so there is no reason to// continue.Slog.w(TAG_WM, "Adding window client " + client.asBinder()+ " that is dead, aborting.");return WindowManagerGlobal.ADD_APP_EXITING;}if (win.getDisplayContent() == null) {Slog.w(TAG_WM, "Adding window to Display that has been removed.");return WindowManagerGlobal.ADD_INVALID_DISPLAY;}mPolicy.adjustWindowParamsLw(win.mAttrs);win.setShowToOwnerOnlyLocked(mPolicy.checkShowToOwnerOnly(attrs));res = mPolicy.prepareAddWindowLw(win, attrs);if (res != WindowManagerGlobal.ADD_OKAY) {return res;}final boolean openInputChannels = (outInputChannel != null&& (attrs.inputFeatures & INPUT_FEATURE_NO_INPUT_CHANNEL) == 0);if  (openInputChannels) {win.openInputChannel(outInputChannel);}...}if (reportNewConfig) {sendNewConfiguration();}Binder.restoreCallingIdentity(origId);return res;}

该方法先创建了一个WindowState对象,然后调用了openInputChannel方法:

    void openInputChannel(InputChannel outInputChannel) {if (mInputChannel != null) {throw new IllegalStateException("Window already has an input channel.");}String name = makeInputChannelName();InputChannel[] inputChannels = InputChannel.openInputChannelPair(name);mInputChannel = inputChannels[0];mClientChannel = inputChannels[1];mInputWindowHandle.inputChannel = inputChannels[0];if (outInputChannel != null) {mClientChannel.transferTo(outInputChannel);mClientChannel.dispose();mClientChannel = null;} else {// If the window died visible, we setup a dummy input channel, so that taps// can still detected by input monitor channel, and we can relaunch the app.// Create dummy event receiver that simply reports all events as handled.mDeadWindowEventReceiver = new DeadWindowEventReceiver(mClientChannel);}mService.mInputManager.registerInputChannel(mInputChannel, mInputWindowHandle);}

这一段代码主要就是创建一对InputChannel,同时这一对InputChannel中实现了一组全双工管道。InputChannel创建完成后,会将其中一个的native InputChannel 赋值给outInputChannel,也就是对ViewRootImpl端InputChannel对象的初始化,这样随着ViewRootImpl和WindowManagerService两端的InputChannel对象的创建,事件传输系统的管道通信也就建立了起来。后续的具体内容暂时不是分析重点,以后有机会再进行剖析。

按键、触屏等事件是经由WindowManagerService获取,并通过共享内存和管道的方式传递给ViewRootImpl,ViewRootImpl再调用dispatch给Application。当有事件从硬件设备输入时,system_server端在检测到事件发生时,通过管道(pipe)通知ViewRootImpl事件发生,此时ViewRootImpl再去内存中读取这个事件信息。

下面的结构图比较经典,是从网络上找来的,有一部分类对不上最新代码。其中InputManager变成了InputManagerService,ViewRoot变成了ViewRootImpl。

Android事件处理结构图

2、事件接收

通过上面的分析知道在addView时将事件传输系统的管道建立了起来,那么随后当Linux检测到事件发生,会经过层层传递到ViewRootImpl中,就来一起分析一下事件是如何从Native传递给Activity的。

继续来分析ViewRootImpl类的setView方法,方法结束前的代码先创建一个与当前窗口已经生成的InputChannel相关的接受输入事件的处理对象,最后设置当前各种不同类别输入事件到来时候按对应类型依次分别调用的处理对象。

上面分析的生成两个InputChannel输入事件通道,其中一个转移到当前顶层ViewRootImpl中并生成一个与输入事件通道关联的事件处理mInputEventReceiver对象,随着这条线索继续分析。

    final class WindowInputEventReceiver extends InputEventReceiver {public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {super(inputChannel, looper);}@Overridepublic void onInputEvent(InputEvent event) {enqueueInputEvent(event, this, 0, true);}@Overridepublic void onBatchedInputEventPending() {if (mUnbufferedInputDispatch) {super.onBatchedInputEventPending();} else {scheduleConsumeBatchedInput();}}@Overridepublic void dispose() {unscheduleConsumeBatchedInput();super.dispose();}}WindowInputEventReceiver mInputEventReceiver;

看到WindowInputEventReceiver类继承了InputEventReceiver类:

/*** Provides a low-level mechanism for an application to receive input events.* @hide*/
public abstract class InputEventReceiver {.../*** Called when an input event is received.* The recipient should process the input event and then call {@link #finishInputEvent}* to indicate whether the event was handled.  No new input events will be received* until {@link #finishInputEvent} is called.** @param event The input event that was received.*/public void onInputEvent(InputEvent event) {finishInputEvent(event, false);}...// Called from native code.@SuppressWarnings("unused")private void dispatchInputEvent(int seq, InputEvent event) {mSeqMap.put(event.getSequenceNumber(), seq);onInputEvent(event);}// Called from native code.@SuppressWarnings("unused")private void dispatchBatchedInputEventPending() {onBatchedInputEventPending();}public static interface Factory {public InputEventReceiver createInputEventReceiver(InputChannel inputChannel, Looper looper);}
}

注意InputEventReceiver类的dispatchInputEvent方法,当输入事件到来时该方法由native层代码发起调用,然后调用了onInputEvent(event)方法。从前面知道WindowInputEventReceiver类重写了onInputEvent方法,因此事件会传递到enqueueInputEvent方法。

    void enqueueInputEvent(InputEvent event,InputEventReceiver receiver, int flags, boolean processImmediately) {adjustInputEventForCompatibility(event);QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);// Always enqueue the input event in order, regardless of its time stamp.// We do this because the application or the IME may inject key events// in response to touch events and we want to ensure that the injected keys// are processed in the order they were received and we cannot trust that// the time stamp of injected events are monotonic.QueuedInputEvent last = mPendingInputEventTail;if (last == null) {mPendingInputEventHead = q;mPendingInputEventTail = q;} else {last.mNext = q;mPendingInputEventTail = q;}mPendingInputEventCount += 1;Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,mPendingInputEventCount);if (processImmediately) {doProcessInputEvents();} else {scheduleProcessInputEvents();}}

该方法先获取一个指向当前事件的输入事件队列QueuedInputEvent对象,最后调用doProcessInputEvents方法:

    void doProcessInputEvents() {// Deliver all pending input events in the queue.while (mPendingInputEventHead != null) {QueuedInputEvent q = mPendingInputEventHead;mPendingInputEventHead = q.mNext;if (mPendingInputEventHead == null) {mPendingInputEventTail = null;}q.mNext = null;mPendingInputEventCount -= 1;Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,mPendingInputEventCount);long eventTime = q.mEvent.getEventTimeNano();long oldestEventTime = eventTime;if (q.mEvent instanceof MotionEvent) {MotionEvent me = (MotionEvent)q.mEvent;if (me.getHistorySize() > 0) {oldestEventTime = me.getHistoricalEventTimeNano(0);}}mChoreographer.mFrameInfo.updateInputEventTime(eventTime, oldestEventTime);deliverInputEvent(q);}// We are done processing all input events that we can process right now// so we can clear the pending flag immediately.if (mProcessInputEventsScheduled) {mProcessInputEventsScheduled = false;mHandler.removeMessages(MSG_PROCESS_INPUT_EVENTS);}}

只要当前待处理事件队列还有事件需要处理,就一直循环调用deliverInputEvent方法:

    private void deliverInputEvent(QueuedInputEvent q) {Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, "deliverInputEvent",q.mEvent.getSequenceNumber());if (mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0);}InputStage stage;if (q.shouldSendToSynthesizer()) {stage = mSyntheticInputStage;} else {stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;}if (stage != null) {stage.deliver(q);} else {finishInputEvent(q);}}

这里会根据q.shouldSendToSynthesizer()和q.shouldSkipIme()判断给stage赋值,然后调用stage的deliver方法。

    /*** Base class for implementing a stage in the chain of responsibility* for processing input events.* <p>* Events are delivered to the stage by the {@link #deliver} method.  The stage* then has the choice of finishing the event or forwarding it to the next stage.* </p>*/abstract class InputStage {private final InputStage mNext;protected static final int FORWARD = 0;protected static final int FINISH_HANDLED = 1;protected static final int FINISH_NOT_HANDLED = 2;/*** Creates an input stage.* @param next The next stage to which events should be forwarded.*/public InputStage(InputStage next) {mNext = next;}/*** Delivers an event to be processed.*/public final void deliver(QueuedInputEvent q) {if ((q.mFlags & QueuedInputEvent.FLAG_FINISHED) != 0) {forward(q);} else if (shouldDropInputEvent(q)) {finish(q, false);} else {apply(q, onProcess(q));}}/*** Marks the the input event as finished then forwards it to the next stage.*/protected void finish(QueuedInputEvent q, boolean handled) {q.mFlags |= QueuedInputEvent.FLAG_FINISHED;if (handled) {q.mFlags |= QueuedInputEvent.FLAG_FINISHED_HANDLED;}forward(q);}/*** Forwards the event to the next stage.*/protected void forward(QueuedInputEvent q) {onDeliverToNext(q);}/*** Applies a result code from {@link #onProcess} to the specified event.*/protected void apply(QueuedInputEvent q, int result) {if (result == FORWARD) {forward(q);} else if (result == FINISH_HANDLED) {finish(q, true);} else if (result == FINISH_NOT_HANDLED) {finish(q, false);} else {throw new IllegalArgumentException("Invalid result: " + result);}}/*** Called when an event is ready to be processed.* @return A result code indicating how the event was handled.*/protected int onProcess(QueuedInputEvent q) {return FORWARD;}/*** Called when an event is being delivered to the next stage.*/protected void onDeliverToNext(QueuedInputEvent q) {if (DEBUG_INPUT_STAGES) {Log.v(mTag, "Done with " + getClass().getSimpleName() + ". " + q);}if (mNext != null) {mNext.deliver(q);} else {finishInputEvent(q);}}protected boolean shouldDropInputEvent(QueuedInputEvent q) {if (mView == null || !mAdded) {Slog.w(mTag, "Dropping event due to root view being removed: " + q.mEvent);return true;} else if ((!mAttachInfo.mHasWindowFocus&& !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) || mStopped|| (mIsAmbientMode && !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_BUTTON))|| (mPausedForTransition && !isBack(q.mEvent))) {// This is a focus event and the window doesn't currently have input focus or// has stopped. This could be an event that came back from the previous stage// but the window has lost focus or stopped in the meantime.if (isTerminalInputEvent(q.mEvent)) {// Don't drop terminal input events, however mark them as canceled.q.mEvent.cancel();Slog.w(mTag, "Cancelling event due to no window focus: " + q.mEvent);return false;}// Drop non-terminal input events.Slog.w(mTag, "Dropping event due to no window focus: " + q.mEvent);return true;}return false;}void dump(String prefix, PrintWriter writer) {if (mNext != null) {mNext.dump(prefix, writer);}}private boolean isBack(InputEvent event) {if (event instanceof KeyEvent) {return ((KeyEvent) event).getKeyCode() == KeyEvent.KEYCODE_BACK;} else {return false;}}}

看到这里,是否感觉到熟悉了,这就是setView方法最后设置各种不同类别输入事件分别调用的处理对象。在setView方法中一共生成了6个InputStage的子类对象,分别是ViewPostImeInputStage、NativePostImeInputStage、EarlyPostImeInputStage、ImeInputStage、ViewPreImeInputStage、NativePreImeInputStage。如果不是当前事件就不断指向下一个,直到找到对应的对象。

这里我们分析的是Touch事件,所以只关注ViewPostImeInputStage。

    /*** Delivers post-ime input events to the view hierarchy.*/final class ViewPostImeInputStage extends InputStage {public ViewPostImeInputStage(InputStage next) {super(next);}@Overrideprotected int onProcess(QueuedInputEvent q) {if (q.mEvent instanceof KeyEvent) {return processKeyEvent(q);} else {final int source = q.mEvent.getSource();if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {return processPointerEvent(q);} else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {return processTrackballEvent(q);} else {return processGenericMotionEvent(q);}}}...}

对于触摸事件,这里的(source & InputDevice.SOURCE_CLASS_POINTER) != 0为true,所以会调用processPointerEvent方法。

        private int processPointerEvent(QueuedInputEvent q) {final MotionEvent event = (MotionEvent)q.mEvent;mAttachInfo.mUnbufferedDispatchRequested = false;final View eventTarget =(event.isFromSource(InputDevice.SOURCE_MOUSE) && mCapturingView != null) ?mCapturingView : mView;mAttachInfo.mHandlingPointerEvent = true;boolean handled = eventTarget.dispatchPointerEvent(event);maybeUpdatePointerIcon(event);mAttachInfo.mHandlingPointerEvent = false;if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) {mUnbufferedInputDispatch = true;if (mConsumeBatchedInputScheduled) {scheduleConsumeBatchedInputImmediately();}}return handled ? FINISH_HANDLED : FORWARD;}

这里的View就是窗口顶层视图DecroView,所以接下来继续分析View的dispatchPointerEvent方法:

    /*** Dispatch a pointer event.* <p>* Dispatches touch related pointer events to {@link #onTouchEvent(MotionEvent)} and all* other events to {@link #onGenericMotionEvent(MotionEvent)}.  This separation of concerns* reinforces the invariant that {@link #onTouchEvent(MotionEvent)} is really about touches* and should not be expected to handle other pointing device features.* </p>** @param event The motion event to be dispatched.* @return True if the event was handled by the view, false otherwise.* @hide*/public final boolean dispatchPointerEvent(MotionEvent event) {if (event.isTouchEvent()) {return dispatchTouchEvent(event);} else {return dispatchGenericMotionEvent(event);}}

这里调用了dispatchTouchEvent方法,由于DecorView重写了该方法,所以继续查看DecorView的dispatchTouchEvent方法:

    @Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {final Window.Callback cb = mWindow.getCallback();return cb != null && !mWindow.isDestroyed() && mFeatureId < 0? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);}

这里做了一个判断,当cb!=null且mFeatureId<0是执行cb.dispatchTouchEvent(ev),否则执行super.dispatchTouchEvent(ev),也就是经过FrameLayout继承ViewGroup的dispatchTouchEvent方法。

这里的cb对象是调用mWindow(即PhoneWindow对象)的getCallback方法获取的,是在之前Activity的attach方法中创建PhoneWindow对象后调用setCallback时被赋值的:

    final void attach(Context context, ActivityThread aThread,Instrumentation instr, IBinder token, int ident,Application application, Intent intent, ActivityInfo info,CharSequence title, Activity parent, String id,NonConfigurationInstances lastNonConfigurationInstances,Configuration config, String referrer, IVoiceInteractor voiceInteractor,Window window) {attachBaseContext(context);mFragments.attachHost(null /*parent*/);mWindow = new PhoneWindow(this, window);mWindow.setWindowControllerCallback(this);mWindow.setCallback(this);mWindow.setOnWindowDismissedCallback(this);mWindow.getLayoutInflater().setPrivateFactory(this);if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {mWindow.setSoftInputMode(info.softInputMode);}...}

因此这里cb.dispatchTouchEvent(ev)即为Activity类的dispatchTouchEvent方法。

Touch事件传递到Activity后,就是我们经常看到的比较熟悉的Activity事件传递流程了。由于篇幅问题,我们下一篇再做具体分析,也欢迎关注,后续会继续推出更多精彩内容。


更多文章:

手把手教你读懂源码,View的加载流程详细剖析

手把手教你读懂源码,View的绘制流程详细剖析

经常被问到的有深度有内涵的数据结构面试题

巧用模板,不仅能提升AS开发效率,还能装逼

高薪安卓开发工程师必备技能——框架,看看你都掌握了哪些

Android常见内存泄露,学会这六招大大优化APP性能

史上最新最全的Android培训机构大揭秘

程序猿的工作和生活,你真的不懂

情人节里谁说程序员不懂浪漫

原来微信清粉,不仅显脑残,还是天大骗局,没想到那么多人上当​


今天就先分享到这里,后续将推出更多精彩内容,欢迎一起探讨学习进步。

此文章版权为微信公众号分享达人秀(ShareExpert)——鑫鱻所有,若转载请备注出处,特此声明!

手把手教你读懂源码,View事件的注册和接收详细剖析相关推荐

  1. 手把手教你如何导入源码,zookeeper为例

    要学习zookeeper,不可避免的一项就是zookeeper源码的导入工作.本次使用的idea. 步骤: 安装java就省略啦 手把手教你如何导入源码,zookeeper为例 软件 一,安装idea ...

  2. java 事件分发机制_读Android源码之事件分发机制最全总结

    原标题:读Android源码之事件分发机制最全总结 本文源码来自andorid sdk 22,不同版本会有细微差别,但核心机制是一致的 一.概述 事件分发有多种类型, 本文主要介绍Touch相关的事件 ...

  3. 「读懂源码系列2」我从 lodash 源码中学到的几个知识点

    前言 上一篇文章 「前端面试题系列8」数组去重(10 种浓缩版) 的最后,简单介绍了 lodash 中的数组去重方法 _.uniq,它可以实现我们日常工作中的去重需求,能够去重 NaN,并保留 {.. ...

  4. 启明云端分享| 手把手教你基于DEMO源码快速进行86盒应用开发

    提示:启明云端从2013年起就作为Espressif(乐鑫科技)大中华区合作伙伴,我们不仅用心整理了你在开发过程中可能会遇到的问题以及快速上手的简明教程.同时也用心推出了基于乐鑫的相关应用方案!希望你 ...

  5. 手把手教你Java项目源码安全审查!

    你知道的越多,不知道的就越多,业余的像一棵小草! 你来,我们一起精进!你不来,我和你的竞争对手一起精进! 编辑:业余草 来源:cnblogs.com/xdecode/p/9252113.html 推荐 ...

  6. BF706的开发入门,手把手教你(含源码)

    作者的话 BF706是ADI的Blackfin处理器之一,具体性能参数可以通过官网去查,我的这个文章,是写给已经准备,或者正在使用BF706的用户看,怎么从0开始进入开发呢? 硬件准备 ADSP-BF ...

  7. 【朝花夕拾】Android自定义View之(一)手把手教你看懂View绘制流程——向源码要答案

    前言 原文:Android自定义View之(一)手把手教你看懂View绘制流程--向源码要答案 View作为整个app的颜值担当,在Android体系中占有重要的地位.深入理解Android View ...

  8. 码农不会英语怎么行?一个公式教你读懂英文文档

    不背公式和语法- 一个公式教你读懂英文文档.往下看↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ ↓↓↓ ↓↓↓ ↓↓↓ ↓↓↓ ↓↓↓ ↓↓↓ 还在翻 背单词去 在做什么梦???

  9. koa2异常处理_读 koa2 源码后的一些思考与实践

    koa2的特点优势 什么是 koa2 Nodejs官方api支持的都是callback形式的异步编程模型.问题:callback嵌套问题 koa2 是由 Express原班人马打造的,是现在比较流行的 ...

最新文章

  1. python server page_python web-server
  2. 答TOGAF企业架构的一些问题
  3. R语言观察日志(part2)--preProcess函数
  4. java实现数字转mac,Java Ethernet.getSourceMAC方法代码示例
  5. java 读取1m文件_java的FileInputStream类读取文件
  6. Linux安装ntp同步时间
  7. Docker学习篇(一)Docker概述、安装和常用命令
  8. 服务器报告它来自digest_2020年全球服务器市场规模及竞争格局分析
  9. C语言10进制写法,用c语言编写函数Htoi(s)把由16进制数成10进制的数
  10. 网页打不开微信连接到服务器失败是怎么回事,微信打不开网页怎么回事?
  11. java web流量阀值_Javaweb应用使用限流处理大量的并发请求详解
  12. php curl加密获取数据,PHP利用Curl模拟登录并获取数据例子
  13. redis info 信息
  14. python程序开发入门_程序设计入门—Python
  15. Java的继承 以学生管理系统为例
  16. 自己训练,日文识别效果还可以,有部分常见错字
  17. 联想计算机怎么设置硬盘,联想电脑硬盘模式怎么更改
  18. 技术方案的讨论过程来看化繁为简
  19. 8.法律法规与标准化知识
  20. FBI针对HTTPS网络钓鱼发布警告

热门文章

  1. 初识OFDM(六):从零开始的OFDM误码率仿真
  2. Python画星星和星空
  3. oracle ods平台建立,如何利用Oracle表分区技术建设ODS平台
  4. 中国天气网天气预报API接口城市代码,XML格式,数据具体到县、区级别
  5. xcode9.4网盘资源
  6. 【数据结构】带头结点的单链表
  7. Spring Cloud Alibaba微服务组件快速上手
  8. MySQL卸载后,服务仍然存在的解决方案
  9. 机械硬盘哪个好?买1T好还是2T好?注意别买到叠瓦盘
  10. 网易视频云郭再荣:打造一体化多场景的视频云平台