Android Input子系统浅谈

本文主要讲解[Android Input 子系统][6],我会从一下几个方面讲解:

  • linux kernel的input子系统框架
  • 以触摸屏驱动为例讲解内核input子系统
  • Android framework层Input子系统的框架
  • Input子系统的应用程序接口

linux kernel里面input子系统框架

  • **主要作用是维护两个重要的链表input_dev_list和input_handler_list
  • 下面这段代码便是内核里面Input子系统的框架层部分代码
    代码位置:/kernel/driver/input/input.c
    可以看到input的内核框架层也是以类似于driver的方式注册的
    这段代码便是Input子系统在内核中的核心框架;大家可能会疑惑怎么这
    么简单,看着什么也没有做,其实却是这样,这这段code里面主要建立
    了一些用于debug的节点,主要有如下节点:

class节点/sys/class/input :调用class_register生成

proc节点/proc/bus/input/devices和handles:调用input_proc_init生成

dev节点:/dev/input:调用register_chrdev_region

以上三种节点在后面会讲解分别用作什么

static int __init input_init(void){    int err;    err = class_register(&input_class);    if (err) {        pr_err("unable to register input_dev class\n");        return err;    }    err = input_proc_init();    if (err)        goto fail1;    err = register_chrdev_region(MKDEV(INPUT_MAJOR, 0),                     INPUT_MAX_CHAR_DEVICES, "input");    if (err) {        pr_err("unable to register char major %d", INPUT_MAJOR);        goto fail2;    }    return 0; fail2: input_proc_exit(); fail1: class_unregister(&input_class);    return err;}static void __exit input_exit(void){    input_proc_exit();    unregister_chrdev_region(MKDEV(INPUT_MAJOR, 0),                 INPUT_MAX_CHAR_DEVICES);    class_unregister(&input_class);}subsys_initcall(input_init);

两个重要的链表

两个重要的链表
input_register_device():
向input_dev_list里面添加input device
调用input_attach_handler去匹配input_handler
input_register_handler():
向input_handler_list天剑input handler
调用input_attach_handler去匹配input_handler

注意上面这两个函数都提到了input_attach_handler()这意味着我们在注册我们的input device的时候会去匹配我们的input handler同时当我们去注册input handler的时候会去匹配input device
通常这两个函数是在我们的driver里面调用的

以触摸屏驱动为例讲解内核input子系统

input_dev = input_allocate_device();    if (!input_dev) {        err = -ENOMEM;        dev_err(&client->dev, "[Focal][Touch] %s: failed to allocate input device\n", __func__);        goto exit_input_dev_alloc_failed;    }    ftxxxx_ts->input_dev = input_dev;    set_bit(KEY_BACK, input_dev->keybit);    set_bit(KEY_HOME, input_dev->keybit);    set_bit(KEY_APPSELECT, input_dev->keybit);    //set_bit(KEY_POWER, input_dev->keybit);    __set_bit(EV_ABS, input_dev->evbit);    __set_bit(EV_KEY, input_dev->evbit);    __set_bit(BTN_TOUCH, input_dev->keybit);    __set_bit(INPUT_PROP_DIRECT, input_dev->propbit);//  printk("maxx=%d,maxy=%d\n",ftxxxx_ts->x_max,ftxxxx_ts->y_max);    input_mt_init_slots(input_dev, CFG_MAX_TOUCH_POINTS, 0);//  input_set_abs_params(input_dev, ABS_MT_TRACKING_ID, 0, CFG_MAX_TOUCH_POINTS, 0, 0);    input_set_abs_params(input_dev, ABS_MT_TOUCH_MAJOR, 0, 31, 0, 0);    input_set_abs_params(input_dev, ABS_MT_POSITION_X, 0, ftxxxx_ts->x_max, 0, 0);    input_set_abs_params(input_dev, ABS_MT_POSITION_Y, 0, ftxxxx_ts->y_max, 0, 0);    input_set_abs_params(input_dev, ABS_MT_PRESSURE, 0, PRESS_MAX, 0, 0);    input_dev->name = Focal_input_dev_name;    err = input_register_device(input_dev);

