由内部类引发的Android内存泄漏的一些思考

在Java内部类详解的一文中,我们对Java内部类进行了分析,其中有一点引人深思:普通内部类和匿名内部类都会持有外部类的引用,而如果外部类也持有内部类(匿名内部类)的引用,这不就造成相互引用了吗? 面对这种情况GC如何处理,如何释放这两个对象资源? 要弄清GC如何回收相互引用的对象,那就必须了解JVM如何对存活的对象进行判定的。

存活对象的判定

当一个对象不会再被使用时,我们会说这对象已经死亡。对象何时死亡,写程序的人应当是最清楚的。如果计算机也要弄清这件事情,就需要使用一些方法来进行对象存活的判定,常见的方法有引用计数(Reference Counting)可达性分析(Reachability Analysis)两种。

引用计数

引用计数算法的大致思想是给堆中每个对象(不是引用)添加一个引用计数器。每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1。任何时刻计数器为0的对象就是不可能再被使用的,就可以被当作垃圾收集。就如图下所示:
60825012.png-43.4kB

它优点在于实现简单,判定效率也很高,对程序不被长时间打断的实时环境比较有利。在大部分情况下它都是一个不错的算法,也有一些比较著名的应用案例,例如微软COM(Component Object Model)技术、使用ActionScript 3的FlashPlayer、Python语言和在游戏脚本领域得到许多应用的Squirrel中都使用了引用计数算法进行内存管理。

但是,Java语言里面并没有选用引用计数算法来管理内存,其中最主要原因是它没有一个优雅的方案去对象之间相互循环引用的问题:当两个对象互相引用,即使它们都无法被外界使用时,它们的引用计数器也不会为0。这也就是我开头所提到的问题:外部类和匿名内部类之间相互引用,显然JVM肯定不是使用这种算法对存活对象进行判定的。

可达性分析

