Android Handler机制的补充

1、handler启动

APP启动时,会启动ActivityThread类中的main方法。main方法主要作用是做消息循环,一旦消息循环停止,APP程序就会退出。Android是事件驱动的,在Loop.loop()中不断接收、处理事件,而Activity的生命周期都由主线程的Loop.loop()来调度,所以 主线程Looper的存活周期和App的生命周期基本是一致的。当目前没有事件需要处理的时候,主线程就会阻塞;当 子线程向消息队列发送消息,主线程就被唤醒。 ActivityThread是一个 final 类。

public static void main(String[] args) {
    ...
    Looper.prepareMainLooper();
    ...
    //之前SystemServer调用attach传入的是true,这里到应用进程传入false就行
    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);
    ...
    //一直循环,保障进程一直执行,如果退出,说明程序关闭
    Looper.loop();
    throw new RuntimeException("Main thread loop unexpectedly exited");
}

从上面代码可以知道,主线程的looper和其他的线程的 looper是一样的,只不过已经完成了Looper.prepare()和Looper.loop(),这就是为什么主线程中不需要调用这两个方法的原因。如果没有消息,那么loop里面就会被 messageQueue 阻塞。

因为我们写的代码就是通过 handler驱动起来的,我们activity的onCreate、onResume、onStop等等这些生命周期方法,包括我们的UI绘制的 信号,这些UI绘制的事件都是通过Handler Looper循环内部发起的。这些所有的事务都被封装成为了一个一个的 message,然后通过looper来调用handleMessage回调我们的各Activity和 各Fragment,然后执行这些组件里面的各个生命周期方法,所以我们的代码就是在循环里面执行的,也就是主线程一切皆Message。

所以如果某个消息执行时间过长,就可能会影响 UI 线程的 刷新效率,造成卡顿的现象。

2、Looper.loop无限循环会阻塞主线程么? 

答案是肯定的,但是这并不是问题。

Looper无限循环是Looper不停取MessageQueen中的Message并执行这个message的一种机制。我们的APP中的事件,如Activity的生命周期切换、点击、长按、滑动、都是依赖这种机制。

如果主线程的MessageQueue中没有消息,便会阻塞在Loop的queue.next()中的nativePollOnce方法。这个方法是个native方法,调用这个方法其实就是通过调用我们在Linux里面的一个管道机制epoll。这时候调用完epoll_wait之后,主线程会进入休眠状态并释放CPU资源,如果下一个消息到达或者有事物发生,通过向pipe管道写入数据来进行唤醒主线程工作。

Message next() {
     ...
 
     int pendingIdleHandlerCount = -1; // -1 only during first iteration
 
     int nextPollTimeoutMillis = 0;
     for (;;) {
         if (nextPollTimeoutMillis != 0) {
             Binder.flushPendingCommands();
         }
		 // 阻塞nextPollTimeoutMillis
         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;
            //msg.target == null 消息屏障
             if (msg != null && msg.target == null) {
                 // 循环往下查找异步消息
                 do {
                     prevMsg = msg;
                     msg = msg.next;
                 } while (msg != null && !msg.isAsynchronous());
             }
             // 到这里说明是普通的消息
             if (msg != null) {
             	 // 还没有到执行的时候,减一下得到剩余的时间
                 if (now < msg.when) {
                     nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                 } else {
                     // 取出Message
                     mBlocked = false;
                     if (prevMsg != null) {
                         prevMsg.next = msg.next;
                     } else {
                         mMessages = msg.next;
                     }
                     msg.next = null;
                     if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                     msg.markInUse();
                     return msg;//返回
                 }
             } else {
                 // No more messages.
                 nextPollTimeoutMillis = -1;
             }
   			 ...
			
             // 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.
             if (pendingIdleHandlerCount < 0
                     && (mMessages == null || now < mMessages.when)) {
                 pendingIdleHandlerCount = mIdleHandlers.size();
             }
             // 没有要执行的IdleHandler,那么就continue,就会到下一轮循环然后阻塞
             if (pendingIdleHandlerCount <= 0) {
                 // No idle handlers to run.  Loop and wait some more.
                 mBlocked = true;
                 continue;
             }
 
             if (mPendingIdleHandlers == null) {
                 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.
         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();
             } catch (Throwable t) {
                 Log.wtf(TAG, "IdleHandler threw exception", t);
             }
 
             if (!keep) {
                 synchronized (this) {
                     mIdleHandlers.remove(idler);
                 }
             }
         }
 
         // Reset the idle handler count to 0 so we do not run them again.
         pendingIdleHandlerCount = 0;
 
         // 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;
     }
 }