上面这段代码便是我摘抄的触摸屏驱动的部分代码,主要是input device的注册过程,重点函数是input_register_device()正如我们上面所说的他会去input_handler_list里面去寻找匹配input handler,对于向触摸屏和鼠标等我们的内核已经为我们注册好了input handler代码位于:/kernel/driver/input/evdev.c:
这段代码主要注册了一个input handler,我们的触摸屏驱动匹配到的就是这个handler

/kernel/driver/input/evdev.c:static int __init evdev_init(void){    return input_register_handler(&evdev_handler);}static void __exit evdev_exit(void){    input_unregister_handler(&evdev_handler);}module_init(evdev_init);module_exit(evdev_exit);

在touch driver里面调用input_register_device()会有如下关键的一段代码:
将input device添加进input_dev_list,并且对于每一个注册好的input handler调用input_attach_handler()去匹配device(在这里使我们的触摸屏)

list_add_tail(&dev->node, &input_dev_list);    list_for_each_entry(handler, &input_handler_list, node)        input_attach_handler(dev, handler);。。。

下面便是匹配的代码:对于我们的touch 这里匹配到的就是我们在evdev.c里面注册好的handler,紧接着调用handler->connect(handler, dev, id);
会到evdev.c里面会知道这里的connect是evdev_connect()

static int input_attach_handler(struct input_dev *dev, struct input_handler *handler){    const struct input_device_id *id;    int error;    id = input_match_device(handler, dev);    if (!id)        return -ENODEV;    error = handler->connect(handler, dev, id);    if (error && error != -ENODEV)        pr_err("failed to attach handler %s to device %s, error: %d\n",               handler->name, kobject_name(&dev->dev.kobj), error);    return error;}

这个函数里面用我们的设备号作为标号作为设备名:
dev_set_name(&evdev->dev, “event%d”, dev_no);
紧接着得到我们的input device在这里就是我们的touch:
evdev->handle.dev = input_get_device(dev);
还有之前在input.c里面注册的input_class:
evdev->dev.class = &input_class;
还记得我们之前在input.c里面register_chrdev_region吗:
evdev->dev.devt = MKDEV(INPUT_MAJOR, minor);这里的INPUT_MAJOR就是我们在那里注册的主设备号
下面紧接着注册字符设备:此时注册的字符设备在/dev/input/event%d:
/dev/input:这个是我们在我们的input.c里面注册的,这里的event%d是以次设备号作为标号命名的,

static int evdev_connect(struct input_handler *handler, struct input_dev *dev,             const struct input_device_id *id){    struct evdev *evdev;    。。。。。    。。。。。    dev_set_name(&evdev->dev, "event%d", dev_no);    evdev->handle.dev = input_get_device(dev);    evdev->handle.name = dev_name(&evdev->dev);    evdev->handle.handler = handler;    evdev->handle.private = evdev;    evdev->dev.devt = MKDEV(INPUT_MAJOR, minor);    evdev->dev.class = &input_class;    evdev->dev.parent = &dev->dev;    evdev->dev.release = evdev_free;    device_initialize(&evdev->dev);    error = input_register_handle(&evdev->handle);    if (error)        goto err_free_evdev;    cdev_init(&evdev->cdev, &evdev_fops);    evdev->cdev.kobj.parent = &evdev->dev.kobj;    error = cdev_add(&evdev->cdev, evdev->dev.devt, 1);    if (error)        goto err_unregister_handle;    error = device_add(&evdev->dev);    if (error)        goto err_cleanup_evdev;    return 0; err_cleanup_evdev:    evdev_cleanup(evdev); err_unregister_handle:    input_unregister_handle(&evdev->handle); err_free_evdev:    put_device(&evdev->dev); err_free_minor:    input_free_minor(minor);    return error;}

到此我们的touch有关的内核注册函数讲解完毕,下面讲解,当我们的手指触摸touch的时候内核里面的数据是如何上报的
当我们的手指触摸touch的时候会触发中断:下面是我贴出来的focal的touchdriver里面的中断函数:首先在中断里面会调用ftxxxx_read_Touchdata()读取手指数据(一般为i2c读取),这里不是我们关注的重点;紧接着调用ftxxxx_report_value():上报读到的数据

static irqreturn_t ftxxxx_ts_interrupt(int irq, void *dev_id){            ret = ftxxxx_read_Touchdata(ftxxxx_ts);             。。。。。            ftxxxx_report_value(ftxxxx_ts);            。。。。。    return IRQ_HANDLED;}

