Android - Handler 、AsyncTask(二)

Android - Handler 、AsyncTask(一)一文中,我们提到,为了解决不能阻塞主线程和不能在子线程中更新UI的问题,Android提供了handler消息机制。

那么,如果有很多耗时的操作需要进行,并且需要在操作执行完之后或者是在操作过程中更新UI呢?创建很多线程吗?根据我们学过的知识,这个时候可以考虑使用 线程池+handler 组合的方式了(线程池在本篇博文中暂不总结),而Android已经为我们提供了相应的类来实现我们的需求,这个类就是 AsyncTask


二、AsyncTask的实现原理

通常,我们使用AsyncTask的步骤如下:
1、创建一个类myAsyncTask继承AsyncTask
    A、选择性地复写onPreExecute()方法,该方法在UI线程中被执行,可以做一些初始化的工作,比如在界面上创建一个进度条。
    B、(必须)复写doInBackground()方法,将耗时的操作定义在这个方法中,该方法会在onPreExecute()方法执行之后立即执行。
    C、选择性地复写onProgressUpdate()方法,(任何时候都可以)在doInBackground()方法中执行publishProgress()方法来调用onProgressUpdate()方法以实现在耗时的操作过程中更新UI
    D、(必须)复写onPostExecute(Result)方法,利用doInBackground()方法执行完之后返回的结果更新UI。
2、创建myAsyncTask类的实例myAsyncTaskInstance
3、执行myAsyncTaskInstance.execute(Params...)

需要注意的是:
a、AsyncTask类的实例只能在UI线程中创建
b、execute方法只能在UI线程中调用
c、不能手动调用onPreExecute()、onPostExecute(Result)、onProgressUpdate()和doInBackground()这几个方法
d、一个AsyncTask的实例只能执行一次execute方法


那么,AsyncTask的具体实现到底是什么样的呢?我们就从execute()方法的调用开始疏理
[java]  view plain  copy
  1. public final AsyncTask<Params, Progress, Result> execute(Params... params) {  
  2.     return executeOnExecutor(sDefaultExecutor, params);  
  3.     //从AsyncTask.java中可知,sDefaultExecutor为AsyncTask的内部类SerialExecutor的实例:  
  4.     /*public static final Executor SERIAL_EXECUTOR = new SerialExecutor(); 
  5.     private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;*/           
  6. }  
executeOnExecutor()方法的具体逻辑如下:
[java]  view plain  copy
  1. public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,  
  2.         Params... params) {  
  3.     //PENDING的API注释为:Indicates that the task has not been executed yet.  
  4.     if (mStatus != Status.PENDING) {  
  5.         switch (mStatus) {  
  6.             case RUNNING:  
  7.                 throw new IllegalStateException("Cannot execute task:"  
  8.                         + " the task is already running.");  
  9.             case FINISHED:  
  10.                 throw new IllegalStateException("Cannot execute task:"  
  11.                         + " the task has already been executed "  
  12.                         + "(a task can be executed only once)");  
  13.         }  
  14.     }  
  15.     //Status是AsyncTask的一个内部枚举类,用来标示任务的状态,从上边的代码中可以看到,如果  
  16.     //mStatus 的值为RUNNING或者FINISHED,则会抛出异常,并且在execute()方法一执行时  
  17.     //就将mStatus 的值设为了RUNNING,即一个任务只能执行一次  
  18.     mStatus = Status.RUNNING;  
  19.     onPreExecute();   
  20.     mWorker.mParams = params;  
  21.     exec.execute(mFuture);  
  22.     return this;  
  23. }  
关于上边第19行的onPreExecute()方法,在AsyncTask类中的定义及注释如下:
[java]  view plain  copy
  1. /** 
  2.  * Runs on the UI thread before {@link #doInBackground}. 
  3.  */  
  4. protected void onPreExecute() {  
  5.  //该方法在UI线程中被执行,可以做一些初始化的工作,AsyncTask的子类可以选择性的复写该方法  
  6. }  
那上边的代码中mWorker和mFuture分别代表什么呢?

