版本基于:Android R

0. 前言

《Android 基于Handler 剖析消息机制》一文中,以 Handler 类为起点详细分析了异步通信,分析了Java 端 HandlerLooperMessageQueueMessage 之前的通信关系。

框架如下:

在Java 端的 Looper 中会创建一个 Java 端的 MessageQueue实例,并在loop() 函数中的死循环里通过 queue.next() 不停的获取监听到的下一个 Message,然后将其通过 dispatchMessage() 分发处理。详细看《Android 基于Handler 剖析消息机制》一文第 4.3 节。

在 Java 端的 MessageQueue 实现核心机制是通过 Native 端的 MessageQueue,该类的对象中维护了一个 native Looper,上面说到的 Java 端的next() 函数就是通过 native 端的 epoll 机制 进行阻塞监听。

之前《Android 基于Handler 剖析消息机制》一文只是对 native Looper 做了简单的介绍,本文将重点分析一下 native Looper 实现机制。

1. Looper 类

system/core/libutils/include/utils/Looper.hclass Looper : public RefBase {
protected:virtual ~Looper();public:Looper(bool allowNonCallbacks);bool getAllowNonCallbacks() const;int pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData);inline int pollOnce(int timeoutMillis) {return pollOnce(timeoutMillis, nullptr, nullptr, nullptr);}int pollAll(int timeoutMillis, int* outFd, int* outEvents, void** outData);inline int pollAll(int timeoutMillis) {return pollAll(timeoutMillis, nullptr, nullptr, nullptr);}void wake();int addFd(int fd, int ident, int events, Looper_callbackFunc callback, void* data);int addFd(int fd, int ident, int events, const sp<LooperCallback>& callback, void* data);int removeFd(int fd);void sendMessage(const sp<MessageHandler>& handler, const Message& message);void sendMessageDelayed(nsecs_t uptimeDelay, const sp<MessageHandler>& handler,const Message& message);void sendMessageAtTime(nsecs_t uptime, const sp<MessageHandler>& handler,const Message& message);void removeMessages(const sp<MessageHandler>& handler);void removeMessages(const sp<MessageHandler>& handler, int what);bool isPolling() const;static sp<Looper> prepare(int opts);static void setForThread(const sp<Looper>& looper);static sp<Looper> getForThread();private:...const bool mAllowNonCallbacks; // immutableandroid::base::unique_fd mWakeEventFd;  // immutableVector<MessageEnvelope> mMessageEnvelopes; // guarded by mLock...
};

简单了列举了下 Looper 类中的成员,后面根据流程再详细说明。

2. Looper 构造函数

system/core/libutils/Looper.cppLooper::Looper(bool allowNonCallbacks): mAllowNonCallbacks(allowNonCallbacks),mSendingMessage(false),mPolling(false),mEpollRebuildRequired(false),mNextRequestSeq(0),mResponseIndex(0),mNextMessageUptime(LLONG_MAX) {mWakeEventFd.reset(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC));LOG_ALWAYS_FATAL_IF(mWakeEventFd.get() < 0, "Could not make wake event fd: %s", strerror(errno));AutoMutex _l(mLock);rebuildEpollLocked();
}

参数 allowNonCallbacks 指定该 Looper 中是否允许在 addFd() 的时候可以不设定 callback,如果为true,则在addFd() 时可以不用设定callback,如果为 false,则在 addFd() 的时候需要指定 callback,否则会报错,详细可以查看 addFd() 函数。

构造函数中主要做了两件事情:

  • 创建一个 wakeEventFd;
  • 调用 rebuildEpollLocked() 初始化 epoll;

2.1 rebuildEpollLocked()