这个函数里面会调用一系列的内核input子系统提供的API如:input_report_abs(),input_mt_slot(),input_report_key()等这些函数主要是按照一定的格式将我们读到的touch手指触摸信息写入我们在evdev.c注册好的/dev/input/event0:这里我们假设我们的touch对应的次设备号是0
然后我们的用户空间就可以读取这个节点的数据得到touch的触摸事件。这个在Android 的framwork层里面会讲解,其实不只是Android其他的只要是使用linux内核的只要使用input子系统都应该是类似的在用户空间读取这个节点

static void ftxxxx_report_value(struct ftxxxx_ts_data *data){       .....    /*protocol B*/    filter_touch_point = event->touch_point;    for (i = 0; i < event->touch_point; i++) {        report_point=true;        if(!report_point)            continue;        input_mt_slot(data->input_dev,event->au8_finger_id[i]);        if (event->au8_touch_event[i]== 0 || event->au8_touch_event[i] == 2) {            input_mt_report_slot_state(data->input_dev,MT_TOOL_FINGER,true);            input_report_abs(data->input_dev, ABS_MT_PRESSURE, event->pressure[i]);            input_report_abs(data->input_dev, ABS_MT_TOUCH_MAJOR, event->area[i]);            input_report_abs(data->input_dev, ABS_MT_POSITION_X, event->au16_x[i]);            input_report_abs(data->input_dev, ABS_MT_POSITION_Y, event->au16_y[i]);        } else {            uppoint++;            input_mt_report_slot_state(data->input_dev,MT_TOOL_FINGER,false);        }    }    if((last_touchpoint>0)&&(event->Cur_touchpoint==0))    {        for(i=0;i<CFG_MAX_TOUCH_POINTS;i++)        {            input_mt_slot(data->input_dev,i);            input_mt_report_slot_state(data->input_dev,MT_TOOL_FINGER,false);        }        last_touchpoint=0;    }    if(filter_touch_point == uppoint) {        input_report_key(data->input_dev, BTN_TOUCH, 0);    } else {        input_report_key(data->input_dev, BTN_TOUCH, event->touch_point > 0);    }    input_sync(data->input_dev);    last_touchpoint=event->Cur_touchpoint;}

我们以input_report_key()为例看看我们的数据是如何写入event0的。
这里我省略了很多判断,只留下关键部分

static inline void input_report_key(struct input_dev *dev, unsigned int code, int value){    input_event(dev, EV_KEY, code, !!value);}void input_event(struct input_dev *dev,         unsigned int type, unsigned int code, int value){    unsigned long flags;    if (is_event_supported(type, dev->evbit, EV_MAX)) {        spin_lock_irqsave(&dev->event_lock, flags);        input_handle_event(dev, type, code, value);        spin_unlock_irqrestore(&dev->event_lock, flags);    }}static void input_handle_event(struct input_dev *dev,                   unsigned int type, unsigned int code, int value){    int disposition;    。。。。。    if (disposition & INPUT_FLUSH) {        if (dev->num_vals >= 2)            input_pass_values(dev, dev->vals, dev->num_vals);        dev->num_vals = 0;    } else if (dev->num_vals >= dev->max_vals - 2) {        dev->vals[dev->num_vals++] = input_value_sync;        input_pass_values(dev, dev->vals, dev->num_vals);        dev->num_vals = 0;    }}static void input_pass_values(struct input_dev *dev,                  struct input_value *vals, unsigned int count){    struct input_handle *handle;    。。。。。    handle = rcu_dereference(dev->grab);    if (handle) {        count = input_to_handler(handle, vals, count);    } else {        list_for_each_entry_rcu(handle, &dev->h_list, d_node)            if (handle->open)                count = input_to_handler(handle, vals, count);    }    。。。。。}static unsigned int input_to_handler(struct input_handle *handle,            struct input_value *vals, unsigned int count){    struct input_handler *handler = handle->handler;    。。。。。    if (handler->events)        handler->events(handle, vals, count);    else if (handler->event)        for (v = vals; v != end; v++)            handler->event(handle, v->type, v->code, v->value);    return count;}