许多主流程序语言中(如Java、C#、Lisp),都是使用可达性分析来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为GC根节点(GC Roots)的对象作为起始点,从这些节点开始进行向下搜索,搜索所走过的路径成为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如图所示,对象object 5、object 6、object 7虽然互相有关联,它们的引用并不为0,但是它们到GC Roots是不可达的,因此它们将会被判定为是可回收的对象。
60896346.png-54.8kB

注意,内存中可以作为GC Roots的对象并不是只有一个,只要满足条件的对象都可以作为GC Roots:

  1. Class:由System Class Loader/Boot Class Loader加载的类对象,这些类永远不会被卸载,且这些类创建的对象都是 static 的。但需要注意的是,自定义类加载器(Custom Class Loader)加载的类对象不一定属于GC roots,除非是java.lang.Class的相应实例有可能会称为其他类的 GC Roots。( unless corresponding instances of java.lang.Class happen to be roots of other kind(s).)

  2. 静态属性和常量:方法区(Method Area,即 Non-Heap)中的类静态属性引用的对象和常量引用的对象。

  3. Thread:正在运行的线程。

  4. Stack Local: Java虚拟机栈(栈帧中的本地变量表)中的引用对象。换句话说,当前所有正在被调用的方法的引用类型的参数/局部变量/临时值。每个线程都有一个属于自己的Java虚拟机栈,每当执行一个Java方法的时候就会创建一个栈帧,并放入到Java虚拟机栈中,栈顶的栈帧就是当前线程正在执行的方法。调用方法的过程就是压栈和出栈的过程。

  5. JNI Local:本地方法栈的局部变量表中引用的对象。这里和上面类似只是Java虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为使用到的本地操作系统(Native)方法服务,这两个栈都属于线程私有内存区。

  6. JNI Global: JNI中的全局变量引用的对象。

  7. Monitor Used:用于保证同步的对象,例如wait(),notify()中使用的对象、锁等。

实际上GC判断对象是否可达看的是强引用。更准确的描述是,一个对象存在强引用,必定是从其它强引用对象的本地变量,静态变量或者其它类似的地方直接引用过来的。换句话说,如果一堆对象通过某个不存活的对象“强引用”过来的话,它们会被一起回收掉。

通过了解GC Roots后,我们就很清晰的知道哪些被引用的对象是不会被GC回收的,这些被标记为GC Roots的对象很大程度上的造成了内存泄漏 的原因,尤其是被static引用的对象。

内部类回收的问题

既然JVM使用可达性分析算法对存活的对象进行判定,那么内部类的回就很好弄清楚啦。

1.如果一个匿名内部类没有被任何引用持有,那么匿名内部类对象用完以后即成垃圾:

class NoName {
    public void finalize() {
        System.out.println("Free the occupied memory...");
    }
}

public class TestJc {
    public void Test() throws InterruptedException {
        System.out.println("Start of program.");

        new NoName() {};
        new NoName() {};
        NoName n=new NoName() {};

        System.gc();

        System.out.println("End of program.");

        Thread.sleep(5000);

        System.out.println("End of Thread.");
    }

    public static void main(String args[]) throws InterruptedException {
       new TestJc().Test();
    }
}

程序的运行结果是:

Start of program.
End of program.
Free the occupied memory...
Free the occupied memory...
End of Thread.

System.gc()有一个特点,就是在对象被当成垃圾从内存中释放前要调用finalize()方法,而且释放一个对象调用一次finalize()方法。从程序的运行结果可以看到:垃圾回收器启动以后,并不一定马上开始回收垃圾,很可能要等待一段时间才执行。这是因为在程序运行过程中,垃圾收集线程的优先级比较低,如果有比这个线程优先级高的线程,先运行这些优先级高的线程,等这些线程执行完毕,才进行垃圾回收。所以System.gc()方法只是一种“建议”,它建议Java虚拟机执行垃圾回收,释放内存空间,但什么时候能够回收就不能够预知了。

如果我们把“System.gc();”语句,放在第一个匿名对象语句后面,再进行编译和执行,会发现结果是这样的:

Start of program.
Free the occupied memory...
End of program.
End of Thread.

这是因为,启动完垃圾回收器以后,它只能检测到在垃圾回收器强制启动之前程序运行所产生的垃圾,Java的虚拟机尽最大的努力从被丢弃的对象上回收垃圾;对于在启动垃圾回收器以后产生的垃圾,这个线程检测到的概率就非常小了,如果检测不到,就不能回收这些垃圾。因此,Java中的垃圾回收器机制及System.gc()方法,并不能够完全避免内存泄露的问题,只是尽可能降低内存泄露的可能性和程度。

2.如果内部类仅仅只是在外部类中被引用,当外部类的不再被引用时,外部类和内部类就可以都被GC回收:

class NoName {
    public void finalize() {
        System.out.println("NoName Free the occupied memory...");
    }

    Object object = new Object() {
        public void finalize() {
            System.out.println("Object Free the occupied memory...");
        }
    };
}

public class TestJc {

    public void Test() throws InterruptedException {
        System.out.println("Start of program.");

        NoName noName = new NoName();

        System.out.println("Execute GC");
        System.gc();

        Thread.sleep(3000);

        noName=null;

        System.out.println("Execute GC");
        System.gc();

        Thread.sleep(3000);
        System.out.println("End of program.");
    }

    public static void main(String args[]) throws InterruptedException {
        new TestJc().Test();
    }
}

程序运行结果:

Start of program.
Execute GC   
Execute GC
NoName Free the occupied memory...
Object Free the occupied memory...
End of program.

当外部类对象被引用持有的时候,执行 System.gc()后并没有回收外部类和匿名内部类,而当外部类对象不再被引用的时候(引用置为null),执行GC,则外部类对象和匿名内部类对象都被回收。

3.如果当内部类的引用被外部类以外的其他类引用时,就会造成内部类和外部类无法被GC回收的情况,即使外部类没有被引用,因为内部类持有指向外部类的引用)。

  public void Test() throws InterruptedException {
        System.out.println("Start of program.");

        NoName noName = new NoName();
        Object object=noName.object;
        noName=null;

        System.out.println("Execute GC");
        System.gc();

        Thread.sleep(3000);
        System.out.println("End of program.");
    }

程序输出如下:

Start of program.
Execute GC
End of program.

