android-handler-2

1.Handler简介

转:android 11 Handler消息机制-CSDN博客
我们都知道进程间通信一般是用binder、文件、AIDL等,那么线程间通信一般是如何进行的呢?一般通过Handler消息机制来进行线程间的通信,标准的一个场景就是子线程进行耗时操作(网络、下载等),完成后发送消息去通知主线程更新UI。Handler消息机制中主要有四大成员,它们的主要功能如下:

Handler:消息处理器。可以发送消息Handler.sendMessage()和处理消息Handler.handleMessage()。

Message:消息对象。线程间通信的数据单元,Message可携带数据。

MessageQueue:消息队列。实际上是一个单链表结构(插入、删除比较便利,先进先出方式),主要的功能就是向消息池插入消息MessageQueue.enqueueMessage()和取出消息MessageQueue.next()。

Looper:循环器。通过调用Looper.loop()方法不断从消息队列中取出消息,然后按照target将消息分发给对应的Handler去处理。

Handler的消息机制流程:Handler通过sendMessage()方法发送消息(最终会调用MessageQueue中的enqueueMessage()方法)将消息插入到消息队列中去。Looper中的loop()方法开启死循环,会不断的从消息队列中取出消息(通过调用MessageQueue.next()方法取出消息),然后调用目标Handler(即发送该消息的Handler)的 dispatchMessage()方法分发消息,目标Handler收到消息,调用 handleMessage()方法,接收消息,处理消息。

放一张官方的流程图:

2.流程
2.1.Handler
2.1.1.sendMessage()发送消息
以Handler开始,一个典型的Handler发送消息例子:

public class HandlerActivity extends AppCompatActivity {
    private Button button;
    private static final int MESSAGE_TEST = 0;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        button = findViewById(R.id.button1);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Message message = new Message();//在子线程中创建消息对象
                        message.what = MESSAGE_TEST;
                        textHandler.sendMessage(message);//开启线程,向主线程发送消息
                    }
                }).start();
            }
        });
    }
 
    ///Handler对象是在主线程中创建的,所以Handler对象接收到消息之后回调handleMessage()方法对UI做出修改是在主线程进行的
    private final Handler textHandler = new Handler(Looper.myLooper()){//android 11之前可以不添加Looper.myLooper()参数,表示隐式使用当前线程关联的looper,android 11之后无参方法被弃用
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                case MESSAGE_TEST:
                    button.setText("handler-text");//更新UI
                    break;
                default:
                    break;
            }
        }
    };
}
在主线程中创建Handler实例,可以看出上面创建Handler实例我们传入了一个Looper.myLooper()方法参数,这个参数可以获取到当前线程所绑定的Looper,所以我们在实例化Handler的同时也获取到了当前线程相关的Looper。

frameworks/base/core/java/android/os/Handler.java
 
public Handler(@NonNull Looper looper) {
    this(looper, null, false);
}
 
public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
    mLooper = looper;//获取Looper
    mQueue = looper.mQueue;//获取Looper里的MessageQueue
    mCallback = callback;
    mAsynchronous = async;
}
在Handler的构造方法里会获取到对应的Looper以及Looper里的MessageQueue对象。每个Handler都会关联一个MessageQueue消息队列,MessageQueue又是封装在Looper对象中,而每个Looper又会关联一个线程,所以线程、MessageQueue、Looper之间是一一对应的关系(后面Looper分析)。

然后通过Handler.sendMessage()方法发送消息,其实Handler提供了好几种发送消息的方法,Handler.post/sendMessage/sendMessageDelayed,这几个方法最终调用的都是Hanlder.sendMessageDelayed()方法。

frameworks/base/core/java/android/os/Handler.java
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
    if (delayMillis < 0) {//delayMillis是传入的时间,表示多久之后执行这个消息
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);//当前时间+delayMillis就是Message的执行时间
}
执行Message的时间通过delayMillis决定。如果通过sendMessage()来发送消息,那么delayMillis为0,立刻执行Message;如果是通过sendMessageDelayed()方法来发送延迟消息,那么传入的参数就是延迟时间。后面调用sendMessageAtTime()方法:

public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;//获取到当前线程的MessageQueue,这个是在Handler的构造方法中赋值的
    if (queue == null) {//如果MessageQueue为null,表明当前线程异常,无法处理消息。
        RuntimeException e = new RuntimeException(
                this + " sendMessageAtTime() called with no mQueue");
        Log.w("Looper", e.getMessage(), e);
        return false;
    }
    return enqueueMessage(queue, msg, uptimeMillis);//插入消息
}
通过调用enqueueMessage方法去插入消息,并传入MessageQueue对象,上文我们也提到对应的MessageQueue对象是在Handler的构造方法里赋值的。

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
        long uptimeMillis) {
    msg.target = this;//表明当前Handler,后面分发、处理消息的时候会通过这个来找到对应的Handler
    msg.workSourceUid = ThreadLocalWorkSource.getUid();
 
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);//通过MessageQueue去插入消息
}
最终通过MessageQueue里的enqueueMessage()方法来插入消息(后面分析)。