3、阻塞线程为什么不会ANR?

ANR(Application Not Responding)是应用无响应。Android系统对于一些事件需要在一定的时间范围内完成,如果超过预定时间能未能得到有效响应或者响应时间过长,都会造成ANR。

发生ANR的主要四种情况:

1)Service Timeout:前台服务在20s内未执行完成;

2)BroadcastQueue Timeout:前台广播在10s内未执行完成;

3)ContentProvider Timeout:内容提供者在publish过超时10s;

4)InputDispatching Timeout:输入 事件分发超时5s,包括按键和触摸事件。

Looper循环的阻塞是在消息队列无消息需要处理时的一种机制,这种机制就是让CPU停下来去做别的事,避免cpu空转,这个机制和ANR是没有关系的,完全不是同一个事,所以自然不会导致ANR

4、ThreadLocal

认识ThreadLocal

ThreadLocal 是一个关于创建线程局部变量的类。

其实就是这个变量的作用域是线程,其他线程访问不了。

通常我们创建的变量是可以被任何一个线程访问的,而使用 ThreadLocal 创建的变量只能被当前线程访问,其他线程无法访问。顾名思义,本地线程。

那么ThreadLocal是如何确保只有当前线程可以访问呢?我们先来分析一下ThreadLocal里面最重要的两个函数,get()和set()两个函数。

public T get() {
    Thread t = Thread.currentThread(); //code 1
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}


public void set(T value) {
    Thread t = Thread.currentThread(); //code 2
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}


ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}


/**
 * Variant of set() to establish initialValue. Used instead
 * of set() in case user has overridden the set() method.
 *
 * @return the initial value
*/
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

从code1 和code2 可知,在给ThreadLocal去set值或者get值的时候都会先获取当前线程,然后基于线程去调用getMap(thread),getMap返回的就是线程thread的成员变量threadLocals。所以通过get 和set执行的函数,这样就保证了threadLocal的访问,一定是只能访问或许修改当前线程的值,这就保障了这个变量是线程的局部变量。

ThreadLocal在Looper中的使用

Looper与线程的关系是一对一的关系,一个线程只有一个Looper,一个Looper,只有一个MessageQueue,不同线程之间Looper对象是隔离的,那么Looper是怎么保障这一点的呢?

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {//code 1
        throw new RuntimeException("Only one Looper may be create per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));// code 2
}

通过上面的代码发现Looper初始化是调用prepare函数进行,在调用prepare函数的时候代码会执行 到code 1,在code1会先去判断当前线程对于的ThreadLocal中是否存在looper的value,如果存在,那么就抛出异常,这样就保证了一个线程只会设置一次Looper。这个代码的执行流程,就是确保了一个线程只有一个 ThreadLocal,一个ThreadLocal就只有一个looper。

那么Handler和Looper是怎么关联起来的呢?

我们可以看一下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;
}

public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

在Handler中有两个全局变量mLooper(当前Handler关联Looper)和mQueue(消息队列),并在构造函数中进行了初始化,重要的就是调用了:Looper.myLooper(),在myLooper()中handler通过调用线程局部变量sThreadLocal,获取当前线程的Looper,这里需要注意的是,如果当前线程没有关联的Looper,这个方法会返回null。

注意:Handler在哪个线程创建的,就跟哪个线程的Looper 关联,也可以在Handler的构造方法中传入指定的Looper。(详看第6点)

5、handler.post和postDelayed

public final boolean post(@NonNull Runnable r) {
    return sendMessageDelayed(getPostMessage(r), 0);
}

public final boolean postDelayed(@NonNull Runnable r, long delayMillis) {
    return sendMessageDelayed(getPostMessage(r), delayMillis);
}

private static Message getPostMessage(Runnable r) {
    Message m = Message.obtain();
    m.callback = r;
    return m;
}


public final boolean sendMessage(@NonNull Message msg) {
    return sendMessageDelayed(msg, 0);
}

