2024年安卓最全Handler使用及源码解析,面试要掌握这几个关键点的技巧

文末

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

这里放上一部分我工作以来以及参与过的大大小小的面试收集总结出来的相关的几十套腾讯、头条、阿里、美团等公司21年的面试专题,其中把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分免费分享给大家,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家~

还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

【延伸Android必备知识点】

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

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

return queue.enqueueMessage(msg, uptimeMillis);

}

可以看到最终调用了MessageQueue的私有方法enqueueMessage(msg, uptimeMillis) 方法,点进MessageQueue看看。

boolean enqueueMessage(Message msg, long when) {

if (msg.target == null) {

//判断msg.target是否为空,target就是Handler

throw new IllegalArgumentException(“Message must have a target.”);

}

synchronized (this) {

if (msg.isInUse()) {

throw new IllegalStateException(msg + " This message is already in use.");

}

if (mQuitting) {

//这里判断线程是否已经销毁

IllegalStateException e = new IllegalStateException(

msg.target + " sending message to a Handler on a dead thread");

Log.w(TAG, e.getMessage(), e);

msg.recycle();

return false;

}

msg.markInUse();

msg.when = when;

Message p = mMessages;

boolean needWake;

if (p == null || when == 0 || when < p.when) {

// 当消息队列为空或者将要入队的消息(msg)的时间(when)在所有消息队列的消息最前面,则把 msg插入到队头,最先执行

// New head, wake up the event queue if blocked.

msg.next = p;

mMessages = msg;

needWake = mBlocked;

} else {

// Inserted within the middle of the queue. Usually we don’t have to wake

// up the event queue unless there is a barrier at the head of the queue

// and the message is the earliest asynchronous message in the queue.

//将消息按时间顺序插入到MessageQueue。

needWake = mBlocked && p.target == null && msg.isAsynchronous();

Message prev;

for (;😉 {

prev = p;

p = p.next;

if (p == null || when < p.when) {

break;

}

if (needWake && p.isAsynchronous()) {

needWake = false;

}

}

msg.next = p; // invariant: p == prev.next

prev.next = msg;

}

// We can assume mPtr != 0 because mQuitting is false.

if (needWake) {

nativeWake(mPtr);

}

}

return true;

}

看Message类的实现可以看出它是个单链表,消息队列中的消息都是按照时间先后顺序链接起来的。

在上面的if分支中有三个条件判断p == null || when == 0 || when < p.when,

  • p == null:当p也就是mMessages为空,表示消息队列中还没添加过消息

  • when == 0:when正常情况下表示当前时间SystemClock.uptimeMillis()加上延迟时间delayMillis,如果直接调用Handler的sendMessageAtTime,可能会出现when=0的情况

  • when < p.when:新消息的时间when小于p消息的时间p.when

以上三个条件只要满足任一条件都会执行到if代码块中的语句,将新消息添加到p消息的前面,p消息为空时,即队头。

当添加新消息时,消息队列中有消息的时候,就会执行到else语句中,将消息按时间顺序插入到MessageQueue。

prev = p;

p = p.next;

第一行代码中的p就是在if代码块中添加的消息,说明指针指向p消息,第二行代码又将指针指向了p消息的下一个消息,指针向后移动了一位。消息添加到MessageQueue的流程大致如下:

MeessaegeQueue

取出消息

Looper.loop()方法不断的从MessageQueue中取消息。那么Looper.loop()方法是在哪里调用的呢?由于系统都是由消息驱动的,所以在系统启动的时候就应该有动力驱动了,在SystemServer.main()中调用了Looper.loop(),这是在系统层面的驱动,而对于APP应用层面来说,应用入口ActivityThread.main()中也需要调用Looper.loop(),毕竟APP也是需要各种事件响应。

下面分别看看SystemServer和ActivityThread的main方法:

com.android.server.SystemServer.java:407

public static void main(String[] args) {

new SystemServer().run();

}

private void run() {

try {

// Prepare the main looper thread (this thread).

android.os.Process.setThreadPriority(

android.os.Process.THREAD_PRIORITY_FOREGROUND);

android.os.Process.setCanSelfBackground(false);

Looper.prepareMainLooper();

Looper.getMainLooper().setSlowLogThresholdMs(

SLOW_DISPATCH_THRESHOLD_MS, SLOW_DELIVERY_THRESHOLD_MS);

} finally {

t.traceEnd(); // InitBeforeStartServices

}

// Loop forever.

Looper.loop();

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

}

android.app.ActivityThread.java:6707

public static void main(String[] args) {

Looper.prepareMainLooper();

ActivityThread thread = new ActivityThread();

thread.attach(false, startSeq);

if (sMainThreadHandler == null) {

sMainThreadHandler = thread.getHandler();

}

if (false) {

Looper.myLooper().setMessageLogging(new

LogPrinter(Log.DEBUG, “ActivityThread”));

}

// End of event ActivityThreadMain.

Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);

Looper.loop();

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

}

