Android 相册媒体库读写优化

一、快速查询手机中的图片和视频

本方案适合通过媒体库实现快速查询视频和图片,对于SD卡扫描,也可以参考。

我们知道,媒体库属于数据库,CURD数据库属于IO操作,但是数据的IO相对特殊,很难使用一次拷贝,共享内存方式去优化,因此往往都是通过多线程操作去处理。一次查询所有图片和视频,极端情况下可能出现ANR或者查询慢的问题,因为媒体库接口时通过ContentProvider暴露的,虽然可以Hack ActivityThread使得ContentProviderClient不去发送ANR Error,但这种方式毕竟不太好。

二、方案设计

可以采用分段查询方式,创建初始任务时 图片和视频比与线程最大核心线程数相关,如果最大核心线程数为N(N必须大于4),那么图片线程为(N-2) : 视频线程为2,因为绝大多数情况下,手机中的视频数量比图片少很多倍,线程数量按分段方式查询,如果查询数量等于分页数目,那么继续创建线程查询下一页,最终所有线程收敛

1、创建配置类

public class MediaQueryConfig {
    public static final int QUERY_ONLY_IMAGES = 1;
    public static final int QUERY_ONLY_VIDEOS = 1<<1;
    public static final int QUERY_IMAGES_AND_VIDEOS = QUERY_ONLY_IMAGES | QUERY_ONLY_VIDEOS;
    private final int PAGE_SIZE = 1000;
    private String orderBy = null;
    private int queryType  = QUERY_IMAGES_AND_VIDEOS;
    private int pageSize = PAGE_SIZE;

    public void setOrderBy(String orderBy,boolean isAsc) {
        if(!TextUtils.isEmpty(orderBy)){
            orderBy += " "+((isAsc)?" ASC ":" DESC ");
        }
        this.orderBy = orderBy;
    }
    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }
    public void setQueryType(int queryType) {
        this.queryType = queryType;
    }

    public int getPageSize() {
        return pageSize;
    }

    public int getQueryType() {
        return queryType;
    }

    public String getOrderBy() {
        return orderBy;
    }
}

2、创建线程管理类

public class MediaQueryTaskManager implements Runnable {

    private MediaQueryConfig mQueryConfig;
    private Context context;
    private static volatile ThreadPoolExecutor sThreadPoolExecutor = null;
    private List<QueryTask> futureTasks;
    private Thread monitorThread;
    private Handler monitorHandler;
    private int imagePageIndex = 0;
    private int videoPageIndex = 0;
    private AtomicInteger taskCounter;
    private QueryResultListener listener;
    private final int coreThreadNum = 4;

    public MediaQueryTaskManager(Context context,MediaQueryConfig mediaQueryConfig) {
        this.context = context.getApplicationContext();
        if(mediaQueryConfig==null){
            mediaQueryConfig = new MediaQueryConfig();
        }
        this.mQueryConfig = mediaQueryConfig;
        this.futureTasks = new LinkedList<>();
        this.taskCounter = new AtomicInteger();
    }

    public ThenTask getTask() {
        ThenTask thenTask = new ThenTask(this);
        return thenTask;
    }

    @SuppressLint("NewApi")
    @Override
    public void run() {
        Looper.prepare();
        final long startQueryTime = System.currentTimeMillis();
        final List<QueryResult.MediaInfo> videoList = new ArrayList<>();
        final List<QueryResult.MediaInfo> imageList = new ArrayList<>();
        final int PAGE_SIZE = mQueryConfig.getPageSize();

        Handler.Callback callback = new Handler.Callback() {
            @Override
            public boolean handleMessage(Message msg) {

                if (msg.what == QueryTask.MSG_START) {
                    futureTasks.add((QueryTask) msg.obj);
                } else if (msg.what == QueryTask.MSG_FINISH) {
                    QueryTask queryTask = (QueryTask) msg.obj;
                    futureTasks.remove(queryTask);
                    taskCounter.decrementAndGet();
                    QueryResult queryResult = queryTask.getQueryResult();
                    if (queryTask.requestVideo) {
                        List<QueryResult.MediaInfo> videos = queryResult.getVideos();
                        if (videos != null) {
                            int size = videos.size();
                            if (size>0) {
                                videoList.addAll(videos);
                            }
                            Log.d("QueryResult", "查询视频: " + size +", costTime = "+queryResult.costTime);
                            if (size >=PAGE_SIZE) {
                                startNextPageVideos(monitorHandler);
                            }
                        }
                    } else {
                        List<QueryResult.MediaInfo> images = queryResult.getImages();
                        int size = images.size();
                        if (size>0) {
                            imageList.addAll(images);
                        }
                        Log.d("QueryResult", "查询图片: " + size+", costTime = "+queryResult.costTime);
                        if (size>=PAGE_SIZE) {
                            startNextPageImages(monitorHandler);
                        }
                    }
                    Log.d("QueryResult", "当前查询任务数量" + futureTasks.size() + " , " + taskCounter.get());
                    if (taskCounter.get() == 0) {
                        Message message = Message.obtain(monitorHandler);
                        message.what = QueryTask.MSG_ALL_FINISH;
                        message.sendToTarget();
                    }
                } else if (msg.what == QueryTask.MSG_ALL_FINISH) {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                        monitorHandler.getLooper().quitSafely();
                    } else {
                        monitorHandler.getLooper().quit();
                    }
                }
                return false;
            }
        };
        monitorHandler = new Handler(Looper.myLooper(), callback);
        startQueryTaskOnReady(monitorHandler);
        Looper.loop();

