Android Loader 异步加载详解一:基础概念

转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/70241844
本文出自【赵彦军的博客】

Android Loader 异步加载详解一:基础概念
Android Loader 异步加载详解二:探寻Loader内部机制

前言

Android 3.0 中引入了 Loader (加载器),支持轻松在 Activity 或片段中异步加载数据。 加载器具有以下特征:

  • 可用于每个 Activity 和 Fragment。
  • 支持异步加载数据。
  • 监控其数据源并在内容变化时传递新结果。
  • 在某一配置更改后重建加载器时,会自动重新连接上一个加载器的游标。 因此,它们无需重新查询其数据。

Loader API概述说明

如下是我们开发中常用的一些Loader相关接口:

Class/InterfaceDescription
LoaderManager一个与Activity、Fragment关联的抽象类,用于管理一个或多个Loader实例。每个Activity或Fragment只能有一个LoaderManager,而一个LoaderManager可以有多个Loader。
LoaderManager.LoaderCallbacks用于和LoaderManager交互的回调接口。譬如,可以使用onCreateLoader()创建一个新的Loader。
AsyncTaskLoader抽象的Loader,提供一个AsyncTask继承实现。
CursorLoaderAsyncTaskLoader的子类,用于向ContentResover请求返回一个Cursor。该类以标准游标查询实现了Loader协议,使用后台线程进行查询,使用这个Loader是从ContentProvider加载异步数据最好的方式。

启动一个 Loader

initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback)

可以看见上面的initLoader()方法有三个参数:

  • 第一个参数代表当前Loader的ID;
  • 第二个参数代表提供给Loader构造函数的参数,可选;
  • 第三个参数代表LoaderManager.LoaderCallbacks的回调实现;

上面initLoader()方法的调用确保了一个Loader被初始化和激活的状态,该方法的调运有如下两种结果:

  • 如果代表该Loader的ID已经存在,则后面创建的Loader将直接复用已经存在的;
  • 如果代表该Loader的ID不存在,initLoader()会触发LoaderManager.LoaderCallbacks回调的onCreateLoader()方法创建一个Loader;

可以看见通过initLoader()方法可以将LoaderManager.LoaderCallbacks实例与Loader进行关联,且当Loader的状态变化时就被回调。所以说,如果调用者正处于其开始状态并且被请求的Loader已经存在,且已产生了数据,那么系统会立即调用onLoadFinished()(在initLoader()调用期间),所以你必须考虑到这种情况的发生。

当然了,intiLoader()会返回一个创建的Loader,但是你不用获取它的引用,因为LoadeManager会自动管理该Loader的生命周期,你只用在它回调提供的生命周期方法中做自己数据逻辑的处理即可。

Loader 基类的源码分析

Loader 是最底层的代码逻辑封装,没有具体的业务实现的部分。这个有点像我们平时写的 BaseActivity 一样。现在我们来对这个基类的源码做一个简单的分析。

public class Loader<D> {
    int mId;
    OnLoadCompleteListener<D> mListener;
    OnLoadCanceledListener<D> mOnLoadCanceledListener;
    Context mContext;
    boolean mStarted = false;
    boolean mAbandoned = false;
    boolean mReset = true;
    boolean mContentChanged = false;
    boolean mProcessingChange = false;

    //数据源变化监听器(观察者模式),实现了ContentObserver类
    public final class ForceLoadContentObserver extends ContentObserver {
        public ForceLoadContentObserver() {
            super(new Handler());
        }

        @Override
        public boolean deliverSelfNotifications() {
            return true;
        }

        @Override
        public void onChange(boolean selfChange) {
           //实质是调运Loader的forceLoad方法
            onContentChanged();
        }
    }


 //Loader加载完成接口,当加载完成时Loader通知loaderManager,loaderManager再回调我们initLoader方法的callback

    public interface OnLoadCompleteListener<D> {
        /**
         * Called on the thread that created the Loader when the load is complete.
         *
         * @param loader the loader that completed the load
         * @param data the result of the load
         */
        public void onLoadComplete(Loader<D> loader, D data);
    }