即使外部类没有被任何变量引用,只要其内部类被外部类以外的变量持有,外部类就不会被GC回收。我们要尤其注意内部类被外面其他类引用的情况,这点导致外部类无法被释放,极容易导致内存泄漏。一个很明显的例子就是Androd中使用Handler的情况。

Android内存泄漏

Android中使用的是Dalvik虚拟机和ART虚拟机。它们和JVM主要区别在于:Dalvik是基于寄存器的,而JVM是基于栈的。Dalvik运行dex文件,而JVM运行java字节码。Android内存的回收策略和JVM是一样的,我们上面所分析的问题同样适用于Android系统。下面这张图就展示了 Android 内存的回收管理策略(图来自Google 2011的IO大会):
61549564.png-69.5kB

图中的每个圆节点代表对象的内存资源,箭头代表可达路径。当圆节点与 GC Roots 存在可达路径时,表示当前资源正被引用,虚拟机是无法对其进行回收的(如图中的黄色节点)。反过来,如果圆节点与 GC Roots 不存在可达路径,则意味着这块对象的内存资源不再被程序引用,系统虚拟机可以在 GC 过程中将其回收掉。可以看出,Android中对存活对象的判定标准与JVM是一样的,都是采用可达性分析的算法。

有了上面的内存回收的栗子,那么接下来就可以说说什么是内存泄漏了。从定义上讲,Android(Java)平台的内存泄漏是指没有用的对象资源任与GC-Root保持可达路径,导致系统无法进行回收。举一个最简单的栗子,我们在 Activity 的 onCreate 函数中注册一个广播接收者,但是在 onDestory 函数中并没有执行反注册,当 Activity 被 finish 掉时,Activity 对象已经走完了自身的生命周期,应该被资源回收释放掉,但由于没有反注册, 此时 Activity 和 GC-Root 间任然有可达路径存在,导致 Activity 虽然被销毁,但是所占用的内存资源却无法被回收掉。类似的栗子其实有很多,不一一例举了。对于 Android(Java)内存回收管理想要再深入了解的童鞋,可以看看下面资源:

Android GC系统

在Android 2.3之前,GC是同步发生的,而且是一次完整的Heap遍历。也就是说,每次GC都会打断应用的正常运行,而且应用占用内存越大,GC时间越长。而在Android 2.3之后,系统修改了GC,将GC作为并发线程,同时每次GC并不会遍历整个Heap,而是只遍历一部分内存。(这里的GC同步与非同步是指:Android 2.3之前的版本,GC是会打断被GC的线程的,而Android 2.3之后的版本,GC与被GC线程可以同时工作而互不影响。)

这是GC算法的第一次提升,一直到Android 4.4 都没有发生大的变化,直到Android 5.0的到来。在Android 5.0之前,GC一直扮演着一个清理工的角色,GC打断进程,遍历Heap,清理垃圾。但是在清理过程中,没有对内存碎片进行整理,这就导致当系统进行了大量GC后,内存碎片越来越多,完整的内存区域越来越少。即是整个碎片加起来有10 MB,但完整的一个连续内存区域却可能只有1MB。 这就导致了OOM,即是此时你还有很多内存。因此在Android 5.0之后,系统第二次优化了GC,加快了GC清理速度,减少了打断进程的次数。同时GC不仅扮演清理工的角色,还扮演了一个管家的角色,它可以为大内存对象分配特殊的地址,方便内存碎片的管理。当你的应用在后台运行后,GC系统会对整个APP的内存进行对齐,将小的内存碎片清理出来,构成完整的内存区域,从而提高内存的使用率。

Handler内存泄漏分析

在Android中我们经常用如下的方式创建一个Handler类型的匿名内部类:

public class MainActivity extends AppCompatActivity {
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            //....
        }
    } ;
}

这时编译器会对上面的代码发出黄色警告:

Since this Handler is declared as an inner class, it may prevent the outer class from being garbage collected. If the Handler is using a Looper or MessageQueue for a thread other than the main thread, then there is no issue. If the Handler is using the Looper or MessageQueue of the main thread, you need to fix your Handler declaration, as follows: Declare the Handler as a static class; In the outer class, instantiate a WeakReference to the outer class and pass this object to your Handler when you instantiate the Handler; Make all references to members of the outer class using the WeakReference object.

