InputManager(3)--键盘事件的分发[part 4:InputDispatcher 接收消息处理结果]

④. InputDispatcher 接收消息处理结果

  • 当一个输入事件经过上面的层层判断,确认了处理者并且完成处理之后,就会调用 finishInputEvent() 函数结束输入事件的调用,那么在结束输入事件的处理时又会有哪些处理逻辑,最终如何将 InputDipatcher 对象中存储在等待处理反馈队列 waitQueue 中的输入事件给删除的呢?接下来就让我们从 finishInputEvent() 函数入手,开始逐一分析

1. finishInputEvent()

android/frameworks/…/ViewRootImpl.java

private void finishInputEvent(QueuedInputEvent q) {
    Trace.asyncTraceEnd(Trace.TRACE_TAG_VIEW, "deliverInputEvent",
            q.mEvent.getId());
   
    //传入的 q,就是在 enqueueInputEvent()函数中入队列的输入事件,此时 QueuedInputEvent.mReceiver 是
    //为 WindowInputEventReceiver.onInputEvent() 函数中传入的 this 也就是 WindowInputEventReceiver 对象
    if (q.mReceiver != null) {
        //判断输入事件是否处理完成
        boolean handled = (q.mFlags & QueuedInputEvent.FLAG_FINISHED_HANDLED) != 0;
        //兼容性相关的判断,Android M之后不会再添加
        boolean modified = (q.mFlags & QueuedInputEvent.FLAG_MODIFIED_FOR_COMPATIBILITY) != 0;
        //当前系统是Android R,所以不会命中if
        if (modified) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "processInputEventBeforeFinish");
            InputEvent processedEvent;
            try {
                processedEvent =
                        mInputCompatProcessor.processInputEventBeforeFinish(q.mEvent);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
            if (processedEvent != null) {
                q.mReceiver.finishInputEvent(processedEvent, handled);
            }
        } else {
            //调用 WindowInputEventReceiver 也就是其父类 InputEventReceiver 的 finishInputEvent()函数
            q.mReceiver.finishInputEvent(q.mEvent, handled);
        }
    } else {
        //该函数就是将 InputEvent 中的 mRecycled 设置为 true
        q.mEvent.recycleIfNeededAfterDispatch();
    }
    //回收QueuedInputEvent对象,享元设计模式
    recycleQueuedInputEvent(q);
}

private void recycleQueuedInputEvent(QueuedInputEvent q) {
    //将QueuedInputEvent置空
    q.mEvent = null;
    q.mReceiver = null;
 
    //如果当前的mQueuedInputEventPoolSize未满,那么就将该QueuedInputEvent放到mQueuedInputEventPool中去
    if (mQueuedInputEventPoolSize < MAX_QUEUED_INPUT_EVENT_POOL_SIZE) {
        mQueuedInputEventPoolSize += 1;
        q.mNext = mQueuedInputEventPool;
        mQueuedInputEventPool = q;
    }
}
  • 在该函数中,首先会判断对应QueuedInputEvent消息中是否保存了 mReceiver 对象,如果存在 mReceiver 对象,那么会将当前输入事件传给 mReceiver 做取消处理,在当前的分析流程中,QueuedInputEvent 中是存在 mReceiver 对象的,且就是 WindowInputEventReceiver ,而 WindowInputEventReceiver 是继承自 InputEventReceiver ,所以这里就会先调用到 InputEventReceiver 中的 finishInputEvent() 函数做进一步处理,而在完成这一步的处理之后,接下来就会回收 QueuedInputEvent 对象

2. finishInputEvent()

android/frameworks/…/InputEventReceiver.java

