Choreographer原理及应用,双非渣本Android四年磨一剑

首先我们需要知道mChoreographer是什么,在什么地方进行的初始化。在ViewRootImpl的构造方法里面,我看到了它的初始化。

public ViewRootImpl(Context context, Display display) {

mContext = context;

//Binder代理IWindowSession,与WMS通信

mWindowSession = WindowManagerGlobal.getWindowSession();

mDisplay = display;

//初始化当前线程 一般就是主线程,一般是在WindowManagerGlobal.addView()里面调用的

mThread = Thread.currentThread();

mWidth = -1;

mHeight = -1;

//Binder代理 IWindow

mWindow = new W(this);

//当前是不可见的

mViewVisibility = View.GONE;

mFirst = true; // true for the first time the view is added

mAdded = false;

//初始化Choreographer,从getInstance()方法名,看起来像是单例

mChoreographer = Choreographer.getInstance();

}

在ViewRootImpl的构造方法中初始化Choreographer,利用Choreographer的getInstance方法,看起来像是单例。

//Choreographer.java

/**

  • Gets the choreographer for the calling thread. Must be called from

  • a thread that already has a {@link android.os.Looper} associated with it.

  • 获取当前线程中的单例Choreographer,在获取之前必须保证该线程已初始化好Looper

  • @return The choreographer for this thread.

  • @throws IllegalStateException if the thread does not have a looper.

*/

public static Choreographer getInstance() {

return sThreadInstance.get();

}

// Thread local storage for the choreographer.

//线程私有

private static final ThreadLocal sThreadInstance =

new ThreadLocal() {

@Override

protected Choreographer initialValue() {

//从当前线程的ThreadLocalMap中取出Looper

Looper looper = Looper.myLooper();

if (looper == null) {

throw new IllegalStateException(“The current thread must have a looper!”);

}

//初始化

Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);

if (looper == Looper.getMainLooper()) {

mMainInstance = choreographer;

}

return choreographer;

}

};

从上面的代码可以看出,其实getInstance()的实现并不是真正意义上的单例,而是线程内的单例。其实现原理是利用ThreadLocal来实现数据线程私有化,不了解的同学可以看一下Handler机制你需要知道的一切

在ThreadLocal的initialValue()中,先是取出已经在当前线程初始化好的私有数据Looper,如果当前线程没有初始化Looper,那么对不起了,先抛个IllegalStateException表示一下。

这里的初始化一般是在主线程中,主线程中的Looper早就初始化好了,所以这里不会抛异常。by the way,主线程Looper是在什么时候初始化好的?先看一下应用进程的创建流程:

  1. AMS通过调用Process.start()来创建应用进程

  2. 在Process.start()里面通过ZygoteProcess的zygoteSendArgsAndGetResult与Zygote进程(Zygote是谁?它是进程孵化大师,创建之初就使用zygoteServer.registerServerSocketFromEnv创建zygote通信的服务端;然后还通过调用forkSystemServer启动system_server;然后是zygoteServer.runSelectLoop进入循环模式)建立Socket连接,并将创建进程所需要的参数发送给Zygote的Socket服务端

  3. Zygote进程的Socket服务端(ZygoteServer)收到参数后调用ZygoteConnection.processOneCommand() 处理参数,并 fork 进程

  4. 然后通过RuntimeInit的findStaticMain()找到ActivityThread类的main方法并执行

想必分析到这里,大家已经很熟悉了吧

//ActivityThread.java

public static void main(String[] args) {

//初始化主线程的Looper

Looper.prepareMainLooper();

//创建好ActivityThread 并调用attach

ActivityThread thread = new ActivityThread();

thread.attach(false, startSeq);

if (sMainThreadHandler == null) {

sMainThreadHandler = thread.getHandler();

}

//主线程处于loop循环中

Looper.loop();

//主线程的loop循环是不能退出的

throw new RuntimeException(“Main thread loop unexpectedly exited”);

}

应用进程一启动,主线程的Looper就首当其冲的初始化好了,说明它在Android中的地位重要性非常大。它的初始化,就是将Looper存于ThreadLocal中,然后再将该ThreadLocal存于当前线程的ThreadLocalMap中,以达到线程私有化的目的。

回到Choreographer的构造方法

//Choreographer.java