    //LoaderManager中监听cancel,同上类似
    public interface OnLoadCanceledListener<D> {
        /**
         * Called on the thread that created the Loader when the load is canceled.
         *
         * @param loader the loader that canceled the load
         */
        public void onLoadCanceled(Loader<D> loader);
    }

    //构造方法
    public Loader(Context context) {
        //mContext持有Application的Context,防止泄露内存等
        mContext = context.getApplicationContext();
    }

    //加载完成时回调传递加载数据结果,实质是对OnLoadCompleteListener接口方法的封装
    public void deliverResult(D data) {
        if (mListener != null) {
            mListener.onLoadComplete(this, data);
        }
    }

   //类似同上,对OnLoadCanceledListener的方法的封装
    public void deliverCancellation() {
        if (mOnLoadCanceledListener != null) {
            mOnLoadCanceledListener.onLoadCanceled(this);
        }
    }

    public Context getContext() {
        return mContext;
    }

    public int getId() {
        return mId;
    }

    public void registerListener(int id, OnLoadCompleteListener<D> listener) {
        if (mListener != null) {
            throw new IllegalStateException("There is already a listener registered");
        }
        mListener = listener;
        mId = id;
    }

    public void unregisterListener(OnLoadCompleteListener<D> listener) {
        if (mListener == null) {
            throw new IllegalStateException("No listener register");
        }
        if (mListener != listener) {
            throw new IllegalArgumentException("Attempting to unregister the wrong listener");
        }
        mListener = null;
    }


    public void registerOnLoadCanceledListener(OnLoadCanceledListener<D> listener) {
        if (mOnLoadCanceledListener != null) {
            throw new IllegalStateException("There is already a listener registered");
        }
        mOnLoadCanceledListener = listener;
    }


    public void unregisterOnLoadCanceledListener(OnLoadCanceledListener<D> listener) {
        if (mOnLoadCanceledListener == null) {
            throw new IllegalStateException("No listener register");
        }
        if (mOnLoadCanceledListener != listener) {
            throw new IllegalArgumentException("Attempting to unregister the wrong listener");
        }
        mOnLoadCanceledListener = null;
    }


    public boolean isStarted() {
        return mStarted;
    }


    public boolean isAbandoned() {
        return mAbandoned;
    }


    public boolean isReset() {
        return mReset;
    }


    //开始加载数据时LoaderManager会调用该方法
    //必须在 main thread 线程调用
    public final void startLoading() {
        mStarted = true;
        mReset = false;
        mAbandoned = false;
        onStartLoading();
    }

    //真正开始加载数据的地方******空方法,子类实现!!!!!!
    protected void onStartLoading() {
    }

    //取消Loader的方法
    public boolean cancelLoad() {
        return onCancelLoad();
    }

   //真正取消的地方******,子类实现!!!!!!return false表示取消失败(因为已完成或未开始)
    protected boolean onCancelLoad() {
        return false;
    }

    //强制重新Loader,放弃旧数据
    public void forceLoad() {
        onForceLoad();
    }

    //真正重新Loader的地方******空方法,子类实现!!!!!!
    protected void onForceLoad() {
    }

    //停止 Loading ,具体实现交给子类
    //必须在 main 线程调用
    public void stopLoading() {
        mStarted = false;
        onStopLoading();
    }

    //空实现
    protected void onStopLoading() {
    }

   //同上
    public void abandon() {
        mAbandoned = true;
        onAbandon();
    }

   //同上
    protected void onAbandon() {
    }

    //同上
    public void reset() {
        onReset();
        mReset = true;
        mStarted = false;
        mAbandoned = false;
        mContentChanged = false;
        mProcessingChange = false;
    }

    //同上
    protected void onReset() {
    }

    //Loader数据变化的一些标记处理
    public boolean takeContentChanged() {
        boolean res = mContentChanged;
        mContentChanged = false;
        mProcessingChange |= res;
        return res;
    }

    public void commitContentChanged() {
        mProcessingChange = false;
    }

    public void rollbackContentChanged() {
        if (mProcessingChange) {
            onContentChanged();
        }
    }