2.1.2.dispatchMessage分发消息
frameworks/base/core/java/android/os/Handler.java
public void dispatchMessage(@NonNull Message msg) {
    if (msg.callback != null) {//如果Message存在回调方法,优先通过message.callback.run来处理
        handleCallback(msg);
    } else {
        if (mCallback != null) {//如果Handler的mCallback不为null时,优先通过mCallback的handleMessage来处理
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);//调用Handler.handleMessage方法来处理message,这个方法默认为空,所以我们一般在子类中重写这个方法去处理消息
    }
}
可以看出,如果message和Handler存在callback回调的话,优先调用这两者的callbcak,所以我们可以通过重写上面两个方法去改变分发及处理流程,但是一般情况下我们通常通过重写handleMessage()方法去处理消息。

2.1.3.handleMessage处理消息
接着来看处理message的流程。

    /**
     * Subclasses must implement this to receive messages.
     */
    public void handleMessage(@NonNull Message msg) {
    }
handleMessage()方法一般为空,子类必须重写这个方法去实现消息的自定义处理。

2.2.MessageQueue
MessageQueue主要用于enqueueMessage()插入/next()取出消息,主要来看下这两个重要的方法。

2.2.1enqueueMessage()插入消息
frameworks/base/core/java/android/os/MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {//绑定处理消息的handler,不能为null
            throw new IllegalArgumentException("Message must have a target.");
        }
 
        synchronized (this) {//可能有多个线程同时往这个队列插入信息,所以需要做同步处理
            if (msg.isInUse()) {//检查当前消息是否正在被处理,后面消息处理的话,这个值会被置位true,避免消息重复处理
                throw new IllegalStateException(msg + " This message is already in use.");
            }
 
            if (mQuitting) {//检查looper循环是否被停止
                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();//消息处理中,置为true
            msg.when = when;//延迟时间,when就是消息可以开始被处理的时间点
            Message p = mMessages;//p表示队头消息
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {//MessageQueue没有消息、当前消息不需要等待(when = 0)、当前消息等待的时间比队列中最前面的消息的等待时间更短,那么让当前消息插入到最队列最前面
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;//如果是阻塞状态则唤醒。
            } else {
                //如果当前消息的when时间小于当前消息队列中某条消息的when,将消息按时间顺序插入到MessageQueue,退出循环
                // 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.
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {//循环遍历消息,找到比当前消息的when小的消息
                    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;
    }
如果满足以下三种条件之一,则直接将当前消息插入到消息队列的队头

(1)当前MessageQueue中还没有消息

(2)该条消息的when为0,表明消息需要立即执行

(3)该消息等待的时间when比队列中最前面的消息的等待时间更短

否则,则会循环遍历消息队列,通过when(消息执行的时间)来将Message插入到MessageQueue中合适的位置。

2.2.2.next()取出消息
Message next() {
        // Return here if the message loop has already quit and been disposed.
        // This can happen if the application tries to restart a looper after quit
        // which is not supported.
        //mPtr是MessageQueue的一个long型成员变量,当队列被dispose的时候其变为0
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }
 
        int pendingIdleHandlerCount = -1; // -1 only during first iteration//idle handler的数量,首次都为-1,闲时handler机制.
        int nextPollTimeoutMillis = 0;//下一个消息到来前,还需要等待的时间
        for (;;) {//
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();//释放那些在处理过程的部分不再需要的引用对象
            }
 
            //阻塞操作,除非有消息被插入或者nextPollTimeoutMillis到时间,否则一直阻塞在这
            nativePollOnce(ptr, nextPollTimeoutMillis);
 
            synchronized (this) {//锁住该MessageQueue对象
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();//获取当前时间
                Message prevMsg = null;
                Message msg = mMessages;//msg表示当前的message
                if (msg != null && msg.target == null) {//当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());//只取异步消息,即msg.isAsynchronous为true
                }
                if (msg != null) {//当前消息非空,且有对应处理handle
                    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 {
                            mMessages = msg.next;//无异步消息直接取出这个消息,并重置MessageQueue队列的头
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();//标记该消息对象被使用
                        return msg;//返回将要执行的message
                    }
                } else {//没有消息,一直休眠,等待被唤醒
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }
 
                // Process the quit message now that all pending messages have been handled.
                if (mQuitting) {//消息正在退出,返回null
                    dispose();
                    return null;
                }
 
                // If first time idle, then get the number of idlers to run.
                // Idle handles only run if the queue is empty or if the first message
                // in the queue (possibly a barrier) is due to be handled in the future.
                //重要的参数 pendingIdleHandlerCount,它表示的就是需要执行的空闲任务数,空闲时间要执行的任务
                //在进入这个方法的时候pendingIdleHandlerCount的值是-1
                //pendingIdleHandlerCount小于0,消息队列为空,或者是消息队列的第一个消息还没有到执行时间。
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();//获取idlehandler的数量
                }
                if (pendingIdleHandlerCount <= 0) {//如果idlehandler数量小于0,结束这次循环,将这个线程挂起等待
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }
 
                if (mPendingIdleHandlers == null) {//如果 pendingIdleHandlerCount不小于零,就把所有需要执行的空闲任务拿出来执行。
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }
 
            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            //处理idle handler,只有第一次循环时,会运行idle handlers,执行完成后,重置pendingIdleHandlerCount为0.
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler
 
                boolean keep = false;
                try {
                    keep = idler.queueIdle();//调用idler的queueIdle方法处理,Activity的Destory()方法就是放在这个handler里处理的
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }
 
                if (!keep) {//停止该空闲Handler的运行
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }
 
            // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;//重置pendingIdleHandlerCount为0,防止再次创建和运行空闲Handler
 
            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
        }
    }
next()方法开启一个死循环,首先调用nativePollOnce()方法,来判断是否需要进行阻塞操作,当没有消息需要处理、或者还没到消息的处理时间时,则进行休眠操作。后面有消息的时候会进行唤醒操作(enqueueMessage方法中)。然后分别对异步、同步消息进行处理,返回当前的message。最后当在MessageQueue队列里没有需要立即执行的任务时,如果有IdleHandler则会去执行IdelHandler的任务,通过idler.queueIdle()处理任务。IdelHandler里的任务一般是不怎么重要的任务,Activity的onDestory()方法就是在这里完成的。

2.3.Looper
处理消息的永动机,开启loop()循环后不断从消息队列中取出消息,并将消息分发给对应的目标处理者。

2.3.1.prepare()方法
上文提到在主线程使用Looper,那么是否只能在主线程中使用?其实在子线程中我们也可以使用Looper,一个在子线程中使用Looper的例子:

如果我们按照上文中主线程的方式,新建一个子线程,然后在子线程中使用Looper,代码如下:

public class TextThread extends Thread{
    public Handler textHandler;
    public void run(){
        textHandler = new Handler(Looper.myLooper()){
            @Override
            public void handleMessage(@NonNull Message msg) {
                super.handleMessage(msg);
            }
        };
    }
}
出现了运行时异常

AndroidRuntime: FATAL EXCEPTION: Thread-2
AndroidRuntime: Process: xxx, PID: xxx
AndroidRuntime: java.lang.NullPointerException: Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue' on a null object reference
AndroidRuntime:     at android.os.Handler.<init>(Handler.java:257)
AndroidRuntime:     at android.os.Handler.<init>(Handler.java:162)
AndroidRuntime:     at com.example.myapplication.TextThread$1.<init>(TextThread.java:12)
AndroidRuntime:     at com.example.myapplication.TextThread.run(TextThread.java:12)
AndroidRuntime:     at com.example.myapplication.MainActivity$1$1.run(MainActivity.java:75)
为什么我们可以直接在主线程中使用Looper,但是在子线程中使用就会出现异常呢?是因为主线程默认创建了Looper。

frameworks/base/core/java/android/app/ActivityThread.java
public static void main(String[] args) {
    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
 
    ......
 
    Looper.prepareMainLooper();//prepare方法用来创建该线程的Looper对象,因为当前是主线程,所以这里创建的是主线程的Looper对象
 
    ......
    Looper.loop();//启用无限循环
 
}
frameworks/base/core/java/android/os/Looper.java
public static void prepareMainLooper() {
    prepare(false);//调用prepare方法创建Looper对象,主线程中quitAllowed为false,表示不允许退出。
    synchronized (Looper.class) {
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        sMainLooper = myLooper();
    }
}
 
 
//在子线程中使用Looper都要先调用prepare()方法,它还有个同名的public方法,供外部直接调用
private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {//ThreadLocal确保线中Looper的唯一性,超过一个,则会抛出异常
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));//创建Looper
}
 
 
//Looper的私有构造方法
private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);//获取Looper里的MessageQueuue
    mThread = Thread.currentThread();//当前线程
}
上文我们提到过在实例化Handler的时候,传入了Looper.myLooper()参数。

