Android AsyncTask 源码分析

Android异步任务AsyncTask实例源码 Android是单线程模型,耗时的操作必须放在非主线程中执行,对此,我们需要使用多线程/线程池或者AsyncTask等来完成异步加载任务。 博客地址:http://blog.youkuaiyun.com/chenzheng8975/article/details/53893666 立即下载

1、AsyncTask的使用

使用 AsyncTask 可以更加简单地实现任务的异步执行,以及任务执行完毕之后与主线程的交互。它被设计用来执行耗时比较短的任务,通常是几秒种的那种,如果要执行耗时比较长的任务,那么就应该使用 JUC 包中的框架,比如 ThreadPoolExecutor 和 FutureTask等。

AsyncTask 用来在后台线程中执行任务,当任务执行完毕之后将结果发送到主线程当中。它有三个重要的泛类型参数,分别是 Params、Progress 和 Result,分别用来指定参数、进度和结果的值的类型。
以及四个重要的方法,分别是 onPreExecute(), doInBackground(), onProgressUpdate() 和 onPostExecute()。
这四个方法中,除了 doInBackground(),其他三个都是运行在UI线程的,分别用来处理在任务开始之前、任务进度改变的时候以及任务执行完毕之后的逻辑,而 doInBackground() 运行在后台线程中,用来执行耗时的任务。

一种典型的使用方法如下:

private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
    
    @Override
    protected Long doInBackground(URL... urls) {
        int count = urls.length;
        long totalSize = 0;
        for (int i = 0; i < count; i++) {
            totalSize += Downloader.downloadFile(urls[i]);
            publishProgress((int) ((i / (float) count) * 100));
            if (isCancelled()) break;
        }
        return totalSize;
    }

    @Override
    protected void onProgressUpdate(Integer... progress) {
        setProgressPercent(progress[0]);
    }

    @Override
    protected void onPostExecute(Long result) {
        showDialog("Downloaded " + result + " bytes");
    }
}

上面说 AsyncTask 有4个重要的方法,这里我们覆写了3个。doInBackground() 运行在线程当中,耗时的任务可以放在这里进行;onProgressUpdate() 用来处理当任务的进度发生变化时候的逻辑;onPostExecute() 用来处理当任务执行完毕之后的逻辑。另外,这里我们还用到了 publishProgress() 和 isCancelled() 两个方法,分别用来发布任务进度和判断任务是否被取消。

然后,我们可以用下面的方式来使用它:

new DownloadFilesTask().execute(url1, url2, url3);

使用AsyncTask的时候要注意以下几点内容:

  1. AsyncTask 的类必须在主线程中进行加载,当在4.1之后这个过程会自动进行;
  2. AsyncTask 的对象必须在主线程中创建;
  3. execute() 方法必须在UI线程中被调用;
  4. 不要直接调用 onPreExecute(), doInBackground(), onProgressUpdate() 和 onPostExecute();
  5. 一个AsyncTask对象的 execute() 方法只能被调用一次;

Android 1.6 之前,AsyncTask 是串行执行任务的;1.6 采用线程池处理并行任务;从 3.0 开始,又采用一个线程来串行执行任务。
3.0 之后可以用 executeOnExecutor() 来并行地执行任务,如果我们希望在3.0之后能并行地执行上面的任务,那么我们应该这样去写:

new DownloadFilesTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url1, url2, url3);

这里的 AsyncTask.THREAD_POOL_EXECUTOR 是 AsyncTask 内部定义的一个线程池,我们可以使用它来将 AsyncTask 设置成并行的。

2、AsyncTask源码分析

2.1 AsyncTask 的初始化过程