    //上面ForceLoadContentObserver内部类的onChange方法调运
    public void onContentChanged() {
        if (mStarted) {
            forceLoad();
        } else {
            // This loader has been stopped, so we don't want to load
            // new data right now...  but keep track of it changing to
            // refresh later if we start again.
            mContentChanged = true;
        }
    }

    //一些方便调试的方法
    public String dataToString(D data) {
        StringBuilder sb = new StringBuilder(64);
        DebugUtils.buildShortClassTag(data, sb);
        sb.append("}");
        return sb.toString();
    }

    //一些方便调试的方法
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder(64);
        DebugUtils.buildShortClassTag(this, sb);
        sb.append(" id=");
        sb.append(mId);
        sb.append("}");
        return sb.toString();
    }

    //一些方便调试的方法
    public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
        writer.print(prefix); writer.print("mId="); writer.print(mId);
                writer.print(" mListener="); writer.println(mListener);
        if (mStarted || mContentChanged || mProcessingChange) {
            writer.print(prefix); writer.print("mStarted="); writer.print(mStarted);
                    writer.print(" mContentChanged="); writer.print(mContentChanged);
                    writer.print(" mProcessingChange="); writer.println(mProcessingChange);
        }
        if (mAbandoned || mReset) {
            writer.print(prefix); writer.print("mAbandoned="); writer.print(mAbandoned);
                    writer.print(" mReset="); writer.println(mReset);
        }
    }

通过上面粗略的分析可以发现,Loader基类无非也就是一个方法接口的定义类,组织预留了一些方法供LoaderManager去调运处理,同时需要子类实现其提供的一些onXXX方法,以便LoaderManager调运Loader的方法时可以触发Loader子类的实现逻辑。

AsyncTaskLoader 运用实例详解

上面我们说了 Loader 只是一个基类,那么要实现具体的业务类,必须有 子类继承Loader ,并且实现 Loader 里面的空方法。幸运的是,系统已经帮我们实现了一个子类 , 它就是 AsyncTaskLoader 。

AsyncTaskLoader 源码分析:


public abstract class AsyncTaskLoader<D> extends Loader<D> {
    static final String TAG = "AsyncTaskLoader";
    static final boolean DEBUG = false;

    //LoadTask内部类是对AsyncTask的封装,实现了Runnable接口
    final class LoadTask extends AsyncTask<Void, Void, D> implements Runnable {
        private final CountDownLatch mDone = new CountDownLatch(1);
        boolean waiting;

        //AsyncTask的子线程中执行AsyncTaskLoader的onLoadInBackground方法!!!!重点

        @Override
        protected D doInBackground(Void... params) {

            try {
                D data = AsyncTaskLoader.this.onLoadInBackground();
                //把执行结果数据D返回到UI线程
                return data;
            } catch (OperationCanceledException ex) {
                if (!isCancelled()) {
                    throw ex;
                }

                return null;
            }
        }

        //AsyncTask子线程执行完毕后在主线程回调AsyncTaskLoader的dispatchOnLoadComplete方法
        /* Runs on the UI thread */
        @Override
        protected void onPostExecute(D data) {
            try {
                AsyncTaskLoader.this.dispatchOnLoadComplete(this, data);
            } finally {
                mDone.countDown();
            }
        }

        /* Runs on the UI thread */
        @Override
        protected void onCancelled(D data) {
            if (DEBUG) Log.v(TAG, this + " onCancelled");
            try {
                //取消AsyncTask时调用
                AsyncTaskLoader.this.dispatchOnCancelled(this, data);
            } finally {
                mDone.countDown();
            }
        }

        //Runnable的实现方法
        /* Runs on the UI thread, when the waiting task is posted to a handler.
         * This method is only executed when task execution was deferred (waiting was true). */
        @Override
        public void run() {
            waiting = false;
            AsyncTaskLoader.this.executePendingTask();
        }

        /* Used for testing purposes to wait for the task to complete. */
        public void waitForLoader() {
            try {
                mDone.await();
            } catch (InterruptedException e) {
                // Ignore
            }
        }
    }

    private final Executor mExecutor;

    volatile LoadTask mTask;
    volatile LoadTask mCancellingTask;