frameworks/base/core/java/android/os/Looper.java
public static @Nullable Looper myLooper() {
    return sThreadLocal.get();//获取当前线程所绑定的Looper
}
可以看出在Looper.prepare()方法中通过sThreadLocal来创建Looper,然后实例化Handler的时候通过Looper.myLooper()方法来获取到当前线程绑定的Looper。

2.3.2.loop()方法
回到ActivityThread.main方法里,主线程调用Looper.prepareMainLooper()去创建主线程专属Looer之后又调用了Looper.loop()方法。这个方法是消息处理的核心方法,主要是开启无限循环去处理消息。

frameworks/base/core/java/android/os/Looper.java
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.");//可以看出使用Looper必须先调用Looper.prepare()方法
        }
        if (me.mInLoop) {//该线程是否已经调用过loop方法
            Slog.w(TAG, "Loop again would have the queued messages be executed"
                    + " before this one completed.");
        }
 
        me.mInLoop = true;
        final MessageQueue queue = me.mQueue;//获取Looper里对应的MessageQueue
 
        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();//清空远程调用端的uid和pid,确保该线程是基于本地进程的
        final long ident = Binder.clearCallingIdentity();
 
        // Allow overriding a threshold with a system prop. e.g.
        // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
        final int thresholdOverride =
                SystemProperties.getInt("log.looper."
                        + Process.myUid() + "."
                        + Thread.currentThread().getName()
                        + ".slow", 0);//如果消息调度和传递的时间较长,可以显示一些log,可手动setprop设置
 
        boolean slowDeliveryDetected = false;//用于打印log
 
        for (;;) {//开启循环,不断从消息队列里拿出消息进行处理
            Message msg = queue.next(); // might block//调用MessageQueue.netx()获取队列头的消息,前面有分析,这个过程可能会阻塞。
            if (msg == null) {//如果获取到的消息为null,直接返回,退出循环。
                // No message indicates that the message queue is quitting.
                return;
            }
 
            // This must be in a local variable, in case a UI event sets the logger
            final Printer logging = me.mLogging;//log打印
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }
            ......
            try {
                msg.target.dispatchMessage(msg);//msg.target其实是Handler,调用了Handler的dispatchMessage方法分发消息。
                if (observer != null) {
                    observer.messageDispatched(token, msg);
                }
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } ......
            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }
 
            ......
 
            msg.recycleUnchecked();//回收消息
        }
    }