从上面代码我们可以看出, post(Runnable r) 方法使用到的sendMessageDelayed函数,其实也就是调用了 sendMessageDelayed(Message msg) 。只是它使用到了我们的 getPostMessage 函数,将我们的Runnable转化为了我们的 Message,由此可知handler.post(Runnable r) 分发阶段实质上是和普通的Message是一样的。而postDelayed只是比post多一个延时时间而已。

6、callback返回true,handlerMessage是否执行?

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        // 1. 设置了Message.Callback(Runnable)
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            // 2. 设置了 Handler.Callback(Callback )
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        // 3. 未设置 Handler.Callback 或 返回 false
        handleMessage(msg);
    }
}


public interface Callback {
    public boolean handleMessage(Message msg);
}

通过以上代码可知,callback接口中会执行handlerMessage,此返回值如果返回true,那么在2处会直接return,因此handlerMessage不会执行。

7、HandlerThread

首先了解一下new Handler()和new Handler(Looper.myLooper())有什么区别,如果new Handler初始化()不传参数,默认绑定当前线程的Looper对象,如果new Handler(Looper.myLooper())初始化传参数,则绑定指定Looper对象。

比如在子线程中初始化主线程的hanlder,则可以new Handler(Looper.getMainLooper())用来指定主线程的Looper对象。在主线程如果需要绑定子线程的Looper对象,我们通常不会使用Looper.myLooper(),因为主线程执行初始化handler时,并不能保证子线程Looper对象已经创建,因此此时需要借助handlerThread来完成Looper对象的绑定。看下面的代码:

//code 1 创建子线程
class MyThread extends Thread{
    private Looper looper;
    @Override
    public void run() {
        Looper.prepare();//创建该子线程的Looper
        looper = Looper.myLooper();//取出该子线程的Looper
        Looper.loop();//只要调用了该方法才能不断循环取出消息
    }
}



private MyThread thread;
private Handler mHandler;//将mHandler指定轮询的Looper
 
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);     
    setContentView(tv);
    thread = new MyThread();
    thread.start();//千万别忘记开启这个线程

    //code 2 下面是主线程发送消息
    mHandler = new Handler(thread.looper){
         @Override
         public void handleMessage(android.os.Message msg) {
             Log.d("当前子线程是----->", Thread.currentThread()+"");
         };
    };
     mHandler.sendEmptyMessage(1);
}

此时,就很可能发生空指针异常,因此需要借助HandlerThread类来完成。将上面的code1的子线程删除,code2处的代码修改一下,就不会报错了。

//实例化一个特殊的线程HandlerThread,必须给其指定一个名字
HandlerThread thread = new HandlerThread("handler thread");
thread.start();//千万不要忘记开启这个线程
//将mHandler与thread相关联
mHandler = new Handler(thread.getLooper()){
    public void handleMessage(android.os.Message msg) {
        Log.d("当前子线程是----->", Thread.currentThread()+"");
    };
};
mHandler.sendEmptyMessage(1);//发送消息

这是为什么呢?我们看一下handlerThread的源码:

@Override
public void run() {
    mTid = Process.myTid();
    Looper.prepare();
    synchronized (this) {
        mLooper = Looper.myLooper();
        notifyAll();
    }
    Process.setThreadPriority(mPriority);
    onLooperPrepared();
    Looper.loop();
    mTid = -1;
}

通过上面代码可以看到,在执行Looper.myLooper()时,handlerThread的run方法加入了锁机制,这样就保证了Handler在与HandlerThread进行绑定时,发现Looper为空,Handler会一直等待Looper创建成功,然后才会执行后续的代码。并且,run方法自动帮我们执行了Looper.prepare()和Looper.loop();

8、子线程发消息到主线程有几种方式?

我们通常使用异步线程更新UI,比如AsyncTask(),runOnUiThread(),view.post()等等。但是这些方法底层都是调用handler完成的。

runOnUiThread():

new Thread(){
    public void run(){
        runOnUiThread(new Runnable(){
            @Override
            public void run(){
                //更新UI
            }
        });
    }
}

/*先判断当前线程是否是主线程,如果是,则直接运行。
  如果不是,调用handler.post();
 */