    long mUpdateThrottle;
    long mLastLoadCompleteTime = -10000;
    Handler mHandler;

    //public构造方法 
    public AsyncTaskLoader(Context context) {
        this(context, AsyncTask.THREAD_POOL_EXECUTOR);
    }

    /** {@hide}  无法被外部调运的构造方法 */
    public AsyncTaskLoader(Context context, Executor executor) {
        super(context);
        mExecutor = executor;
    }


    public void setUpdateThrottle(long delayMS) {
        mUpdateThrottle = delayMS;
        if (delayMS != 0) {
            mHandler = new Handler();
        }
    }

    @Override
    protected void onForceLoad() {
        super.onForceLoad();
        //取消当前的Loader执行
        cancelLoad();
        //新建task并执行
        mTask = new LoadTask();
        if (DEBUG) Log.v(TAG, "Preparing load: mTask=" + mTask);
        executePendingTask();
    }

    @Override
    protected boolean onCancelLoad() {
        if (DEBUG) Log.v(TAG, "onCancelLoad: mTask=" + mTask);
        if (mTask != null) {
            if (mCancellingTask != null) {
                if (mTask.waiting) {
                    mTask.waiting = false;
                    mHandler.removeCallbacks(mTask);
                 }
                mTask = null;
                return false;
            } else if (mTask.waiting) {
                // There is a task, but it is waiting for the time it should
                // execute.  We can just toss it.
                if (DEBUG) Log.v(TAG, "cancelLoad: task is waiting, dropping it");
                mTask.waiting = false;
                mHandler.removeCallbacks(mTask);
                mTask = null;
                return false;
            } else {
                boolean cancelled = mTask.cancel(false);
                if (DEBUG) Log.v(TAG, "cancelLoad: cancelled=" + cancelled);
                if (cancelled) {
                    mCancellingTask = mTask;
                    cancelLoadInBackground();
                }
                mTask = null;
                return cancelled;
            }
        }
        return false;
    }

    /**
     * Called if the task was canceled before it was completed.  Gives the class a chance
     * to clean up post-cancellation and to properly dispose of the result.
     *
     * @param data The value that was returned by {@link #loadInBackground}, or null
     * if the task threw {@link OperationCanceledException}.
     */
    public void onCanceled(D data) {
    }

    //LoadTask的Runnable方法run中执行
    void executePendingTask() {
        if (mCancellingTask == null && mTask != null) {
            if (mTask.waiting) {
                mTask.waiting = false;
                mHandler.removeCallbacks(mTask);
            }
            if (mUpdateThrottle > 0) {
                long now = SystemClock.uptimeMillis();
                if (now < (mLastLoadCompleteTime+mUpdateThrottle)) {
                    // Not yet time to do another load.

                    mTask.waiting = true;
                    mHandler.postAtTime(mTask, mLastLoadCompleteTime+mUpdateThrottle);
                    return;
                }
            }

            //真正的触发执行AsyncTask方法
            mTask.executeOnExecutor(mExecutor, (Void[]) null);
        }
    }

    void dispatchOnCancelled(LoadTask task, D data) {
        onCanceled(data);
        if (mCancellingTask == task) {
            rollbackContentChanged();
            mLastLoadCompleteTime = SystemClock.uptimeMillis();
            mCancellingTask = null;
           //触发Loader的接口方法onLoadCanceled,在LoaderManager中实现
            deliverCancellation();
            executePendingTask();
        }
    }

    void dispatchOnLoadComplete(LoadTask task, D data) {
        if (mTask != task) {
            if (DEBUG) Log.v(TAG, "Load complete of old task, trying to cancel");
            dispatchOnCancelled(task, data);
        } else {
            if (isAbandoned()) {
                // This cursor has been abandoned; just cancel the new data.
                onCanceled(data);
            } else {
                commitContentChanged();
                mLastLoadCompleteTime = SystemClock.uptimeMillis();
                mTask = null;
                if (DEBUG) Log.v(TAG, "Delivering result");
                //触发Loader的接口方法onLoadComplete,在LoaderManager中实现
                deliverResult(data);
            }
        }
    }

    //需要子类实现!!!!!在子线程中执行
    public abstract D loadInBackground();