我们创建myAsyncTask类的实例时,在AsyncTask的构造函数中就已经完成了对mWorker和mFuture两个对象的初始化,mWorker是AsyncTask内部实现了Callable接口的一个类WorkerRunnable的实例:

[java]  view plain  copy
  1. private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {  
  2.     Params[] mParams;  
  3. }  
mWorker.mParams = params;这条语句将我们调用execute(Params...)方法时传入的参数赋值给了mWorker的成员变量mParams , mFuture则是FutureTask的一个实例,FutureTask的继承关系如下:
public class FutureTask<V> implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable, Future<V>
即mFuture是Runnable的一个间接子类对象。
关于mWorker和mFuture,暂时讲到此,等后边涉及到时再来详解。

接下来,看exec.execute(mFuture)这条语句
从上文的分析中得知,执行exec.execute(mFuture)会调用SerialExecutor的execute方法,
来看SerialExecutor的完整类定义:
[java]  view plain  copy
  1. private static class SerialExecutor implements Executor {  
  2.   
  3.     //需要注意的是,Executor为线程池体系的顶级接口,内部只定义了一个空方法:     
  4.       
  5.     /*public interface Executor { 
  6.         *//** 
  7.          * Executes the given command at some time in the future.  The command 
  8.          * may execute in a new thread, in a pooled thread, or in the calling 
  9.          * thread, at the discretion of the {@code Executor} implementation. 
  10.          *//* 
  11.         void execute(Runnable command); 
  12.     }*/   
  13.       
  14.     //即SerialExecutor 并不是线程池,它只是复写了execute()方法,可认为它就是一个可执行任务的执行器  
  15.     //定义一个ArrayDeque成员变量,ArrayDeque没有容量限制,是线程不安全的(此处不详解)  
  16.     final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();  
  17.     //定义一个Runnable类型的临时变量mActive  
  18.     Runnable mActive;  
  19.     //注意该方法使用了synchronized 关键字进行修饰  
  20.     public synchronized void execute(final Runnable r) {  
  21.         //在mTasks队列的尾部插入一个Runnable r  
  22.         mTasks.offer(new Runnable() {  
  23.             public void run() {  
  24.                 try {  
  25.                     r.run();  
  26.                 } finally {  
  27.                     scheduleNext();  
  28.                 }  
  29.             }  
  30.         });  
  31.         if (mActive == null) {  
  32.             scheduleNext();  
  33.         }  
  34.     }  
  35.     protected synchronized void scheduleNext() {  
  36.         //poll()方法从队列的头部检索并删除一个对象,并将检索到的对象返回  
  37.         if ((mActive = mTasks.poll()) != null) {  
  38.             //如果poll()方法返回的mActive不为null的话,  
  39.             //将会调用THREAD_POOL_EXECUTOR.execute(mActive);  
  40.             THREAD_POOL_EXECUTOR.execute(mActive);  
  41.         }  
  42.     }  
  43. }  
关于THREAD_POOL_EXECUTOR:
[java]  view plain  copy
  1. public static final Executor THREAD_POOL_EXECUTOR =   
  2.     new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,  
  3.         TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);  
  4. //THREAD_POOL_EXECUTOR是AsyncTask类的成员变量,被static和final修饰,是一个线程池对象,  
  5. //它的各个参数的值分别如下(关于这些值,这里不做解释):  
  6. private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();  
  7. private static final int CORE_POOL_SIZE = CPU_COUNT + 1;  
  8. private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;  
  9. private static final int KEEP_ALIVE = 1;  
  10.   
  11. private static final ThreadFactory sThreadFactory = new ThreadFactory() {  
  12.     private final AtomicInteger mCount = new AtomicInteger(1);  
  13.   
  14.     public Thread newThread(Runnable r) {  
  15.         return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());  
  16.     }  
  17. };  
  18.   
  19. private static final BlockingQueue<Runnable> sPoolWorkQueue =  
  20.     new LinkedBlockingQueue<Runnable>(128);     
THREAD_POOL_EXECUTOR.execute(mActive):
将Runnable的间接子类对象mFuture交由线程池THREAD_POOL_EXECUTOR处理

