事件系统

文章为本人理解,如有理解不到位之处,烦请各位指正。

@

Qt的事件循环,应该是所有Qter都避不开的一个点,所以,这篇博客,咱们来了解源码中一些关于Qt中事件循环的部分。

先抛出几个疑问,根据源代码,下面一一进行解析。

  1. 事件循环是什么?
  2. 事件是怎么产生的?
  3. 事件是如何处理的?

什么是事件循环?

对于Qt事件循环个人理解是,事件循环是一个队列去循环处理事件。当队列中有事件时,则去处理事件,如果没有事件时,则会阻塞等待。

事件是如何产生的?

事件的产生可以分为两种:

  1. 程序外部产生
  2. 程序内部产生

程序外部所产生的事件主要是指系统产生的事件,比如说鼠标按下(MouseButtonPress)、按键按下(KeyPress)等,Qt捕捉系统的事件,然后将系统事件封装成自己的QEvent类,再将事件发送出去。

程序内部产生的事件主要指我们在代码里,手动创建一个事件,然后将事件通过sendEvent/postEvent,来发送到事件循环中。而sendEventpostEvent区别又在于一个是阻塞的(sendEvent)一个是非阻塞的(postEvent)。

我们结合源码分析,看一下sendEventpostEvent分别干了什么导致一个是阻塞的一个是非阻塞的。

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;
}

然后我们可以看到主要有几个流程:

  1. 判断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;
    }
  2. 判断事件接受对象,有没有安装事件过滤器,有就将信号发送到事件过滤器。

    // 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.

    后插入的事件过滤器会被优先响应。 具体安装事件过滤器,我们在后面进行分析。

  3. 直接调用事件接受对象的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();
}
  1. 判断事件接收对象是否为空

    // 事件的接收者不能为空
    if (receiver == nullptr) {
    qWarning("QCoreApplication::postEvent: Unexpected null receiver");
    delete event;
    return;
    }
  2. 将事件接收对象所在线程的post事件列表上锁,如果已经被锁了,就把事件删除掉,并返回,防止泄露。

    // 对事件接受对象所在线程的事件处理列表上锁
    auto locker = QCoreApplicationPrivate::lockThreadPostEventList(receiver);
    if (!locker.threadData) {
    // posting during destruction? just delete the event to prevent a leak
    delete event;
    return;
    }
  3. 将一些可以压缩的事件进行压缩,及多个事件压缩成只推送最后的一个事件。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;
    }
  4. 将事件插入接收对象所在线程的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进行处理。

而事件的处理,主要分为三个部分:

  1. 先是由事件循环遍历事件
  2. 然后判断事件接受对象有没有安装事件过滤器(installEventFilter),有安装的话,就把事件丢给事件过滤器(eventFilter)进行处理。
  3. 如果没有安装事件过滤器或者事件过滤器对该事件不进行处理的话,那么,事件将会进一步转发到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());
}

可以看到,事件调度器最终还是调用了QCoreApplicationsendPostEvents

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;
}

我们一个一个的分块分析:

  1. 判断是否在一个线程

    if (receiver && receiver->d_func()->threadData != data) {
    qWarning("QCoreApplication::sendPostedEvents: Cannot send "
    "posted events for objects in another thread");
    return;
    }
  2. 一个有意思的异常安全的处理,不需要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的析构函数。

  1. 将事件发送出去(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;
}

夹带私货时间

  1. 之前有说到processEvent,添加一个小经验。当我们有时候不得不在主线程循环执行很耗时的操作的时候,这个时候,界面就会刷新不过来,就会导致界面卡顿,影响使用。但是,我们可以在这个循环里,手动调用qApp->processEvent(),这样就可以手动调用处理掉所有的事件,就可以解决卡顿的问题