当初始化一个 AsyncTask 的时候,所有的重载构造方法都会调用下面的这个构造方法。这里做了几件事情:

  1. 初始化一个 Handler 对象 mHandler,该 Handler 用来将消息发送到它所在的线程中,通常使用默认的值,即主线程的 Handler;

  2. 初始化一个 WorkerRunnable 对象 mWorker。它是一个 WorkerRunnable 类型的实例,而 WorkerRunnable 又继承自 Callable,因此它是一个可以被执行的对象。我们会把在该对象中回调 doInBackground() 来将我们的业务逻辑放在线程池中执行。

  3. 初始化一个 FutureTask 对象 mFuture。该对象包装了 mWorker 并且当 mWorker 执行完毕之后会调用它的 postResultIfNotInvoked() 方法来通知主线程(不论任务已经执行完毕还是被取消了,都会调用这个方法)。

    public AsyncTask(@Nullable Looper callbackLooper) {
    // 1. 初始化用来发送消息的 Handler
    mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper()
    ? getMainHandler()
    : new Handler(callbackLooper);

     // 2. 封装一个对象用来执行我们的任务
     mWorker = new WorkerRunnable<Params, Result>() {
         public Result call() throws Exception {
             mTaskInvoked.set(true);
             Result result = null;
             try {
                 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                 // 回调我们的业务逻辑
                 result = doInBackground(mParams);
                 Binder.flushPendingCommands();
             } catch (Throwable tr) {
                 mCancelled.set(true);
                 throw tr;
             } finally {
                 // 发送结果给主线程
                 postResult(result);
             }
             return result;
         }
     };
    
     // 3. 初始化一个 FutureTask,并且当它执行完毕的时候,会调用 postResultIfNotInvoked 来将消息的执行结果发送到主线程中
     mFuture = new FutureTask<Result>(mWorker) {
         @Override
         protected void done() {
             try {
                 // 如果任务没有被触发,也要发送一个结果
                 postResultIfNotInvoked(get());
             } 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);
             }
         }
     };
    

    }

当这样设置完毕之后,我们就可以使用 execute() 方法来开始执行任务了。

2.2 AsyncTask 中任务的串行执行过程

我们从 execute() 方法开始分析 AsyncTask,

@MainThread
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
    return executeOnExecutor(sDefaultExecutor, params);
}

@MainThread
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec, Params... params) {
    if (mStatus != Status.PENDING) { // 1.判断线程当前的状态
        switch (mStatus) {
            case RUNNING: throw new IllegalStateException(...);
            case FINISHED: throw new IllegalStateException(...);
        }
    }
    mStatus = Status.RUNNING;
    onPreExecute(); // 2.回调生命周期方法
    mWorker.mParams = params; // 3.赋值给可执行的对象 WorkerRunnable
    exec.execute(mFuture); // 4.在线程池中执行任务
    return this;
}

当我们调用 AsyncTask 的 execute() 方法的时候会立即调用它的 executeOnExecutor() 方法。这里传入了两个参数,分别是一个 Executor 和任务的参数 params。从上面我们可以看出,当直接调用 execute() 方法的时候会使用默认的线程池 sDefaultExecutor,而当我们指定了线程池之后,会使用我们指定的线程池来执行任务。

在 1 处,会对 AsyncTask 当前的状态进行判断,这就对应了前面说的,一个任务只能被执行一次。在 2 处会调用 onPreExecute() 方法,如果我们覆写了该方法,那么它就会在这个时候被调用。在 3 处的操作是在为 mWorker 赋值,即把调用 execute 方法时传入的参数赋值给了 mWorker。接下来,会将 mFuture 添加到线程池中执行。

当我们不指定任何线程池的时候使用的 sDefaultExecutor 是一个串行的线程池,它的定义如下:

public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;

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 {
                    // 相当于对传入的Runnable进行了一层包装
                    r.run();
                } finally {
                    // 分配下一个任务
                    scheduleNext();
                }
            }
        });
        // 如果当前没有正在执行的任务,那么就尝试从队列中取出并执行
        if (mActive == null) {
            scheduleNext();
        }
    }

    protected synchronized void scheduleNext() {
        // 从队列中取任务并使用THREAD_POOL_EXECUTOR执行
        if ((mActive = mTasks.poll()) != null) {
            THREAD_POOL_EXECUTOR.execute(mActive);
        }
    }
}