public final void finishInputEvent(InputEvent event, boolean handled) {
    if (event == null) {
        throw new IllegalArgumentException("event must not be null");
    }
    if (mReceiverPtr == 0) {
        Log.w(TAG, "Attempted to finish an input event but the input event "
                + "receiver has already been disposed.");
    } else {
        //首先会从 mSeqMap 中查找是否存在当前输入事件,Seq 也就是事件唯一的标识符
        int index = mSeqMap.indexOfKey(event.getSequenceNumber());
        if (index < 0) {
            Log.w(TAG, "Attempted to finish an input event that is not in progress.");
        } else {
            //获取到当前事件的 seq 值
            int seq = mSeqMap.valueAt(index);
            //然后将代表当前输入事件的seq值从 mSeqMap 中删除
            mSeqMap.removeAt(index);
            //调用 native 函数进行进一步的处理
            nativeFinishInputEvent(mReceiverPtr, seq, handled);
        }
    }
    //最后将该输入事件event对象进行回收,也就是将event.mRecycled置为true
    event.recycleIfNeededAfterDispatch();
}
  • InputEventReceiver 中,首先会根据 每个输入事件唯一的标识符 seq 值,去 mSeqMap 中进行查找,判断该输入事件是否在处理的,如果在 mSeqMap 中查找到了当前的输入事件,那么就将其从 mSeqMap 中删除,接下来调用 native 函数继续处理

3. nativeFinishInputEvent()

android/framworks/…/android_view_InputEventReceiver.cpp

static void nativeFinishInputEvent(JNIEnv* env, jclass clazz, jlong receiverPtr,
        jint seq, jboolean handled) {
    //获取到 NativeInputEventReceiver 对象
    sp<NativeInputEventReceiver> receiver =
            reinterpret_cast<NativeInputEventReceiver*>(receiverPtr);
    //调用 NativeInputEventReceiver 对象的 finishInputEvent() 函数清除输入事件
    status_t status = receiver->finishInputEvent(seq, handled);
    //清除失败,则抛出异常
    if (status && status != DEAD_OBJECT) {
        String8 message;
        message.appendFormat("Failed to finish input event.  status=%d", status);
        jniThrowRuntimeException(env, message.string());
    }
}
  • 接下来通过 NativeInputEventReceiver 继续处理,此时传入的是 输入事件的唯一标识符值 seq 和 代表需要清除的值 handled

4. finishInputEvent()

android/framworks/…/android_view_InputEventReceiver.cpp

status_t NativeInputEventReceiver::finishInputEvent(uint32_t seq, bool handled) {
    if (kDebugDispatchCycle) {
        ALOGD("channel '%s' ~ Finished input event.", getInputChannelName().c_str());
    }
    //mInputConsumer中存放了客户端的 InputChannel 对象,这里就是在向InputDispatcher发消息,告知其当前的输入事件已处理完毕
    status_t status = mInputConsumer.sendFinishedSignal(seq, handled);
    //如果status为0,代表消息发送成功,否则命中if,代表消息发送失败
    if (status) {
        //当status为 WOULD_BLOCK,代表当前socket可能正处于被占用状态,那么需要过一会再尝试发送一次
        if (status == WOULD_BLOCK) {
            if (kDebugDispatchCycle) {
                ALOGD("channel '%s' ~ Could not send finished signal immediately.  "
                        "Enqueued for later.", getInputChannelName().c_str());
            }
            Finish finish;
            finish.seq = seq;
            finish.handled = handled;
            //将当前的输入事件封装成一个 Finish 对象,然后放入 mFinishQueue 队列中
            mFinishQueue.add(finish);
            if (mFinishQueue.size() == 1) {
                //如果当前 mFinishQueue 队列中存在数据,此时就需要重新设置Socket监听消息,设置其同时监听
                // ALOOPER_EVENT_INPUT | ALOOPER_EVENT_OUTPUT
                setFdEvents(ALOOPER_EVENT_INPUT | ALOOPER_EVENT_OUTPUT);
            }
            return OK;
        }
        ALOGW("Failed to send finished signal on channel '%s'.  status=%d",
                getInputChannelName().c_str(), status);
    }
    return status;
}
  • 在此处首先会尝试通过 Client 端的 InputChannel 去通知 InputDispatcher 线程,通知方式就是向 Client 端的 InputChannelSocket 写入一个 InputMessage 消息;而如果尝试写入不成功,那么接下来就会将该输入事件封装成一个 Finish 对象,并保存到 mFinishQueue 队列中,然后重新设置 ClientInputChannelSocket 的监听事件,设置为 ALOOPER_EVENT_INPUT | ALOOPER_EVENT_OUTPUT,注册了 ALOOPER_EVENT_OUTPUT 那么在下一个输入事件发送过来的时候,会去处理上一个输入事件;那么接下来让我们逐一看一下处理的流程,首先我们先假设此次写入 socket 失败,即向当前应用程序窗口的 looper 设置了 ALOOPER_EVENT_OUTPUT 消息监听,在设置了 该消息监听后,会在下一次接收到 输入事件时,首先命中 ALOOPER_EVENT_OUTPUT 逻辑进行执行,那么我们就从此处开始看起:
4.1.0. handleEvent()

android/frameworks/…/android_view_InputEventReceiver.cpp

int NativeInputEventReceiver::handleEvent(int receiveFd, int events, void* data) {
    ...
    //命中该if,表示当前应用程序已经完成了输入事件的分发
    if (events & ALOOPER_EVENT_OUTPUT) {
        //遍历 mFinishQueue 队列,处理每一个 Finish 数据
        for (size_t i = 0; i < mFinishQueue.size(); i++) {
            const Finish& finish = mFinishQueue.itemAt(i);
            //mInputConsumer中存放了客户端的 InputChannel 对象,这里就是在向InputDispatcher发消息,告知其当前的输入事件已处理完毕
            status_t status = mInputConsumer.sendFinishedSignal(finish.seq, finish.handled);
            //如果消息发送不成功,那么进入if
            if (status) {
                //从 mFinishQueue 中移除该 Finish 数据
                mFinishQueue.removeItemsAt(0, i);

                //如果当前 socket 处于阻塞状态,说明其可能正在使用,那么命中if会直接返回1,再次尝试执行发送
                if (status == WOULD_BLOCK) {
                    if (kDebugDispatchCycle) {
                        ALOGD("channel '%s' ~ Sent %zu queued finish events; %zu left.",
                                getInputChannelName().c_str(), i, mFinishQueue.size());
                    }
                    //返回1,当前此次的looper回调会再次被执行,即再次尝试发送消息取消输入事件
                    return 1; // keep the callback, try again later
                }

                ALOGW("Failed to send finished signal on channel '%s'.  status=%d",
                        getInputChannelName().c_str(), status);
                //如果当前的socket返回的并不是DEAD_OBJECT,那么移除异常信息
                if (status != DEAD_OBJECT) {
                    JNIEnv* env = AndroidRuntime::getJNIEnv();
                    String8 message;
                    message.appendFormat("Failed to finish input event.  status=%d", status);
                    jniThrowRuntimeException(env, message.string());
                    mMessageQueue->raiseAndClearException(env, "finishInputEvent");
                }
                //直接返回0,那么Looper就会移除此次回调
                return 0; // remove the callback
            }
        }
        ...
        //如果一切执行正常,那么就会清空 mFinishQueue 队列
        mFinishQueue.clear();
        //然后重新设置 客户端的 Socket 只监听 ALOOPER_EVENT_INPUT 消息
        setFdEvents(ALOOPER_EVENT_INPUT);
        return 1;
    }
    return 1;
}
  • 当上一次通过 socket 发送数据失败后,且返回的失败状态是 WOULD_BLOCK ,代表该 socket 可能正被占用,那么就会设置 客户端的 socket 监听 ALOOPER_EVENT_OUTPUT 消息,当下一次输入事件被分发时,会首先命中 ALOOPER_EVENT_OUTPUT 场景,那么就会再次尝试向 InputDispatcher 对象发送 finish 消息,让 InputDipatcher 去取消该输入事件,而如果再次发送失败,同样判断失败的原因,如果仍旧是 socket 为 阻塞状态,那么就返回再次尝试发送,而如果不是 阻塞状态,那么就直接返回,取消本次回调
  • 可以看到其实在 ALOOPER_EVENT_OUTPUT 场景中,也就是再次尝试向 InputDispatcher 对象发送消息,最后也是调用到 sendFinishedSignal() 函数的,也就是说和正常发送输入事件处理结束的逻辑是一致的,那么接下来我们就来分析一下 sendFinishedSignal() 都做了些什么?
4.2.0. sendFinishedSignal()

android/frameworks/…/InputTransport.cpp