private Choreographer(Looper looper, int vsyncSource) {

//把Looper传进来放起

mLooper = looper;

//FrameHandler初始化 传入Looper

mHandler = new FrameHandler(looper);

// USE_VSYNC 在 Android 4.1 之后默认为 true,

// FrameDisplayEventReceiver是用来接收VSYNC信号的

mDisplayEventReceiver = USE_VSYNC

? new FrameDisplayEventReceiver(looper, vsyncSource)
null;

mLastFrameTimeNanos = Long.MIN_VALUE;

//一帧的时间,60FPS就是16.66ms

mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());

// 回调队列

mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];

for (int i = 0; i <= CALLBACK_LAST; i++) {

mCallbackQueues[i] = new CallbackQueue();

}

// b/68769804: For low FPS experiments.

setFPSDivisor(SystemProperties.getInt(ThreadedRenderer.DEBUG_FPS_DIVISOR, 1));

}

构造Choreographer基本上是完了,构造方法里面有些新东西,后面详细说。

Choreographer 流程原理

现在我们来说一下Choreographer的postCallback(),也就是ViewRootImpl使用的地方

//Choreographer.java

//ViewRootImpl是使用的这个

public void postCallback(int callbackType, Runnable action, Object token) {

postCallbackDelayed(callbackType, action, token, 0);

}

public void postCallbackDelayed(int callbackType,

Runnable action, Object token, long delayMillis) {

postCallbackDelayedInternal(callbackType, action, token, delayMillis);

}

private final CallbackQueue[] mCallbackQueues;

private void postCallbackDelayedInternal(int callbackType,

Object action, Object token, long delayMillis) {

synchronized (mLock) {

final long now = SystemClock.uptimeMillis();

final long dueTime = now + delayMillis;

//将mTraversalRunnable存入mCallbackQueues数组callbackType处的队列中

mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

//传入的delayMillis是0,这里dueTime是等于now的

if (dueTime <= now) {

scheduleFrameLocked(now);

} else {

Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);

msg.arg1 = callbackType;

msg.setAsynchronous(true);

mHandler.sendMessageAtTime(msg, dueTime);

}

}

}

有2个关键地方,第一个是将mTraversalRunnable存起来方便待会儿调用,第二个是执行scheduleFrameLocked方法

//Choreographer.java

private void scheduleFrameLocked(long now) {

if (!mFrameScheduled) {

mFrameScheduled = true;

if (USE_VSYNC) {

//走这里

// 如果当前线程是初始化Choreographer时的线程,直接申请VSYNC,否则立刻发送一个异步消息到初始化Choreographer时的线程中申请VSYNC

if (isRunningOnLooperThreadLocked()) {

scheduleVsyncLocked();

} else {

Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);

msg.setAsynchronous(true);

mHandler.sendMessageAtFrontOfQueue(msg);

}

} else {

//这里是未开启VSYNC的情况,Android 4.1之后默认开启

final long nextFrameTime = Math.max(

mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);

if (DEBUG_FRAMES) {

Log.d(TAG, “Scheduling next frame in " + (nextFrameTime - now) + " ms.”);

}

Message msg = mHandler.obtainMessage(MSG_DO_FRAME);

msg.setAsynchronous(true);

mHandler.sendMessageAtTime(msg, nextFrameTime);

}

}

}

通过调用scheduleVsyncLocked()来监听VSYNC信号,这个信号是由硬件发出来的,信号来了的时候才开始绘制工作。

//Choreographer.java

private final FrameDisplayEventReceiver mDisplayEventReceiver;

private void scheduleVsyncLocked() {

mDisplayEventReceiver.scheduleVsync();

}

private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {

}

//DisplayEventReceiver.java

public void scheduleVsync() {

if (mReceiverPtr == 0) {

Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "

  • “receiver has already been disposed.”);

} else {

//注册监听VSYNC信号,会回调dispatchVsync()方法

nativeScheduleVsync(mReceiverPtr);

}

}

mDisplayEventReceiver是一个FrameDisplayEventReceiver,FrameDisplayEventReceiver继承自DisplayEventReceiver。在DisplayEventReceiver里面有一个方法scheduleVsync(),这个方法是用来注册监听VSYNC信号的,它是一个native方法,水平有限,暂不继续深入了。

当有VSYNC信号来临时,native层会回调DisplayEventReceiver的dispatchVsync方法

//DisplayEventReceiver.java

// Called from native code.

@SuppressWarnings(“unused”)