   //LoadTask(AsyncTask的子线程中回调)中调运
    protected D onLoadInBackground() {
        return loadInBackground();
    }

    //LoadTask(AsyncTask的onCancelLoad中回调)调运
    public void cancelLoadInBackground() {
    }


    public boolean isLoadInBackgroundCanceled() {
        return mCancellingTask != null;
    }

    //锁标记处理
    public void waitForLoader() {
        LoadTask task = mTask;
        if (task != null) {
            task.waitForLoader();
        }
    }

    @Override
    public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
        super.dump(prefix, fd, writer, args);
        if (mTask != null) {
            writer.print(prefix); writer.print("mTask="); writer.print(mTask);
                    writer.print(" waiting="); writer.println(mTask.waiting);
        }
        if (mCancellingTask != null) {
            writer.print(prefix); writer.print("mCancellingTask="); writer.print(mCancellingTask);
                    writer.print(" waiting="); writer.println(mCancellingTask.waiting);
        }
        if (mUpdateThrottle != 0) {
            writer.print(prefix); writer.print("mUpdateThrottle=");
                    TimeUtils.formatDuration(mUpdateThrottle, writer);
                    writer.print(" mLastLoadCompleteTime=");
                    TimeUtils.formatDuration(mLastLoadCompleteTime,
                            SystemClock.uptimeMillis(), writer);
                    writer.println();
        }
    }
}

可以看见上面继承Loader的AsyncTaskLoader其实质是提供了一个基于AsyncTask工作机制的Loader(子类LoadTask继承AsyncTask< Void, Void, D >,并且实现了Runable接口,功能十分强大。),但是不可直接用,因为其为abstract抽象类,所以我们需要继承实现它才可以使用,然而好在系统API已经帮我们提供了他现成的子类CursorLoader,但CursorLoader同时也限制了Loader的泛型数据为Cursor类型。当然了,我们如果想要Loader自己的类型数据那也很简单—继承实现AsyncTaskLoader即可,后面会给出例子的。

CursorLoader子类源码浅析

有了上面继承自Loader的抽象AsyncTaskLoader,接下来我们就来看看SDK为我们提供的抽象AsyncTaskLoader实现类CursorLoader,我们先来粗略看看该类的方法图,如下:

这里写图片描述

//CursorLoader 继承 AsyncTaskLoader , 数据类型为Cursor的Loader异步加载实现类
public class CursorLoader extends AsyncTaskLoader<Cursor> {
    //ContentObserver的子类ForceLoadContentObserver
    final ForceLoadContentObserver mObserver;

    Uri mUri;
    String[] mProjection;
    String mSelection;
    String[] mSelectionArgs;
    String mSortOrder;

    Cursor mCursor;
    CancellationSignal mCancellationSignal;