可以看到SystemServer和ActivityThread的main方法中都调用了Looper.prepareMainLooper()Looper.loop() 方法。首先看看Looper.prepareMainLooper() 的源码:

/**

  • Initialize the current thread as a looper, marking it as an

  • application’s main looper. See also: {@link #prepare()}

  • @deprecated The main looper for your application is created by the Android environment,

  • so you should never need to call this function yourself.

*/

@Deprecated

public static void prepareMainLooper() {

prepare(false);

synchronized (Looper.class) {

if (sMainLooper != null) {

throw new IllegalStateException(“The main Looper has already been prepared.”);

}

sMainLooper = myLooper();

}

}

private static void prepare(boolean quitAllowed) {

if (sThreadLocal.get() != null) {

throw new RuntimeException(“Only one Looper may be created per thread”);

}

sThreadLocal.set(new Looper(quitAllowed));

}

private Looper(boolean quitAllowed) {

mQueue = new MessageQueue(quitAllowed);

mThread = Thread.currentThread();

}

public static void prepare() {

prepare(true);

}

prepareMainLooper方法中又调用了prepare(boolean quitAllowed)方法,然后prepare方法中创建了一个Looper实例并设置给sThreadLocal变量,Looper构造函数中又创建了MessageQueue对象。sThreadLocal保证了一个线程只有一个Looper,而一个Looper又只有一个MessageQueue。

prepareMainLooper方法标记为Deprecated,是因为应用程序的主线程Looper应该由系统自动为我们初始化,不需要我们自己去调。

从源码中看到私有方法prepare有一个参数quitAllowed,prepareMainLooper中传的是false,prepare()方法中传的是true,说明主线程中的Looper不允许退出。

分析完Looper.prepareMainLooper() 之后,再来看看Looper.loop() 方法。