private void dispatchVsync(long timestampNanos, int builtInDisplayId, int frame) {

onVsync(timestampNanos, builtInDisplayId, frame);

}

public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {

}

当收到VSYNC信号时,回调dispatchVsync方法,走到了onVsync方法,这个方法被子类FrameDisplayEventReceiver覆写了的

//FrameDisplayEventReceiver.java

//它是Choreographer的内部类

private final class FrameDisplayEventReceiver extends DisplayEventReceiver

implements Runnable {

@Override

public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {

if (builtInDisplayId != SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) {

Log.d(TAG, "Received vsync from secondary display, but we don’t support "

  • "this case yet. Choreographer needs a way to explicitly request "

  • "vsync for a specific display to ensure it doesn’t lose track "

  • “of its scheduled vsync.”);

scheduleVsync();

return;

}

//timestampNanos是VSYNC回调的时间戳 以纳秒为单位

long now = System.nanoTime();

if (timestampNanos > now) {

timestampNanos = now;

}

if (mHavePendingVsync) {

Log.w(TAG, "Already have a pending vsync event. There should only be "

  • “one at a time.”);

} else {

mHavePendingVsync = true;

}

mTimestampNanos = timestampNanos;

mFrame = frame;

//自己是一个Runnable,把自己传了进去

Message msg = Message.obtain(mHandler, this);

//异步消息,保证优先级

msg.setAsynchronous(true);

mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);

}

@Override

public void run() {

doFrame(mTimestampNanos, mFrame);

}

}

在onVsync()方法中,其实主要内容就是发个消息(应该是为了切换线程),然后执行run方法。而在run方法中,调用了Choreographer的doFrame方法。这个方法有点长,我们来理一下。

//Choreographer.java

//frameTimeNanos是VSYNC信号回调时的时间

void doFrame(long frameTimeNanos, int frame) {

final long startNanos;

synchronized (mLock) {

if (!mFrameScheduled) {

return; // no work to do

}

long intendedFrameTimeNanos = frameTimeNanos;

startNanos = System.nanoTime();

//jitterNanos为当前时间与VSYNC信号来时的时间的差值,如果Looper有很多异步消息等待处理(或者是前一个异步消息处理特别耗时,当前消息发送了很久才得以执行),那么处理当来到这里时可能会出现很大的时间间隔

final long jitterNanos = startNanos - frameTimeNanos;

//mFrameIntervalNanos是帧间时长,一般手机上为16.67ms

if (jitterNanos >= mFrameIntervalNanos) {

final long skippedFrames = jitterNanos / mFrameIntervalNanos;

//想必这个日志大家都见过吧,主线程做了太多的耗时操作或者绘制起来特别慢就会有这个

//这里的逻辑是当掉帧个数超过30,则输出相应日志

if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {

Log.i(TAG, "Skipped " + skippedFrames + " frames! "

  • “The application may be doing too much work on its main thread.”);

}

final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;

frameTimeNanos = startNanos - lastFrameOffset;

}

}

try {

AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

mFrameInfo.markInputHandlingStart();

doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

mFrameInfo.markAnimationsStart();

doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);

//执行回调

mFrameInfo.markPerformTraversalsStart();

doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);

} finally {

AnimationUtils.unlockAnimationClock();

}

}

doFrame大体做了2件事,一个是可能会给开发者打个日志提醒下卡顿,另一个是执行回调。

当VSYNC信号来临时,记录了此时的时间点,也就是这里的frameTimeNanos。而执行doFrame()时,是通过Looper的消息循环来的,这意味着前面有消息没执行完,那么当前这个消息的执行就会被阻塞在那里。时间太长了,而这个是处理界面绘制的,如果时间长了没有即时进行绘制,就会出现掉帧。源码中也打了log,在掉帧30的时候。

下面来看一下执行回调的过程

//Choreographer.java

void doCallbacks(int callbackType, long frameTimeNanos) {

CallbackRecord callbacks;

synchronized (mLock) {

final long now = System.nanoTime();

//根据callbackType取出相应的CallbackRecord

callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(

now / TimeUtils.NANOS_PER_MS);

if (callbacks == null) {

return;

}

mCallbacksRunning = true;

}

try {

Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);

for (CallbackRecord c = callbacks; c != null; c = c.next) {

//

c.run(frameTimeNanos);

}

} finally {

}

}