Qt源码阅读(四) 事件循环的更多相关文章

  1. 40 网络相关函数(八)——live555源码阅读(四)网络

    40 网络相关函数(八)——live555源码阅读(四)网络 40 网络相关函数(八)——live555源码阅读(四)网络 简介 15)writeSocket向套接口写数据 TTL的概念 函数send ...

  2. 37 网络相关函数(五)——live555源码阅读(四)网络

    37 网络相关函数(五)——live555源码阅读(四)网络 37 网络相关函数(五)——live555源码阅读(四)网络 简介 10)MAKE_SOCKADDR_IN构建sockaddr_in结构体 ...

  3. 39 网络相关函数(七)——live555源码阅读(四)网络

    39 网络相关函数(七)——live555源码阅读(四)网络 39 网络相关函数(七)——live555源码阅读(四)网络 简介 14)readSocket从套接口读取数据 recv/recvfrom ...

  4. 38 网络相关函数(六)——live555源码阅读(四)网络

    38 网络相关函数(六)——live555源码阅读(四)网络 38 网络相关函数(六)——live555源码阅读(四)网络 简介 12)makeSocketNonBlocking和makeSocket ...

  5. 36 网络相关函数(四)——live555源码阅读(四)网络

    36 网络相关函数(四)——live555源码阅读(四)网络 36 网络相关函数(四)——live555源码阅读(四)网络 简介 7)createSocket创建socket方法 8)closeSoc ...

  6. 35 网络相关函数(三)——live555源码阅读(四)网络

    35 网络相关函数(三)——live555源码阅读(四)网络 35 网络相关函数(三)——live555源码阅读(四)网络 简介 5)NoReuse不重用地址类 6)initializeWinsock ...

  7. 34 网络相关函数(二)——live555源码阅读(四)网络

    34 网络相关函数(二)——live555源码阅读(四)网络 34 网络相关函数(二)——live555源码阅读(四)网络 2)socketErr 套接口错误 3)groupsockPriv函数 4) ...

  8. 33 网络相关函数(一)——live555源码阅读(四)网络

    33 网络相关函数(一)——live555源码阅读(四)网络 33 网络相关函数(一)——live555源码阅读(四)网络 简介 1)IsMulticastAddress多播(组播)地址判断函数 多播 ...

  9. 32 GroupSock(AddressPortLookupTable)——live555源码阅读(四)网络

    32 GroupSock(AddressPortLookupTable)——live555源码阅读(四)网络 32 GroupSock(AddressPortLookupTable)——live555 ...

  10. 31 GroupSock(AddressString)——live555源码阅读(四)网络

    31 GroupSock(AddressString)——live555源码阅读(四)网络 31 GroupSock(AddressString)——live555源码阅读(四)网络 简介 Addre ...

随机推荐

  1. SQL查询 错误 [1843] [22008]: ORA-01843: 无效的月份

    dbeaver客户端运行sql查询Oracle库报错. 正确示例: select count(*) from PRODUCTS WHERE CREATE_TIME > '15-7月-2021 ' ...

  2. c++学习9 结构体

    一 结构体赋值 结构体赋值的方法有三种,逐个成员赋值,整体赋值和拷贝赋值. 设一个结构体有struck student{ int age;char ch[32]; }: 逐个成员赋值:student ...

  3. OpenCV 4.5.2环境配置 + 图片灰度化处理

    一,OpenCV环境配置 注意:以下配置内容为Android开发环境配置好的基础上的OpenCV配置环境 1.官网下载OpenCV的sdk包,下载的是4.5.2的Android版本 Releases ...

  4. input button

    即使你在文本输入下方添加了按钮,它们也会在页面上彼此相邻. 这是因为 input 和 button 元素都是内联元素,它们不会出现在新的行上. <button type='submit'> ...

  5. Charles 抓取 HTTPS 协议内容,需要做什么操作?

    抓取 HTTPS 需要安装证书,Charles 端需要安装 Android.iOS手机端也需要安装 电脑的 Charles 操作:1.proxy - proxy setting - http prox ...

  6. js使用sort将JSON数据进行排序

    在把数据通过Echarts展示成统计图模式时,柱状统计图需要将数据进行从大到小来排序! 下面为所需要的数据: 1 { 2 mapData: [ 3 {name: '北京',value: '555'}, ...

  7. nuttx理解

    操作系统:为啥要引入操作系统,个人的理解是为了实时性(即及时的响应性). 没有操作系统下多个任务都只能以前后台的方式排队执行,对某个任务的输入不能得到及时的响应:虽然后台有中断,但不能把所有的任务都放 ...

  8. seleniumUI自动化学习记录

    2019.2.9 尝试了一个启动浏览器并打开指定网址的程序: 这里首先要注意的就是浏览器的版本和selenium jar包的版本必须符合才行,不然会报错 2019.9.16 必须要下载相应的chrom ...

  9. 更多Linux实用命令

    更多实用命令 进程相关 当程序运行在系统上时,我们称之为进程(process).想监测这些进程,需要熟悉 ps/top 等命令的用法.ps 命令好比工具中的瑞士军刀,它能输出运行在系统上的所有程序的许 ...

  10. CSS 常用样式-字体属性

    字体类样式我们已经学习过字号font-size.字体font-family两个属性,接下来还有几个常用的字体属性. 粗细 font-weight: 作用:设置文字是否加粗显示. 属性名:font-we ...