public final void runOnUiThread(Runnable action) {
    if (Thread.currentThread() != mUiThread) {
        mHandler.post(action);
    } else {
    action.run();
}

View.post():

public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }

    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    getRunQueue().post(action);
    return true;
}

/*上面的方法调用的是HandlerActionQueue的post方法,而在Handler内部调用post的时候,先执行的是 
  sendMessageDelayed方法,然后执行sendMessageAtTime方法,紧接着执行enqueueMessage,最终执行 
  的是queue.enqueueMessage,最终执行行的方式都是一样的。
 */
public class HandlerActionQueue {
    private HandlerAction[] mActions;
    private int mCount;
    
    public void post(Runnable action) {
        postDelayed(action, 0);
    }

    public void postDelayed(Runnable action, long delayMillis) {
        final HandlerAction handlerAction = new HandlerAction(action, delayMillis);
        synchronized (this) {
            if (mActions == null) {
                mActions = new HandlerAction[4];
            }
            mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
            mCount++;

        }
    }
}

8、IdleHandler

IdleHandler 是 MessageQueue内定义的一个接口,一般可用于做性能优化。

当消息队列内没有需要立即执行的 message时,会主动触发 IdleHandler 的 queueIdle方法。返回值为 false,在执行完会remove,即只会执行一次;返回值为 true,即每次当消息队列内没有需要立即执行的消息时,都会触发该方法。

因此,IdleHandler 是闲时Handler 机制,可以在 Looper 事件循环的过程中,当出现空闲的时候,允许我们执行任务。 IdleHandler 被定义在 MessageQueue 中,它是一个接口。

// MessageQueue.java
public static interface IdleHandler {
    boolean queueIdle();
}

使用IdleHanlder也很简单:

// MessageQueue.java
public void addIdleHandler(@NonNull IdleHandler handler) {
    // ...
    synchronized (this) {
        mIdleHandlers.add(handler);
    }
}

public void removeIdleHandler(@NonNull IdleHandler handler) {
    synchronized (this) {
        mIdleHandlers.remove(handler);
    }
}

可以看到 add 、 remove 其实操作的都是 mIdleHandlers,它的类型是一个 ArrayList。

既然 IdleHandler 主要是在 MessageQueue 出现空闲的时候被执行,那么什么时候出现空闲? MessageQueue 是一个基于消息触发时间的优先级队列,队列出现空闲存在两种情况:

1. MessageQueue 为空,没有消息;

2. MessageQueue 中最近需要处理的消息,是一个延迟消息(when>currentTime),需要滞后执行;

这两个场景,都会尝试调用 IdleHandler。 处理 IdleHandler 的情况,就在 Message.next() 这个获取消息队列下一 个待执行消息的方法中:

Message next() {
    // ...
    // step 1
    int pendingIdleHandlerCount = -1;
    int nextPollTimeoutMillis = 0;
    for (;;) {
        nativePollOnce(ptr, nextPollTimeoutMillis);
        synchronized (this) {
            // ...
            if (msg != null) {
                if (now < msg.when) {
                    // 得出休眠的时间
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now,
                        Integer.MAX_VALUE);
                } else {
                    // Other code
                    // 寻找消息处理后返回
                    return msg;
                }
            } else {
                // 没有更多的消息
                nextPollTimeoutMillis = -1;
            }
            // step 2
            if (pendingIdleHandlerCount < 0
                && (mMessages == null || now < mMessages.when)) {
                pendingIdleHandlerCount = mIdleHandlers.size();
            }
            // step 3
            if (pendingIdleHandlerCount <= 0) {
                mBlocked = true;
                continue;
            }
            if (mPendingIdleHandlers == null) {
                mPendingIdleHandlers = new                     
                       IdleHandler[Math.max(pendingIdleHandlerCount,4)];
            }
            mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);

        }
        for (int i = 0; i < pendingIdleHandlerCount; i++) {
            final IdleHandler idler = mPendingIdleHandlers[i];
            mPendingIdleHandlers[i] = null;
            boolean keep = false;
            try {
                keep = idler.queueIdle();
            } catch (Throwable t) {
                Log.wtf(TAG, "IdleHandler threw exception", t);
            }
            if (!keep) {
                synchronized (this) {
                    mIdleHandlers.remove(idler);
                }
            }
        }
        // step 4
        pendingIdleHandlerCount = 0;
        nextPollTimeoutMillis = 0;
    }
}