        QueryResult queryResult = new QueryResult();
        synchronized (queryResult) {
            queryResult.videoIndex = videoPageIndex;
            queryResult.imageIndex = imagePageIndex;
        }
        queryResult.videos = videoList;
        queryResult.images = imageList;
        queryResult.costTime = (System.currentTimeMillis() - startQueryTime);
        Log.e("QueryResult", "查询结束 : " + queryResult + ", 查询耗时:" + queryResult.costTime + ", Looper exit ");
        this.listener.onResult(queryResult);
    }

    private void startNextPageVideos(Handler handler) {
        int flag = mQueryConfig.getQueryType() & MediaQueryConfig.QUERY_ONLY_VIDEOS;
        if(flag==0){
            return;
        }
        QueryTask queryTask = new QueryTask(context, true, videoPageIndex, mQueryConfig, handler);
        startQueryTask(queryTask);
        videoPageIndex++;
    }

    private void startNextPageImages(Handler handler) {
        int flag = mQueryConfig.getQueryType() & MediaQueryConfig.QUERY_ONLY_IMAGES;
        if(flag==0){
            return;
        }
        QueryTask queryTask = new QueryTask(context, false, imagePageIndex, mQueryConfig,handler);
        startQueryTask(queryTask);
        imagePageIndex++;
    }

    private void startQueryTaskOnReady(Handler handler) {
        startNextPageImages(handler);
        startNextPageImages(handler);
        startNextPageImages(handler);
        startNextPageImages(handler);
        startNextPageVideos(handler);
    }

    private void startQueryTask(QueryTask task) {
        this.taskCounter.incrementAndGet();
        sThreadPoolExecutor.execute(task);
    }

    static class QueryTask implements Runnable {

        private MediaQueryConfig mediaQueryConfig;
        private Handler handler;
        private boolean requestVideo;
        private int pageIndex;
        private Context context;
        private static final int MSG_FINISH = 2;
        private static final int MSG_ALL_FINISH = 3;
        private static final int MSG_START = 1;
        private QueryResult queryResult;
        public QueryResult getQueryResult() {
            return queryResult;
        }

        public QueryTask(Context context, boolean requestVideo, int pageIndex,MediaQueryConfig mediaQueryConfig, Handler handler) {
            this.pageIndex = pageIndex;
            this.context = context;
            this.handler = handler;
            this.requestVideo = requestVideo;
            this.mediaQueryConfig = mediaQueryConfig;
        }
        @Override
        public void run() {
            try {
                onStartTask(this);
                long startQueryTime = System.currentTimeMillis();
                MediaQuery fastMediaQuery = new MediaQuery(context, requestVideo, pageIndex,mediaQueryConfig);
                List<QueryResult.MediaInfo> result = fastMediaQuery.call();
                QueryResult queryResult = new QueryResult();
                queryResult.costTime = (System.currentTimeMillis() - startQueryTime);
                if (requestVideo) {
                    queryResult.videos = result;
                    queryResult.videoIndex = pageIndex;
                } else {
                    queryResult.images = result;
                    queryResult.imageIndex = pageIndex;
                }
                this.queryResult = queryResult;

            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                onFinishTask(this);
            }
        }

        private void onFinishTask(QueryTask queryTask) {
            Message msg = Message.obtain(handler);
            msg.what = MSG_FINISH;
            msg.obj = queryTask;
            msg.sendToTarget();
        }

        private void onStartTask(QueryTask queryTask) {
            Message msg = Message.obtain(handler);
            msg.what = MSG_START;
            msg.obj = queryTask;
            msg.sendToTarget();
        }
    }

    public static class ThenTask {
        private MediaQueryTaskManager initTask;
        public ThenTask(MediaQueryTaskManager initTask) {
            this.initTask = initTask;
        }
        public void doThen(QueryResultListener listener) {
            if (listener == null) return;
            initTask.start(listener);
        }
    }

    private void start(QueryResultListener listener) {
        this.listener = listener;
        this.monitorThread = new Thread(this);
        if(sThreadPoolExecutor ==null || sThreadPoolExecutor.isShutdown()) {
            int maxCores = coreThreadNum + 2;
            sThreadPoolExecutor = new ThreadPoolExecutor(coreThreadNum, maxCores, 120, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), new RejectedExecutionHandler() {
                @Override
                public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                    Log.d("QueryResult", "查询失败 : rejectedExecution = " + r);
                }
            });
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
                this.sThreadPoolExecutor.allowCoreThreadTimeOut(true);
            }
        }
        this.monitorThread.start();
    }

    public interface QueryResultListener {
        public void onResult(QueryResult queryResult);
    }

}

