Android输入系统是人与机器交互最主要的手段。我们通过按键或者触碰屏幕,会先经由linux产生中断,进行统一的处理过后,转换成Android能识别的事件信息,然后Android的输入系统去获取事件,分发给上层用户程序进行处理。

下面在细分一下输入事件在Android系统中的流程:

从图上能看到,输入事件有四个处理的地方:

  1. InputReaderThread
  2. InputDispatcherThread
  3. WindowInputEventReceiver
  4. handleReceiverCallback

上面四个地方按功能来划分,其中:

  1. InputReaderThread负责从输入设备中获取事件,事件加入inboundQueue队列。
  2. InputDispatcherThread负责把inboundQueue中的事件信息取出,并且从系统中获取该事件所需要分发到的目标(窗口),把事件与目标分别整合成分发项,把分发项加入outboundQueue。另外,这里还是事件的分发端,负责把outboundQueue中的事件取出,通过InputChannel进行分发。分发完成后把该事件入waitQueue。
  3. WindowInputEventReceiver是事件的接收端。事件会在这里被onTouch这类回调函数处理
  4. handleReceiveCallback用于接收处理过后的反馈信息,事件在WindowInputEventReceiver端被处理成功或者失败,将会通过InputChannel返回Handled或者UNHandled消息。handleReceiveCallback接收到消息后将会对waitQueue中的事件进行出队列处理。

InputManager

InputManager用于启动InputReaderThread与InputDispatcherThread,会在system_server初始化的时候被创建并且调用InputManager的start方法启动这两个线程。

InputManager的构造函数如下:

  1. InputManager::InputManager(
  2. const sp<EventHubInterface>& eventHub,
  3. const sp<InputReaderPolicyInterface>& readerPolicy,
  4. const sp<InputDispatcherPolicyInterface>& dispatcherPolicy) {
  5. mDispatcher = new InputDispatcher(dispatcherPolicy);
  6. mReader = new InputReader(eventHub, readerPolicy, mDispatcher);
  7. initialize();
  8. }

可以看到构造了InputDispatcher与InputReader两个类,这两个类是功能类,分别为InputDispatcherThread与InputReaderThread提供功能。另外,在构建InputReader的时候,把mDispatcher传递了进去,用于构建QueueInputListener。在这里可以提前说明一下这个成员的作用:把输入事件添加到inboundQueue。

构造函数最后调用了initialize,构建InputReaderThread、InputDispatcherThread。

  1. void InputManager::initialize() {
  2. mReaderThread = new InputReaderThread(mReader);
  3. mDispatcherThread = new InputDispatcherThread(mDispatcher);
  4. }

InputManager的start用于启动InputReaderThread与InputDispatcherThread这两个线程。

  1. status_t InputManager::start() {
  2. status_t result = mDispatcherThread->run("InputDispatcher", PRIORITY_URGENT_DISPLAY);
  3. if (result) {
  4. ALOGE("Could not start InputDispatcher thread due to error %d.", result);
  5. return result;
  6. }
  7.  
  8. result = mReaderThread->run("InputReader", PRIORITY_URGENT_DISPLAY);
  9. if (result) {
  10. ALOGE("Could not start InputReader thread due to error %d.", result);
  11.  
  12. mDispatcherThread->requestExit();
  13. return result;
  14. }
  15.  
  16. return OK;
  17. }

InputReaderThread

InputReaderThread是用来从输入设备中读取输入事件的,首先看一下该线程的threadLoop函数

  1. bool InputReaderThread::threadLoop() {
  2. mReader->loopOnce();
  3. return true;
  4. }

mReader即在构建InputReaderThread时传进来的InputReader,负责实现读取输入事件所需要的各种功能。InputReader::loopOnce用于读取一次输入事件。其中,读取一次包含三个主要动作:

  1. 获取输入事件
  2. 处理输入事件
  3. 输入数据flush
  1. void InputReader::loopOnce() {
  2. size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);
  3.  
  4. { // acquire lock
  5. processEventsLocked(mEventBuffer, count);
  6. }
  7.  
  8. mQueuedListener->flush();
  9. }

1. 获取输入事件getEvents

几乎所有与输入有关的事件都会从这里获得。其中包含:

  1. EPOLL_ID_INORIFY.输入设备打开或者删除的事件
  2. EPOLL_ID_WAKE.管道发送过来的模拟事件
  3. EPOLL_IN.按键,触摸这类实际操作事件