从上面我们可以看出,我们添加到线程池中的任务实际上并没有直接交给线程池来执行,而是对其进行了处理之后才执行的,SerialExecutor 通过内部维护了双端队列,每当一个 AsyncTask 调用 execute() 方法的时候都会被放在该队列当中进行排队。如果当前没有正在执行的任务,那么就从队列中取一个任务交给 THREAD_POOL_EXECUTOR 执行;当一个任务执行完毕之后又会调用 scheduleNext() 取下一个任务执行。也就是说,实际上 sDefaultExecutor 在这里只是起了一个任务调度的作用,任务最终还是交给 THREAD_POOL_EXECUTOR 执行的。

这里的THREAD_POOL_EXECUTOR也是一个线程池,它在静态代码块中被初始化:

static {
    // 使用指定的参数创建一个线程池
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
            sPoolWorkQueue, sThreadFactory);
    threadPoolExecutor.allowCoreThreadTimeOut(true);
    THREAD_POOL_EXECUTOR = threadPoolExecutor;
}

我们也可以直接将这个静态的线程池作为我们任务执行的线程池而不是放在上面的队列中被串行地执行。

2.3 将任务执行的结果发送到其他线程

上面的 WorkerRunnable 中已经用到了 postResult 方法,它用来将任务执行的结果发送给 Handler:

private Result postResult(Result result) {
    @SuppressWarnings("unchecked")
    Message message = mHandler.obtainMessage(MESSAGE_POST_RESULT, new AsyncTaskResult<Result>(this, result));
    message.sendToTarget();
    return result;
}

mHandler 会在创建 AsyncTask 的时候初始化。我们可以通过 AsyncTask 的构造方法传入 Handler 和 Looper 来指定该对象所在的线程。当我们没有指定的时候,会使用 AsyncTask 内部的 InternalHandler 创建 Handler:

private final Handler mHandler;

public AsyncTask(@Nullable Looper callbackLooper) {
    // 根据传入的参数创建Handler对象
    mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper() 
        ? getMainHandler() : new Handler(callbackLooper);
}

private static Handler getMainHandler() {
    synchronized (AsyncTask.class) {
        if (sHandler == null) {
            // 使用 InternalHandler 创建对象
            sHandler = new InternalHandler(Looper.getMainLooper());
        }
        return sHandler;
    }
}

// AsyncTask 内部定义 的Handler 类型
private static class InternalHandler extends Handler {
    public InternalHandler(Looper looper) {
        super(looper);
    }

    @Override public void handleMessage(Message msg) {
        AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
        // 根据传入的消息类型进行处理
        switch (msg.what) {
            case MESSAGE_POST_RESULT: result.mTask.finish(result.mData[0]); break;
            case MESSAGE_POST_PROGRESS: result.mTask.onProgressUpdate(result.mData); break;
        }
    }
}

3、总结

上面我们梳理了 AsyncTask 的大致过程,我们来梳理下:

每当我们实例化一个 AsyncTask 的时候都会在内部封装成一个 Runnable 对象,该对象可以直接放在线程池中执行。这里存在两个线程池,一个是 SerialExecutor 一个是 THREAD_POOL_EXECUTOR,前者主要用来进行任务调度,即把交给线程的任务放在队列中进行排队执行,而时机上所有的任务都是在后者中执行完成的。这个两个线程池都是静态的字段,所以它们对应于整个类的。也就是说,当使用默认的线程池的时候,实例化的 AsyncTask 会一个个地,按照加入到队列中的顺序依次执行。

当任务执行完毕之后,使用 Handler 来将消息发送到主线程即可,这部分的逻辑主要与 Handler 机制相关,可以通过这篇文章来了解:《Android 消息机制:Handler、MessageQueue 和 Looper》。