system/core/libutils/Looper.cppvoid Looper::rebuildEpollLocked() {//step1,如果是重构epoll 时的逻辑,需要进行resetif (mEpollFd >= 0) {
#if DEBUG_CALLBACKSALOGD("%p ~ rebuildEpollLocked - rebuilding epoll set", this);
#endifmEpollFd.reset();}//step2,分配一个新的epoll,并将wakeEventFd添加到监听mEpollFd.reset(epoll_create1(EPOLL_CLOEXEC));LOG_ALWAYS_FATAL_IF(mEpollFd < 0, "Could not create epoll instance: %s", strerror(errno));struct epoll_event eventItem;memset(& eventItem, 0, sizeof(epoll_event)); // zero out unused members of data field unioneventItem.events = EPOLLIN;eventItem.data.fd = mWakeEventFd.get();int result = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, mWakeEventFd.get(), &eventItem);LOG_ALWAYS_FATAL_IF(result != 0, "Could not add wake event fd to epoll instance: %s",strerror(errno));//step3,是否有其他的request,如果有,也重新加入到epoll中监听//一般request 通过addFd 函数添加for (size_t i = 0; i < mRequests.size(); i++) {const Request& request = mRequests.valueAt(i);struct epoll_event eventItem;request.initEventItem(&eventItem);int epollResult = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, request.fd, &eventItem);if (epollResult < 0) {ALOGE("Error adding epoll events for fd %d while rebuilding epoll set: %s",request.fd, strerror(errno));}}
}

大致流程如上面注释,主要是三步:

  • 如果是重构 epoll 时的逻辑,需要进行reset;
  • 分配一个新的 epoll,并将 wakeEventFd添加到监听;
  • 是否有其他的 request,如果有,也重新加入到epoll中监听;

3. MessageHandler 类

system/core/libutils/include/utils/Looper.hclass MessageHandler : public virtual RefBase {
protected:virtual ~MessageHandler();public:/*** Handles a message.*/virtual void handleMessage(const Message& message) = 0;
};

在 Looper.h 中还定义了 MessageHandler 这样的类,同Java 端的 Handler类,都是用来处理消息的。

当创建好 MessageHandler 对象之后,可以通过 Looper::sendMessage() 函数发送消息到Looper 中,Looper 会在后续的 pollOnce() 中进行监听,最终通过 handleMessage() 函数进行回调处理此次消息。

3.1 sendMessage()

在 Looper.h 中我们看到三个详细的函数:

    void sendMessage(const sp<MessageHandler>& handler, const Message& message);void sendMessageDelayed(nsecs_t uptimeDelay, const sp<MessageHandler>& handler,const Message& message);void sendMessageAtTime(nsecs_t uptime, const sp<MessageHandler>& handler,const Message& message);

但其实前面两个函数最终调用的都是 sendMessageAtTime() 函数:

system/core/libutils/Looper.cppvoid Looper::sendMessage(const sp<MessageHandler>& handler, const Message& message) {nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);sendMessageAtTime(now, handler, message);
}void Looper::sendMessageDelayed(nsecs_t uptimeDelay, const sp<MessageHandler>& handler,const Message& message) {nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);sendMessageAtTime(now + uptimeDelay, handler, message);
}void Looper::sendMessageAtTime(nsecs_t uptime, const sp<MessageHandler>& handler,const Message& message) {
#if DEBUG_CALLBACKSALOGD("%p ~ sendMessageAtTime - uptime=%" PRId64 ", handler=%p, what=%d",this, uptime, handler.get(), message.what);
#endifsize_t i = 0;{ // acquire lockAutoMutex _l(mLock);size_t messageCount = mMessageEnvelopes.size();while (i < messageCount && uptime >= mMessageEnvelopes.itemAt(i).uptime) {i += 1;}MessageEnvelope messageEnvelope(uptime, handler, message);mMessageEnvelopes.insertAt(messageEnvelope, i, 1);// Optimization: If the Looper is currently sending a message, then we can skip// the call to wake() because the next thing the Looper will do after processing// messages is to decide when the next wakeup time should be.  In fact, it does// not even matter whether this code is running on the Looper thread.if (mSendingMessage) {return;}} // release lock// Wake the poll loop only when we enqueue a new message at the head.if (i == 0) {wake();}
}

这里涉及到了 Looper 中的另外一个成员变量:mMessageEnvelopes,这是一个 Vector 类型变量,用以存放通过 sendMessage() 发送到 Looper 中的消息。

如代码所示,会将 message、message handler、uptime 都存放在一个信封对象中,然后将该信封添加到 mMessageEnvelopes 中。

这里还涉及另外一个成员变量:mSendingMessage,用以标记当前是否正在 handleMessage() 处理消息。当该函数执行的时候,mSendingMessage 被置 true,标记正在处理,在处理函数完成之后会将其再次置 false。

4. LooperCallback 类