EPOLL_ID_INOTIFY,用于监控某个目录(子目录)下是否有新增或者删除文件,在这里用于监视/dev/input,这个是输入设备文件所在的目录,如果有新增设备,则会在该目录内创建新文件;如果删除设备,则该目录的相应文件会被删除。

  1. if (eventItem.data.u32 == EPOLL_ID_INOTIFY) {
  2. if (eventItem.events & EPOLLIN) {
  3. mPendingINotify = true;
  4. } else {
  5. ALOGW("Received unexpected epoll event 0x%08x for INotify.", eventItem.events);
  6. }
  7. continue;
  8. }
  9.  
  10. ......
  11.  
  12. if (mPendingINotify && mPendingEventIndex >= mPendingEventCount) {
  13. mPendingINotify = false;
  14. readNotifyLocked();
  15. deviceChanged = true;
  16. }
  17.  
  18. status_t EventHub::readNotifyLocked() {
  19. if(event->mask & IN_CREATE) {
  20. openDeviceLocked(devname);
  21. } else {
  22. ALOGI("Removing device '%s' due to inotify event\n", devname);
  23. closeDeviceByPathLocked(devname);
  24. }
  25. }

EPOLL_ID_WAKE,EventHub有维护一个pipe,当pipe的写入端按照适当格式写入时间后,getEvents可以通过pipe的读取端获取这个虚拟事件

  1. if (eventItem.data.u32 == EPOLL_ID_WAKE) {
  2. if (eventItem.events & EPOLLIN) {
  3. ALOGV("awoken after wake()");
  4. awoken = true;
  5. char buffer[16];
  6. ssize_t nRead;
  7. do {
  8. nRead = read(mWakeReadPipeFd, buffer, sizeof(buffer));
  9. } while ((nRead == -1 && errno == EINTR) || nRead == sizeof(buffer));
  10. } else {
  11. ALOGW("Received unexpected epoll event 0x%08x for wake read pipe.",
  12. eventItem.events);
  13. }
  14. continue;
  15. }

EPOLL_IN,用于监控设备文件的输入状态,当我们按键或者触摸设备时,我们就能获得EPOLL_IN状态,从而到该设备读取输入事件

  1. if (eventItem.events & EPOLLIN) {
  2. int32_t readSize = read(device->fd, readBuffer,
  3. sizeof(struct input_event) * capacity);
  4. event->when = now;
  5. event->deviceId = deviceId;
  6. event->type = iev.type;
  7. event->code = iev.code;
  8. event->value = iev.value;
  9. event += 1;
  10. capacity -= 1;
  11. }

监听事件用的是epoll_wait,由于epoll_wait一次能获取的事件可能会有多个,所以一次的getEvents需要对所获得的每个事件都进行上述代码的打包操作,最后返回事件数组。

  1. int pollResult = epoll_wait(mEpollFd, mPendingEventItems, EPOLL_MAX_EVENTS, timeoutMillis);

2. 处理输入事件processEventsLocked

由getEvents获得的事件数组会在这个函数内进行处理,其中事件数组中的事件大致可以分为两类,在这个函数将他们分开处理

  1. 按键、触摸事件
  2. 设备增加、删除事件
  1. void InputReader::processEventsLocked(const RawEvent* rawEvents, size_t count) {
  2. for (const RawEvent* rawEvent = rawEvents; count;) {
  3. int32_t type = rawEvent->type;
  4. size_t batchSize = 1;
  5. if (type < EventHubInterface::FIRST_SYNTHETIC_EVENT) {
  6. int32_t deviceId = rawEvent->deviceId;
  7. while (batchSize < count) {
  8. if (rawEvent[batchSize].type >= EventHubInterface::FIRST_SYNTHETIC_EVENT
  9. || rawEvent[batchSize].deviceId != deviceId) {
  10. break;
  11. }
  12. batchSize += 1;
  13. }
  14. #if DEBUG_RAW_EVENTS
  15. ALOGD("BatchSize: %d Count: %d", batchSize, count);
  16. #endif
  17. processEventsForDeviceLocked(deviceId, rawEvent, batchSize);
  18. } else {
  19. switch (rawEvent->type) {
  20. case EventHubInterface::DEVICE_ADDED:
  21. addDeviceLocked(rawEvent->when, rawEvent->deviceId);
  22. break;
  23. case EventHubInterface::DEVICE_REMOVED:
  24. removeDeviceLocked(rawEvent->when, rawEvent->deviceId);
  25. break;
  26. case EventHubInterface::FINISHED_DEVICE_SCAN:
  27. handleConfigurationChangedLocked(rawEvent->when);
  28. break;
  29. default:
  30. ALOG_ASSERT(false); // can't happen
  31. break;
  32. }
  33. }
  34. count -= batchSize;
  35. rawEvent += batchSize;
  36. }
  37. }