我们先了解一下 next() 中关于 IdleHandler 执行的主要逻辑:

1. 准备调用 IdleHandler 时,说明当前待调用的消息为 null,或者这条消息的执行时间没有到;

2. 当 pendingIdleHandlerCount < 0 时,根据 mIdleHandlers.size() 赋值给 pendingIdleHandlerCount;

3. 将 mIdleHandlers 中的 IdleHandler 复制到 mPendingIdleHandlers 数组中,这个数组是临时的,之后进入 for 循环;

4. 循环中从mPendingIdleHandlers数组中取出 IdleHandler,并执行其 queueIdle() 记录返回值存到 keep 中;

5. 当 keep 为 false 时,从 mIdleHandler 中移除当前循环的 IdleHandler,反之则保留;

 需要特别注意的是,对 mIdleHandler 这个 List 的所有操作,都是通过 synchronized 来保证线程安全的。

那么,在keep为true时,IdleHandler 机制是如何保证不会进入死循环的?

我们梳理一下:

Step 1,循环执行前,pendingIdleHandlerCount 的初始值为 -1;nextPollTimeoutMillis=0代表不会进入休眠状态,natievPollOnce() 进入休眠所以不会死循环是不正确的;

Step 2,在 pendingIdleHandlerCount<0 时,才会通过 mIdleHandlers.size() 赋值。也就是说只有第一次循环 才会改变 pendingIdleHandlerCount 的值;

Step 3,如果 pendingIdleHandlerCount<=0 时,则循环 continue;

Step 4,重置 pendingIdleHandlerCount 为 0;

当第一次循环时,pendingIdleHandlerCount<0,会给pendingIdleHandlerCount赋值,然后执行到Step4,此时 Step3不会执行,当第二次循环时,pendingIdleHandlerCount 等于 0,在 Step 2 不会改变它的值,那么在 Step 3 中会直接 continus 继续会下一次循环,此时没有机会修改 nextPollTimeoutMillis。 那么 nextPollTimeoutMillis 有 两种可能:-1 或者下次唤醒的等待间隔时间,在执行到 nativePollOnce() 时则会进入休眠,等待下一次被唤醒。 下 次唤醒的时候,mMessage 必然会有一个等待执行的 Message,则 MessageQueue.next() 返回到 Looper.loop() 的 循环中,分发处理这个 Message,之后又是一轮新的 next() 中去循环。

dleHandler能解决什么问题呢?我们可以将一些相对 耗时或者一些影响整个线程运行的事务放到IdleHandler里面处理,譬如当我们需要收到调用GC的时候,GC一般会带来STW问题,于是我们可以将这个动作在IdleHandler里面执行,而android源码确实也是这样进行的。

9、handler内存泄漏

handler持有外部activity的引用,导致了activity走了destory的话,也无法释放资源,所以会造成内存泄漏。

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
        long uptimeMillis) {
    //code 1
    msg.target = this;// 1.指定目标Handler为当前handler
    
    msg.workSourceUid = ThreadLocalWorkSource.getUid();
 
	// 如果是个异步Handler,那么把消息设为异步消息
    if (mAsynchronous) { 
        msg.setAsynchronous(true);
    }
    // 2. 把msg插入到消息队列中
    return queue.enqueueMessage(msg, uptimeMillis);
}

通过looper.loop()可以看到msg.target.dispatchMessage(msg),而code1又反应了target就是handler,所以message持有handler,那么说明在messagequeue消息队列里,持有了message,而message持有了handler的引用,而handler又持有activity的引用。looper持有了messageQueue。threadLocal持有了looper。

threadLocal-->looper-->messageQueue-->message-->handler-->activity;

threadLocal是static修饰的,那就是说,threadLocal是GC Root,根据可达性分析可知,这些被持有的都不可回收(threadLocal-->looper-->messageQueue不可回收,message-->handler-->activity不可回收,messageQueue-->message可能不会回收,因为messageQueue有delay方法,在activity执行后,delay也许还未执行,所以message是不会被回收的),activity自然就不能被回收了。

为什么其他的内部类没有说过这个问题?

viewholder(内部类):没有另外的类持有这个内部类,所以不会有这个问题。

如何解决:

软引用+static

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值