input分析

```

Input设备的核心结构是struct input_dev

下面来分析下input的代码流程:

Input的本质是copy_to_user
核心初始化
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;
    }
//初始化proc文件,将设备信息显示在内存中
/*shell@p201:/ $ ls /proc/bus/input/                                             
devices
Handlers
shell@p201:/proc/bus/input $ cat devices                                       
I: Bus=0010 Vendor=0001 Product=0001 Version=0100
......
I: Bus=0010 Vendor=0001 Product=0001 Version=0100
N: Name="gpio_keypad"
P: Phys=gpio_keypad/input0
S: Sysfs=/devices/gpio_keypad.45/input/input1
......
I: Bus=0010 Vendor=1b8e Product=0cec Version=0001
N: Name="cec_input"
P: Phys=
.....
*/
    err = input_proc_init();
    if (err)
        goto fail1;
//注册一个字符设备主设备号为31,允许的数为1024,从0开始为第一个次设备号
    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);
module_exit(input_exit);

//初始化事件处理层,注册一个handler
//匹配所有设备
static const struct input_device_id evdev_ids[] = {
    { .driver_info = 1 },   /* Matches all devices */
    { },            /* Terminating zero entry */
}
static struct input_handler evdev_handler = {
    .event      = evdev_event,
    .events     = evdev_events,
    .connect    = evdev_connect,
    .disconnect = evdev_disconnect,
    .legacy_minors  = true,
    .minor      = EVDEV_MINOR_BASE,
    .name       = "evdev",
    .id_table   = evdev_ids,
}
static int __init evdev_init(void)
{
    return input_register_handler(&evdev_handler);
}


申请内存空间
struct input_dev *input_allocate_device(void)
{
    static atomic_t input_no = ATOMIC_INIT(0);
    struct input_dev *dev;

    dev = kzalloc(sizeof(struct input_dev), GFP_KERNEL);//申请内存大小
    if (dev) {
        dev->dev.type = &input_dev_type;
        dev->dev.class = &input_class;
        device_initialize(&dev->dev);
        mutex_init(&dev->mutex);
        spin_lock_init(&dev->event_lock);
        init_timer(&dev->timer);//初始化timer,用到自动repeat功能,根据位图来设置
        INIT_LIST_HEAD(&dev->h_list);
        INIT_LIST_HEAD(&dev->node);//初始化设备链表

        dev_set_name(&dev->dev, "input%ld",
                 (unsigned long) atomic_inc_return(&input_no) - 1);//设置设备名,/dev/input/ 

        __module_get(THIS_MODULE);
    }

    return dev;
}



注册设备:
int input_register_device(struct input_dev *dev)
{
    struct input_devres *devres = NULL;
    struct input_handler *handler;
    unsigned int packet_size;
    const char *path;
    int error;

    if (dev->devres_managed) {
        devres = devres_alloc(devm_input_device_unregister,
                      sizeof(struct input_devres), GFP_KERNEL);
//Linux设备模型借助device resource management(设备资源管理),帮我们解决了这个问题。就是:driver你只管申请就行了,不用考虑释放,我设备模型帮你释放
        if (!devres)
            return -ENOMEM;

        devres->input = dev;
    }

    /* Every input device generates EV_SYN/SYN_REPORT events. */
    __set_bit(EV_SYN, dev->evbit);//支持同步事件

    /* KEY_RESERVED is not supposed to be transmitted to userspace. */
    __clear_bit(KEY_RESERVED, dev->keybit);

    /* Make sure that bitmasks not mentioned in dev->evbit are clean. */
    input_cleanse_bitmasks(dev);//清除位图

    packet_size = input_estimate_events_per_packet(dev);
    if (dev->hint_events_per_packet < packet_size)
        dev->hint_events_per_packet = packet_size;

    dev->max_vals = dev->hint_events_per_packet + 2;
    dev->vals = kcalloc(dev->max_vals, sizeof(*dev->vals), GFP_KERNEL);
    if (!dev->vals) {
        error = -ENOMEM;
        goto err_devres_free;
    }
//内核自动repeat timer初始化
    /*
     * If delay and period are pre-set by the driver, then autorepeating
     * is handled by the driver itself and we don't do it in input.c.
     */
    if (!dev->rep[REP_DELAY] && !dev->rep[REP_PERIOD]) {
        dev->timer.data = (long) dev;
        dev->timer.function = input_repeat_key;
        dev->rep[REP_DELAY] = 250;
        dev->rep[REP_PERIOD] = 33;
    }

    if (!dev->getkeycode)
        dev->getkeycode = input_default_getkeycode;

    if (!dev->setkeycode)
        dev->setkeycode = input_default_setkeycode;

    error = device_add(&dev->dev);
    if (error)
        goto err_free_vals;

    path = kobject_get_path(&dev->dev.kobj, GFP_KERNEL);
    pr_info("%s as %s\n",
        dev->name ? dev->name : "Unspecified device",
        path ? path : "N/A");
    kfree(path);

    error = mutex_lock_interruptible(&input_mutex);
    if (error)
        goto err_device_del;
//将当前节点加入到设备链表中去,static LIST_HEAD(input_dev_list);
    list_add_tail(&dev->node, &input_dev_list);
//从input_handler_list链表中查询对应的handler
//static LIST_HEAD(input_handler_list);
    list_for_each_entry(handler, &input_handler_list, node)
        input_attach_handler(dev, handler);
//唤醒异步读操作,driver可向上抛键值,调用input_event接口时
    input_wakeup_procfs_readers();

    mutex_unlock(&input_mutex);

    if (dev->devres_managed) {
        dev_dbg(dev->dev.parent, "%s: registering %s with devres.\n",
            __func__, dev_name(&dev->dev));
        devres_add(dev->dev.parent, devres);
    }
    return 0;

err_device_del:
    device_del(&dev->dev);
err_free_vals:
    kfree(dev->vals);
    dev->vals = NULL;
err_devres_free:
    devres_free(devres);
    return error;
}

static int input_attach_handler(struct input_dev *dev, struct input_handler *handler)
{
    const struct input_device_id *id;
    int error;
//用id号进行匹配
    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;
}
static const struct input_device_id *input_match_device(struct input_handler *handler,
                            struct input_dev *dev)
{
    const struct input_device_id *id;

    for (id = handler->id_table; id->flags || id->driver_info; id++) {

        if (id->flags & INPUT_DEVICE_ID_MATCH_BUS)
            if (id->bustype != dev->id.bustype)
                continue;

        if (id->flags & INPUT_DEVICE_ID_MATCH_VENDOR)
            if (id->vendor != dev->id.vendor)
                continue;

        if (id->flags & INPUT_DEVICE_ID_MATCH_PRODUCT)
            if (id->product != dev->id.product)
                continue;

        if (id->flags & INPUT_DEVICE_ID_MATCH_VERSION)
            if (id->version != dev->id.version)
                continue;

        if (!bitmap_subset(id->evbit, dev->evbit, EV_MAX))
            continue;

        if (!bitmap_subset(id->keybit, dev->keybit, KEY_MAX))
            continue;

        if (!bitmap_subset(id->relbit, dev->relbit, REL_MAX))
            continue;

        if (!bitmap_subset(id->absbit, dev->absbit, ABS_MAX))
            continue;

        if (!bitmap_subset(id->mscbit, dev->mscbit, MSC_MAX))
            continue;

        if (!bitmap_subset(id->ledbit, dev->ledbit, LED_MAX))
            continue;

        if (!bitmap_subset(id->sndbit, dev->sndbit, SND_MAX))
            continue;

        if (!bitmap_subset(id->ffbit, dev->ffbit, FF_MAX))
            continue;

        if (!bitmap_subset(id->swbit, dev->swbit, SW_MAX))
            continue;
//用事件处理自己的match进行匹配,目前除了joydev有自己的match函数外,其他都没有
        if (!handler->match || handler->match(handler, dev))
            return id;
    }

    return NULL;
}

Evdev.c自己的connect
/*
 * Create new evdev device. Note that input core serializes calls
 * to connect and disconnect.
 */
static int evdev_connect(struct input_handler *handler, struct input_dev *dev,
             const struct input_device_id *id)
{
    struct evdev *evdev;
    int minor;
    int dev_no;
    int error;

    minor = input_get_new_minor(EVDEV_MINOR_BASE, EVDEV_MINORS, true);
    if (minor < 0) {
        error = minor;
        pr_err("failed to reserve new minor: %d\n", error);
        return error;
    }
//申请事件内存空间
    evdev = kzalloc(sizeof(struct evdev), GFP_KERNEL);
    if (!evdev) {
        error = -ENOMEM;
        goto err_free_minor;
    }
//初始化事件链表,struct list_head client_list;
    INIT_LIST_HEAD(&evdev->client_list);
    spin_lock_init(&evdev->client_lock);
    mutex_init(&evdev->mutex);
//该函数初始化一个已经存在的等待队列头,它将整个队列设置为"未上锁"状态,并将链表指针prev和next指向它自身。
    init_waitqueue_head(&evdev->wait);
    evdev->exist = true;

    dev_no = minor;
    /* Normalize device number if it falls into legacy range */
    if (dev_no < EVDEV_MINOR_BASE + EVDEV_MINORS)
        dev_no -= EVDEV_MINOR_BASE;
    dev_set_name(&evdev->dev, "event%d", dev_no);
//初始化一个handle
    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);
//注册一个handle用于链接dev与handler
    error = input_register_handle(&evdev->handle);
    if (error)
        goto err_free_evdev;

/*
加入链接
int input_register_handle(struct input_handle *handle)
{
    struct input_handler *handler = handle->handler;
    struct input_dev *dev = handle->dev;
    int error;

    ....
    /*
     * Filters go to the head of the list, normal handlers
     * to the tail.
     */

将handler节点加入input_dev设备链表中去
    if (handler->filter)
        list_add_rcu(&handle->d_node, &dev->h_list);
    else
        list_add_tail_rcu(&handle->d_node, &dev->h_list);
    /*
     * Since we are supposed to be called from ->connect()
     * which is mutually exclusive with ->disconnect()
     * we can't be racing with input_unregister_handle()
     * and so separate lock is not needed here.
     */
将handler节点加入evdev事件链表中去

    list_add_tail_rcu(&handle->h_node, &handler->h_list);
......
    return 0;
}
实现你中有我,我中有你,数据嵌套,通过链表操作互相访问
*/
//初始化操作函数,应用层操作/dev/input/event0   /dev/input/event1等获取设备信息与数据,注册一个字符设备
    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;
}

发送键值:
相对运动,如鼠标
static inline void input_report_rel(struct input_dev *dev, unsigned int code, int value)
绝对运动,如移动游戏杆
static inline void input_report_abs(struct input_dev *dev, unsigned int code, int value)
压力,如压力感应
static inline void input_report_ff_status(struct input_dev *dev, unsigned int code, int value)
开关,如手提电脑开关,两种状态
static inline void input_report_switch(struct input_dev *dev, unsigned int code, int value)
键值,如遥控器,键盘
static inline void input_report_key(struct input_dev *dev, unsigned int code, int value)
同步事件,当一个事件采集完后,同步成一个事件
static inline void input_sync(struct input_dev *dev)
用于多点触摸屏同步事件
static inline void input_mt_sync(struct input_dev *dev)
以上的接口都是封装了
void input_event(struct input_dev *dev,
         unsigned int type, unsigned int code, int value)

Input流程
Input_event
input_event(dev, EV_KEY,116,type);

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;

    disposition = input_get_disposition(dev, type, code, &value);

    if ((disposition & INPUT_PASS_TO_DEVICE) && dev->event)
        dev->event(dev, type, code, value);

    if (!dev->vals)
        return;

    if (disposition & INPUT_PASS_TO_HANDLERS) {
        struct input_value *v;

        if (disposition & INPUT_SLOT) {
            v = &dev->vals[dev->num_vals++];
            v->type = EV_ABS;
            v->code = ABS_MT_SLOT;
            v->value = dev->mt->slot;
        }

        v = &dev->vals[dev->num_vals++];
        v->type = type;
        v->code = code;
        v->value = value;
    }

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

}
input_pass_values
input_to_handler
handler->events
evdev_events
evdev_pass_values

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;

    if (client->revoked)
        return;
//获取事件时间
    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);//里面发送异步消息给应用层,应用层得到消息后去读数据
/*kill_fasync(&client->fasync, SIGIO, POLL_IN);*/
//当driver发送同步信号input_sync(dev);时,wakeup = true;
/*static inline void input_sync(struct input_dev *dev)
{
    input_event(dev, EV_SYN, SYN_REPORT, 0);
}

static inline void input_mt_sync(struct input_dev *dev)
{
    input_event(dev, EV_SYN, SYN_MT_REPORT, 0);
}
*/
        if (v->type == EV_SYN && v->code == SYN_REPORT)
            wakeup = true;
    }

    spin_unlock(&client->buffer_lock);

    if (wakeup)
        wake_up_interruptible(&evdev->wait);//唤醒读操作,此时已可读状态
}

事件的读操作
static ssize_t evdev_read(struct file *file, char __user *buffer,
              size_t count, loff_t *ppos)
{
    .....
        if (client->packet_head == client->tail &&
            (file->f_flags & O_NONBLOCK))
            return -EAGAIN;

        /*
         * count == 0 is special - no IO is done but we check
         * for error conditions (see above).
         */
        if (count == 0)
            break;

        while (read + input_event_size() <= count &&
               evdev_fetch_next_event(client, &event)) {

            if (input_event_to_user(buffer + read, &event))
                return -EFAULT;

            read += input_event_size();
        }

        if (read)
            break;

        if (!(file->f_flags & O_NONBLOCK)) {
            error = wait_event_interruptible(evdev->wait,
                    client->packet_head != client->tail ||
                    !evdev->exist || client->revoked);
                }
    }

    return read;
}
发送到应用层,被应用层读取
int input_event_to_user(char __user *buffer,
            const struct input_event *event)
{
    if (copy_to_user(buffer, event, sizeof(struct input_event)))
        return -EFAULT;

    return 0;
}



Android 层处理键值
Eventhub.cpp
当有新设备加入时,扫描/dev/input/下面的文件并打开,获取设备信息
static const char *DEVICE_PATH = "/dev/input";

void EventHub::scanDevicesLocked() {
status_t res = scanDirLocked(DEVICE_PATH);

status_t EventHub::scanDirLocked(const char *dirname)


status_t EventHub::scanDirLocked(const char *dirname)
{
    char devname[PATH_MAX];
    char *filename;
    DIR *dir;
    struct dirent *de;
    dir = opendir(dirname);
    if(dir == NULL)
        return -1;
    strcpy(devname, dirname);
    filename = devname + strlen(devname);
    *filename++ = '/';
    while((de = readdir(dir))) {
        if(de->d_name[0] == '.' &&
           (de->d_name[1] == '\0' ||
            (de->d_name[1] == '.' && de->d_name[2] == '\0')))
            continue;
        strcpy(filename, de->d_name);
        openDeviceLocked(devname);
    }
    closedir(dir);
    return 0;
}

status_t EventHub::openDeviceLocked(const char *devicePath) {
    char buffer[80];

    ALOGV("Opening device: %s", devicePath);
//打开设备节点
    int fd = open(devicePath, O_RDWR | O_CLOEXEC);
    if(fd < 0) {
        ALOGE("could not open %s, %s\n", devicePath, strerror(errno));
        return -1;
    }

    InputDeviceIdentifier identifier;

    // Get device name.获取设备名字
    if(ioctl(fd, EVIOCGNAME(sizeof(buffer) - 1), &buffer) < 1) {
        //fprintf(stderr, "could not get device name for %s, %s\n", devicePath, strerror(errno));
    } else {
        buffer[sizeof(buffer) - 1] = '\0';
        identifier.name.setTo(buffer);
    }

    // Check to see if the device is on our excluded list
    for (size_t i = 0; i < mExcludedDevices.size(); i++) {
        const String8& item = mExcludedDevices.itemAt(i);
        if (identifier.name == item) {
            ALOGI("ignoring event id %s driver %s\n", devicePath, item.string());
            close(fd);
            return -1;
        }
    }

    // Get device driver version.
    int driverVersion;//获取设备驱动版本
    if(ioctl(fd, EVIOCGVERSION, &driverVersion)) {
        ALOGE("could not get driver version for %s, %s\n", devicePath, strerror(errno));
        close(fd);
        return -1;
    }

    // Get device identifier.
    struct input_id inputId;//获取设备vendor id version号
    if(ioctl(fd, EVIOCGID, &inputId)) {
        ALOGE("could not get device input id for %s, %s\n", devicePath, strerror(errno));
        close(fd);
        return -1;
    }
    identifier.bus = inputId.bustype;
    identifier.product = inputId.product;
    identifier.vendor = inputId.vendor;
    identifier.version = inputId.version;

    // Get device physical location.
    if(ioctl(fd, EVIOCGPHYS(sizeof(buffer) - 1), &buffer) < 1) {
        //fprintf(stderr, "could not get location for %s, %s\n", devicePath, strerror(errno));
    } else {
        buffer[sizeof(buffer) - 1] = '\0';
        identifier.location.setTo(buffer);
    }

    // Get device unique id.
    if(ioctl(fd, EVIOCGUNIQ(sizeof(buffer) - 1), &buffer) < 1) {
        //fprintf(stderr, "could not get idstring for %s, %s\n", devicePath, strerror(errno));
    } else {
        buffer[sizeof(buffer) - 1] = '\0';
        identifier.uniqueId.setTo(buffer);
    }

    // Fill in the descriptor.
    assignDescriptorLocked(identifier);

    // Make file descriptor non-blocking for use with poll().
    if (fcntl(fd, F_SETFL, O_NONBLOCK)) {
        ALOGE("Error %d making device file descriptor non-blocking.", errno);
        close(fd);
        return -1;
    }

    // Allocate device.  (The device object takes ownership of the fd at this point.)
    int32_t deviceId = mNextDeviceId++;
    Device* device = new Device(fd, deviceId, String8(devicePath), identifier);

    ALOGV("add device %d: %s\n", deviceId, devicePath);
    ALOGV("  bus:        %04x\n"
         "  vendor      %04x\n"
         "  product     %04x\n"
         "  version     %04x\n",
        identifier.bus, identifier.vendor, identifier.product, identifier.version);
    ALOGV("  name:       \"%s\"\n", identifier.name.string());
    ALOGV("  location:   \"%s\"\n", identifier.location.string());
    ALOGV("  unique id:  \"%s\"\n", identifier.uniqueId.string());
    ALOGV("  descriptor: \"%s\"\n", identifier.descriptor.string());
    ALOGV("  driver:     v%d.%d.%d\n",
        driverVersion >> 16, (driverVersion >> 8) & 0xff, driverVersion & 0xff);

    // Load the configuration file for the device.获取设备键值匹配表idc kl kcm
    loadConfigurationLocked(device);

    // Figure out the kinds of events the device reports.
    ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(device->keyBitmask)), device->keyBitmask);
    ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(device->absBitmask)), device->absBitmask);
    ioctl(fd, EVIOCGBIT(EV_REL, sizeof(device->relBitmask)), device->relBitmask);
    ioctl(fd, EVIOCGBIT(EV_SW, sizeof(device->swBitmask)), device->swBitmask);
    ioctl(fd, EVIOCGBIT(EV_LED, sizeof(device->ledBitmask)), device->ledBitmask);
    ioctl(fd, EVIOCGBIT(EV_FF, sizeof(device->ffBitmask)), device->ffBitmask);
    ioctl(fd, EVIOCGPROP(sizeof(device->propBitmask)), device->propBitmask);

    .......

    addDeviceLocked(device);
    return 0;
}

loadConfigurationLocked
getInputDeviceConfigurationFilePathByDeviceIdentifier

String8 getInputDeviceConfigurationFilePathByDeviceIdentifier(
        const InputDeviceIdentifier& deviceIdentifier,
        InputDeviceConfigurationFileType type) {
    if (deviceIdentifier.vendor !=0 && deviceIdentifier.product != 0) {
        if (deviceIdentifier.version != 0) {
            // Try vendor product version.
            String8 versionPath(getInputDeviceConfigurationFilePathByName(
                    String8::format("Vendor_%04x_Product_%04x_Version_%04x",
                            deviceIdentifier.vendor, deviceIdentifier.product,
                            deviceIdentifier.version),
                    type));
            if (!versionPath.isEmpty()) {
                return versionPath;
            }
        }

        // Try vendor product.
        String8 productPath(getInputDeviceConfigurationFilePathByName(
                String8::format("Vendor_%04x_Product_%04x",
                        deviceIdentifier.vendor, deviceIdentifier.product),
                type));
        if (!productPath.isEmpty()) {
            return productPath;
        }
    }

“`

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值