上面的意思是,如果你把Handler声明为一个内部类,并使用主线程的Looper,那么就有可能导致内存泄漏(外部类无法被回收)。所以建议把Handler声明为一个静态类,用弱引用封装传递进来的外部类实例,并确保静态类所有外部类成员的引用都用弱引用进行封装。

我如果仅仅声明一个普通的匿名内部类并不会引起外部类不被回收的情况,除非匿名内部类对象的引用被其他类所持有,很明显Handler就是属于这种情况。那Hnadler对象是被哪个对象持有呢?那就要说说Handler的通信机制了。

当一个Android应用启动的时候,会自动创建一个供应用主线程使用的Looper实例。Looper的主要工作就是一个一个处理消息队列中的消息对象。在Android中,所有Android框架的UI事件都是放入到消息中,然后加入到Looper要处理的消息队列中,由Looper负责一条一条地进行处理。主线程中的Looper生命周期和当前应用一样长。要使用Handler前,必须先执行Looper.prepare()方法用于创建消息队列,而在主线程类ActivityThread已经调用了该方法,所以后面在主线程中声明Handler对象时,我们就不必再执行该方法。我们看看prepare()方法内部做了什么:

public final class Looper { 

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

public static void prepare() {
        prepare(true);
}

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();
}
...
}

首先创建一个Looper对象,在其构造方法中创建一个消息队列MessageQueue。并把该Looper对象放入到当前线程的ThreadLocal中。

当创建好了一个消息队列,那么我现在就要创建了一个用于处理消息的Handler对象了:

public class Handler { 

public Handler() {
        this(null, false);
    }

public Handler(Callback callback, boolean async) {
 ...
  mLooper = Looper.myLooper();
 if (mLooper == null) {
         throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
 }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}
...

}

创建Handler对象前必须保证Looper已经被创建,否则会抛异常。创建Handler对象的同时并存储了Looper中消息队列的引用。

当Handler对象创建后,最后一步就是执行Looper.looper()方法。该方法会进入到一个死循环中,把消息源源不断的从消息队列中去取出,交给Handler去处理,当没有消息的时候,则会进行阻塞。如果没有主动退出Looper,该线程就会一直执行。具体源码后面再贴出。

我们通常使用Handler#sendMessage方法来发送一个消息,该方法最终调用了enqueueMessage方法:

public class Handler {

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
  }

 private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }
 ....
}

在enqueueMessage方法中,我们可以很明显的看到一个操作msg.target = this; 把Handler对象的引用赋予了Message的target字段。该字段是Handler类型,引用了当前Handler对象。这个地方很关键,这是Handler对象第一次被其他对象所引用,这个地方也是Handler会导致内存泄漏的原因。

接下来通过执行消息队列MessageQueue 的enqueueMessage方法,把消息放到了消息队列中:
MessageQueue#enqueueMessage

boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }

        synchronized (this) {
            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) {
                // 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.
                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;
    }

enqueueMessage中把消息以链表的形式串起来,最后加入消息的放到链表的尾部。链表的表头则存储在mMessages变量中。当Looper取消息的时候,就会优先从mMessages的表头取出并依次处理。

接下来我们看看Looper.looper()方法如何取出消息进行处理的:
Looper#looper

public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;

        ...

       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);
            } finally {
            ... 
            }

            ...
            msg.recycleUnchecked();
        }
    }

可以看出当执行loop方法时,就已经进入到了一个死循环中了。首先取出Looper中的消息队列,并执行消息队列的next方法取出链表头部(如果没有特殊设置的话)的消息Message,并执行Message中Handler对象的dispatchMessage方法,把消息交给Handler处理。从这里我们也可以看出Handler充当了消息的发送者和处理则两种角色。 消息处理完成后执行msg.recycleUnchecked()方法释放Message所持有的对象资源:

 void recycleUnchecked() {
        // Mark the message as in use while it remains in the recycled object pool.
        // Clear out all other details.
        flags = FLAG_IN_USE;
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = -1;
        when = 0;
        target = null; //释放Handler对象
        callback = null; //释放Runnable对象
        data = null;
         //...
    }