到这里调用到了input_to_handler();这个函数里面调用handler->events;
对于touch这里的handler就是我们在evdev.c里面注册的因策这里的events函数就是:evdev.c里面的evdev_events():
evdev_events函数紧接着就会调用evdev_pass_values()

static void evdev_events(struct input_handle *handle,             const struct input_value *vals, unsigned int count){    struct evdev *evdev = handle->private;    struct evdev_client *client;    ktime_t time_mono, time_real;    time_mono = ktime_get();    time_real = ktime_sub(time_mono, ktime_get_monotonic_offset());    rcu_read_lock();    client = rcu_dereference(evdev->grab);    if (client)        evdev_pass_values(client, vals, count, time_mono, time_real);    else        list_for_each_entry_rcu(client, &evdev->client_list, node)            evdev_pass_values(client, vals, count,                      time_mono, time_real);    rcu_read_unlock();}static void evdev_pass_values(struct evdev_client *client,            const struct input_value *vals, unsigned int count,            ktime_t mono, ktime_t real){    struct evdev *evdev = client->evdev;    const struct input_value *v;    struct input_event event;    bool wakeup = false;    event.time = ktime_to_timeval(client->clkid == CLOCK_MONOTONIC ?                      mono : real);    /* Interrupts are disabled, just acquire the lock. */    spin_lock(&client->buffer_lock);    for (v = vals; v != vals + count; v++) {        event.type = v->type;        event.code = v->code;        event.value = v->value;        __pass_event(client, &event);        if (v->type == EV_SYN && v->code == SYN_REPORT)            wakeup = true;    }    spin_unlock(&client->buffer_lock);    if (wakeup)        wake_up_interruptible(&evdev->wait);}

evdev_pass_value()会调用__pass_event()将我们的touch数据写入event0的buffer,这里我们就不继续往下看了,最后调用wake_up_interruptible()唤醒用户空间读取这里数据的进程对于Android就是我们framwork层的input子系统后面会讲

Android framework层Input子系统的框架

现在请大家跟着我的思绪,想象假设我们的kernel已经跑起来然后跑进了第一个init进程,init进程会启动zygote进程,而zygote进程会启动systemserver进程,这里的细节之后有时间我会在其他的博文里面讲解,在这里我们重点关注和input子系统相关的部分。现在我们假设系统跑到了zygote启动systemserver,之所以讲这里是由于我们的input子系统是systemserver里面的一个service:代码如下:
在systemserver进程启动的过程中会调用startOtherService(),这个函数里面会new InputManagerService(context);
ServiceManager.addService(Context.INPUT_SERVICE, inputManager);
inputManager.start();这三个函数分别创建inputmanager,将inputmanager注册进binder机制(android 的进程间通信机制)
最后调用inputmanager.start();启动inputmanager;这里我们重点分析inputmanager的创建以及启动