在处理按键、触摸事件时,会根据他们设备的类型调用不同的process函数进行处理。对于触摸事件,基本上只是进行赋值,而按键事件则需要通过映射,把从设备文件读取进来的值转换成Android上层能统一处理的按键事件。

  1. void InputReader::processEventsForDeviceLocked(int32_t deviceId,
  2. const RawEvent* rawEvents, size_t count) {
  3. InputDevice* device = mDevices.valueAt(deviceIndex);
  4. device->process(rawEvents, count);
  5. }
  6.  
  7. void KeyboardInputMapper::process(const RawEvent* rawEvent) {
  8. switch (rawEvent->type) {
  9. case EV_KEY: {
  10. if (getEventHub()->mapKey(getDeviceId(), scanCode, usageCode, &keyCode, &flags)) {
  11. keyCode = AKEYCODE_UNKNOWN;
  12. flags = 0;
  13. }
  14. processKey(rawEvent->when, rawEvent->value != 0, keyCode, scanCode, flags);
  15. }
  16. break;
  17. }
  18. }

上面的mapKey对按键进行了映射处理,processKey用于区分按键的按下或者松开。在processKey的最后,会把事件打包成NotifyKeyArgs,然后通过QueueInputListener把事件push进mArgQueue。由于这里是一个事件数组,所以mArgQueue是必须的。

  1. void KeyboardInputMapper::processKey(nsecs_t when, bool down, int32_t keyCode,
  2. int32_t scanCode, uint32_t policyFlags) {
  3.  
  4. if (down) {
  5. ...
  6. } else {
  7. ...
  8. }
  9. NotifyKeyArgs args(when, getDeviceId(), mSource, policyFlags,
  10. down ? AKEY_EVENT_ACTION_DOWN : AKEY_EVENT_ACTION_UP,
  11. AKEY_EVENT_FLAG_FROM_SYSTEM, keyCode, scanCode, newMetaState, downTime);
  12. getListener()->notifyKey(&args);
  13. }
  14.  
  15. void QueuedInputListener::notifyKey(const NotifyKeyArgs* args) {
  16. mArgsQueue.push(new NotifyKeyArgs(*args));
  17. }

3. 输入数据flush

在事件数组都push进mArgQueue之后,就需要把mArgQueue队列给推送出去进行下一步的操作,mQueuedListener->flush();就是负责进行队列的推送。还记得我们最开始说的”在构建InputReader的时候,把mDispatcher传递了进去,用于构建QueueInputListener”,我们这里的flush最终就是调用了InputDispatcher的notifyKey

  1. void QueuedInputListener::flush() {
  2. size_t count = mArgsQueue.size();
  3. for (size_t i = 0; i < count; i++) {
  4. NotifyArgs* args = mArgsQueue[i];
  5. args->notify(mInnerListener);
  6. delete args;
  7. }
  8. mArgsQueue.clear();
  9. }
  10.  
  11. void NotifyKeyArgs::notify(const sp<InputListenerInterface>& listener) const {
  12. listener->notifyKey(this);
  13. }

以notifyKey为例,其目的实际上是把事件队列加入mInboundQueue,但是在入mInboundQueue队列之前,调用了interceptKeyBeforeQueueing,该函数通过jni,调用到PhoneWindowManager的interceptKeyBeforeQueueing。而在入了mInboundQueue队列后,就会调用wake函数去唤醒InputDispatcherThread。下一步就是InputDispatcherThread的工作了。

  1. void InputDispatcher::notifyKey(const NotifyKeyArgs* args) {
  2. mPolicy->interceptKeyBeforeQueueing(&event, /*byref*/ policyFlags);
  3. needWake = enqueueInboundEventLocked(newEntry);
  4. if (needWake) {
  5. mLooper->wake();
  6. }
  7. }

InputDispatcherThread