那么,我们为什么不直接用THREAD_POOL_EXECUTOR来处理一个任务呢?
事实上,我们在使用AsyncTask时,有两种执行任务的方式可以选择:
1、调用public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,Params... params)方法,指定参数exec为THREAD_POOL_EXECUTOR来执行mFuture任务。
2、直接调用execute(Params... params)方法,这种方式较常用。
在采用第二种方式的情况下,任务会被SerialExecutor的execute方法重新调度之后再由THREAD_POOL_EXECUTOR来进行有序的执行,上文中我们也提到过SerialExecutor的execute方法被synchronized关键字所修饰。这种设计为我们的任务执行提供了更多的选择。
(关于这两种执行方法的区别以及和线程池相关的其他知识,将在其他博文中总结)

根据线程池的相关知识可知,接下来,要执行的就是Runnable的间接子类对象mFuture的run方法了。

同时,上文提到的,AsyncTask的构造函数中mWorker和mFuture这两个对象的初始化的代码也需要贴出来了:
[java]  view plain  copy
  1. public AsyncTask() {  
  2.     mWorker = new WorkerRunnable<Params, Result>() {  
  3.         public Result call() throws Exception {  
  4.             mTaskInvoked.set(true);  
  5.   
  6.             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);  
  7.             //noinspection unchecked  
  8.             return postResult(doInBackground(mParams));  
  9.         }  
  10.     };  
  11.     mFuture = new FutureTask<Result>(mWorker) {  
  12.         @Override  
  13.         protected void done() {  
  14.             try {  
  15.                 postResultIfNotInvoked(get());  
  16.             } catch (InterruptedException e) {  
  17.                 android.util.Log.w(LOG_TAG, e);  
  18.             } catch (ExecutionException e) {  
  19.                 throw new RuntimeException("An error occured while executing doInBackground()",  
  20.                         e.getCause());  
  21.             } catch (CancellationException e) {  
  22.                 postResultIfNotInvoked(null);  
  23.             }  
  24.         }  
  25.     };  
  26. }  
这里还有一个问题,为什么不把要在后台执行的任务定义在一个runnable对象的run方法中然后再执行THREAD_POOL_EXECUTOR.execute(runnable),而要把任务(doInBackground()方法)定义在mWorker的call方法中,将mWorker作为参数构造出一个mFuture,然后执行execute(mFuture)呢?

我们来看一下FutureTask类的定义:
[java]  view plain  copy
  1. /** 
  2.  * A cancellable asynchronous computation.  This class provides a base implementation of {@link Future}, 
  3.  * with methods to start and cancel a computation, query to see if the computation is complete, and 
  4.  * retrieve the result of the computation.  The result can only be retrieved when the computation has  
  5.  * completed; the {@code get} methods will block if the computation has not yet completed.  Once 
  6.  * the computation has completed, the computation cannot be restarted 
  7.  * or cancelled (unless the computation is invoked using {@link #runAndReset}). 
  8.  * <p>A {@code FutureTask} can be used to wrap a {@link Callable} or 
  9.  * {@link Runnable} object.  Because {@code FutureTask} implements 
  10.  * {@code Runnable}, a {@code FutureTask} can be submitted to an 
  11.  * {@link Executor} for execution. 
  12.  */  
  13.  //一个可取消的异步计算,该类提供了基于Future的一些方法实现,通过这些方法可以开启或退出一个计算,可以  
  14.  //查看一个计算是否完成并检索计算的结果,如果计算尚未完成,用于检索结果的get方法将会被阻塞,一旦计算  
  15.  //完成,除非调用runAndReset,否则计算不会被重新启动或者取消,一个FutureTask可以用来包装一个  
  16.  //Callable或者Runnable对象,因为FutureTask implements Runnable,  
  17.  //一个FutureTask可以被提交给Executor进行执行(只是大致翻译,对这个类有个大概了解)  
  18. public class FutureTask<V> implements RunnableFuture<V> {  
  19.     //public interface RunnableFuture<V> extends Runnable, Future<V>{}  
  20. }  