private static final class CallbackRecord {

public CallbackRecord next;

public long dueTime;

public Object action; // Runnable or FrameCallback

public Object token;

public void run(long frameTimeNanos) {

if (token == FRAME_CALLBACK_TOKEN) {

((FrameCallback)action).doFrame(frameTimeNanos);

} else {

//会走到这里来,因为ViewRootImpl的scheduleTraversals时,postCallback传过来的token是null。

((Runnable)action).run();

}

}

}

从mCallbackQueues数组中找到callbackType对应的CallbackRecord,然后执行队列里面的所有元素(CallbackRecord)的run方法。然后也就是执行到了ViewRootImpl的scheduleTraversals时,postCallback传过来的mTraversalRunnable(是一个Runnable)。回顾一下:

//ViewRootImpl.java

final class TraversalRunnable implements Runnable {

@Override

public void run() {

doTraversal();

}

}

也是,我们整个流程也就完成了,从doTraversal()开始就是View的三大流程(measure、layout、draw)了。Choreographer的使命也基本完成了。

上面就是Choreographer的工作流程。简单总结一下:

  1. 从ActivityThread.handleResumeActivity开始,ActivityThread.handleResumeActivity()->WindowManagerImpl.addView()->WindowManagerGlobal.addView()->初始化ViewRootImpl->初始化Choreographer->ViewRootImpl.setView()

  2. 在ViewRootImpl的setView中会调用requestLayout()->scheduleTraversals(),然后是建立同步屏障

  3. 通过Choreographer线程单例的postCallback()提交一个任务mTraversalRunnable,这个任务是用来做View的三大流程的(measure、layout、draw)

  4. Choreographer.postCallback()内部通过DisplayEventReceiver.nativeScheduleVsync()向系统底层注册VSYNC信号监听,当VSYNC信号来临时,会回调DisplayEventReceiver的dispatchVsync(),最终会通知FrameDisplayEventReceiver.onVsync()方法。

  5. 在onVsync()中取出之前传入的任务mTraversalRunnable,执行run方法,开始绘制流程。

4. 应用

在了解了Choreographer的工作原理之后,我们来点实际的,将Choreographer这块的知识利用起来。它可以帮助我们检测应用的fps。

检测FPS

有了上面的分析,我们知道Choreographer内部去监听了VSYNC信号,并且当VSYNC信号来临时会发个异步消息给Looper,在执行到这个消息时会通知外部观察者(上面的观察者就是ViewRootImpl),通知ViewRootImpl可以开始绘制了。Choreographer的每次回调都是在通知ViewRootImpl绘制,我们只需要统计出1秒内这个回调次数有多少次,即可知道是多少fps。

反正Choreographer是线程单例,我在主线程调用获取它的实例,然后模仿ViewRootImpl调用postCallback注册一个观察者。于是我将该思路写成代码,然后发现,postCallback是居然是hide方法。/无语

但是,有个意外收获,Choreographer提供了另外一个postFrameCallback方法。我看了下源码,与postCallback差异不大,只不过注册的观察者类型是CALLBACK_ANIMATION,但这不影响它回调

//Choreographer.java

public void postFrameCallback(FrameCallback callback) {

postFrameCallbackDelayed(callback, 0);

}

public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {

postCallbackDelayedInternal(CALLBACK_ANIMATION,

callback, FRAME_CALLBACK_TOKEN, delayMillis);

}

直接上代码吧,show me the code

object FpsMonitor {

private const val FPS_INTERVAL_TIME = 1000L

/**

  • 1秒内执行回调的次数 即fps

*/

private var count = 0

private val mMonitorListeners = mutableListOf<(Int) -> Unit>()

@Volatile

private var isStartMonitor = false

private val monitorFrameCallback by lazy { MonitorFrameCallback() }

private val mainHandler by lazy { Handler(Looper.getMainLooper()) }

fun startMonitor(listener: (Int) -> Unit) {

mMonitorListeners.add(listener)

if (isStartMonitor) {

return

}

isStartMonitor = true

Choreographer.getInstance().postFrameCallback(monitorFrameCallback)

//1秒后结算 count次数

mainHandler.postDelayed(monitorFrameCallback, FPS_INTERVAL_TIME)

}

fun stopMonitor() {

isStartMonitor = false

count = 0

Choreographer.getInstance().removeFrameCallback(monitorFrameCallback)

mainHandler.removeCallbacks(monitorFrameCallback)

}

class MonitorFrameCallback : Choreographer.FrameCallback, Runnable {

//VSYNC信号到了,且处理到当前异步消息了,才会回调这里

override fun doFrame(frameTimeNanos: Long) {

//次数+1 1秒内

count++

//继续下一次 监听VSYNC信号

Choreographer.getInstance().postFrameCallback(this)

}

override fun run() {

//将count次数传递给外面

mMonitorListeners.forEach {

it.invoke(count)

}

count = 0

//继续发延迟消息 等到1秒后统计count次数

mainHandler.postDelayed(this, FPS_INTERVAL_TIME)

}

}

}