位置:/framwork/base/services/java/com/android/server/systemserver.java:private void startOtherServices() {    。。。。。    inputManager = new InputManagerService(context);    Slog.i(TAG, "Window Manager");    wm = WindowManagerService.main(context, inputManager,                    mFactoryTestMode != FactoryTest.FACTORY_TEST_LOW_LEVEL,                    !mFirstBoot, mOnlyCore);            ServiceManager.addService(Context.WINDOW_SERVICE, wm);            ServiceManager.addService(Context.INPUT_SERVICE, inputManager);            mActivityManagerService.setWindowManager(wm);   inputManager.setWindowManagerCallbacks(wm.getInputMonitor());            inputManager.start();            。。。。。}public InputManagerService(Context context) {        this.mContext = context;        this.mHandler = new InputManagerHandler(DisplayThread.get().getLooper());        mUseDevInputEventForAudioJack =                context.getResources().getBoolean(R.bool.config_useDevInputEventForAudioJack);        Slog.i(TAG, "Initializing input manager, mUseDevInputEventForAudioJack="                + mUseDevInputEventForAudioJack);        mPtr = nativeInit(this, mContext, mHandler.getLooper().getQueue());        LocalServices.addService(InputManagerInternal.class, new LocalService());}这里有一个jni调用nativeInit()会调用到/framwork/base/services/core/jni/com_android_server_input_InputManagerService.cpp里面的nativeInit()代码如下static jlong nativeInit(JNIEnv* env, jclass clazz,        jobject serviceObj, jobject contextObj, jobject messageQueueObj) {    sp messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj);    if (messageQueue == NULL) {        jniThrowRuntimeException(env, "MessageQueue is not initialized.");        return 0;    }    NativeInputManager* im = new NativeInputManager(contextObj, serviceObj,            messageQueue->getLooper());    im->incStrong(0);    return reinterpret_cast(im);}调用到了NativeInputManager::NativeInputManager(jobject contextObj,        jobject serviceObj, const sp& looper) :        mLooper(looper), mInteractive(true) {    JNIEnv* env = jniEnv();    mContextObj = env->NewGlobalRef(contextObj);    mServiceObj = env->NewGlobalRef(serviceObj);    {        AutoMutex _l(mLock);        mLocked.systemUiVisibility = ASYSTEM_UI_VISIBILITY_STATUS_BAR_VISIBLE;        mLocked.pointerSpeed = 0;        mLocked.pointerGesturesEnabled = true;        mLocked.showTouches = false;    }     sp eventHub = new EventHub();    mInputManager = new InputManager(eventHub, this, this);}两个重点函数 EventHub()和InputManager()第一个参数是eventhub主要创建InputReader和InputDispatcher以及在initialize里面创建InputReaderThread和InputDispatcherThread两个线程,不过此时线程并没有启动代码位于:/framwork/native/services/inputflinger/InputManager.cppInputManager::InputManager(        const sp& eventHub,        const sp& readerPolicy,        const sp& dispatcherPolicy) {    mDispatcher = new InputDispatcher(dispatcherPolicy);    mReader = new InputReader(eventHub, readerPolicy, mDispatcher);    initialize();}void InputManager::initialize() {    mReaderThread = new InputReaderThread(mReader);    mDispatcherThread = new InputDispatcherThread(mDispatcher);}

下面我们返回到syetemserver.java里面inputManager.start();
这个函数里面又是一个jni调用这里调用到/framwork/base/services/core/jni/com_android_server_input_InputManagerService.cpp里面的nativeStart()

 public void start() {        Slog.i(TAG, "Starting input manager");        nativeStart(mPtr);        。。。。。}这里的im->getInputManager()->start()调用的是/framwork/native/services/inputflinger/InputManager.cpp的startstatic void nativeStart(JNIEnv* env, jclass clazz, jlong ptr) {    NativeInputManager* im = reinterpret_cast(ptr);    status_t result = im->getInputManager()->start();    if (result) {        jniThrowRuntimeException(env, "Input manager could not be started.");    }}在这里会启动刚才常见的两个线程status_t InputManager::start() {    status_t result = mDispatcherThread->run("InputDispatcher", PRIORITY_URGENT_DISPLAY);    if (result) {        ALOGE("Could not start InputDispatcher thread due to error %d.", result);        return result;    }    result = mReaderThread->run("InputReader", PRIORITY_URGENT_DISPLAY);    if (result) {        ALOGE("Could not start InputReader thread due to error %d.", result);        mDispatcherThread->requestExit();        return result;    }    return OK;}

下面我们重点分析:
mReaderThread->run(“InputReader”, PRIORITY_URGENT_DISPLAY);
这里会调用到/framwork/native/services/inputflinger/InputReader.cpp里面的thread_loop();