可以看出,FutureTask类的存在是为了对一个任务进行包装,为的是能够更好地控制这个任务。
FutureTask类有两个构造函数:
第一个:
[java]  view plain  copy
  1. public FutureTask(Runnable runnable, V result) {  
  2.     this.callable = Executors.callable(runnable, result);  
  3.     this.state = NEW;       // ensure visibility of callable  
  4. }  
Executors类中的callable(runnable, result)方法:
[java]  view plain  copy
  1. public static <T> Callable<T> callable(Runnable task, T result) {  
  2.     if (task == null)  
  3.         throw new NullPointerException();  
  4.     return new RunnableAdapter<T>(task, result);  
  5. }  
Executors类中的内部类RunnableAdapter:
[java]  view plain  copy
  1. /** 
  2.  * A callable that runs given task and returns given result 
  3.  */  
  4. static final class RunnableAdapter<T> implements Callable<T> {  
  5.    final Runnable task;  
  6.    final T result;  
  7.    RunnableAdapter(Runnable task, T result) {  
  8.        this.task = task;  
  9.        this.result = result;  
  10.    }  
  11.    public T call() {  
  12.        task.run();  
  13.        return result;  
  14.    }  
  15. }  
可以看到,这个构造函数将接收的Runnable task和T result作为参数构造出一个Callable的间接子类对象,并且该对象的call方法中执行的就是task的run方法。然后将这个对象赋给了FutureTask的成员变量callable。

第二个 (也就是我们AsyncTask中的FutureTask走的构造函数):
[java]  view plain  copy
  1. public FutureTask(Callable<V> callable) {//同样注意泛型  
  2.     if (callable == null)  
  3.         throw new NullPointerException();  
  4.     //将接收的callable赋给FutureTask的成员变量private Callable<V> callable;  
  5.     this.callable = callable;  
  6.     this.state = NEW;       // ensure visibility of callable  
  7. }  
和第一个构造函数一样,重点也是在 为FutureTask的成员变量callable赋值
接下来, mFuture 的run方法:
[java]  view plain  copy
  1. public void run() {  
  2.     if (state != NEW ||!UNSAFE.compareAndSwapObject(this, runnerOffset,null, Thread.currentThread()))  
  3.         return;  
  4.     try {  
  5.         Callable<V> c = callable;//将callable赋值给临时定义的allable<V>变量c  
  6.         if (c != null && state == NEW) {  
  7.             V result;//定义临时变量V result(泛型)  
  8.             boolean ran;//定义临时boolean变量ran  
  9.             try {  
  10.                 //执行c的call()方法,这一步是关键  
  11.                 result = c.call();  
  12.                 //将ran的值设置为true  
  13.                 ran = true;  
  14.             } catch (Throwable ex) {  
  15.                 result = null;  
  16.                 ran = false;  
  17.                 //异常处理,略过,不影响流程理解  
  18.                 setException(ex);  
  19.             }  
  20.             if (ran)  
  21.                 set(result);  
  22.         }  
  23.     } finally {  
  24.         // runner must be non-null until state is settled to  
  25.         // prevent concurrent calls to run()  
  26.         runner = null;  
  27.         // state must be re-read after nulling runner to prevent  
  28.         // leaked interrupts  
  29.         int s = state;  
  30.         if (s >= INTERRUPTING)  
  31.             handlePossibleCancellationInterrupt(s);  
  32.     }  
  33. }  
根据上边的分析可知,FutureTask类是Runnable的间接子类对象,它复写了run方法,不过,在它的run方法中,真正的任务是在FutureTask的成员变量callable的call方法中执行的,其余代码的作用是为了更好地控制任务的执行,即FutureTask类的注释中提到的。

而AsyncTask中mFuture的创建——mFuture = new FutureTask<Result>(mWorker)——走的是上文提到的FutureTask的第二个构造函数,mWorker是WorkerRunnable类型的实例,关于WorkerRunnable的定义,上文已经列出,WorkerRunnable实现了Callable接口,并复写了call方法。

