Qt源码阅读(四) 事件循环
事件系统
文章为本人理解,如有理解不到位之处,烦请各位指正。
@
Qt的事件循环,应该是所有Qter都避不开的一个点,所以,这篇博客,咱们来了解源码中一些关于Qt中事件循环的部分。
先抛出几个疑问,根据源代码,下面一一进行解析。
什么是事件循环?
对于Qt事件循环个人理解是,事件循环是一个队列去循环处理事件。当队列中有事件时,则去处理事件,如果没有事件时,则会阻塞等待。
事件是如何产生的?
事件的产生可以分为两种:
- 程序外部产生
- 程序内部产生
程序外部所产生的事件主要是指系统产生的事件,比如说鼠标按下(MouseButtonPress)、按键按下(KeyPress)等,Qt捕捉系统的事件,然后将系统事件封装成自己的QEvent
类,再将事件发送出去。
程序内部产生的事件主要指我们在代码里,手动创建一个事件,然后将事件通过sendEvent
/postEvent
,来发送到事件循环中。而sendEvent
和postEvent
区别又在于一个是阻塞的(sendEvent
)一个是非阻塞的(postEvent
)。
我们结合源码分析,看一下sendEvent
和postEvent
分别干了什么导致一个是阻塞的一个是非阻塞的。
sendEvent
完整源码如下:
bool QCoreApplication::sendEvent(QObject *receiver, QEvent *event)
{
// sendEvent是阻塞调用
Q_TRACE(QCoreApplication_sendEvent, receiver, event, event->type());
if (event)
event->spont = false;
return notifyInternal2(receiver, event);
}
可以看到,sendEvent
是调用了notifyInternal2
这个函数
bool QCoreApplication::notifyInternal2(QObject *receiver, QEvent *event)
{
...
// Qt enforces the rule that events can only be sent to objects in
// the current thread, so receiver->d_func()->threadData is
// equivalent to QThreadData::current(), just without the function
// call overhead.
// 事件只能在同一个线程被send
QObjectPrivate *d = receiver->d_func();
QThreadData *threadData = d->threadData;
QScopedScopeLevelCounter scopeLevelCounter(threadData);
if (!selfRequired)
return doNotify(receiver, event);
return self->notify(receiver, event);
}
进一步跟踪到其doNotify
函数
static bool doNotify(QObject *receiver, QEvent *event)
{
if (receiver == nullptr) { // serious error
qWarning("QCoreApplication::notify: Unexpected null receiver");
return true;
}
#ifndef QT_NO_DEBUG
// 检查接受线程与当前是否同线程
QCoreApplicationPrivate::checkReceiverThread(receiver);
#endif
// QWidget类必须用QApplication
return receiver->isWidgetType() ? false : QCoreApplicationPrivate::notify_helper(receiver, event);
}
再到QCoreApplicationPrivate::notify_helper
bool QCoreApplicationPrivate::notify_helper(QObject *receiver, QEvent * event)
{
// Note: when adjusting the tracepoints in here
// consider adjusting QApplicationPrivate::notify_helper too.
Q_TRACE(QCoreApplication_notify_entry, receiver, event, event->type());
bool consumed = false;
bool filtered = false;
Q_TRACE_EXIT(QCoreApplication_notify_exit, consumed, filtered);
// send to all application event filters (only does anything in the main thread)
if (QCoreApplication::self
&& receiver->d_func()->threadData.loadRelaxed()->thread.loadAcquire() == mainThread()
&& QCoreApplication::self->d_func()->sendThroughApplicationEventFilters(receiver, event)) {
filtered = true;
return filtered;
}
// send to all receiver event filters
if (sendThroughObjectEventFilters(receiver, event)) {
filtered = true;
return filtered;
}
// deliver the event
// 直接调用对象的event函数,所以是阻塞的
consumed = receiver->event(event);
return consumed;
}
然后我们可以看到主要有几个流程:
判断QCoreApplication有没有安装事件过滤器,有就把信号发送到事件过滤器里,由事件过滤器对事件进行处理。
// send to all application event filters (only does anything in the main thread)
if (QCoreApplication::self
&& receiver->d_func()->threadData.loadRelaxed()->thread.loadAcquire() == mainThread()
&& QCoreApplication::self->d_func()->sendThroughApplicationEventFilters(receiver, event)) {
filtered = true;
return filtered;
}
判断事件接受对象,有没有安装事件过滤器,有就将信号发送到事件过滤器。
// send to all receiver event filters
if (sendThroughObjectEventFilters(receiver, event)) {
filtered = true;
return filtered;
}
具体遍历事件接受对象所安装的事件过滤器的代码如下:
bool QCoreApplicationPrivate::sendThroughObjectEventFilters(QObject *receiver, QEvent *event)
{
if (receiver != QCoreApplication::instance() && receiver->d_func()->extraData) {
for (int i = 0; i < receiver->d_func()->extraData->eventFilters.size(); ++i) {
QObject *obj = receiver->d_func()->extraData->eventFilters.at(i);
if (!obj)
continue;
if (obj->d_func()->threadData != receiver->d_func()->threadData) {
qWarning("QCoreApplication: Object event filter cannot be in a different thread.");
continue;
}
if (obj->eventFilter(receiver, event))
return true;
}
}
return false;
}
我们可以看到,只要事件被一个事件过滤器所成功处理,那么后续的事件过滤器就不会被响应。同时,参看Qt帮助手册中有提及到:
If multiple event filters are installed on a single object, the filter that was installed last is activated first.
后插入的事件过滤器会被优先响应。 具体安装事件过滤器,我们在后面进行分析。
直接调用事件接受对象的
event
函数进行处理。因为是直接调用的对象的event
,所以说,sendEvent
函数会阻塞等待。// deliver the event
// 直接调用对象的event函数,所以是阻塞的
consumed = receiver->event(event);
return consumed
postEvent
完整代码如下:
void QCoreApplication::postEvent(QObject *receiver, QEvent *event, int priority)
{
Q_TRACE_SCOPE(QCoreApplication_postEvent, receiver, event, event->type());
// 事件的接收者不能为空
if (receiver == nullptr) {
qWarning("QCoreApplication::postEvent: Unexpected null receiver");
delete event;
return;
}
// 对事件接受对象所在线程的事件处理列表上锁
auto locker = QCoreApplicationPrivate::lockThreadPostEventList(receiver);
if (!locker.threadData) {
// posting during destruction? just delete the event to prevent a leak
delete event;
return;
}
QThreadData *data = locker.threadData;
// if this is one of the compressible events, do compression
// 将重复的事件,进行压缩
if (receiver->d_func()->postedEvents
&& self && self->compressEvent(event, receiver, &data->postEventList)) {
Q_TRACE(QCoreApplication_postEvent_event_compressed, receiver, event);
return;
}
if (event->type() == QEvent::DeferredDelete)
receiver->d_ptr->deleteLaterCalled = true;
if (event->type() == QEvent::DeferredDelete && data == QThreadData::current()) {
// remember the current running eventloop for DeferredDelete
// events posted in the receiver's thread.
// Events sent by non-Qt event handlers (such as glib) may not
// have the scopeLevel set correctly. The scope level makes sure that
// code like this:
// foo->deleteLater();
// qApp->processEvents(); // without passing QEvent::DeferredDelete
// will not cause "foo" to be deleted before returning to the event loop.
// If the scope level is 0 while loopLevel != 0, we are called from a
// non-conformant code path, and our best guess is that the scope level
// should be 1. (Loop level 0 is special: it means that no event loops
// are running.)
int loopLevel = data->loopLevel;
int scopeLevel = data->scopeLevel;
if (scopeLevel == 0 && loopLevel != 0)
scopeLevel = 1;
static_cast<QDeferredDeleteEvent *>(event)->level = loopLevel + scopeLevel;
}
// delete the event on exceptions to protect against memory leaks till the event is
// properly owned in the postEventList
QScopedPointer<QEvent> eventDeleter(event);
Q_TRACE(QCoreApplication_postEvent_event_posted, receiver, event, event->type());
data->postEventList.addEvent(QPostEvent(receiver, event, priority));
eventDeleter.take();
event->posted = true;
++receiver->d_func()->postedEvents;
data->canWait = false;
locker.unlock();
QAbstractEventDispatcher* dispatcher = data->eventDispatcher.loadAcquire();
if (dispatcher)
dispatcher->wakeUp();
}
判断事件接收对象是否为空
// 事件的接收者不能为空
if (receiver == nullptr) {
qWarning("QCoreApplication::postEvent: Unexpected null receiver");
delete event;
return;
}
将事件接收对象所在线程的post事件列表上锁,如果已经被锁了,就把事件删除掉,并返回,防止泄露。
// 对事件接受对象所在线程的事件处理列表上锁
auto locker = QCoreApplicationPrivate::lockThreadPostEventList(receiver);
if (!locker.threadData) {
// posting during destruction? just delete the event to prevent a leak
delete event;
return;
}
将一些可以压缩的事件进行压缩,及多个事件压缩成只推送最后的一个事件。Qt界面的
update
就是这个操作,为了防止多次刷新导致卡顿,短时间内多次的调用update
可能只会刷新一次// if this is one of the compressible events, do compression
// 将重复的事件,进行压缩
if (receiver->d_func()->postedEvents
&& self && self->compressEvent(event, receiver, &data->postEventList)) {
Q_TRACE(QCoreApplication_postEvent_event_compressed, receiver, event);
return;
}
将事件插入接收对象所在线程的post事件列表中,并唤醒线程的事件调度器,来进行事件的处理。所以
postEvent
是非阻塞的,因为其只是把事件插入了线程的事件列表,唤醒事件调度器之后便返回。// delete the event on exceptions to protect against memory leaks till the event is
// properly owned in the postEventList
QScopedPointer<QEvent> eventDeleter(event);
Q_TRACE(QCoreApplication_postEvent_event_posted, receiver, event, event->type());
data->postEventList.addEvent(QPostEvent(receiver, event, priority));
eventDeleter.take();
event->posted = true;
++receiver->d_func()->postedEvents;
data->canWait = false;
locker.unlock(); QAbstractEventDispatcher* dispatcher = data->eventDispatcher.loadAcquire();
if (dispatcher)
dispatcher->wakeUp();
事件是如何处理的?
在Qt中,事件的接收者都是QObject
,而QObject
中事件处理是调用event
函数。如果当时对象不处理某个事件,就会将其转发到父类的event
进行处理。
而事件的处理,主要分为三个部分:
- 先是由事件循环遍历事件
- 然后判断事件接受对象有没有安装事件过滤器(
installEventFilter
),有安装的话,就把事件丢给事件过滤器(eventFilter
)进行处理。 - 如果没有安装事件过滤器或者事件过滤器对该事件不进行处理的话,那么,事件将会进一步转发到
event
函数里进行处理。
所以,在这一章节,我们同样一步一步的分析这三个点。
事件循环是怎么遍历的?
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
上面是一个经典的QtGUI程序的main函数,调用a.exec()
int QCoreApplication::exec()
{
...
threadData->quitNow = false;
QEventLoop eventLoop;
self->d_func()->in_exec = true;
self->d_func()->aboutToQuitEmitted = false;
int returnCode = eventLoop.exec();
...
}
而看QApplication::exec
的源码,实际上就是开启了一个事件循环(QEventLoop
)。同样,我们去看QEventLoop::exec
的源码,进一步看处理事件的步骤是什么。
int QEventLoop::exec(ProcessEventsFlags flags)
{
...
while (!d->exit.loadAcquire())
processEvents(flags | WaitForMoreEvents | EventLoopExec);
ref.exceptionCaught = false;
return d->returnCode.loadRelaxed();
}
上面可以看到,QEvenLoop::exec
里,是一个while
循环,循环的去调用processEvent
,而且设置了WaitForMoreEvents
就是说,如果没有事件,就阻塞等待。
void QCoreApplication::processEvents(QEventLoop::ProcessEventsFlags flags, int ms)
{
// ### Qt 6: consider splitting this method into a public and a private
// one, so that a user-invoked processEvents can be detected
// and handled properly.
QThreadData *data = QThreadData::current();
if (!data->hasEventDispatcher())
return;
QElapsedTimer start;
start.start();
while (data->eventDispatcher.loadRelaxed()->processEvents(flags & ~QEventLoop::WaitForMoreEvents)) {
if (start.elapsed() > ms)
break;
}
}
阅读processEvent
,其调用了线程的事件调度器QAbstrctEventDispatcher
,而这个类是一个抽象基类,根据不同的平台,有不同的实现,我们以windows下(QEventDispatcherWin32
)的为例,接着分析事件处理的流程。
bool QEventDispatcherWin32::processEvents(QEventLoop::ProcessEventsFlags flags)
{
Q_D(QEventDispatcherWin32);
...
// To prevent livelocks, send posted events once per iteration.
// QCoreApplication::sendPostedEvents() takes care about recursions.
sendPostedEvents();
...
}
void QEventDispatcherWin32::sendPostedEvents()
{
Q_D(QEventDispatcherWin32);
if (d->sendPostedEventsTimerId != 0)
KillTimer(d->internalHwnd, d->sendPostedEventsTimerId);
d->sendPostedEventsTimerId = 0;
// Allow posting WM_QT_SENDPOSTEDEVENTS message.
d->wakeUps.storeRelaxed(0);
QCoreApplicationPrivate::sendPostedEvents(0, 0, d->threadData.loadRelaxed());
}
可以看到,事件调度器最终还是调用了QCoreApplication
的sendPostEvents
void QCoreApplicationPrivate::sendPostedEvents(QObject *receiver, int event_type,
QThreadData *data)
{
if (event_type == -1) {
// we were called by an obsolete event dispatcher.
event_type = 0;
}
if (receiver && receiver->d_func()->threadData != data) {
qWarning("QCoreApplication::sendPostedEvents: Cannot send "
"posted events for objects in another thread");
return;
}
...
// Exception-safe cleaning up without the need for a try/catch block
struct CleanUp {
QObject *receiver;
int event_type;
QThreadData *data;
bool exceptionCaught;
inline CleanUp(QObject *receiver, int event_type, QThreadData *data) :
receiver(receiver), event_type(event_type), data(data), exceptionCaught(true)
{}
inline ~CleanUp()
{
if (exceptionCaught) {
// since we were interrupted, we need another pass to make sure we clean everything up
data->canWait = false;
}
--data->postEventList.recursion;
if (!data->postEventList.recursion && !data->canWait && data->hasEventDispatcher())
data->eventDispatcher.loadRelaxed()->wakeUp();
// clear the global list, i.e. remove everything that was
// delivered.
if (!event_type && !receiver && data->postEventList.startOffset >= 0) {
const QPostEventList::iterator it = data->postEventList.begin();
data->postEventList.erase(it, it + data->postEventList.startOffset);
data->postEventList.insertionOffset -= data->postEventList.startOffset;
Q_ASSERT(data->postEventList.insertionOffset >= 0);
data->postEventList.startOffset = 0;
}
}
};
CleanUp cleanup(receiver, event_type, data);
while (i < data->postEventList.size()) {
...
// first, we diddle the event so that we can deliver
// it, and that no one will try to touch it later.
pe.event->posted = false;
QEvent *e = pe.event;
QObject * r = pe.receiver;
--r->d_func()->postedEvents;
Q_ASSERT(r->d_func()->postedEvents >= 0);
// next, update the data structure so that we're ready
// for the next event.
const_cast<QPostEvent &>(pe).event = nullptr;
locker.unlock();
const auto relocker = qScopeGuard([&locker] { locker.lock(); });
QScopedPointer<QEvent> event_deleter(e); // will delete the event (with the mutex unlocked)
// after all that work, it's time to deliver the event.
QCoreApplication::sendEvent(r, e);
// careful when adding anything below this point - the
// sendEvent() call might invalidate any invariants this
// function depends on.
}
cleanup.exceptionCaught = false;
}
我们一个一个的分块分析:
判断是否在一个线程
if (receiver && receiver->d_func()->threadData != data) {
qWarning("QCoreApplication::sendPostedEvents: Cannot send "
"posted events for objects in another thread");
return;
}
一个有意思的异常安全的处理,不需要try/catch块
// Exception-safe cleaning up without the need for a try/catch block
struct CleanUp {
QObject *receiver;
int event_type;
QThreadData *data;
bool exceptionCaught; inline CleanUp(QObject *receiver, int event_type, QThreadData *data) :
receiver(receiver), event_type(event_type), data(data), exceptionCaught(true)
{}
inline ~CleanUp()
{
if (exceptionCaught) {
// since we were interrupted, we need another pass to make sure we clean everything up
data->canWait = false;
} --data->postEventList.recursion;
if (!data->postEventList.recursion && !data->canWait && data->hasEventDispatcher())
data->eventDispatcher.loadRelaxed()->wakeUp(); // clear the global list, i.e. remove everything that was
// delivered.
if (!event_type && !receiver && data->postEventList.startOffset >= 0) {
const QPostEventList::iterator it = data->postEventList.begin();
data->postEventList.erase(it, it + data->postEventList.startOffset);
data->postEventList.insertionOffset -= data->postEventList.startOffset;
Q_ASSERT(data->postEventList.insertionOffset >= 0);
data->postEventList.startOffset = 0;
}
}
};
CleanUp cleanup(receiver, event_type, data);
定义了一个结构体CleanUp
,结构体的析构函数(~CleanUp
)保存了函数退出时需要执行的清理操作。然后在栈上创建了一个结构体对象,遍历事件列表时,异常退出,那么就会调用自动调用~CleanUp
的析构函数。
将事件发送出去(
sendEvent
)while (i < data->postEventList.size()) {
... // first, we diddle the event so that we can deliver
// it, and that no one will try to touch it later.
pe.event->posted = false;
QEvent *e = pe.event;
QObject * r = pe.receiver; --r->d_func()->postedEvents;
Q_ASSERT(r->d_func()->postedEvents >= 0); // next, update the data structure so that we're ready
// for the next event.
const_cast<QPostEvent &>(pe).event = nullptr; locker.unlock();
const auto relocker = qScopeGuard([&locker] { locker.lock(); }); QScopedPointer<QEvent> event_deleter(e); // will delete the event (with the mutex unlocked) // after all that work, it's time to deliver the event.
QCoreApplication::sendEvent(r, e); // careful when adding anything below this point - the
// sendEvent() call might invalidate any invariants this
// function depends on.
}
可以看到,核心还是调用sendEvent
将事件发送出去,而前面我们对sendEvent
的源码分析我们可以看到,事件先是经过事件过滤器,再经过对象的event函数,来进行事件的处理。所以就引出我们的下一个话题:事件过滤器
事件过滤器
在实际应用中,我们经常要将某一个窗口部件的某个事件如鼠标滑轮滚动拦截,然后执行我们自己想要的操作。这个时候,我们就可以用到事件过滤器(EventFilter
**) **
首先,我们需要自己编写一个eventFilter
函数,
bool Class::eventFilter(QObject* watcher, QEvent* event)
{
//以过滤鼠标滚轮事件为例
if (object == m_watcherObject && event->type() == QEvent::Wheel) {
// do something
return true;
}
QWidget::eventFilter(watcher, event);
}
然后,我们需要为要拦截的某个窗口部件,安装事件过滤器
void Class::initUI()
{
QWidget* m_watcherObject = new QWidget(this);
// 为对象安装一个事件过滤器
m_watcherObject->installEventFilterr(this);
}
initUI();
那么一个对象安装的多个事件过滤器,会以什么样的顺序触发呢?我们在前面的讲过,后安装的事件过滤器会先触发,这一点,我们可以在源码里得到佐证:
void QObject::installEventFilter(QObject *obj)
{
Q_D(QObject);
if (!obj)
return;
if (d->threadData != obj->d_func()->threadData) {
qWarning("QObject::installEventFilter(): Cannot filter events for objects in a different thread.");
return;
}
if (!d->extraData)
d->extraData = new QObjectPrivate::ExtraData;
// clean up unused items in the list
d->extraData->eventFilters.removeAll((QObject*)nullptr);
d->extraData->eventFilters.removeAll(obj);
d->extraData->eventFilters.prepend(obj);
}
可以清楚的看到,事件过滤器,是以prepend
的形式被添加进事件过滤器列表的。
那么,当有鼠标滚轮事件触发的时候,我们可以看到sendEvent
会优先走到事件过滤器里,如果eventFilter
返回一个true,那么事件就不会被继续派发,否则,将会将事件发送到其他的事件过滤器里进行处理,如果其他的事件过滤器均对该事件不进行处理,那么事件将会继续往下派发,走到事件的处理函数event
event
接下来,就到了事件处理的最后一站,event
函数,这个函数比较简单,我们可以自己重写这个函数,对事件进行自定义的处理。
bool Class::event(QEvent *e)
{
switch (e->type()) {
case QEvent::Whell:
// do something
return true;
default:
if (e->type() >= QEvent::User) {
customEvent(e);
break;
}
return false;
}
return true;
}
夹带私货时间
- 之前有说到
processEvent
,添加一个小经验。当我们有时候不得不在主线程循环执行很耗时的操作的时候,这个时候,界面就会刷新不过来,就会导致界面卡顿,影响使用。但是,我们可以在这个循环里,手动调用qApp->processEvent()
,这样就可以手动调用处理掉所有的事件,就可以解决卡顿的问题。
Qt源码阅读(四) 事件循环的更多相关文章
- 40 网络相关函数(八)——live555源码阅读(四)网络
40 网络相关函数(八)——live555源码阅读(四)网络 40 网络相关函数(八)——live555源码阅读(四)网络 简介 15)writeSocket向套接口写数据 TTL的概念 函数send ...
- 37 网络相关函数(五)——live555源码阅读(四)网络
37 网络相关函数(五)——live555源码阅读(四)网络 37 网络相关函数(五)——live555源码阅读(四)网络 简介 10)MAKE_SOCKADDR_IN构建sockaddr_in结构体 ...
- 39 网络相关函数(七)——live555源码阅读(四)网络
39 网络相关函数(七)——live555源码阅读(四)网络 39 网络相关函数(七)——live555源码阅读(四)网络 简介 14)readSocket从套接口读取数据 recv/recvfrom ...
- 38 网络相关函数(六)——live555源码阅读(四)网络
38 网络相关函数(六)——live555源码阅读(四)网络 38 网络相关函数(六)——live555源码阅读(四)网络 简介 12)makeSocketNonBlocking和makeSocket ...
- 36 网络相关函数(四)——live555源码阅读(四)网络
36 网络相关函数(四)——live555源码阅读(四)网络 36 网络相关函数(四)——live555源码阅读(四)网络 简介 7)createSocket创建socket方法 8)closeSoc ...
- 35 网络相关函数(三)——live555源码阅读(四)网络
35 网络相关函数(三)——live555源码阅读(四)网络 35 网络相关函数(三)——live555源码阅读(四)网络 简介 5)NoReuse不重用地址类 6)initializeWinsock ...
- 34 网络相关函数(二)——live555源码阅读(四)网络
34 网络相关函数(二)——live555源码阅读(四)网络 34 网络相关函数(二)——live555源码阅读(四)网络 2)socketErr 套接口错误 3)groupsockPriv函数 4) ...
- 33 网络相关函数(一)——live555源码阅读(四)网络
33 网络相关函数(一)——live555源码阅读(四)网络 33 网络相关函数(一)——live555源码阅读(四)网络 简介 1)IsMulticastAddress多播(组播)地址判断函数 多播 ...
- 32 GroupSock(AddressPortLookupTable)——live555源码阅读(四)网络
32 GroupSock(AddressPortLookupTable)——live555源码阅读(四)网络 32 GroupSock(AddressPortLookupTable)——live555 ...
- 31 GroupSock(AddressString)——live555源码阅读(四)网络
31 GroupSock(AddressString)——live555源码阅读(四)网络 31 GroupSock(AddressString)——live555源码阅读(四)网络 简介 Addre ...
随机推荐
- SQL查询 错误 [1843] [22008]: ORA-01843: 无效的月份
dbeaver客户端运行sql查询Oracle库报错. 正确示例: select count(*) from PRODUCTS WHERE CREATE_TIME > '15-7月-2021 ' ...
- c++学习9 结构体
一 结构体赋值 结构体赋值的方法有三种,逐个成员赋值,整体赋值和拷贝赋值. 设一个结构体有struck student{ int age;char ch[32]; }: 逐个成员赋值:student ...
- OpenCV 4.5.2环境配置 + 图片灰度化处理
一,OpenCV环境配置 注意:以下配置内容为Android开发环境配置好的基础上的OpenCV配置环境 1.官网下载OpenCV的sdk包,下载的是4.5.2的Android版本 Releases ...
- input button
即使你在文本输入下方添加了按钮,它们也会在页面上彼此相邻. 这是因为 input 和 button 元素都是内联元素,它们不会出现在新的行上. <button type='submit'> ...
- Charles 抓取 HTTPS 协议内容,需要做什么操作?
抓取 HTTPS 需要安装证书,Charles 端需要安装 Android.iOS手机端也需要安装 电脑的 Charles 操作:1.proxy - proxy setting - http prox ...
- js使用sort将JSON数据进行排序
在把数据通过Echarts展示成统计图模式时,柱状统计图需要将数据进行从大到小来排序! 下面为所需要的数据: 1 { 2 mapData: [ 3 {name: '北京',value: '555'}, ...
- nuttx理解
操作系统:为啥要引入操作系统,个人的理解是为了实时性(即及时的响应性). 没有操作系统下多个任务都只能以前后台的方式排队执行,对某个任务的输入不能得到及时的响应:虽然后台有中断,但不能把所有的任务都放 ...
- seleniumUI自动化学习记录
2019.2.9 尝试了一个启动浏览器并打开指定网址的程序: 这里首先要注意的就是浏览器的版本和selenium jar包的版本必须符合才行,不然会报错 2019.9.16 必须要下载相应的chrom ...
- 更多Linux实用命令
更多实用命令 进程相关 当程序运行在系统上时,我们称之为进程(process).想监测这些进程,需要熟悉 ps/top 等命令的用法.ps 命令好比工具中的瑞士军刀,它能输出运行在系统上的所有程序的许 ...
- CSS 常用样式-字体属性
字体类样式我们已经学习过字号font-size.字体font-family两个属性,接下来还有几个常用的字体属性. 粗细 font-weight: 作用:设置文字是否加粗显示. 属性名:font-we ...