Android实现异步任务机制AsyncTask 的使用及源码分析

1、AsyncTask的使用。

  • 1.1、AsyncTask的简介: 在Android中,我们更新UI的操作必须要在主线程(UI线程)中进行,而下载图片、文件这种操作必须要在子线程中进行
    ,而下载图片、文件这种操作必须在子线程中进行,Android提供了Handler机制,实现了子线程与主线程之间的通信。通常做法就是先new出一个子线程Thread,在子线程中完成下载操作后,通过Handler发送一条Message给主线程,主线程收到消息后,就可以进行UI的更新工作了。代码如下:

Handler mHandler=new Handler(){
        @Override
        public void handleMessage(Message msg) {
            if (msg.what==1) {
                Bitmap bitmap= (Bitmap) msg.obj;
                //更新UI界面
            }
        }
    };

 private void dowoload() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                //在这里进行下载操作  获得了图片Bitmap;
                //下载完成以后向主线程发送Message
                Message message = Message.obtain();
                message.obj = mBitmap;
                message.what = 1;//用来区分那个线程发送的消息
                mHandler.sendMessage(message);
            }
        }).start();
}    
  • 可以看到,每次要进行下载工作,我们就得先创建出Thread,然后在主线程中写好Handler,为了对这个过程进行封装,Android
    提供了AsyncTask异步任务,Asynctask对线程和Handler进行了封装,使得我们可以直接在AsyncTask中进行UI的更新,就好像在子
    线程中更新UI一样。

    1. 1.2创建AsyncTask子类

      AsyncTask是一个抽象类,我们必须写一个子类去继承他,在子类中完成具体的业务下载操作,为了对各种情况进行更好的封装,AsyncTask抽象类指定了三个泛型参数类型,如下图;
      
public abstract class AsyncTask <Params, Progress, Result> {.......}
  1. 其中三个泛型参数的含义如下:

    Params:开始异步任务执行时传入的参数类型,即doinBackground()方法中的参数类型。
    Progress:异步任务执行过程中,返回下载进度值得类型,即在 doinBackground()中调用publishProgress()时传入的参数类型。
    Result:异步任务执行完成后,返回的结果类型,即doinBackground()方法的返回值类型。

    有了这三个参数类型之后,也就控制了这个AsyncTask子类各个阶段的返回值类型,如果有不同业务,我们就需要在另写一个 AsyncTask子类进行处理。

  2. 1.3 AsyncTask的回调方法
    前面我们说过,AsyncTask对线程和Handler进行了封装,那他的封装性体现在哪呢?其实,AsyncTask的几个回调方法只是这种封装性的体现,使得我们感觉在子线程进行UI更新一样,一个基本的AsyncTask有如下几个回调方法:

    (1)onPreExecute():在执行后台下载操作之前调用,运行在主线程中

    (2)doInBackground():核心方法,执行后台下载操作的方法,不许实现的一个方法,运行在子线程中

    (3)onPostExecute():在后台下载完成之后调用,运行在主线程中
    因此,AsyncTask的生命周期为:onProExecute()→doInBackground()→onPostExecute(),其中,onProExecute()和onPostExecute()分别在下载操作前和下载操作和调用,同时他们是在主线程中进行的,因此可以在这两个方法中进行UI的更新操作, 比如:在onProExecute()方法中,将下载等待动画显示出来,在onPostExecute()方法中,将下载等待动画隐藏起来。
    如果我们想向用户展示文件的下载进度情况,这时,我们在doInBackground下载操作中,调用publishProgress();将当前进度值传入该方法,而publishProgress()内部会调用AsyncTask的另一个回调方法。

    (4)onProgressUpdate():在下载操作doInBackground()中调用publishProgress()的回调方法,用于更新下载进度,运行在主线程中。
    因此,在需要更新进度值时,AsyncTask的生命周期为;onProExecute()→doInBackground()→publishiProgress()→
    onProgressUpdate()→onPostExecute().
    可以看到,AsyncTask的优秀之处在于几个回调方法的设置上,只要doInBackground()运行在子线程中,其他三个回调方法都是运行在主线程中的,因此,只要在AsyncTask中,就可以实现文件的后台下载、UI的更新操作。

    实战操作:

    首先实现一个AsyncTask的具体实现类,进行图片的下载。代码如下;

 public class MyAsyncTask extends AsyncTask<String, Integer, Bitmap> {
        private ProgressBar mProgressBar;//进度条
        private ImageView mImageView;//图片显示控件

        public MyAsyncTask(ProgressBar progressBar, ImageView imageView) {
            mProgressBar = progressBar;
            mImageView = imageView;
        }

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            mProgressBar.setVisibility(View.VISIBLE);
        }

        @Override
        protected Bitmap doInBackground(String... strings) {
            String urlParms = strings[0];//拿到execute()传过来的图片的url
            Bitmap bitmap = null;
            URLConnection conn = null;
            InputStream inputStream = null;
            try {
                URL url = new URL(urlParms);
                conn = url.openConnection();
                inputStream = conn.getInputStream();
                //这里只是为了演示更新进度的功能,实际的更新进度需要从输入流中读取时逐步获取
                for (int i = 0; i < 100; i++) {
                    publishProgress(i);
                    Thread.sleep(50);//为了看清楚效果,睡眠一段时间。
                }
                //将获取到的输入流转换为Bitmap格式。
                BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
                bitmap = BitmapFactory.decodeStream(bufferedInputStream);
                inputStream.close();
                bufferedInputStream.close();
            } catch (MalformedURLException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return bitmap;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);
            Log.e("更新进度","===="+values[0]);
            mProgressBar.setProgress(values[0]);
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            //将加载动画隐藏掉
            mProgressBar.setVisibility(View.GONE);
            mImageView.setImageBitmap(bitmap);
        }
    }