所以,THREAD_POOL_EXECUTOR.execute(mActive)的执行最终导致mWorker的call方法的执行,具体逻辑为:
[java]  view plain  copy
  1. mWorker = new WorkerRunnable<Params, Result>() {  
  2.     public Result call() throws Exception {  
  3.         mTaskInvoked.set(true);  
  4.         Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);  
  5.         //noinspection unchecked  
  6.         //主要就是这个方法了  
  7.         return postResult(doInBackground(mParams));  
  8.     }  
  9. };  
终于看到了我们的doInBackground()方法了,顺便看一下这个方法 在AsyncTask.java中的定义吧:
[java]  view plain  copy
  1. /** 
  2.  * Override this method to perform a computation on a background thread. The 
  3.  * specified parameters are the parameters passed to {@link #execute} 
  4.  * by the caller of this task. 
  5.  * This method can call {@link #publishProgress} to publish updates 
  6.  * on the UI thread. 
  7.  * @param params The parameters of the task. 
  8.  * @return A result, defined by the subclass of this task. 
  9.  */  
  10.  //可以看到,该方法是个空方法,注释也通俗易懂,不翻译了  
  11. protected abstract Result doInBackground(Params... params);   
看到这里,也就明白了AsyncTask是怎样把我们定义到doInBackground()方法中的任务放到后台去执行的了。doInBackground()方法执行完之后,其返回值将作为postResult()方法的参数,来看postResult()方法: 
[java]  view plain  copy
  1. private Result postResult(Result result) {  
  2.         @SuppressWarnings("unchecked")  
  3.         Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT,  
  4.                 new AsyncTaskResult<Result>(this, result));  
  5.         message.sendToTarget();  
  6.         return result;  
  7. }  
该方法的主要逻辑就是: 创建一条消息
what值为MESSAGE_POST_RESULT,obj值为new AsyncTaskResult<Result>(this, result)) ),并且发送出去。
sHandler是AsyncTask的成员变量private static final InternalHandler sHandler = new InternalHandler();   InternalHandler是AsyncTask的内部类, AsyncTaskResult也是AsyncTask的内部类 :
[java]  view plain  copy
  1. private static class AsyncTaskResult<Data> {//同样需要注意泛型  
  2.         final AsyncTask mTask;  
  3.         final Data[] mData;  
  4.         AsyncTaskResult(AsyncTask task, Data... data) {  
  5.             mTask = task;  
  6.             mData = data;  
  7.         }  
  8. }  
代码比较简单,需要发送的消息的obj值就是一个AsyncTaskResult对象,其成员变量final AsyncTask mTask的值为this,成员变量final Data[] mData的值则是doInBackground()方法的返回值。消息创建好,并且发送出去了,我们来看一下消息的处理:
[java]  view plain  copy
  1. private static class InternalHandler extends Handler {  
  2.     @SuppressWarnings({"unchecked""RawUseOfParameterizedType"})  
  3.     @Override  
  4.     public void handleMessage(Message msg) {  
  5.         //先取得AsyncTaskResult 类型的消息的obj--result  
  6.         AsyncTaskResult result = (AsyncTaskResult) msg.obj;  
  7.         switch (msg.what) {  
  8.             case MESSAGE_POST_RESULT:  
  9.                 //接收到上文中的MESSAGE_POST_RESULT消息后的处理逻辑为:  
  10.                 // There is only one result  
  11.                 result.mTask.finish(result.mData[0]);  
  12.                 break;  
  13.             case MESSAGE_POST_PROGRESS:  
  14.                 result.mTask.onProgressUpdate(result.mData);  
  15.                 break;  
  16.         }  
  17.     }  
  18. }  
消息处理方式为  result.mTask(即该AsyncTask对象)的finish方法(),参数为result.mData[0](即doInBackground()方法的返回值),来看finish方法()的具体逻辑:
[java]  view plain  copy
  1. private void finish(Result result) {  
  2.     if (isCancelled()) {  
  3.         onCancelled(result);  
  4.     } else {  
  5.         //如果任务没有退出,则执行onPostExecute(result)方法  
  6.         onPostExecute(result);  
  7.     }  
  8.     mStatus = Status.FINISHED;  
  9. }  