InputDispatcherThread是用来进行事件分发的线程。内部也是调用InputDispatcher来实现所需要的功能。

  1. bool InputDispatcherThread::threadLoop() {
  2. mDispatcher->dispatchOnce();
  3. return true;
  4. }

每次分发,调用的都是dispatchOnce,其内部调用dispatchOnceInnerLocked进行分发后,线程会调用pollOnce进入睡眠,等待下次InputReaderThread的wake操作

  1. void InputDispatcher::dispatchOnce() {
  2. dispatchOnceInnerLocked(&nextWakeupTime);
  3. mLooper->pollOnce(timeoutMillis);
  4. }

分发的过程可以大概分成以下几个步骤:

  1. 从mInboundQueue的队列头取出事件
  2. 特殊事件的处理,如POLICY_FLAG_PASS_TO_USER这类事件能直接发送到用户,类似于电量不足的这类事件:当电量低于20%时,直接往上层发送事件,而不用知道当前是在哪个Activity
  3. 一般事件的处理,进行分发
  1. void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
  2. mPendingEvent = mInboundQueue.dequeueAtHead();
  3.  
  4. // Poke user activity for this event.
  5. if (mPendingEvent->policyFlags & POLICY_FLAG_PASS_TO_USER) {
  6. pokeUserActivityLocked(mPendingEvent);
  7. }
  8.  
  9. switch (mPendingEvent->type) {
  10. case EventEntry::TYPE_KEY: {
  11. done = dispatchKeyLocked(currentTime, typedEntry, &dropReason, nextWakeupTime);
  12. break;
  13. }
  14. }
  15. }

分发事件,肯定需要知道事件要分发到哪里,即分发的目标窗口,不过目标窗口可能不止一个。

  1. bool InputDispatcher::dispatchKeyLocked(nsecs_t currentTime, KeyEntry* entry,
  2. DropReason* dropReason, nsecs_t* nextWakeupTime) {
  3. int32_t injectionResult = findFocusedWindowTargetsLocked(currentTime,
  4. entry, inputTargets, nextWakeupTime);
  5.  
  6. // Dispatch the key.
  7. dispatchEventLocked(currentTime, entry, inputTargets);
  8. return true;
  9. }

由于可能存在多个目标窗口,所以需要对每个目标窗口都进行事件分发

  1. void InputDispatcher::dispatchEventLocked(nsecs_t currentTime,
  2. EventEntry* eventEntry, const Vector<InputTarget>& inputTargets) {
  3. for (size_t i = 0; i < inputTargets.size(); i++) {
  4. prepareDispatchCycleLocked(currentTime, connection, eventEntry, &inputTarget);
  5. }
  6. }

在分发前的准备,就是把事件入outboundQueue队列,不过请注意,这里的队列不同于inboundQueue,因为outboundQueue是窗口相关的,窗口跟InputDispatcherThread间建立起一个连接(connection),该outboundQueue就是connection的成员。

  1. void InputDispatcher::prepareDispatchCycleLocked(nsecs_t currentTime,
  2. const sp<Connection>& connection, EventEntry* eventEntry, const InputTarget* inputTarget) {
  3. // Not splitting. Enqueue dispatch entries for the event as is.
  4. enqueueDispatchEntriesLocked(currentTime, connection, eventEntry, inputTarget);
  5. }
  6.  
  7. void InputDispatcher::enqueueDispatchEntriesLocked(nsecs_t currentTime,
  8. const sp<Connection>& connection, EventEntry* eventEntry, const InputTarget* inputTarget) {
  9. bool wasEmpty = connection->outboundQueue.isEmpty();
  10.  
  11. // Enqueue dispatch entries for the requested modes.
  12. enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
  13. InputTarget::FLAG_DISPATCH_AS_HOVER_EXIT);
  14. enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
  15. InputTarget::FLAG_DISPATCH_AS_OUTSIDE);
  16. enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
  17. InputTarget::FLAG_DISPATCH_AS_HOVER_ENTER);
  18. enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
  19. InputTarget::FLAG_DISPATCH_AS_IS);
  20. enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
  21. InputTarget::FLAG_DISPATCH_AS_SLIPPERY_EXIT);
  22. enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
  23. InputTarget::FLAG_DISPATCH_AS_SLIPPERY_ENTER);
  24.  
  25. // If the outbound queue was previously empty, start the dispatch cycle going.
  26. if (wasEmpty && !connection->outboundQueue.isEmpty()) {
  27. startDispatchCycleLocked(currentTime, connection);
  28. }
  29. }
  30.  
  31. void InputDispatcher::enqueueDispatchEntryLocked(
  32. const sp<Connection>& connection, EventEntry* eventEntry, const InputTarget* inputTarget,
  33. int32_t dispatchMode) {
  34. // Enqueue the dispatch entry.
  35. connection->outboundQueue.enqueueAtTail(dispatchEntry);
  36. }