status_t InputConsumer::sendFinishedSignal(uint32_t seq, bool handled) {
    ...
    //将当前的输入事件,包装成一个 InputMessage 消息,并且设置的类型是 FINISHED ,
    //写入到了客户端的 InputChannel 中的 Socket 中
    return sendUnchainedFinishedSignal(seq, handled);
}
  • 省去一些无关的细节,最终就是会调用 sendUnchainedFinishedSignal() 去处理该输入事件
4.2.1. sendUnchainedFinishedSignal()

android/frameworks/…/InputTransport.cpp

status_t InputConsumer::sendUnchainedFinishedSignal(uint32_t seq, bool handled) {
    InputMessage msg;
    msg.header.type = InputMessage::Type::FINISHED;
    msg.body.finished.seq = seq;
    msg.body.finished.handled = handled ? 1 : 0;
    //将该finished类型的保存有 seq 和 handled 值的消息写入了 客户端的InputChannel 的 Socket 中
    return mChannel->sendMessage(&msg);
}
  • 可以看到最终,就是将代表该输入事件的 seq 封装到了 InputMessage 中,写入了 Client 端的 InputChannelSocket 中,那么接下来,注册在InputDispatcher 对象中的 server 端的 InputChannel 中的 Socket 就会读取到数据,并调用 handleReceiveCallback() 函数进行处理
4.2.2. handleReceiveCallback()

android/frameworks/…/InputDispatcher.cpp

int InputDispatcher::handleReceiveCallback(int fd, int events, void* data) {
    InputDispatcher* d = static_cast<InputDispatcher*>(data);

    { // acquire lock
        std::scoped_lock _l(d->mLock);
        //判断当前fd的合法性
        if (d->mConnectionsByFd.find(fd) == d->mConnectionsByFd.end()) {
            ALOGE("Received spurious receive callback for unknown input channel.  "
                  "fd=%d, events=0x%x",
                  fd, events);
            return 0; // remove the callback
        }

        bool notify;
        //根据Socket的文件描述符fd,找到对应的connection对象
        sp<Connection> connection = d->mConnectionsByFd[fd];
        //命中if
        if (!(events & (ALOOPER_EVENT_ERROR | ALOOPER_EVENT_HANGUP))) {
            //确保当前事件不是 ALOOPER_EVENT_INPUT 类型的
            if (!(events & ALOOPER_EVENT_INPUT)) {
                ALOGW("channel '%s' ~ Received spurious callback for unhandled poll event.  "
                      "events=0x%x",
                      connection->getInputChannelName().c_str(), events);
                return 1;
            }

            nsecs_t currentTime = now();
            bool gotOne = false;
            status_t status;
            for (;;) {
                uint32_t seq;
                bool handled;
                //从 socket 中读取出对应的 seq 和 handled值
                status = connection->inputPublisher.receiveFinishedSignal(&seq, &handled);
                if (status) {
                    break;
                }
                //将该输入事件以及对应的Connection封装成一个 CommandEntry对象,设置的command为:
                //doDispatchCycleFinishedLockedInterruptible() 函数,放入 mCommandQueue 队列中
                d->finishDispatchCycleLocked(currentTime, connection, seq, handled);
                gotOne = true;
            }
            if (gotOne) {
                //执行mCommandQueue队列中的元素,也就是执行doDispatchCycleFinishedLockedInterruptible() 函数
                d->runCommandsLockedInterruptible();
                //socket阻塞,表示当前已经没有数据了,socket进入阻塞状态
                //命中if,直接返回
                if (status == WOULD_BLOCK) {
                    return 1;
                }
            }

            notify = status != DEAD_OBJECT || !connection->monitor;
            if (notify) {
                ALOGE("channel '%s' ~ Failed to receive finished signal.  status=%d",
                      connection->getInputChannelName().c_str(), status);
            }
        } else {
            ...
        }

        //如果函数执行到此处,说明是对应窗口已销毁,那么就需要注销该InputChannel
        d->unregisterInputChannelLocked(connection->inputChannel, notify);
        return 0; // remove the callback
    }             // release lock
}
  • 可以看到在当前函数中,会首先尝试从 Socket 中读取消息,并将读取出来的消息,交给finishDispatchCycleLocked()函数进行封装和入队列,其实就是将该输入事件封装成 CommandEntry 对象,放入了 mCommandQueue 队列中,封装的 CommanddoDispatchCycleFinishedLockedInterruptible() 函数;在读取完毕 Sokcet 中的消息后,Socket 会再次进入阻塞状态,接下来就是从 mCommandQueue 队列中获取元素进行执行,也就是执行刚刚放入其中的 doDispatchCycleFinishedLockedInterruptible() 函数,执行完毕后,由于当前socket继续阻塞,那么就直接返回
  • 那么接下来我们就直接来看一下 doDispatchCycleFinishedLockedInterruptible() 都会干些什么