system/core/libutils/include/utils/Looper.hclass LooperCallback : public virtual RefBase {
protected:virtual ~LooperCallback();public:/*** Handles a poll event for the given file descriptor.* It is given the file descriptor it is associated with,* a bitmask of the poll events that were triggered (typically EVENT_INPUT),* and the data pointer that was originally supplied.** Implementations should return 1 to continue receiving callbacks, or 0* to have this file descriptor and callback unregistered from the looper.*/virtual int handleEvent(int fd, int events, void* data) = 0;
};

该类用以处理通过 addFd() 添加监听的 fd。在 addFd() 函数中会同步指定 callback,就是这里的 LooperCallback 对象。

4.1 addFd()

在 Looper.h 中可以看到该函数的声明:

    int addFd(int fd, int ident, int events, Looper_callbackFunc callback, void* data);int addFd(int fd, int ident, int events, const sp<LooperCallback>& callback, void* data);
system/core/libutils/Looper.cppint Looper::addFd(int fd, int ident, int events, Looper_callbackFunc callback, void* data) {return addFd(fd, ident, events, callback ? new SimpleLooperCallback(callback) : nullptr, data);
}int Looper::addFd(int fd, int ident, int events, const sp<LooperCallback>& callback, void* data) {if (!callback.get()) {if (! mAllowNonCallbacks) {ALOGE("Invalid attempt to set NULL callback but not allowed for this looper.");return -1;}if (ident < 0) {ALOGE("Invalid attempt to set NULL callback with ident < 0.");return -1;}} else {ident = POLL_CALLBACK;}{ // acquire lockAutoMutex _l(mLock);Request request;request.fd = fd;request.ident = ident;request.events = events;request.seq = mNextRequestSeq++;request.callback = callback;request.data = data;if (mNextRequestSeq == -1) mNextRequestSeq = 0; // reserve sequence number -1struct epoll_event eventItem;request.initEventItem(&eventItem);ssize_t requestIndex = mRequests.indexOfKey(fd);if (requestIndex < 0) {int epollResult = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, fd, &eventItem);if (epollResult < 0) {ALOGE("Error adding epoll events for fd %d: %s", fd, strerror(errno));return -1;}mRequests.add(fd, request);} else {int epollResult = epoll_ctl(mEpollFd.get(), EPOLL_CTL_MOD, fd, &eventItem);if (epollResult < 0) {if (errno == ENOENT) {epollResult = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, fd, &eventItem);if (epollResult < 0) {ALOGE("Error modifying or adding epoll events for fd %d: %s",fd, strerror(errno));return -1;}scheduleEpollRebuildLocked();} else {ALOGE("Error modifying epoll events for fd %d: %s", fd, strerror(errno));return -1;}}mRequests.replaceValueAt(requestIndex, request);}} // release lockreturn 1;
}

这里涉及到 Looper 中的另外一个成员变量:mRequests,这是一个KeyedVector 类型的变量,用以存放通过 addFd() 添加的监听请求。

如代码所示,首先确认 mRequests 中是否已经存放该 fd 的监听,如果没有,则通过 epoll_ctl() 将对该 fd 的监听添加到 epoll 中,并将此次的请求添加到 mRequests 中;如果已经存在对该 fd 的监听,则对 epoll 进行更改,并将 mRequests 中的request 进行替换。

当 pollOnce() 中收到 epoll 的event 为该 mRequests 中的 fd 消息时,会将响应的 event 暂存在 Looper 中的另一个成员变量 mResponses 中,接着 pollOnce() 中会确认这些 fd 消息是否需要 callback 处理,如果callback 处理成功则将此次的响应消息从 mResponses 中移除。详细请查看下面第 6 节pollOnce() 函数。

另外,需要注意这里的 ident (identifier 简称)参数,可以在传入的时候指定为 POLL_CALLBACK 等枚举值(这些值都为负数),也可以设定为一个 >= 0 的其他值。但是,如果该 ident 值为 >=0 时,pollOnce() 调用时会将该次响应的数据取出并return,详细看第 6 节 pollOnce() 函数。

5. wake()

system/core/libutils/Looper.cppvoid Looper::wake() {
#if DEBUG_POLL_AND_WAKEALOGD("%p ~ wake", this);
#endifuint64_t inc = 1;ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd.get(), &inc, sizeof(uint64_t)));if (nWrite != sizeof(uint64_t)) {if (errno != EAGAIN) {LOG_ALWAYS_FATAL("Could not write wake signal to fd %d (returned %zd): %s",mWakeEventFd.get(), nWrite, strerror(errno));}}
}

通过往 mWakeEventFd 中写个数据来达到唤醒的目的。

6. pollOnce()

先来看下在Looper.h 中的声明:

    int pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData);inline int pollOnce(int timeoutMillis) {return pollOnce(timeoutMillis, nullptr, nullptr, nullptr);}