3、创建查询任务

public class MediaQuery implements Callable<List<QueryResult.MediaInfo>> {
    private final int pageIndex;
    private final boolean requestVideo;
    private MediaQueryConfig config;
    private Context context;
    public MediaQuery(Context context, boolean requestVideo, int pageIndex,MediaQueryConfig mediaQueryConfig) {
        if(mediaQueryConfig==null){
            mediaQueryConfig = new MediaQueryConfig();
        }
        this.context = context;
        this.pageIndex = pageIndex;
        this.requestVideo = requestVideo;
        this.config = mediaQueryConfig;
    }
    public static String[] getQueryFields(boolean isVideo) {
        if (isVideo) {
            return new String[]{
                    MediaStore.Images.Media._ID,
                    MediaStore.Video.Media.DATA,
                    MediaStore.Video.Media.SIZE,
                    MediaStore.Video.Media.DURATION,
                    MediaStore.Video.Media.DATE_MODIFIED,
                    MediaStore.Video.Media.DATE_TAKEN,
                    MediaStore.Video.Media.DISPLAY_NAME,
                    MediaStore.Video.Media.MIME_TYPE
            };
        }
        return new String[]{
                 MediaStore.Images.Media._ID
                ,MediaStore.Images.Media.DATA    //路径
                , MediaStore.Images.Media.SIZE    //大小
                , MediaStore.Images.Media.DISPLAY_NAME //名称
                , MediaStore.Images.Media.DATE_TAKEN //生成时间
                , MediaStore.Images.Media.MIME_TYPE //类型
                , MediaStore.Images.Media.DATE_MODIFIED};
    }
    private static List<QueryResult.MediaInfo> readMediaInfo(Cursor cursor, boolean isVideo) {

        List<QueryResult.MediaInfo> mediaInfos = null;
        if (cursor != null && cursor.moveToFirst()) {
            int index_path = cursor.getColumnIndex(MediaStore.Images.Media.DATA);
            int index_displayName = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME);
            int index_mimeType =  cursor.getColumnIndex(MediaStore.Images.Media.MIME_TYPE);
            int index_dataTaken =   cursor.getColumnIndex(MediaStore.Images.Media.DATE_TAKEN);
            int index_dateModify = cursor.getColumnIndex(MediaStore.Images.Media.DATE_MODIFIED);
            int index_size = cursor.getColumnIndex(MediaStore.Images.Media.SIZE);
            int index_id = cursor.getColumnIndex(MediaStore.Images.Media._ID);

            int index_duration = -1;
            if(isVideo) {
                index_duration = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION);
            }
            QueryResult.MediaInfo media = null;
            do {
                String path = cursor.getString(index_path);
                String displayName = cursor.getString(index_displayName);
                String mimeType = cursor.getString(index_mimeType);
                long createTime = cursor.getLong(index_dataTaken);
                long modifiedTime = cursor.getLong(index_dateModify);
                long size = cursor.getLong(index_size);
                long id = cursor.getLong(index_id);

                if (TextUtils.isEmpty(displayName)) {
                    displayName = "";
                }
                media = new QueryResult.MediaInfo();
                if (isVideo) {
                    long duration = cursor.getLong(index_duration);
                    media.setDuration(duration);
                }
                boolean isExist = isExistFile(path);
                media.setExist(isExist);
                media.setId(id);
                media.setDateTaken(createTime);
                media.setDateModified(modifiedTime);
                media.setSize(size);
                media.setDisplayName(displayName);
                media.setMimeType(mimeType);
                if (isVideo) {
                    media.setMediaType(QueryResult.MediaInfo.MEDIA_TYPE_VIDEO);
                } else {
                    media.setMediaType(QueryResult.MediaInfo.MEDIA_TYPE_IMAGE);
                }
                media.setPath(path);
                if (mediaInfos == null) {
                    mediaInfos = new ArrayList<>();
                }
                mediaInfos.add(media);

            } while (cursor.moveToNext());
        }
        return mediaInfos;
    }
    private List<QueryResult.MediaInfo> queryMediaStoreFiles(int pageIndex, boolean isVideo, String orderBy ) {
        List<QueryResult.MediaInfo> mediaInfos = null;
        Uri MediaStoreUri = isVideo ? MediaStore.Video.Media.EXTERNAL_CONTENT_URI : MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
        Cursor cursor = null;
        int PAGE_SIZE = config.getPageSize();
        // 获得图片
        try {
            String[] QUERY_FIELDS = getQueryFields(isVideo); //修改时间
            String[] selectionArgs = null;
            String whereStatement = null;
            StringBuilder sqlLimit = new StringBuilder();
            if (!TextUtils.isEmpty(orderBy)) {
                sqlLimit.append(" ").append(orderBy).append(" ");
            }
            sqlLimit.append(" LIMIT ").append(pageIndex * PAGE_SIZE).append(", ").append(PAGE_SIZE).append(" ");
            String sqlLimitStatement = sqlLimit.toString();
            ContentResolver contentResolver = context.getContentResolver();
            cursor = contentResolver.query(MediaStoreUri,
                    QUERY_FIELDS,
                    whereStatement,
                    selectionArgs,
                    sqlLimitStatement);
            mediaInfos = readMediaInfo(cursor, isVideo);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        return mediaInfos;
    }
    public static boolean isExistFile(String path) {
        try {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                File file = new File(path);
                return file.exists();
            }
            return Os.access(path, OsConstants.F_OK);
        } catch (Exception e) {
            e.printStackTrace();
            File file = new File(path);
            return file.exists();
        }
    }
    @Override
    public List<QueryResult.MediaInfo> call() throws Exception {
        String orderType = config.getOrderBy();
        List<QueryResult.MediaInfo> mediaInfos = queryMediaStoreFiles(this.pageIndex, requestVideo, orderType);
        if (mediaInfos == null) {
            mediaInfos = new ArrayList<>();
        }
        return mediaInfos;
    }
    public static List<QueryResult.MediaInfo> queryMediaStoreFiles(boolean isVideo, List<String> paths) {
        if(paths==null||paths.isEmpty()) {
            return null;
        }
        List<QueryResult.MediaInfo> mediaInfos = null;
        Uri MediaStoreUri = isVideo ? MediaStore.Video.Media.EXTERNAL_CONTENT_URI : MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
        Cursor cursor = null;
        // 获得图片
        try {
            String[] QUERY_FIELDS = getQueryFields(isVideo); //修改时间
            StringBuilder sb = new StringBuilder(isVideo?MediaStore.Video.Media.DATA:MediaStore.Images.Media.DATA);
            sb.append(" IN ").append(" (");
            int size = paths.size();
            for (int i = 0; i < size; i++) {
                sb.append("'").append(paths.get(i)).append("'");
                if(i<size-1){
                    sb.append(",");
                }
            }
            sb.append(") ");
            cursor = HostHelper.getAppContext().getContentResolver().query(MediaStoreUri, QUERY_FIELDS,
                    sb.toString(), null, null);
            mediaInfos = readMediaInfo(cursor, isVideo);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        return mediaInfos;
    }
}