public static void loop() {

final Looper me = myLooper(); // 先获取Looper对象

if (me == null) {

throw new RuntimeException(“No Looper; Looper.prepare() wasn’t called on this thread.”);

}

for (;😉 {

Message msg = queue.next(); // might block 获取消息,无消息时可能会阻塞

if (msg == null) {

// No message indicates that the message queue is quitting.

return;

}

try {

msg.target.dispatchMessage(msg); // 调用target(Handler)分发消息

if (observer != null) {

observer.messageDispatched(token, msg);

}

dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;

} catch (Exception exception) {

if (observer != null) {

observer.dispatchingThrewException(token, msg, exception);

}

throw exception;

}

msg.recycleUnchecked(); //回收消息

}

}

首先要获取到Looper对象,当Looper对象为空时抛出异常No Looper; Looper.prepare() wasn't called on this thread.,我们就知道调用loop()方法之前需要先调用prepare()方法,在上面的prepareMainLooper() 和 prepare()中创建出了Looper对象。

获取到了Looper对象后,就从Looper中获取MessageQueue对象queue,然后在for(,,)循环中调用queue.next()取消息,当消息队列中没有消息时,就会阻塞在queue.next()这里。来看看queue.next()中做了什么:

Message next() {

int nextPollTimeoutMillis = 0;

for (;😉 {

if (nextPollTimeoutMillis != 0) {

Binder.flushPendingCommands();

}

nativePollOnce(ptr, nextPollTimeoutMillis);

synchronized (this) {

// Try to retrieve the next message. Return if found.

final long now = SystemClock.uptimeMillis();

Message prevMsg = null;

Message msg = mMessages;

if (msg != null && msg.target == null) {

// Stalled by a barrier. Find the next asynchronous message in the queue.

do {

prevMsg = msg;

msg = msg.next;

} while (msg != null && !msg.isAsynchronous());

}

if (msg != null) {

if (now < msg.when) {

// Next message is not ready. Set a timeout to wake up when it is ready. 当前时间小于消息的时间,说明还没有到该消息执行的时候,计算出消息执行 的延时时间

nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);

} else {

// Got a message.

mBlocked = false;

if (prevMsg != null) {

prevMsg.next = msg.next;

} else {

// msg将要进行处理,然后会将处理的消息删除,所以消息指针需要向后移一位

mMessages = msg.next;

}

msg.next = null; // 表示将msg从消息队列中剔除

if (DEBUG) Log.v(TAG, "Returning message: " + msg);

msg.markInUse();

return msg;

}

} else {

// No more messages. 没有消息,继续休眠等待

nextPollTimeoutMillis = -1;

}

}

}

}

next()里面是一个死循环,如果消息队列中没有消息的时候会堵塞在next()方法处让CPU休眠,消息队列中有消息的时候,则取出消息,判断当前时间和该消息执行时间的先后关系,没到执行时间则继续休眠等待,否则就返回消息给Looper.loop()方法进行处理并从消息队列中移除该msg,enqueueMessage()next()方法中都用了同步锁,避免在发送消息时取消息以及取消息时发送消息,保证了线程安全。

处理消息

在上一步取出消息之后,loop()方法中调用了:

msg.target.dispatchMessage(msg);

这里的target就是在Handler的enqueueMessage方法在发送消息时设置的Handler对象。所以来看看Handler的dispatchMessage方法:

public void dispatchMessage(@NonNull Message msg) {

if (msg.callback != null) {

handleCallback(msg);

} else {

if (mCallback != null) {

if (mCallback.handleMessage(msg)) {

return;

}

}

handleMessage(msg);

}

}

若msg.callback和mCallback都为空,则会执行handleMessage(msg)。

/**

  • 子类必须实现这个方法才能收到消息

  • Subclasses must implement this to receive messages.

*/

public void handleMessage(@NonNull Message msg) {

}

上面只说了msg.callback和mCallback都为空的情况,会调用Handler的handleMessage方法处理消息,现在来分别看看msg.callback和mCallback不为空处理消息的情况。

首先,msg.callback是在创建Message的时候赋值的,

public static Message obtain(Handler h, Runnable callback) {

Message m = obtain();

m.target = h;

m.callback = callback;

return m;

}

我们现在修改一下MainActivity的代码,然后运行一下发现同样可以更新infoText的信息:

class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding

//这里只创建handler对象,没有重写handleMessage方法

private val handler = Handler()

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

binding = ActivityMainBinding.inflate(layoutInflater)

setContentView(binding.root)

binding.updateBtn.setOnClickListener {

thread {

val message = Message.obtain(handler) {

binding.infoText.text = “Message Callback”

}

handler.sendMessage(message)

}

}

}

}

这里创建message时,使用了Message的obtain(handler, runnable)重载方法,而不是使用new Message()的方式,因为obtain()方法内部是直接从系统的Message缓存池中获取的Message对象,效率更高:

public static Message obtain(Handler h, Runnable callback) {

Message m = obtain();

m.target = h;

m.callback = callback;

return m;

}

public static Message obtain() {

synchronized (sPoolSync) {

if (sPool != null) {

Message m = sPool;

sPool = m.next;

m.next = null;

m.flags = 0; // clear in-use flag

sPoolSize–;

return m;

}

}

return new Message();

}

Message的obtain方法还有很多重载方法,每个方法传的参数不一样,根据不同的需求来选择调用对应的方法即可,具体的源码就不展开了。

public static Message obtain(){}

public static Message obtain(Message orig){}

public static Message obtain(Handler h){}

public static Message obtain(Handler h, Runnable callback){}

public static Message obtain(Handler h, int what){}

public static Message obtain(Handler h, int what, Object obj){}

public static Message obtain(Handler h, int what, int arg1, int arg2){}

public static Message obtain(Handler h, int what, int arg1, int arg2, Object obj){}