在准备完成后就会调用startDispatchCycleLocked进行事件分发,startDispatchCycleLocked这个函数的主体是一个while循环,在循环体内会执行下面三个主要步骤:

  1. 调用connection的inputPublisher来发出事件
  2. 把事件从outboundQueue队列中移除
  3. 把事件加入waitQueue队列,当事件在处理完成后返回,就会从waitQueue中删除该事件
  1. void InputDispatcher::startDispatchCycleLocked(nsecs_t currentTime,
  2. const sp<Connection>& connection) {
  3. while (connection->status == Connection::STATUS_NORMAL
  4. && !connection->outboundQueue.isEmpty()) {
  5. DispatchEntry* dispatchEntry = connection->outboundQueue.head;
  6.  
  7. switch (eventEntry->type) {
  8. case EventEntry::TYPE_KEY: {
  9. KeyEntry* keyEntry = static_cast<KeyEntry*>(eventEntry);
  10.  
  11. // Publish the key event.
  12. status = connection->inputPublisher.publishKeyEvent(dispatchEntry->seq,
  13. keyEntry->deviceId, keyEntry->source,
  14. dispatchEntry->resolvedAction, dispatchEntry->resolvedFlags,
  15. keyEntry->keyCode, keyEntry->scanCode,
  16. keyEntry->metaState, keyEntry->repeatCount, keyEntry->downTime,
  17. keyEntry->eventTime);
  18. }
  19. }
  20.  
  21. // Re-enqueue the event on the wait queue.
  22. connection->outboundQueue.dequeue(dispatchEntry);
  23. traceOutboundQueueLengthLocked(connection);
  24. connection->waitQueue.enqueueAtTail(dispatchEntry);
  25. traceWaitQueueLengthLocked(connection);
  26. }
  27. }

我们来看一下inputPublisher的publishKeyEvent的实现,最后也是调用socket的send接口来实现。

  1. status_t InputPublisher::publishKeyEvent(
  2. uint32_t seq,
  3. int32_t deviceId,
  4. int32_t source,
  5. int32_t action,
  6. int32_t flags,
  7. int32_t keyCode,
  8. int32_t scanCode,
  9. int32_t metaState,
  10. int32_t repeatCount,
  11. nsecs_t downTime,
  12. nsecs_t eventTime) {
  13.  
  14. InputMessage msg;
  15. msg.header.type = InputMessage::TYPE_KEY;
  16. msg.body.key.seq = seq;
  17. msg.body.key.deviceId = deviceId;
  18. msg.body.key.source = source;
  19. msg.body.key.action = action;
  20. msg.body.key.flags = flags;
  21. msg.body.key.keyCode = keyCode;
  22. msg.body.key.scanCode = scanCode;
  23. msg.body.key.metaState = metaState;
  24. msg.body.key.repeatCount = repeatCount;
  25. msg.body.key.downTime = downTime;
  26. msg.body.key.eventTime = eventTime;
  27. return mChannel->sendMessage(&msg);
  28. }
  29.  
  30. status_t InputChannel::sendMessage(const InputMessage* msg) {
  31. do {
  32. nWrite = ::send(mFd, msg, msgLength, MSG_DONTWAIT | MSG_NOSIGNAL);
  33. } while (nWrite == -1 && errno == EINTR);
  34. }
  1.  

总体的流程如下