4、使用方法

MediaQueryConfig mediaQueryConfig = new MediaQueryConfig();
mediaQueryConfig.setOrderBy( MediaStore.Images.ImageColumns.DATE_TAKEN,false);
mediaQueryTaskManager = new MediaQueryTaskManager(mContext,mediaQueryConfig);

mediaQueryTaskManager.getTask().doThen(new MediaQueryTaskManager.QueryResultListener(){

    public void onResult(QueryResult queryResult) {
            try {
                long startTime = System.currentTimeMillis();
                List<QueryResult.MediaInfo> images = queryResult.getImages();
                List<QueryResult.MediaInfo> videos = queryResult.getVideos();
                List<QueryResult.MediaInfo> mediaInfos = new ArrayList<>();
                if (images != null) {
                    mediaInfos.addAll(images);
                }
                if (videos != null) {
                    mediaInfos.addAll(videos);
                }
              
            } finally {
              
            }
        }
});

同步调用可以使用CountDownLatch进行同步等待。

三、图片更新优化

虽然Sqlite支持replace SQL,但是Android的ContentProvider并没有提供相应的接口,因此想更新存入图库中的图片和图片信息,可能常规的是delete->insert ,但是这种方式会触发部分ROM 回收站通知,因此最好是 query->update/insert

public class MediaStoreHelper {
    public static final String PIC_DIR_NAME = "天空云相册";
    //在系统的图片文件夹下创建了一个相册文件夹,名为PIC_DIR_NAME,所有的图片都保存在该文件夹下。