bool InputReaderThread::threadLoop() {    mReader->loopOnce();    return true;}下面函数中的mEventHub->getEvents会依次打开/dev/input/目录底下的节点阻塞等待,其中就包括我们的touch,加入这是我们的手指触摸touch则会触发中断上报数据,会唤醒eventhub读取数据void InputReader::loopOnce() {    int32_t oldGeneration;    int32_t timeoutMillis;    bool inputDevicesChanged = false;    Vector<InputDeviceInfo> inputDevices;    { // acquire lock        AutoMutex _l(mLock);        oldGeneration = mGeneration;        timeoutMillis = -1;        uint32_t changes = mConfigurationChangesToRefresh;        if (changes) {            mConfigurationChangesToRefresh = 0;            timeoutMillis = 0;            refreshConfigurationLocked(changes);        } else if (mNextTimeout != LLONG_MAX) {            nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);            timeoutMillis = toMillisecondTimeoutDelay(now, mNextTimeout);        }    } // release lock    size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);    { // acquire lock        AutoMutex _l(mLock);        mReaderIsAliveCondition.broadcast();        if (count) {            processEventsLocked(mEventBuffer, count);        }        if (mNextTimeout != LLONG_MAX) {            nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);            if (now >= mNextTimeout) {#if DEBUG_RAW_EVENTS                ALOGD("Timeout expired, latency=%0.3fms", (now - mNextTimeout) * 0.000001f);#endif                mNextTimeout = LLONG_MAX;                timeoutExpiredLocked(now);            }        }        if (oldGeneration != mGeneration) {            inputDevicesChanged = true;            getInputDevicesLocked(inputDevices);        }    } // release lock    if (inputDevicesChanged) {        mPolicy->notifyInputDevicesChanged(inputDevices);    }    mQueuedListener->flush();}

这里我裁剪贴出了部分getevent代码
scanDevicesLocked()函数里面会依次打开我们的/dev/input底下的节点
并且将文件描述符fd添加进mEpollFd监控(这里类似于linux的select系统调用)
最后getevent会阻塞在epoll_wait()要么超时返回要么在/dev/input下的节点有事件要读取。关于linux的阻塞IO机制我觉得很重要后面有时间我会专门有一节讲解

size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) {    。。。。。    for (;;) {        。。。。。        if (mNeedToScanDevices) {            mNeedToScanDevices = false;            scanDevicesLocked();            mNeedToSendFinishedDeviceScan = true;        }       .....       ......        // Grab the next input event.        bool deviceChanged = false;        while (mPendingEventIndex < mPendingEventCount) {            const struct epoll_event& eventItem = mPendingEventItems[mPendingEventIndex++];            if (eventItem.data.u32 == EPOLL_ID_INOTIFY) {                if (eventItem.events & EPOLLIN) {                    mPendingINotify = true;                } else {                    ALOGW("Received unexpected epoll event 0x%08x for INotify.", eventItem.events);                }                continue;            }            if (eventItem.data.u32 == EPOLL_ID_WAKE) {                if (eventItem.events & EPOLLIN) {                    ALOGV("awoken after wake()");                    awoken = true;                    char buffer[16];                    ssize_t nRead;                    do {                        nRead = read(mWakeReadPipeFd, buffer, sizeof(buffer));                    } while ((nRead == -1 && errno == EINTR) || nRead == sizeof(buffer));                } else {                    ALOGW("Received unexpected epoll event 0x%08x for wake read pipe.",                            eventItem.events);                }                continue;            }            Device* device = mDevices.valueAt(deviceIndex);            if (eventItem.events & EPOLLIN) {                int32_t readSize = read(device->fd, readBuffer,                        sizeof(struct input_event) * capacity);                .....            }            .....        }      .....        int pollResult = epoll_wait(mEpollFd, mPendingEventItems, EPOLL_MAX_EVENTS, timeoutMillis);       .....    return event - buffer;}

写到这里我们已经进入framwork层的input子系统,不过我不打算向下写了,因为这里有一篇写的不错的文章,这里有别人的一篇写的很好我这里给出连接:
android5.0 Lollipop input子系统

更多相关文章

  1. C语言函数以及函数的使用
  2. android 调用draw(canvas) 函数自动退出
  3. Xposed框架之函数Hook学习
  4. Android5.0挂载子系统
  5. android中去掉空格--trim函数
  6. Android三角函数
  7. ISurfaceComposer接口有13个成员函数
  8. Android 进阶——Android Studio 项目结构详细述及Gradle脚本语
  9. Android jni系统变量、函数、接口定义汇总

随机推荐

  1. PHP OOP和MySQLi连接=致命错误:调用未定
  2. InnoDB瓶颈:放宽ACID以提高性能
  3. Hive安装与部署集成mysql
  4. 0926MySQL中ICP索引下推
  5. 【转载】----CentOS 6.5下安装MySQL 5.6.
  6. 安装mysql5.7解压包中遇见的问题
  7. Linux 下整合Apache+tomcat+mysql
  8. Web分页显示数据
  9. Navicate for MySQL 的一点细节问题
  10. 请求卡在ActiveRecord :: QueryCache中间