以上就是 AsyncTask 的主要内容。


如果您喜欢我的文章,可以在以下平台关注我:

Android AsyncTask 源码解析 代码为博客的示例代码,有问题请博客中留言:http://blog.youkuaiyun.com/lmj623565791/article/details/38614699 立即下载

相关推荐

AsyncTask源码

最新AsyncTask源码

Android开发——AsyncTask的使用以及源码解析

1.AsyncTask使用介绍 AsyncTask封装了Thread和Handler,通过AsyncTask可以很方便地在执行完后台任务后更新UI。如果不太清楚Android的Handler机制,可以查看此篇Android消息机制详解。 下面是一个使用AsyncTask的实例,利用网络下载某URL里的字符串,以模拟耗时任务。在下载过程中,会通过进度条对话框向用户展示进度。在完成任务后将字...

关于Android开发的一些技术点总结 ╮( ̄▽ ̄”)╭ 1万+

双系统引导修复:GRUB消失后恢复Windows与Ubuntu启动菜单

在UEFI引导架构下,操作系统启动依赖于EFI系统分区(ESP)中存储的引导加载程序。当多个系统共存时,引导管理器负责识别并加载不同操作系统的引导文件。然而,Windows更新或系统调整时常会重置UEFI启动顺序,导致GRUB引导菜单消失,直接进入Windows。这一问题的技术价值在于理解UEFI启动流程与多系统引导管理机制,掌握ESP分区结构与引导文件修复方法。常见的应用场景包括双系统安装后的引导修复、系统更新后的启动项丢失以及磁盘分区调整导致的引导故障。本文针对**GRUB引导菜单消失**和**UEFI

weixin_29895109的博客 292

【Android -- 学习笔记】AsyncTask 源码分析

AsyncTask 源码分析

Kevin Learn Android 482

洛谷 P5736 【深基7.例2】质数筛

题目描述 输入 n(n\le100)n(n≤100) 个不大于 100000 的整数。要求全部储存在数组中,去除掉不是质数的数字,依次输出剩余的质数。 输入格式 无 输出格式 无 输入输出样例 输入 #1 复制 5 3 4 5 6 7 输出 #1 复制 3 5 7 板子题,使用一下线性筛 #include <iostream> #include <cstdio> #include <cstring> #include <algorithm> using n

qq_37978559的博客 1117

Mac 不小心断开移动硬盘导致磁盘无法读取和加载(顺利解决!)

Mac移动硬盘不正确断开连接导致无法读取加载的解决方法,亲测有效

Robin_Pi的博客 1万+

Android 源码分析之 AsyncTask 源码分析

1、AsyncTask的使用 使用 AsyncTask 可以更加简单地实现任务的异步执行,以及任务执行完毕之后与主线程的交互。它被设计用来执行耗时比较短的任务,通常是几秒种的那种,如果要执行耗时比较长的任务,那么就应该使用 JUC 包中的框架,比如 ThreadPoolExecutor 和 FutureTask等。 AsyncTask 用来在后台线程中执行任务,当任务执行完毕之后将结果发送到主线程...

weixin_34148508的博客 212

【推荐100个unity插件之34】在unity中实现和Live2D虚拟人物的交互——Cubism SDK for Unity

Live2D是一款2D建模的软件,常用于游戏,动态壁纸,虚拟主播,游戏公司里面是有专门的ive2D岗位的。游戏里面还有一个经常使用的软件——spine,他做的也是2d的模型,但是他更适合做一些动作,Live2D更适合人物交互方面。

向宇的博客,专注php/web全栈 unity游戏开发,欢迎大家评论纠错 4987

AsyncTask源码分析