最后再来看看mCallback不为空的情况,mCallback是在构造Handler的时候赋值的。

public Handler(@Nullable Callback callback, boolean async) {

mLooper = Looper.myLooper();

if (mLooper == null) {

throw new RuntimeException(

"Can’t create handler inside thread " + Thread.currentThread()

  • " that has not called Looper.prepare()");

}

mQueue = mLooper.mQueue;

mCallback = callback;

mAsynchronous = async;

}

继续修改一下MainActivity的代码:

class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding

private val handler = Handler(object : Handler.Callback {

override fun handleMessage(msg: Message): Boolean {

binding.infoText.text = “handle Callback message${msg.what}”

return true

}

})

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

binding = ActivityMainBinding.inflate(layoutInflater)

setContentView(binding.root)

binding.updateBtn.setOnClickListener {

thread {

handler.sendEmptyMessage(12)

}

}

}

}

我们使用了Handler(callback)构造参数创建handler对象,callback的handleMessage方法接收一个Boolean返回值,如果返回false,则执行完Callback的handleMessage方法之后,还会执行Handler的handleMessage方法,所以这里我们返回true,dispatchMessage方法执行mCallback.handleMessage(msg)之后直接return了。

public void dispatchMessage(@NonNull Message msg) {

if (msg.callback != null) {

handleCallback(msg);

} else {

if (mCallback != null) {

if (mCallback.handleMessage(msg)) {

return;

}

}

handleMessage(msg);

}

}

到这里,Handler机制的基本流程:发送消息、取出消息、处理消息 就已经分析完了。这样一些常见的问题就可以知道答案了。

Handler常见问题解答

1.Handler是如何做到在主线程更新UI的?

如果Handler是在主线程中创建或者Handler在子线程创建时构造函数中传入了Looper.getMainLooper(),则可以实现在主线程中更新UI。

答案就在于Looper的创建和loop()的执行了,由前面分析的**“取出消息”** 流程中可以知道,应用的主线程Looper的创建和loop()的执行是在ActivityThread.main() 方法中,因此Looper.loop() 方法取出消息之后,调用msg.target.dispatchMessage(msg); 分发处理消息就自然是在主线程中执行了。

2.如何处理Handler导致的内存泄漏?

Handler使用过程中,我们需要特别注意一个问题,那就是Handler可能会导致内存泄漏。

具体原因如下:

  • Handler的生命周期与Activity不同,Handler会关联Looper来管理Message Queue。这个队列在整个Application的生命周期中存在,因此Handler不会因Activity的finish()方法而被销毁。

  • 非静态(匿名)内部类会持有外部对象,当我们这样重写Handler时它就成为了一个匿名内部类,这样如果调用finish方法时Handler有Message未处理的话,就会导致Activity不能被销毁。

解决方法

  • 可以在外部新建一个类,在外部类对象被销毁时,将MessageQueue中的消息清空。

override fun onDestroy() {

super.onDestroy()

handler.removeCallbacksAndMessages(null)

}

  • 可以同时使用静态内部类和弱引用,当一个对象只被弱引用依赖时它便可以被GC回收。注意,要static和弱引用要同时使用,否则由于非静态内部类隐式持有了外部类Activity的引用,而导致Activity无法被释放

companion object class TestHandler(activity: Activity) : Handler(Looper.getMainLooper()) {

private val mActivity: WeakReference = WeakReference(activity)

override fun handleMessage(msg: Message) {

super.handleMessage(msg)

val activity: MainActivity = mActivity.get() as MainActivity;

activity.binding.infoText.text = “handleMessage”

}

}

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

handler = TestHandler(this@MainActivity)

}

3.子线程创建Handler抛异常?

有时候可能会有主线程发送消息给子线程处理的场景,但是在子线程中创建Handler之后,运行时出现了异常Can't create handler inside thread that has not called Looper.prepare()

class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding

private lateinit var handler: Handler

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

binding = ActivityMainBinding.inflate(layoutInflater)

setContentView(binding.root)

binding.updateBtn.setOnClickListener {

thread {

handler = object: Handler() {

override fun handleMessage(msg: Message) {

super.handleMessage(msg)

binding.infoText.text = “child thread message”

Log.e(“MainActivity”, “handleMessage: child thread message” )

}

}

}

}