4.2.3. doDispatchCycleFinishedLockedInterruptible()

android/frameworks/…/InputDispatcher.cpp

void InputDispatcher::doDispatchCycleFinishedLockedInterruptible(CommandEntry* commandEntry) {
    //拿到该输入事件的相关数据
    sp<Connection> connection = commandEntry->connection;
    const nsecs_t finishTime = commandEntry->eventTime;
    uint32_t seq = commandEntry->seq;
    const bool handled = commandEntry->handled;

    //从connection 的 waitQueue 队列中根据 seq 去查找对应的输入事件
    std::deque<DispatchEntry*>::iterator dispatchEntryIt = connection->findWaitQueueEntry(seq);
    //如果没有找到,那么直接返回
    if (dispatchEntryIt == connection->waitQueue.end()) {
        return;
    }
    DispatchEntry* dispatchEntry = *dispatchEntryIt;
    //获取处理该输入事件的时长
    const nsecs_t eventDuration = finishTime - dispatchEntry->deliveryTime;
    //判断该输入事件处理时间是否长过告警时间,默认是2s
    if (eventDuration > SLOW_EVENT_PROCESSING_WARNING_TIMEOUT) {
        ALOGI("%s spent %" PRId64 "ms processing %s", connection->getWindowName().c_str(),
              ns2ms(eventDuration), dispatchEntry->eventEntry->getDescription().c_str());
    }
    //默认未实现,我们可以在这其中设计对应的操作,如将该输入事件的处理时长记录下来等操作
    reportDispatchStatistics(std::chrono::nanoseconds(eventDuration), *connection, handled);

    bool restartEvent;
    //按照输入事件类型,进行区别处理
    if (dispatchEntry->eventEntry->type == EventEntry::Type::KEY) {
        //假设当前的是按键事件,那么将其转成 KeyEntry 对象
        KeyEntry* keyEntry = static_cast<KeyEntry*>(dispatchEntry->eventEntry);
        //判断是否需要再次发送给应用程序
        restartEvent =
                afterKeyEventLockedInterruptible(connection, dispatchEntry, keyEntry, handled);
    } else if (dispatchEntry->eventEntry->type == EventEntry::Type::MOTION) {
        MotionEntry* motionEntry = static_cast<MotionEntry*>(dispatchEntry->eventEntry);
        restartEvent = afterMotionEventLockedInterruptible(connection, dispatchEntry, motionEntry,
                                                           handled);
    } else {
        restartEvent = false;
    }
    
    //再根据该输入事件的 seq值从 waitQueue 队列中去查找对应的 dispatchEntryIt 对象
    dispatchEntryIt = connection->findWaitQueueEntry(seq);
    //查找到该输入事件命中if
    if (dispatchEntryIt != connection->waitQueue.end()) {
        dispatchEntry = *dispatchEntryIt;
        //将该输入事件从 waitQueue 队列中删除
        connection->waitQueue.erase(dispatchEntryIt);
        //再将该输入事件的 anrTranck 从 mAnrTracker 中删除
        mAnrTracker.erase(dispatchEntry->timeoutTime,
                          connection->inputChannel->getConnectionToken());
        //判断connection中是否存在需要响应的事件,如果存在则命中if
        if (!connection->responsive) {
            //判断 waitQueue 队列中是否有 小于当前事件的事件,如果有则代表需要响应,那么就设置 responsive 为 false
            connection->responsive = isConnectionResponsive(*connection);
        }
        traceWaitQueueLength(connection);
        //如果当前的输入事件需要再次发送给应用程序,即restartEvent为true,且connection状态正常,那么命中if
        if (restartEvent && connection->status == Connection::STATUS_NORMAL) {
            //将当前事件重新再次放入 outboundQueue 待分发队列中
            connection->outboundQueue.push_front(dispatchEntry);
            traceOutboundQueueLength(connection);
        } else {
            //进入else,代表当前事件不需要在进行分发,那么直接删除该输入事件
            releaseDispatchEntry(dispatchEntry);
        }
    }

    //开始下一轮的输入事件分发
    startDispatchCycleLocked(now(), connection);
}
  • 可以看到,在该函数中,会存在两种情况:

    1. 当前输入事件是不需要再次发送给应用程序窗口的,那么就会将该输入事件从 waitQueue 中删除,然后再将该输入事件对应的 anrTrackermAnrTracker 链表中删除,最后会直接释放 该输入事件的空间,接着开始下一轮分发循环
    2. 当前输入事件需要再次发送给应用程序窗口,那么就会将该输入事件再次放入待分发队列 outboundQueue 中,然后直接开始下一轮的分发循环
  • 至此,一个输入事件处理完毕后的答复流程也都梳理完毕了,那么接下来让我们总体的来总结一下输入事件处理的全部流程