1. 创建对象 private class DownloadFilesTask extends AsyncTask&lt;URL, Integer, Long&gt; { protected Long doInBackground(URL... urls) { int count = urls.length; long totalSize = 0; for (int i = 0;...

lyl0530的博客 216

AI辅助需求分析与提示词生成

文章标题 《AI辅助需求分析与提示词生成》 关键词 人工智能 需求分析 提示词生成 深度学习 机器学习 强化学习 数学模型 系统架构

AI天才研究院 1211

【STM32实物】基于STM32设计的智能仓储管理系统

近年来,随着我国仓储发展的和药品需求的不断增多,许多医院都采用药物仓储管理系统。我国的药物仓储产业已经有了长足的发展,仓库的规模不断变大,对仓储的要求也不断增高,药物的存储,识别以及对仓储环境条件的要求成为仓储的重要决策,他们可以根据不同的特点在不同场景中使用。本文详细介绍了一个基于单片机技术的智能仓储管理系统的设计与实现,这个系统可谓是集射频识别(RFID)技术和环境监测传感器于一身,旨在提高仓储操作的自动化和精确度。

嵌入式阿齐Archie 1万+

Android AsyncTask源码解析,深入理解Nginx

3. Result 当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。 因此,一个最简单的自定义AsyncTask就可以写成如下方式: [java]  view plain copy class DownloadTask extends AsyncTask<Void, Integer, Boolean> { …… } 这里我们把AsyncTask的第一个泛型参数指定为Void,表示在

m0_64382868的博客 3436

120、ISP 驱动架构解析:从 V4L2 请求到 ISP 硬件的配置下发流程

本文以实际案例解析ISP驱动架构的V4L2到硬件配置下发流程。作者通过一个因内存共享导致画面闪烁的bug案例,详细剖析了用户空间参数通过V4L2结构体传递、驱动层拆包与硬件寄存器映射、时序同步机制等关键环节。特别强调深拷贝、dirty flag增量更新、shadow register等实现要点,并给出多路并发隔离、寄存器写入失败等典型问题的排查方法。文章建议开发者深入理解参数到寄存器的转换过程,推荐通过debugfs实时监控寄存器状态,避免过度依赖自动调试工具。全文以实践为导向,对ISP驱动开发具有直接指导

sundong_81的博客 159

【安卓学习积累】AsyncTask的源码分析

在记录本文之前有参考下面大神的文章,本文也加入了一些自己的见解。旨在记录和学习一些Android知识。欢迎大家一起来讨论。 别再傻傻得认为AsyncTask只可以在主线程中创建实例和调用execute方法 一、抛出几个问题 1.AsyncTask是否只能在主线程中创建并执行呢? 2.AsyncTask中的onPreExecute()、doInBackground(String... par...

xiangzaixiansheng的博客 219

13、Java 中 MQTT 异步通知与回调的使用及传感器命令处理

本文详细介绍了在 Java 中使用 MQTT 的异步通知与回调机制,并展示了如何通过 Paho Java 客户端处理与传感器交互的命令。文章包含代码示例,讲解了 MQTT 客户端的连接、订阅、消息处理以及模拟传感器数据发布的过程,适用于希望在 Java 应用中实现 MQTT 通信与设备控制的开发者。

eee77的博客 159

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

Android | AsyncTask源码分析

当应用需要执行耗时的任务时,为了保持应用的平滑流畅和避免ANR,我们需要使用工作线程来执行耗时的任务。当耗时的任务执行完毕时,如果需要更新UI界面,那么我们需要使用Handler来通知UI线程更新界面。为了简化这些操作,方便开发人员编程,Android系统提供了一个AsyncTask类。使用AsyncTask,你可以很方便地执行后台操作,并在UI线程中更新界面,而无需直接操作Thread和Handler类。

cyc的专栏 3371

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

【Android】AsyncTask源码分析

在Android中ui是非线程安全的,更新ui只能在主线程操作,所以我们平时如果遇到子线程更新UI的情况,必须要切换到主线程上之后再执行,Handler消息机制就是一种处理方案,为了更方便的处理异步任务以及更新操作,Android在很早的版本就提供了AsyncTask类供我们处理短耗时的异步任务,方便我们子线程处理耗时任务,主线程更新UI。 AsyncTask其实相当于是Handler和Threa...

风吹起如花般细碎流年 463

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

Android AsyncTask使用步骤与源码分析

AsyncTask的一个典型的应用场景是:后台下载文件,并实时跟新下载进度。它既能使得耗时的事情在后台线程中执行,又能和主线程通信,告诉主线程更新UI。同时,AsyncTask内部使用了线程池来执行后台任务,因此它能处理多任务请求。那么它的内部是怎么实现的呢?

阳光玻璃杯 914

Android中AsyncTask的使用和源码分析

前言: 在上一篇Android中的线程和线程池的分析中我们可以看到,异步任务的线程池的一些配置. 连接地址:Android中的线程和线程池 一.在Android中AsyncTask的使用 首先: AsyncTask类是一个轻量级的异步任务类,他可以在线程池中执行后台任务;将执行的进度和任务传递到主线程中来更新UI. 缺点: 是一个轻量级的异步任务类,并不适合与特别耗时的后台操作. A

kunkun5love的博客 432

Android应用AsyncTask处理机制详解及源码分析

工匠若水 http://blog.youkuaiyun.com/yanbober 1 背景 当使用线程和Handler组合实现异步处理时,当每次执行耗时操作都创建一条新线程进行处理,性能开销会比较大。为了提高性能我们使用AsyncTask实现异步处理(其实也是线程和handler组合实现),因为其内部使用了java提供的线程池技术,有效的降低了线程创建数量及限定了同时运行的线程数,还有一些

朱志辉的博客 738

Android应用AsyncTask处理机制详解及源码分析,移动互联网开发

Android异步处理机制一直都是Android的一个核心,也是应用工程师面试的一个知识点。前面我们分析了Handler异步机制原理(不了解的可以阅读我的《Android异步消息处理机制详解及源码分析》文章),这里继续分析Android的另一个异步机制AsyncTask的原理。 当使用线程和Handler组合实现异步处理时,当每次执行耗时操作都创建一条新线程进行处理,性能开销会比较大。为了提高性能我们使用AsyncTask实现异步处理(其实也是线程和handler组合实现),因为其内部使用了java提供的线

m0_64383184的博客 446

【转载】Android应用AsyncTask处理机制详解及源码分析

【工匠若水 http://blog.youkuaiyun.com/yanbober 转载烦请注明出处,尊重分享成果】 1 背景 Android异步处理机制一直都是Android的一个核心,也是应用工程师面试的一个知识点。前面我们分析了Handler异步机制原理(不了解的可以阅读我的《Android异步消息处理机制详解及源码分析》文章),这里继续分析Android的另一个异步机制Async...

aoshu5722的博客 174

Android AsyncTask运作原理和源码分析

Android AsyncTask运作原理和源码分析本文转自:http://www.cnblogs.com/answer1991/archive/2012/04/17/2454073.html 在此感谢原作者的辛勤贡献分享。拿来学习之。。。public abstract class AsyncTask { private static final String LOG_TAG = "AsyncTa

IceDream_hong的专栏 981

android asynctask源码分析,Android AsyncTask源码分析

Android中只能在主线程中进行UI操作,如果是其它子线程,需要借助异步消息处理机制Handler。除此之外,还有个非常方便的AsyncTask类,这个类内部封装了Handler和线程池。本文先简要介绍AsyncTask的用法,然后分析具体实现。基本用法AsyncTask是一个抽象类,我们需要创建子类去继承它,并且重写一些方法。AsyncTask接受三个泛型参数:Params: 指定传给任务执行...

weixin_30082211的博客 352
上一篇: Guava 源码分析之 EventBus 源码分析
下一篇: 设计模式-观察者模式
开发者如是说
博客等级 码龄10年 67粉丝 163原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值