再来看onPostExecute(result)方法的定义:
[java]  view plain  copy
  1. /**      
  2.  * <p>Runs on the UI thread after {@link #doInBackground}. The 
  3.  * specified result is the value returned by {@link #doInBackground}.</p> 
  4.  * <p>This method won't be invoked if the task was cancelled.</p> 
  5.  * @param result The result of the operation computed by {@link #doInBackground}. 
  6.  */  
  7.  //注释同样比较简单,不做翻译了  
  8.  @SuppressWarnings({"UnusedDeclaration"})  
  9. protected void onPostExecute(Result result) { }  
复写onPostExecute()方法利用doInBackground(Params...)方法的返回值更新UI的流程也梳理清楚了

现在来分析一下,如果我们 在doInBackground()方法中执行publishProgress()方法来更新UI 的话,代码的执行逻辑又是什么样的?
publishProgress()方法的定义如下:
[java]  view plain  copy
  1.  /** 
  2.   * This method can be invoked from {@link #doInBackground} to 
  3.   * publish updates on the UI thread while the background computation is 
  4.   * still running. Each call to this method will trigger the execution of 
  5.   * {@link #onProgressUpdate} on the UI thread. 
  6.   * 
  7.   * {@link #onProgressUpdate} will note be called if the task has been 
  8.   * canceled. 
  9.   * 
  10.   * @param values The progress values to update the UI with. 
  11.   */  
  12.   //后台计算正在进行时可以在doInBackground()方法中调用此方法  
  13.   //每一次调用这个方法都会导致onProgressUpdate()方法在UI线程中被执行  
  14.   //接收的参数类型为AsyncTask<Params, Progress, Result>接收的泛型类型Progress  
  15. protected final void publishProgress(Progress... values) {  
  16.      if (!isCancelled()) {  
  17.          sHandler.obtainMessage(MESSAGE_POST_PROGRESS,  
  18.                  new AsyncTaskResult<Progress>(this, values)).sendToTarget();  
  19.      }  
  20. }  
publishProgress()方法和我们上边讲到过的postResult()方法一样,主要逻辑都是创建一条消息并且发送出去。只是在这里,消息的what值是MESSAGE_POST_PROGRESS,消息的obj值为new AsyncTaskResult<Progress>(this, values),消息的处理:
[java]  view plain  copy
  1. public void handleMessage(Message msg) {  
  2.     //先取得AsyncTaskResult 类型的消息的obj--result  
  3.     AsyncTaskResult result = (AsyncTaskResult) msg.obj;  
  4.     switch (msg.what) {  
  5.         ... ...  
  6.         //接收到上文中的MESSAGE_POST_PROGRESS消息后的处理逻辑为:  
  7.         case MESSAGE_POST_PROGRESS:  
  8.             result.mTask.onProgressUpdate(result.mData);  
  9.             break;  
  10.     }  
  11. }  
和wha t值为MESSAGE_POST_RESULT的消息的处理类似,直接看 onProgressUpdate() 方法的定义:
[java]  view plain  copy
  1. /** 
  2.  * Runs on the UI thread after {@link #publishProgress} is invoked. 
  3.  * The specified values are the values passed to {@link #publishProgress}. 
  4.  * @param values The values indicating progress. 
  5.  */  
  6.  //在publishProgress()方法被调用之后,在UI线程中执行  
  7. @SuppressWarnings({"UnusedDeclaration"})  
  8. protected void onProgressUpdate(Progress... values) {//空方法}  

至此,AsyncTask的实现原理基本清楚了,
onPreExecute()、doInBackground()、publishProgress()、onProgressUpdate()和onPostExecute(Result)这几个方法在一次AsyncTask任务执行过程中所扮演的角色也明了了

还有两个主要问题:
1、AsyncTask使用线程池+handler组合的方式,AsyncTask的缺陷或者说它的灵活使用主要也是在线程池上。关于这方面,需要另写博文作总结。
2、关于FutureTask类是怎样对callable的call方法(任务)进行控制的,还有很多细节没有搞清楚。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值