通过记录每秒内Choreographer回调的次数,即可得到FPS。

监测卡顿

Choreographer除了可以用来监测FPS以外还可以拿来进行卡顿检测。

Choreographer 流畅度监测

通过设置Choreographer的FrameCallback,可以在每一帧被渲染的时候记录下它开始渲染的时间,这样在下一帧被处理时,我们可以根据时间差来判断上一帧在渲染过程中是否出现掉帧。Android中,每发出一个VSYNC信号都会通知界面进行重绘、渲染,每一次同步周期为16.6ms,代表一帧的刷新频率。每次需要开始渲染的时候都会回调doFrame(),如果某2次doFrame()之间的时间差大于16.6ms,则说明发生了UI有点卡顿,已经在掉帧了,拿着这个时间差除以16.6就得出了掉过了多少帧。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

总结

学习技术是一条慢长而艰苦的道路,不能靠一时激情,也不是熬几天几夜就能学好的,必须养成平时努力学习的习惯。所以:贵在坚持!

最后如何才能让我们在面试中对答如流呢?

答案当然是平时在工作或者学习中多提升自身实力的啦,那如何才能正确的学习,有方向的学习呢?有没有免费资料可以借鉴?为此我整理了一份Android学习资料路线:

这里是一部分我工作以来以及参与过的大大小小的面试收集总结出来的一套BAT大厂面试资料专题包,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家。

好了,今天的分享就到这里,如果你对在面试中遇到的问题,或者刚毕业及工作几年迷茫不知道该如何准备面试并突破现状提升自己,对于自己的未来还不够了解不知道给如何规划。来看看同行们都是如何突破现状,怎么学习的,来吸收他们的面试以及工作经验完善自己的之后的面试计划及职业规划。

最后,祝愿即将跳槽和已经开始求职的大家都能找到一份好的工作!

这些只是整理出来的部分面试题,后续会持续更新,希望通过这些高级面试题能够降低面试Android岗位的门槛,让更多的Android工程师理解Android系统,掌握Android系统。喜欢的话麻烦点击一个喜欢再关注一下~

DlTng-1711926870989)]
[外链图片转存中…(img-PlZN7DdK-1711926870990)]
[外链图片转存中…(img-XD4i8M0E-1711926870990)]
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-PZMd06DQ-1711926870990)]

总结

学习技术是一条慢长而艰苦的道路,不能靠一时激情,也不是熬几天几夜就能学好的,必须养成平时努力学习的习惯。所以:贵在坚持!

最后如何才能让我们在面试中对答如流呢?

答案当然是平时在工作或者学习中多提升自身实力的啦,那如何才能正确的学习,有方向的学习呢?有没有免费资料可以借鉴?为此我整理了一份Android学习资料路线:

[外链图片转存中…(img-SSHbVF0l-1711926870991)]

这里是一部分我工作以来以及参与过的大大小小的面试收集总结出来的一套BAT大厂面试资料专题包,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家。

[外链图片转存中…(img-Z6eTSTfp-1711926870991)]

好了,今天的分享就到这里,如果你对在面试中遇到的问题,或者刚毕业及工作几年迷茫不知道该如何准备面试并突破现状提升自己,对于自己的未来还不够了解不知道给如何规划。来看看同行们都是如何突破现状,怎么学习的,来吸收他们的面试以及工作经验完善自己的之后的面试计划及职业规划。

最后,祝愿即将跳槽和已经开始求职的大家都能找到一份好的工作!

这些只是整理出来的部分面试题,后续会持续更新,希望通过这些高级面试题能够降低面试Android岗位的门槛,让更多的Android工程师理解Android系统,掌握Android系统。喜欢的话麻烦点击一个喜欢再关注一下~

本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值