只有执行到了recycleUnchecked方法,Handler对象的引用才会被释放(前提是没有手动释放) 。至此整个Handler的通信机制结束。

我通过一张图描述了Handler发送Message消息到消息队列中,各个类对象之间的引用关系:
62179594.png-32.8kB

从上面我们清晰的看出,如果Message对象一直在消息队列中未被处理释放掉(recycleUnchecked方法未被执行),你的Handler对象就不会被释放,进而你的Activity也不会被释放。

这种现象很常见,当消息队列中含有大量的Message等待处理,你发的Message需要等几秒才能被处理,而此时你关闭Activity,就会引起内存泄露。如果你经常send一些delay的消息,即使消息队列不繁忙,在delay到达之前关闭Activity也会造成内存泄露。

下面的代码就是很明显的造成内存泄漏的例子:

public class MainActivity extends AppCompatActivity {

    private int mValue;

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
           //...
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //发送延迟5秒的消息
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                mValue = 5;
            }
        }, 5000);

        //关闭Activity
        finish();
    }
}

分析一下上面的代码,当我们执行了Activity的finish方法,被延迟的消息会在被处理之前存在于主线程消息队列中5秒,而这个消息中又包含了Handler的引用,而Handler是一个匿名内部类的实例,其持有外面的MainActivity 的引用,所以这导致了MainActivity 无法回收,进行导致MainActivity 持有的很多资源都无法回收,这就是我们常说的内存泄露。

注意上面的new Runnable这里也是匿名内部类实现的,同样也会持有MainActivity 的引用,也会阻止MainActivity 被回收。该Runnable对象也是被Message对象的callback变量持有。

Handler内存泄漏解决方案

要解决这种问题,方案有两种:
方案1:在关闭Activity/Fragment时(finish/onStop等函数中),取消还在排队的Message:

mHandler.removeCallbacksAndMessages(null);

调用此方法后,消息队列中所有的callbacks和messages都会被移除。

方案2:
用静态内部类创建Handler。因为静态的内部类不会持有外部类的引用,所以不会导致外部类实例的内存泄露。当你需要在静态内部类中调用外部的Activity时,我们可以使用弱引用(弱引用会在垃圾回收时被回收掉,因而弱引用解决内存泄露的一种方)来处理。另外关于同样也需要将Runnable设置为静态的成员属性。注意:一个静态的内部类实例不会持有外部类的引用。 修改后不会导致内存泄露的代码如下:

ublic class MainActivity extends AppCompatActivity {

    /**
     * Instances of static inner classes do not hold an implicit
     * reference to their outer class.
     */
    private static class MyHandler extends Handler {

        private final WeakReference<MainActivity> mActivity;

        public MyHandler(MainActivity activity) {
            mActivity = new WeakReference<MainActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = mActivity.get();
            if (activity == null || activity.isFinishing()) {
               return;
            }
            // ...
        }
    }

    /**
     * Instances of anonymous classes do not hold an implicit
     * reference to their outer class when they are static.
     */
    private static final Runnable sRunnable = new Runnable() {
        @Override
        public void run() { /* ... */ }
    };

    private final MyHandler mHandler = new MyHandler(this);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mHandler.postDelayed(sRunnable, 5000);

        finish();
    }
}

其实在Android中很多的内存泄露都是由于在Activity中使用了非静态内部类导致的,就像本文提到的一样,所以当我们使用时要非静态内部类时要格外注意,如果其实例的持有对象的生命周期大于其外部类对象,那么就有可能导致内存泄露 。

参考:
https://www.zhihu.com/question/53613423
http://stackoverflow.com/questions/6366211/what-are-the-roots
http://www.infoq.com/cn/articles/jvm-memory-collection
http://www.jianshu.com/p/f5582d9a0f73
http://www.cnblogs.com/tv151579/archive/2014/04/05/3647447.html
http://www.cnblogs.com/caca/p/android_message_memory_leak.html
http://www.jianshu.com/p/f5582d9a0f73
http://droidyue.com/blog/2014/12/28/in-android-handler-classes-should-be-static-or-leaks-might-occur/

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值