[Android] 输入系统(一)的更多相关文章

  1. Android核心分析之十五Android输入系统之输入路径详解

       Android用户事件输入路径 1 输入路径的一般原理 按键,鼠标消息从收集到最终将发送到焦点窗口,要经历怎样的路径,是Android GWES设计方案中需要详细考虑的问题.按键,鼠标等用户消息 ...

  2. 《深入理解Android 卷III》第五章 深入理解Android输入系统

    <深入理解Android 卷III>即将公布.作者是张大伟.此书填补了深入理解Android Framework卷中的一个主要空白.即Android Framework中和UI相关的部分. ...

  3. 10.7 android输入系统_Dispatcher线程情景分析_Reader线程传递事件和dispatch前处理

    android输入系统C++最上层文件是com_android_serve_input_InputManagerService.cpp global key:按下按键,启动某个APP可以自己指定,修改 ...

  4. 10.5 android输入系统_Reader线程_使用EventHub读取事件和核心类及配置文件_实验_分析

    4. Reader线程_使用EventHub读取事件 使用inotify监测/dev/input下文件的创建和删除 使用epoll监测有无数据上报 细节: a.fd1 = inotify_init(& ...

  5. 10.4 android输入系统_框架、编写一个万能模拟输入驱动程序、reader/dispatcher线程启动过程源码分析

    1. 输入系统框架 android输入系统官方文档 // 需FQhttp://source.android.com/devices/input/index.html <深入理解Android 卷 ...

  6. 10.1、android输入系统_必备Linux编程知识_inotify和epoll

    1. inotify和epoll 怎么监测键盘接入与拔出? (1)hotplug机制:内核发现键盘接入/拔出==>启动hotplug进程==>发消息给输入系统 (2)inotify机制:输 ...

  7. 10.12 android输入系统_InputStage理论

    android应用程序对输入系统的处理分为多个阶段,我们把这些阶段称为InputStage 理论处理流程: (1)activity发给window,如果window不能处理,再由activity处理; ...

  8. 10.11 android输入系统_补充知识_activity_window_decor_view关系

    android里:1个application, 有1个或多个activity(比如支付宝有:首页.财富.口碑.朋友.我的,这些就是activity)1个activity, 有1个window(每个ac ...

  9. 10.6 android输入系统_Dispatcher线程_总体框架

    图解Android - Android GUI 系统 (5) - Android的Event Input System - 漫天尘沙 - 博客园.htm // 关注里面的Dispatcher处理流程h ...

随机推荐

  1. lesson10:hashmap变慢原因分析

    下面的英文描述了String.hashCode()方法,在特定情况下,返回值为0的问题: Java offers the HashMap and Hashtable classes, which us ...

  2. JAVAEE filter总结

    1.  为什么需要filter? filter相当于客户端和服务器端之间的一扇门,就像保安一样.作用:比如说设置字符集和权限控制等等. 2.  细节; * . 只能对post请求起作用 *  .可以使 ...

  3. Core OS 层

    Core OS层的底层功能是很多其他技术的构建基础.通常情况下,这些功能不会直接应用于应用程序,而是应用于其他框架.但是,在直接处理安全事务或和某个外设通讯的时候,则必须要应用到该层的框架. Acce ...

  4. 字体图标 icon font

    Icon font icon font 指的是用字体文件代替图片文件,来展示图标.特殊字体等元素的方法. 应用场景: iconfont的优缺点 大小能够自由地变化 颜色能够自由地改动 加入阴影效果 * ...

  5. 使用Listener准备application作用域数据

    在程序中.有些数据我们希望在程序启动的时候就准备好,而且仅仅准备一次,放在application作用域中,这时候.我们一般会用Listener来准备这些数据. 可是,用Listener准备applic ...

  6. [转] weak_ptr解决shared_ptr环状引用所引起的内存泄漏

    http://blog.csdn.net/liuzhi1218/article/details/6993135 循环引用: 引用计数是一种便利的内存管理机制,但它有一个很大的缺点,那就是不能管理循环引 ...

  7. wp-content-index文章目录插件使用效果调整

    安装好wp-content-index后进行如下设置: 其中标红处必须标红,用于检索锚点.在文章页面添加如下js代码: $(function() { var wpindex = $("#co ...

  8. SSL证书制作

    1.创建根证书秘钥文件(自己做CA)root.key: openssl genrsa -out root.key -aes256 2048 2.创建根证书的申请文件root.csr openssl r ...

  9. jquery之隐藏div

    1.$("#demo").attr("style","display:none;");//隐藏div $("#demo" ...

  10. [Codeforces 501D] - Misha and Permutations Summation

    题意是给你两个长度为$n$的排列,他们分别是$n$的第$a$个和第$b$个全排列.输出$n$的第$\left(a+b \right)\textrm{mod} \, n!$个全排列. 一种很容易的想法是 ...