参数 timeoutMillis 单位为 毫秒

注意,pollOnce() 可以只传一个 timeout 的参数,此时其他参数默认为 nullptr。

调用该函数用以等待一个 event 的产生。

  • 如果timeout 为0,则不用阻塞立即返回;
  • 如果timeout 为负数,则无限等待一直等到一个 event 出现;
  • 如果在 timeout 到期之前,或者callback被调用之前,或者没有fd 准备好之前,epoll 被wake() 函数唤醒时,返回 POLL_WAKE,这也是默认返回类型;
  • 如果是 callback 被调用处理时,返回POLL_CALLBACK;
  • 如果是 timeout 到期时,返回 POLL_TIMEOUT;
  • 如果出现错误时,返回 POLL_ERROR;
  • 该函数直到完成所有的 fd 所有对应的合适的 callback 调用之后,才会 return;

详细的逻辑可以查看源码:

system/core/libutils/Looper.cppint Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {int result = 0;for (;;) {//确定是否有fd 响应,如果 ident 为>=0的值,则直接返回;// 如果ident 为负数,则进行下一次的 pollInner()等待、处理;while (mResponseIndex < mResponses.size()) {const Response& response = mResponses.itemAt(mResponseIndex++);int ident = response.request.ident;if (ident >= 0) {int fd = response.request.fd;int events = response.events;void* data = response.request.data;if (outFd != nullptr) *outFd = fd;if (outEvents != nullptr) *outEvents = events;if (outData != nullptr) *outData = data;return ident;}}//pollInner() 处理结果确定,一般不会为0if (result != 0) {if (outFd != nullptr) *outFd = 0;if (outEvents != nullptr) *outEvents = 0;if (outData != nullptr) *outData = nullptr;return result;}//pollOnce 函数的核心处理部分,epoll机制监听、处理的地方result = pollInner(timeoutMillis);}
}

在调用 pollInner() 函数之前,会通过 while() 循环将所有 fd 的响应进行确认,如果出现了 fd 的消息没有经过callback 处理,那么该 fd 的响应还会留在 mResponses 中。设计的初衷应该是希望 fd 的消息可以通过指定的 callback(第4节中的LooperCallback 对象) 处理,如果不需要 callback 处理,则会将 ident 参数值设为 >=0 的值,这样在此处的 while 循环就可以从 pollOnce() 中 return。

6.1 pollInner()

system/core/libutils/Looper.cppint Looper::pollInner(int timeoutMillis) {//step1,确定timeoutif (timeoutMillis != 0 && mNextMessageUptime != LLONG_MAX) {nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);int messageTimeoutMillis = toMillisecondTimeoutDelay(now, mNextMessageUptime);if (messageTimeoutMillis >= 0&& (timeoutMillis < 0 || messageTimeoutMillis < timeoutMillis)) {timeoutMillis = messageTimeoutMillis;}}//step2,进入真正poll处理,先清空 mResponses之前的残留int result = POLL_WAKE;mResponses.clear();mResponseIndex = 0;// We are about to idle.mPolling = true;//step3,等待epoll监听到消息struct epoll_event eventItems[EPOLL_MAX_EVENTS];int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);// No longer idling.mPolling = false;// Acquire lock.mLock.lock();//step4,当fd的修改、删除出现问题时,需要重构epollif (mEpollRebuildRequired) {mEpollRebuildRequired = false;rebuildEpollLocked();goto Done;}//step5, 如果event count 小于0,则表示epoll出错,答应错误并结束event处理if (eventCount < 0) {if (errno == EINTR) {goto Done;}ALOGW("Poll failed with an unexpected error: %s", strerror(errno));result = POLL_ERROR;goto Done;}//step6,如果event count 为0,则表示timeout 到期,进入timeout处理if (eventCount == 0) {result = POLL_TIMEOUT;goto Done;}//step7,收到了event,确定是wake() 触发,还是fd 消息for (int i = 0; i < eventCount; i++) {int fd = eventItems[i].data.fd;uint32_t epollEvents = eventItems[i].events;//是被调用wake() 唤醒的if (fd == mWakeEventFd.get()) {if (epollEvents & EPOLLIN) {awoken();} else {ALOGW("Ignoring unexpected epoll events 0x%x on wake event fd.", epollEvents);}} else {ssize_t requestIndex = mRequests.indexOfKey(fd);if (requestIndex >= 0) {//是fd消息唤醒的,将event暂存mResponses 中int events = 0;if (epollEvents & EPOLLIN) events |= EVENT_INPUT;if (epollEvents & EPOLLOUT) events |= EVENT_OUTPUT;if (epollEvents & EPOLLERR) events |= EVENT_ERROR;if (epollEvents & EPOLLHUP) events |= EVENT_HANGUP;pushResponse(events, mRequests.valueAt(requestIndex));} else {ALOGW("Ignoring unexpected epoll events 0x%x on fd %d that is ""no longer registered.", epollEvents, fd);}}}
Done: ;//step8,处理messages,timeout 触发mNextMessageUptime = LLONG_MAX;while (mMessageEnvelopes.size() != 0) {nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);const MessageEnvelope& messageEnvelope = mMessageEnvelopes.itemAt(0);if (messageEnvelope.uptime <= now) {{ // obtain handlersp<MessageHandler> handler = messageEnvelope.handler;Message message = messageEnvelope.message;mMessageEnvelopes.removeAt(0);mSendingMessage = true;mLock.unlock();//处理messagehandler->handleMessage(message);} // release handlermLock.lock();mSendingMessage = false;result = POLL_CALLBACK;} else {// The last message left at the head of the queue determines the next wakeup time.mNextMessageUptime = messageEnvelope.uptime;break;}}// Release lock.mLock.unlock();//step9,fd 消息响应,进行fd 指定的callback处理for (size_t i = 0; i < mResponses.size(); i++) {Response& response = mResponses.editItemAt(i);//如果fd 指定了callback,则调用handleEvent() 处理if (response.request.ident == POLL_CALLBACK) {int fd = response.request.fd;int events = response.events;void* data = response.request.data;int callbackResult = response.request.callback->handleEvent(fd, events, data);//handleEvent 处理成功,则需要返回0,表示处理完成,退出Looper//当然,也可以返回非0,表示继续等待callback处理,不退出Looperif (callbackResult == 0) {removeFd(fd, response.request.seq);}// Clear the callback reference in the response structure promptly because we// will not clear the response vector itself until the next poll.response.request.callback.clear();result = POLL_CALLBACK;}}return result;
}

流程比较长,详细的分析见代码中的注释,重点情况在addFd() 分析时或者pollOnce() 分析时已经说明,请参考上文。

7. 其他函数

7.1 removeFd()

与addFd() 相反,将 fd 监听从 mRequests 中移除,并从 epoll 监听中移除。

7.2 isPolling()

确定当前Looper 是否处于 epoll_wait() 阻塞,如果epoll_wait() 没有返回,则说明 Looper 处于 polling 状态。

至此,native Looper的原理已经基本分析完成。核心是通过 epoll 机制进行监听,针对:

  • message 的timeout,通过 sendMessage() 加入到 mMessageEnvelopes 中;
  • wake fd 的灵活唤醒,通过创建 wake event fd 监听,wake() 进行唤醒;
  • 其他 fd 的poll event 唤醒,通过 addFd() 加入到 mRequests 中;

其他关联博文:

Android 基于Handler 剖析消息机制

Android 中Handler 详解

Android AsyncTask 详解

Android HandlerThread 详解

Android 中Looper机制详解相关推荐

  1. Android中IPC机制详解

    本文部分内容参照<Android开发艺术探索> IPC是什么? IPC全称为Inter-Process Communication,译为"跨进程通信",在这里要着重提一 ...

  2. Android 系统(199)---Android事件分发机制详解

    Android事件分发机制详解 前言 Android事件分发机制是Android开发者必须了解的基础 网上有大量关于Android事件分发机制的文章,但存在一些问题:内容不全.思路不清晰.无源码分析. ...

  3. Android设备扫描机制详解

    Android设备扫描机制详解 本文基于Android pie,对Android的设备扫描机制做一个全面的解析,由于本人掌握的知识有限,如有讲错的地方还请大家指出来. Android提供了一套扫描机制 ...

  4. View的事件体系之三 android事件分发机制详解(下)

    接着上一篇来分析事件分发机制,在看了各位大牛的关于事件分发机制的分析后茅塞顿开,之前看过好几遍郭霖,弘扬以及玉刚大神关于事件体系的讲解,一直看不懂,比较模糊,最近复习时,看到一篇博文,写的相当精彩,看 ...

  5. Hadoop中RPC机制详解之Server端

    2019独角兽企业重金招聘Python工程师标准>>> Hadoop 中 RPC 机制详解之 Client 端 1. Server.Listener RPC Client 端的 RP ...

  6. android调webview的方法,Android中的WebView详解

    Android中的WebView详解 WebView详解 基本用法 布局文件配置WebView android:id="@+id/wv_news_detail" android:l ...

  7. Java虚拟机中类加载机制详解

    Java虚拟机中类加载机制详解 1,什么是java类加载机制 **首先在java中,是通过编译来生成.class文件(可能在本地,或者网页下载),java的类加载机制就是 将这些.class文件加载到 ...

  8. Android 中malloc_debug 原理详解

    版本基于:Android R 关联博文: Android 中malloc_debug 使用详解 0. 前言 最近上项目中遇到一个native 可能内存泄漏的问题,曾考虑使用HWASAN,但这个工具是针 ...

  9. Android中mesure过程详解 (结合Android 4.0.4 最新源码)

    如何遍历并绘制View树?之前的文章Android中invalidate() 函数详解(结合Android 4.0.4 最新源码)中提到invalidate()最后会发起一个View树遍历的请求,并通 ...

  10. Android中layout过程详解 (结合Android 4.0.4 最新源码)

    上一篇文章Android中mesure过程详解 (结合Android 4.0.4 最新源码)介绍了View树的measure过程,相对与measure过程,本文介绍的layout过程要简单多了,正如l ...

最新文章

  1. HP1020打印机“传递给系统调用的数据区域太小” 如何处理?
  2. linux 运维shell习题
  3. 经典C语言程序100例之五六
  4. this全面解析, 如何定位this指向,一文总结,再也不怕面试官追问啦
  5. java毕设用的框架_分享四个Java低代码快速开发平台贼好用, 私活毕设神器
  6. 数据分析应该要避免的6个错误
  7. 基于JAVA+SpringMVC+Mybatis+MYSQL的外卖点餐系统
  8. 1.6 开发集合测试集的大小
  9. 光伏发电最大功率点跟踪 (mppt) matlab/simulink仿真程序 恒定电压法 扰动观察法(po) 电导增量法(inc)模糊控制法(fuzzy)多种方法
  10. CentOS下安装JDK7
  11. 2018区块链技术及应用峰会(BTA)倒计时2天,最强百人区块链大咖齐聚
  12. excel 置信区间 计算_用Excel求置信区间.ppt
  13. 小程序调用腾讯视频插件
  14. 应届毕业生 求职面试宝典
  15. P5030 长脖子鹿放置
  16. Mockplus是如何节省你的原型时间的?
  17. 下午三点半,公司空空荡荡
  18. 七星配资放量上涨重磅利好引爆市场
  19. 论文翻译[Deep Residual Learning for Image Recognition]
  20. 操作系统原理学习笔记(基础概念与进程)

热门文章

  1. 第K短路(A*算法 启发式搜索算法)
  2. 一次内存不能为read/write的bug解决经历
  3. 手把手教用matlab做无人驾驶(十六)--Reeds-Shepp 曲线
  4. UE4 设置三连屏显示
  5. 中国液态金属量子计算机,中国液态金属逆天!逆重力攀爬
  6. wepy build --watch报错 ERR! Parse WePY config failed. Are you trying to use 解决
  7. Java 8新特性之 Lambda和StreamAPI
  8. 成都市金牛区专利授权资助申报条件奖补
  9. 谷歌翻译影响vue_VuePress 翻译之心路历程 - 印记中文小鲜肉成长记
  10. 慕课项目开工!part2:后端架构 (更新中。。。。)