    /* Runs on a worker thread */
    //最核心的实现方法,在这里查询获取数据 
    @Override
    public Cursor loadInBackground() {
        synchronized (this) {
            if (isLoadInBackgroundCanceled()) {
                throw new OperationCanceledException();
            }
            mCancellationSignal = new CancellationSignal();
        }
        try {
            //不过多解释,耗时的查询操作
            Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection,
                    mSelectionArgs, mSortOrder, mCancellationSignal);
            if (cursor != null) {
                try {
                    // Ensure the cursor window is filled.
                    cursor.getCount();
                    //给Cursor设置观察者;ContentProvider通知Cursor的观察者数据发生了改变,Cursor通知CursorLoader的观察者数据发生了改变,CursorLoader通过ContentProvider重新加载新的数据
                    cursor.registerContentObserver(mObserver);
                } catch (RuntimeException ex) {
                    cursor.close();
                    throw ex;
                }
            }
            return cursor;
        } finally {
            synchronized (this) {
                mCancellationSignal = null;
            }
        }
    }

    @Override
    public void cancelLoadInBackground() {
        super.cancelLoadInBackground();

        synchronized (this) {
            if (mCancellationSignal != null) {
                mCancellationSignal.cancel();
            }
        }
    }

    /* Runs on the UI thread */
    @Override
    public void deliverResult(Cursor cursor) {
        if (isReset()) {
            // An async query came in while the loader is stopped
            if (cursor != null) {
                cursor.close();
            }
            return;
        }
        Cursor oldCursor = mCursor;
        mCursor = cursor;

        if (isStarted()) {
            super.deliverResult(cursor);
        }

        if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) {
            oldCursor.close();
        }
    }

    /**
     * Creates an empty unspecified CursorLoader.  You must follow this with
     * calls to {@link #setUri(Uri)}, {@link #setSelection(String)}, etc
     * to specify the query to perform.
     */
    public CursorLoader(Context context) {
        super(context);
        mObserver = new ForceLoadContentObserver();
    }

    /**
     * Creates a fully-specified CursorLoader.  See
     * {@link ContentResolver#query(Uri, String[], String, String[], String)
     * ContentResolver.query()} for documentation on the meaning of the
     * parameters.  These will be passed as-is to that call.
     */
    public CursorLoader(Context context, Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {
        super(context);
        //新建一个当前类(Loader)的内部类对象,数据库变化时调运ForceLoadContentObserver的onChange方法,onChange调运Loader的onContentChanged方法,onContentChanged调运Loader的forceLoad方法
        mObserver = new ForceLoadContentObserver();
        mUri = uri;
        mProjection = projection;
        mSelection = selection;
        mSelectionArgs = selectionArgs;
        mSortOrder = sortOrder;
    }

    /**
     * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks
     * will be called on the UI thread. If a previous load has been completed and is still valid
     * the result may be passed to the callbacks immediately.
     *
     * Must be called from the UI thread
     */
    @Override
    protected void onStartLoading() {
        if (mCursor != null) {
            deliverResult(mCursor);
        }
        if (takeContentChanged() || mCursor == null) {
            forceLoad();
        }
    }

    /**
     * Must be called from the UI thread
     */
    @Override
    protected void onStopLoading() {
        // Attempt to cancel the current load task if possible.
        cancelLoad();
    }

    @Override
    public void onCanceled(Cursor cursor) {
        if (cursor != null && !cursor.isClosed()) {
            cursor.close();
        }
    }

    @Override
    protected void onReset() {
        super.onReset();

        // Ensure the loader is stopped
        onStopLoading();

        if (mCursor != null && !mCursor.isClosed()) {
            mCursor.close();
        }
        mCursor = null;
    }

    public Uri getUri() {
        return mUri;
    }

    public void setUri(Uri uri) {
        mUri = uri;
    }

    public String[] getProjection() {
        return mProjection;
    }

    public void setProjection(String[] projection) {
        mProjection = projection;
    }

    public String getSelection() {
        return mSelection;
    }

    public void setSelection(String selection) {
        mSelection = selection;
    }

    public String[] getSelectionArgs() {
        return mSelectionArgs;
    }

    public void setSelectionArgs(String[] selectionArgs) {
        mSelectionArgs = selectionArgs;
    }

    public String getSortOrder() {
        return mSortOrder;
    }

    public void setSortOrder(String sortOrder) {
        mSortOrder = sortOrder;
    }

    @Override
    public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
        super.dump(prefix, fd, writer, args);
        writer.print(prefix); writer.print("mUri="); writer.println(mUri);
        writer.print(prefix); writer.print("mProjection=");
                writer.println(Arrays.toString(mProjection));
        writer.print(prefix); writer.print("mSelection="); writer.println(mSelection);
        writer.print(prefix); writer.print("mSelectionArgs=");
                writer.println(Arrays.toString(mSelectionArgs));
        writer.print(prefix); writer.print("mSortOrder="); writer.println(mSortOrder);
        writer.print(prefix); writer.print("mCursor="); writer.println(mCursor);
        writer.print(prefix); writer.print("mContentChanged="); writer.println(mContentChanged);
    }
}

可以发现,CursorLoader的封装大大简化了应用开发者代码的复杂度;它完全就是一个异步的数据库查询瑞士军刀,没有啥特别需要分析的地方,所以不再过多说明。

参考资料

官方文档

使用CursorLoader执行查询任务

Android应用Loaders全面详解及源码浅析

Android之Loader介绍

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值