总结:

  • 经过上面的层层分析,我们知道了,一个输入事件从获取到被分发到对应窗口最后被删除总共经历了以下几个步骤:
    1. 首先是在 InputReader 线程中,通过 EventHub 对象得到了在 /dev/input 节点下发生的输入事件
    2. 然后将发生的输入事件封装成一个消息,放入了 mArgsQueue 队列中,并调用 InputClassifier->notifyKey()mArgsQueue 队列中的消息发送给了 InputDispatcher 线程
    3. InputDispatcher 对象的 notifyKey() 函数中,首先会将该输入事件通过 interceptKeyBeforeQueueing() 方法进行处理,而这个方法最终会调用到 PhoneWindowManager 对象中的同名方法中
    4. 在进行了入队前的处理之后,就会将给输入事件放入分发队列 mInboundQueue 中,然后根据输入事件是否和窗口切换相关以及分发队列是否为空去判断,是否需要将 InputDispatcher 线程唤醒
    5. 在将 InputDispatcher 线程唤醒之后,接下来就会进行输入事件的分发,而在分发输入事件到对应窗口前,首先会构建两个 CommandEntry 对象 放入 mCommandQueue 队列中;存放完毕后,将该输入事件通过 InputChannel 写入了 Socket
    6. 在写入完成后,会遍历 mCommandQueue 队列,去执行其中的 CommandEntry 对象,其中一个是会调用到 PMSuserActivityFromNative() 函数,而另一个是调用到 PhoneWindowManagerinterceptKeyBeforeDispatching() 函数,至此 InputDispatcher 线程的分发流程就结束了,需要注意的是在将输入事件写入 socket 之后,对于 InputDispatcher 来说分发就结束了
    7. 对应的应用程序窗口端,在 Socket 被写入数据之后,就会调用 NativeInputEventReceiverhandleEvent() 函数进行处理,最终通过责任链的形式将该输入事件分发到真正的处理函数中
    8. 在完成了输入事件的处理后,就会调用 finishInputEvent() 函数去通知 InputDispatcher 线程当前输入事件已处理完毕,而这个通知方式,就是将 finish 事件写入了 client 端的 InputChannelSocket
    9. client 端的 InputChannelsocket 被写入数据,那么 InputDispatcher 线程的 Looper 循环就会被唤醒,接下来就会调用 handleReceiveCallback() 函数去进行处理
    10. handleReceiveCallback() 中,会将当前已处理完成的输入事件封装成 CommandEntry 对象,放入 mCommandQueue 队列中,其中该 CommandEntry 对象的 CommanddoDispatchCycleFinishedLockedInterruptible() 函数
    11. 在完成封装和入队列后,接下来就会遍历 mCommandQueue 队列,逐一调用 Command 函数进行处理,此时就会调用 doDispatchCycleFinishedLockedInterruptible() 函数,该函数就是将该输入事件从 waitQueue 中删除,然后再将该输入事件对应的 anrTrackermAnrTracker 链表中删除,最后会直接释放 该输入事件的空间,接着开始下一轮分发循环
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值