binding.sendMessageBtn.setOnClickListener {

handler.sendEmptyMessage(12)

}

}

}

上面的代码中,我们点击updateBtn的时候在子线程中创建了一个Handler, 结果一运行就出现了上面的异常。这是因为我们在子线程创建Handler的时候还没有创建Looper对象,我们需要手动添加Looper.prepare()Looper.loop()

class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding

private lateinit var handler: Handler

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

binding = ActivityMainBinding.inflate(layoutInflater)

setContentView(binding.root)

binding.updateBtn.setOnClickListener {

thread {

Looper.prepare()

handler = object: Handler() {

override fun handleMessage(msg: Message) {

super.handleMessage(msg)

binding.infoText.text = “child thread message”

Log.e(“MainActivity”, “handleMessage: child thread message” )

}

}

Looper.loop()

}

}

binding.sendMessageBtn.setOnClickListener {

handler.sendEmptyMessage(12)

}

}

}

修改之后运行发现,创建Handler不会出现问题了,但是点击sendMessageBtn发送消息时,报如下的异常:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

这是因为我们在子线程中创建的Looper和Handler,handleMessage(msg) 方法就会在子线程中回调,因此不能操作UI了。

Handler机制扩展

为了更加方便的使用Handler消息机制,Android也提供了几种扩展方式,内部实现都是基于Handler消息机制.

  1. Activity.runOnUiThread(Runnable)

结语

  • 现在随着短视频,抖音,快手的流行NDK模块开发也显得越发重要,需要这块人才的企业也越来越多,随之学习这块的人也变多了,音视频的开发,往往是比较难的,而这个比较难的技术就是NDK里面的技术。
  • 音视频/高清大图片/人工智能/直播/抖音等等这年与用户最紧密,与我们生活最相关的技术一直都在寻找最终的技术落地平台,以前是windows系统,而现在则是移动系统了,移动系统中又是以Android占比绝大部分为前提,所以AndroidNDK技术已经是我们必备技能了。
  • 要学习好NDK,其中的关于C/C++,jni,Linux基础都是需要学习的,除此之外,音视频的编解码技术,流媒体协议,ffmpeg这些都是音视频开发必备技能,而且
  • OpenCV/OpenGl/这些又是图像处理必备知识,下面这些我都是当年自己搜集的资料和做的一些图,因为当年我就感觉视频这块会是一个大的趋势。所以提前做了一些准备。现在拿出来分享给大家。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

n.setOnClickListener {

handler.sendEmptyMessage(12)

}

}

}

修改之后运行发现,创建Handler不会出现问题了,但是点击sendMessageBtn发送消息时,报如下的异常:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

这是因为我们在子线程中创建的Looper和Handler,handleMessage(msg) 方法就会在子线程中回调,因此不能操作UI了。

Handler机制扩展

为了更加方便的使用Handler消息机制,Android也提供了几种扩展方式,内部实现都是基于Handler消息机制.

  1. Activity.runOnUiThread(Runnable)

结语

  • 现在随着短视频,抖音,快手的流行NDK模块开发也显得越发重要,需要这块人才的企业也越来越多,随之学习这块的人也变多了,音视频的开发,往往是比较难的,而这个比较难的技术就是NDK里面的技术。
  • 音视频/高清大图片/人工智能/直播/抖音等等这年与用户最紧密,与我们生活最相关的技术一直都在寻找最终的技术落地平台,以前是windows系统,而现在则是移动系统了,移动系统中又是以Android占比绝大部分为前提,所以AndroidNDK技术已经是我们必备技能了。
  • 要学习好NDK,其中的关于C/C++,jni,Linux基础都是需要学习的,除此之外,音视频的编解码技术,流媒体协议,ffmpeg这些都是音视频开发必备技能,而且
  • OpenCV/OpenGl/这些又是图像处理必备知识,下面这些我都是当年自己搜集的资料和做的一些图,因为当年我就感觉视频这块会是一个大的趋势。所以提前做了一些准备。现在拿出来分享给大家。

[外链图片转存中…(img-5ssVKEkN-1715748185667)]

[外链图片转存中…(img-j798S91O-1715748185668)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值