上面的doInBackground()中获取进度值时,我们只是为了做一个进度更新调用的演示,实际项目文件下载中,我们可能会对拿到的输入流进行处理,比如读取输入流,将文件保存在本地,在读取输入流的时候,我们就可以获取到已经读取的输入流大小作为进度值了,代码如下:

//实际项目中如何获取文件大小作为进度值以及更新进度值

                int totalSize=conn.getContentLength();//获取文件总大小
                int size=0;//保存当前下载文件的大小,作为进度值
                int count=0;
                byte[]buffer=new byte[1024];
                while ((count = inputStream.read(buffer)) != -1) {
                    size+=count;//获取已下载的文件大小
                    //调用publishProgress更新进度,他内部会回调onProgressUpdate()方法
                    publishProgress(size,totalSize);
                    Thread.sleep(100);//为了看清 效果睡眠一段时间
                }

取消下载任务特别注意:每一个new出来的AsyncTask只能执行一次execute()方法,如果同一个AsyncTask多次执行execute()会报错。解决办法:代码如下

     case R.id.btn_restart:
                Intent intent = new Intent(this, FiveActivity.class);
                startActivity(intent);
                break;

每次点击重新加载,让他将当前页面重新刷新一次,在第一次运行程序进入FiveActivity,执行execute但在显示出图片之前,立即点击“加载图片”按钮,新打开一个FiveActivity,我们发现这个的进度条没有立即展示出进度出来,说明这个FiveActivity的AsyncTask没有立即执行doInBackground(),这是因为AsyncTask内部使用的是线程池,相当于里面有一个线程队列,执行一次execute时会将一个下载任务加入到线程队列,只有前一个任务完成了,下一个下载任务才会开始执行。为了达到我们想要的效果,我们自然想到把上一个任务给取消掉。的确,AsyncTask为我们提供了cancel()方法来取消一个任务的执行,但是要注意的是,cancel方法并没有能力真正去取消一个任务,其实只是设置这个任务的状态为取消状态,我们需要在doInBackground()下载中进行检测,一旦检测到该任务被用户取消了,立即停止doInBackground()方法的执行。我们先修改FiveActivity,根据不同业务需求,在不同地方进行任务的取消,我们这里在onPause()中进行任务的取消,在MainActivity方法中加入onPause()方法,如下:

@Override
    protected void onPause() {
        super.onPause();
        //判断当前异步任务的状态,如果是正在运行,
        if (myAsyncTask != null && myAsyncTask.getStatus() == AsyncTask.Status.RUNNING) {
            //cancel只是将对应的任务标记为取消状态
            myAsyncTask.cancel(true);
        }
    }

继续修改AsyncTask,在这里面进行任务是否被取消的检测,这里我们只简单修改下doInBackground()和onProgressUpdae()方法,实际项目中开自己的业务逻辑来控制,如下:

@Override
        protected Bitmap doInBackground(String... strings) {
            String urlParms = strings[0];//拿到execute()传过来的图片的url
            Bitmap bitmap = null;
            URLConnection conn = null;
            InputStream inputStream = null;
            try {
                URL url = new URL(urlParms);
                conn = url.openConnection();
                inputStream = conn.getInputStream();
                //这里只是为了演示更新进度的功能,实际的更新进度需要从输入流中读取时逐步获取
//                for (int i = 0; i < 100; i++) {
//                    publishProgress(i);
//                    Thread.sleep(50);//为了看清楚效果,睡眠一段时间。
//                }
                //实际项目中如何获取文件大小作为进度值以及更新进度值
                //将获取到的输入流转换为Bitmap格式。
//                BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
                FileOutputStream outputStream = new FileOutputStream(mFile);
                int totalSize = conn.getContentLength();//获取文件总大小
                int size = 0;//保存当前下载文件的大小,作为进度值
                int count = 0;
                byte[] buffer = new byte[1024];
                while ((count = inputStream.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, count);
                    size += count;//获取已下载的文件大小
                    //调用publishProgress更新进度,他内部会回调onProgressUpdate()方法
                    if (isCancelled()) {
                        //通过isCancelled判断当前任务十分被取消掉
                        Toast.makeText(FiveActivity.this, "当前任务被取消!", Toast.LENGTH_SHORT).show();
                        break;
                    }
                    publishProgress(size, totalSize);
                    Thread.sleep(100);//为了看清 效果睡眠一段时间
                }
                bitmap = BitmapFactory.decodeFile(mFile.getAbsolutePath());
//                bitmap = BitmapFactory.decodeStream(bufferedInputStream);
                inputStream.close();
                outputStream.close();
//                bufferedInputStream.close();
            } catch (MalformedURLException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return bitmap;
        }
  • 使用小结:
    (1)AsyncTask中,只有doInBackground()方法是处于子线程中运行的,其他三个回调onPreExecute()、onPostExecute()、onProgressUpdate()都是在UI线程中进行,因此在这三个方法里面可以进行UI的更新工作;
    (2)每一个new出的AsyncTask只能执行一次execute()方法,多次运行将会报错,如需多次,需要新new一个AsyncTask;
    (3)AsyncTask必须在UI线程中创建实例,execute()方法也必须在UI线程中调用;
  • 2、AsyncTask内部实现原理:
    进入AsyncTask源码,简单分析一下AsyncTask的内部实现,前面说到,AsyncTask内部封装了异步任务队列和Handler,

    2.1、AsyncTask的内部异步队列 在上面的例子中,我们可以直观的看到,AsyncTask的内部肯定是基于工作队列这种方式的,每次执行execute()就会把当前的任务加入到工作队列当中去。但是我们每次点击加载图片,都会重新进入活动的onCreate()方法中去新建一个AsyncTask的对象,既然对象都是新的,为什么AsyncTask中的工作队列还是能够正常管理呢,这就说明,AsyncTask内部的线程池、工作队列的定义都应该是static的,而static定义的变量是属于进程范围内的,只有这样,这些static的变量才能交给AsyncTask这个类来管理,而不是AsyncTask的具体子类对象。如下部分变量声明:

private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);
        public Thread newThread(Runnable r) {
            return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
        }
    };

    private static final BlockingQueue<Runnable> sPoolWorkQueue =
            new LinkedBlockingQueue<Runnable>(128);

    /**
     * 真正用来执行任务的的线程池
     */
    public static final Executor THREAD_POOL_EXECUTOR
            = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
                    TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);

    /**
     * 定义一个线程池,在线程池中有一个Runnable任务队列,用来存放、顺序执行任务
     */
    public static final Executor SERIAL_EXECUTOR = new SerialExecutor();

    //AsyncTask内部默认使用的线程池
    private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
    //自定义的一个Handler
    private static InternalHandler sHandler;
  • 可以看到,优线程池有关的都定义为了static类型,中,SERIAL_EXECUTOR内部就定义了一个任务队列ArrayDeque
    mTasks,当我们调用AsyncTask的execute()时,就会将当前任务加入到该队列中。先来看执行AsyncTask的execute()时的情景:
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
    return executeOnExecutor(sDefaultExecutor, params);
  • AsyncTask的execute()交给了executeOnExecutor()方法,将将默认的线程池作为参数传进来,进入executeOnExecutor方法中:
@MainThread
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
        Params... params) {
    if (mStatus != Status.PENDING) {//1、这里判断当前AsyncTask是否正在执行或已执行完毕
        switch (mStatus) {
            case RUNNING:
                throw new IllegalStateException("Cannot execute task:"
                        + " the task is already running.");
            case FINISHED:
                throw new IllegalStateException("Cannot execute task:"
                        + " the task has already been executed "
                        + "(a task can be executed only once)");
        }
    }
    mStatus = Status.RUNNING;//2、设置正在执行的状态
    onPreExecute();//3、回调onPreExecute()方法
    mWorker.mParams = params;
    exec.execute(mFuture);//4、放到前面默认构造的线程池中去执行
    return this;
}
  • 上面第一步,先判断当前AsyncTask是否正在运行或已经执行完毕,如果正在执行或执行完毕再次执行将抛出异常,这也正是我们前面在使用的时候谈到,同一个AsyncTask不能多次进行execute()的原因!到了第三步的时候,先去调用一下onPreExecute()方法,因为executeOnExecutor方法本身就是在UI线程中运行的,所以onPreExecute也会在UI线程中运行。第四步,才会开始讲当前AsyncTask任务加入到队列中,我们进入默认的线程池中去看一下:
private static class SerialExecutor implements Executor {
    final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
    Runnable mActive;
    public synchronized void execute(final Runnable r) {
        mTasks.offer(new Runnable() {
            public void run() {
                try {
                    r.run();
                } finally {
                    scheduleNext();
                }
            }
        });
        if (mActive == null) {
            scheduleNext();
        }
    }

    protected synchronized void scheduleNext() {
        if ((mActive = mTasks.poll()) != null) {
            THREAD_POOL_EXECUTOR.execute(mActive);
        }
    }
}
  1. 在第四步执行execute时,实际就是调用的SerialExecutor中的execute方法,在这里面,先创建了一个Runnable对象,然后将这个Runnable对象添加到任务队列mTasks中,在当执行到这个Runnable时调用scheduleNext去队列中取出一个任务,然后交给另一个线程池去真正执行这个任务。
  2. 2.2、与UI线程进行交互—handler AsyncTask内部通过自定义的static类型的InternalHandler和UI线程进行交互。InternalHandler实现如下:
private static class InternalHandler extends Handler {
    public InternalHandler() {
        super(Looper.getMainLooper());
    }

    @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
    @Override
    public void handleMessage(Message msg) {
        AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
        switch (msg.what) {
            case MESSAGE_POST_RESULT:
                // There is only one result
                result.mTask.finish(result.mData[0]);
                break;
            case MESSAGE_POST_PROGRESS:
                result.mTask.onProgressUpdate(result.mData);
                break;
        }
    }
}
  • 每当收到子线程发来的和UI线程进行通信的handler请求时,先从Message中拿到子线程发来的结果参数AsyncTaskResult,AsyncTaskResult里面封装了AsyncTask对象和数据信息,如下:
private static class AsyncTaskResult<Data> {
    final AsyncTask mTask;
    final Data[] mData;
    AsyncTaskResult(AsyncTask task, Data... data) {
        mTask = task;
        mData = data;
    }
}
  • 然后根据不同状态调用不同方法,如果是MESSAGE_POST_RESULT状态,就调用AsyncTask的finish()方法,finish方法中会去判断当前任务十分被cancel,如果没有cancel则开始回调onPostExecute()方法;如果状态是MESSAGE_POST_PROGRESS,则回调onProgressUpdate()方法。当子线程需要和UI线程进行通信时,就会通过这个handler,往UI线程发送消息。需要通过handler来发送消息,肯定是在子线程异步任务的时候才需要,在AsyncTask中需要handler的地方其实就是两个地方,一个是doInBackground()在运行过程中,需要更新进度值的时候;一个是doInBackground()运行完成后,需要回到到UI线程中的onPostExecute()方法的时候。
  • 对于一:我们在doInBackground()中调用publicProgress()进行进度值的更新,因此在publicProgress()中肯定会有handler的身影,如下:
protected final void publishProgress(Progress... values) {
    if (!isCancelled()) {
        getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
                new AsyncTaskResult<Progress>(this, values)).sendToTarget();
    }
}
  • 对于二:其实就是一个异步任务执行完后后的返回处理,而FutureTask正是处理处理Runnable运行返回结果的。
    在2.1部分的executeOnExecutor方法中第四步,我们在执行execute(mFuture),传入了一个mFuture,mFuture是在初始化AsyncTask的时候进行构建的,如下:
mFuture = new FutureTask<Result>(mWorker) {
    @Override
    protected void done() {
        try {
            postResultIfNotInvoked(get());//这里面通过handler往UI线程发送消息
        } catch (InterruptedException e) {
            android.util.Log.w(LOG_TAG, e);
        } catch (ExecutionException e) {
            throw new RuntimeException("An error occurred while executing doInBackground()",
                    e.getCause());
        } catch (CancellationException e) {
            postResultIfNotInvoked(null);
        }
    }
};
  • 在上面的postResultIfNotInvoked()中会通过handler进行消息的发送