    private static boolean doSavePhotoToMediaStore(Context context, long photoId, String path) {

        boolean isOk = false;
        try {
            boolean isUpdate = photoId != -1;
            File srcFile = new File(path);
            String fileName = srcFile.getName();
            long time = System.currentTimeMillis();
            String picPath = srcFile.getAbsolutePath();
            ContentValues values = new ContentValues();
            values.put(MediaStore.Images.ImageColumns.DATA, picPath);
            values.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, fileName);
            values.put(MediaStore.Images.ImageColumns.MIME_TYPE, "image/jpg");
            //将图片的拍摄时间设置为当前的时间
            values.put(MediaStore.Images.ImageColumns.DATE_MODIFIED, srcFile.lastModified());
            values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, time);
            values.put(MediaStore.Images.Media.SIZE, srcFile.length());
            ContentResolver resolver = context.getContentResolver();
            if (isUpdate) {
                values.put(MediaStore.Images.Media._ID, photoId);
                String whereSQL = " " + MediaStore.Images.Media._ID + "=? ";
                int id = resolver.update(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values, whereSQL, new String[]{String.valueOf(photoId)});
                if (id >= 0) {
                    isOk = true;
                }
            } else {
                Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
                if (uri != null) {
                    context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(new File(picPath))));
                    isOk = true;
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
        }
        return isOk;
    }

    private static boolean saveToMediaStore(Context context, String path) {

        File srcFile = new File(path);
        if (!srcFile.exists()) {
            return false;
        }
        ContentResolver resolver = context.getContentResolver();
        String[] fields = new String[]{
                MediaStore.Images.Media._ID
        };
        String whereSQL = " " + MediaStore.Images.Media.DATA + "=? ";
        String[] whereValues = {path};
        Cursor cursor = resolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, fields, whereSQL, whereValues, null);
        long id = getPhotoId(cursor);
        return doSavePhotoToMediaStore(context, id, path);
    }

    public static boolean savePhoto(Context context, String path) {
        if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.FROYO) {
            return false;
        }
        File dir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), PIC_DIR_NAME);
        if (!dir.exists()) {
            dir.mkdirs();
        }
        File file = new File(dir, "tiankong_cloud.jpg");
        if (!file.exists()) {
            try {
                RandomAccessFile inputRaf = new RandomAccessFile(path, "rw");
                RandomAccessFile outputRaf = new RandomAccessFile(file.getAbsolutePath(), "rw");
                FileChannel inputRafChannel = inputRaf.getChannel();
                FileChannel outputRafChannel = outputRaf.getChannel();
                inputRafChannel.transferTo(0, inputRafChannel.size(), outputRafChannel);
                inputRafChannel.close();
                outputRafChannel.close();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return saveToMediaStore(context, file.getAbsolutePath());
    }


    public static boolean savePhoto(Context context, int resId) {
        if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.FROYO) {
            return false;
        }
        File dir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), PIC_DIR_NAME);
        if (!dir.exists()) {
            dir.mkdirs();
        }
        File file = new File(dir, "tiankong_cloud.jpg");
        if (!file.exists()) {
            InputStream is = null;
            try {
                is = context.getResources().openRawResource(resId);
                RandomAccessFile outputRaf = new RandomAccessFile(file.getAbsolutePath(), "rw");
                FileChannel outputRafChannel = outputRaf.getChannel();
                MappedByteBuffer map = outputRafChannel.map(FileChannel.MapMode.READ_WRITE, 0, is.available());
                int len = -1;
                byte[] buf = new  byte[1024];
                while ((len=is.read(buf,0,buf.length))!=-1){
                    map.put(buf,0,len);
                }
                outputRafChannel.close();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                if(is!=null){
                    try {
                        is.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return saveToMediaStore(context, file.getAbsolutePath());
    }


    private static long getPhotoId(Cursor cursor) {

        if (cursor != null && cursor.moveToFirst()) {
            do {
                long id = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media._ID));
                return id;
            } while (cursor.moveToNext());
        }
        if(cursor!=null){
            cursor.close();
        }
        return -1;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值