loop()方法主要就是开启了死循环,不断通过调用MessageQueue.next()方法从队列中取出消息,然后进行分发。

这样在主线程中就默认创建了Looper,所以我们可以直接在主线程中直接使用Looper。但是子线程中没有默认创建,所以我们在子线程中使用Looper时需要先调用Looper.prepare()方法启用Looper,还需要调用Looper.loop()方法开启循环去处理消息。

public class TextThread extends Thread{
    public Handler textHandler;
    public void run(){
        Looper.prepare();//创建Looper对象
        textHandler = new Handler(Looper.myLooper()){
            @Override
            public void handleMessage(@NonNull Message msg) {
                super.handleMessage(msg);
            }
        };
        Looper.loop();//开启循环
    }
}
3.其他知识点
3.1.Handler、MessageQueue、looper、线程之间的关系

在调用Looper.prepare()方法创建Looper的时候,同一个线程只能创建一个Looper(ThreadLocal确保每个线程只有一个Looper),每个Looper又只有一个MessageQueue,所以MessageQueue、looper、线程之间是一一对应的关系,它们与Handler之间是一对多的关系,因为可以在一个进程里new多个Handler。

3.2.Handler、Looper必须在主线程中使用吗?

不是,子线程中也可以使用,不过子线程中使用Looper需要先调用Looper.prepare()和Looper.loop()方法

3.3Looper为什么不会发生阻塞

我们提到Looper.loop()方法是不断循环操作的,那么会不会发生卡死现象?

不会,looper会根据情况进行阻塞、唤醒操作。

(1)在MessageQueue.next()方法中会首先做阻塞操作nativePollOnce(ptr, nextPollTimeoutMillis);

如果消息队列为null、或者还没到消息处理时间那么进行阻塞,到时间或者插入了消息,那么会被唤醒。

(2)在MessageQueue.enqueueMessage()方法中通过nativeWake()方法唤醒

所以在没有消息产生的时候,looper会被阻塞,线程会进入休眠状态,一旦Looper添加消息,线程就会被唤醒,从而对事件进行响应,所以不会导致卡死。

3.4Handler内存泄露的原因

因为Message会关联发送消息的Handler对象target,如果存在一个延迟消息,那么因为Message还没有没完全处理,即使Activity退出了,message依旧持有handler的引用,handler持有activity的引用,就会造成内存泄露。
————————————————

                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
                        
原文链接:https://blog.csdn.net/li_5033/article/details/131761630

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值