AsyncTask原理总结:
AsyncTask主要是对异步任务和handler的封装,在处理异步任务时,AsyncTask内部使用了两个线程池,一个线程池sDefaultExecutor是用来处理用户提交(执行AsyncTask的execute时)过来的异步任务,这个线程池中有一个Runnable异步任务队列ArrayDequemTasks,把提交过来的异步任务放到这个队列中;另一个线程池THREAD_POOL_EXECUTOR,用来真正执行异步任务的。在处理handler时,自定义了一InternalHandler,在publicProgress()和doInBackground()运行完成后,会通过这个handler往UI线程发送Message。

  • 3、AsyncTask在使用中的一个特殊情况

    我们在使用AsyncTask的时候,一般会在onPreExecute()和onPostExecute()中进行UI的更新,比如等待图片的显示、进度条的显示…当我们一个Activity中正在使用AsyncTask进行文件的下载时,如果此时屏幕发生了旋转,Activity会进行re-onCreate,又会创建一个AsyncTask进行文件的下载,这个正是我们前面将取消任务的时候谈到的第二个现象,我们只需要在onPause()中进行取消cancel()即可。但是这样仅仅是解决了发生等待的情况,因为Activity再次进入了onCreate()方法,还是会进行文件的下载,为了解决这个问题,一种方案是通过判断onCreate(Bundle
    savedInstanceState)方法参数中的savedInstanceState==null?来判断是哪种情况,只有savedInstanceState==null时才去创建新的AsyncTask。

    4、AsyncTask和Handler的比较 AsyncTask:
    优点:AsyncTask是一个轻量级的异步任务处理类,轻量级体现在,使用方便、代码简洁上,而且整个异步任务的过程可以通过cancel()进行控制;
    缺点:不适用于处理长时间的异步任务,一般这个异步任务的过程最好控制在几秒以内,如果是长时间的异步任务就需要考虑多线程的控制问题;当处理多个异步任务时,UI更新变得困难。
    Handler:
    优点:代码结构清晰,容易处理多个异步任务;
    缺点:当有多个异步任务时,由于要配合Thread或Runnable,代码可能会稍显冗余。

    总之,AsyncTask不失为一个非常好用的异步任务处理类,只要不是频繁对大量UI进行更新,可以考虑使用;而Handler在处理大量UI更新时可以考虑使用。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
异步任务AsyncTask)和线程池都是将网络操作放到子线程中执行的常用方式。 1. AsyncTask方式 使用AsyncTask时需要重写其doInBackground()方法,在其中执行耗时操作,例如网络请求。然后可以在onPostExecute()方法中更新UI界面。 下面是一个使用AsyncTask进行网络请求的例子: ``` public class MyAsyncTask extends AsyncTask<String, Void, String> { @Override protected String doInBackground(String... params) { String url = params[0]; String result = ""; try { URL urlObj = new URL(url); HttpURLConnection conn = (HttpURLConnection) urlObj.openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); if (conn.getResponseCode() == 200) { InputStream is = conn.getInputStream(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len; while ((len = is.read(buffer)) != -1) { bos.write(buffer, 0, len); } result = new String(bos.toByteArray()); bos.close(); is.close(); } conn.disconnect(); } catch (Exception e) { e.printStackTrace(); } return result; } @Override protected void onPostExecute(String result) { // 在这里更新UI界面 super.onPostExecute(result); } } ``` 2. 线程池方式 线程池是一个管理线程的容器,可以创建多个线程,将任务分配给这些线程执行。这样可以避免频繁地创建和销毁线程,提高了线程的利用率。 下面是一个使用线程池进行网络请求的例子: ``` public class MyThreadPool { private static ExecutorService executorService = Executors.newFixedThreadPool(5); public static void execute(Runnable runnable) { executorService.execute(runnable); } } public class MyRunnable implements Runnable { private String url; public MyRunnable(String url) { this.url = url; } @Override public void run() { String result = ""; try { URL urlObj = new URL(url); HttpURLConnection conn = (HttpURLConnection) urlObj.openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); if (conn.getResponseCode() == 200) { InputStream is = conn.getInputStream(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len; while ((len = is.read(buffer)) != -1) { bos.write(buffer, 0, len); } result = new String(bos.toByteArray()); bos.close(); is.close(); } conn.disconnect(); } catch (Exception e) { e.printStackTrace(); } // 在这里更新UI界面 } } ``` 使用线程池时,需要先创建一个ExecutorService对象,然后通过execute()方法提交任务。在任务的run()方法中执行网络请求,并在其中更新UI界面。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值