10 Gallery 源码-基本数据MediaObject、数据源MediaSource和数据管理DataManager

0. 原文拜读

1. 基本数据-MediaObject

1.1 MediaObject

数据渲染的最小单位,它包含丰富的衍生类。MediaObject定义了媒体数据最基本的信息,如SupportedOperations,SupportedOperations定义这个媒体文件支持的操作,如是否可以delete/share/rotate等。定义了最基本的Path路径,用于表示媒体对象的存储地址;

即通俗说,主要负责判断是否支持 剪切,放大,全屏显示,删除等操作以及相应的操作的声明

package com.android.gallery3d.data;

public abstract class MediaObject {

}

Gallery 的数据源都是基于 MediaObject 进行封装
查看所有 extends MediaObject,得到 MediaSet 和 MediaItem

packages/apps/Gallery/src/com/android/gallery3d/data/MediaSet.java:35:public abstract class MediaSet extends MediaObject {
packages/apps/Gallery/src/com/android/gallery3d/data/MediaItem.java:27:public abstract class MediaItem extends MediaObject {
1.2 MediaItem

MediaObject的衍生类,MediaObject的封装,是单个媒体的抽象,代表一张图片或者一个视频。在此抽象类中,定义getMimeType()/getWitdh()/getHeight()等抽象方法

即通俗说,增加了获取图片的名字,模式,旋转角度,大小等信息

package com.android.gallery3d.data;

public abstract class MediaItem extends MediaObject {

}

查看所有 extends MediaItem

grep -irn "extends MediaItem" packages/apps/Gallery/

packages/apps/Gallery/src/com/android/gallery3d/data/LocalMediaItem.java:30:public abstract class LocalMediaItem extends MediaItem {
packages/apps/Gallery/src/com/android/gallery3d/data/ActionImage.java:30:public class ActionImage extends MediaItem {
packages/apps/Gallery/src/com/android/gallery3d/data/UriImage.java:42:public class UriImage extends MediaItem {
packages/apps/Gallery/src/com/android/gallery3d/data/TimeLineTitleMediaItem.java:30:public class TimeLineTitleMediaItem extends MediaItem {
packages/apps/Gallery/src/com/android/gallery3d/data/SnailItem.java:30:public class SnailItem extends MediaItem {

当然我们可以继续查看对应的子类继承关系,不过这里我们暂时关注以下继承关系

1.2.1 MediaItem.LocalMediaItem

MediaItem的衍生类,对本地MediaItem的抽象,代表一张本地图片或者一个本地视频。在此抽象类中,添加定义了bucketId/dataDirty; bucketId由文件夹的绝对路径的hashCode来表示,代表一个专辑,是专辑的索引。通过GalleryUtils的getBucketId可以获得传入路径的bucketId。

package com.android.gallery3d.data;

public abstract class LocalMediaItem extends MediaItem {
    
}

当然我们可以继续查看对应的子类继承关系,不过这里我们暂时关注以下继承关系

grep -irn "extends LocalMediaItem" packages/apps/Gallery/

packages/apps/Gallery/src/com/android/gallery3d/data/LocalVideo.java:36:public class LocalVideo extends LocalMediaItem {
packages/apps/Gallery/src/com/android/gallery3d/data/LocalImage.java:51:public class LocalImage extends LocalMediaItem {

1.2.1.1 MediaItem.LocalMediaItem.LocalImage

LocalImage 是 LocalMediaItem的子类,表示一个本地存储的图片。内部定义了一个ITEM_PATH="/local/image/item"。首先LocalImage的初始化有两种,一种是通过直接传入cursor对象来初始化这个Image对象,另外一种是通过传入id的形式来查询外部存储的数据库,得到cursor,进而初始化这个Image对象。

package com.android.gallery3d.data;

public class LocalImage extends LocalMediaItem {

}

1.3 MediaSet

MediaObject的衍生类,是一个类目录的数据结构,是一组媒体文件的抽象。它提供的主要基础接口有getMediaItemCount, getMediaItem, getSubMediaSetCount, getSubMediaSet, getTotalMediaItemCount. 还定义了getCoverMediaItem来获得一组图片或视频的封面

即通俗说,可以包含 MediaItem也可以包含 MediaSet 除了管理 数据集应有的获取 包含的个数 等加入了 同步加载数据相关的处理操作

package com.android.gallery3d.data;

public abstract class MediaSet extends MediaObject {
    
}

查看所有 extends MediaSet

packages/apps/Gallery/src_pd/com/android/gallery3d/picasasource/PicasaSource.java:57:    private static class EmptyAlbumSet extends MediaSet {

packages/apps/Gallery/src/com/android/gallery3d/data/SingleItemAlbum.java:21:public class SingleItemAlbum extends MediaSet {

packages/apps/Gallery/src/com/android/gallery3d/data/SecureAlbum.java:33:public class SecureAlbum extends MediaSet implements StitchingChangeListener {

packages/apps/Gallery/src/com/android/gallery3d/data/FilterDeleteSet.java:30:public class FilterDeleteSet extends MediaSet implements ContentListener {
packages/apps/Gallery/src/com/android/gallery3d/data/FilterEmptyPromptSet.java:21:public class FilterEmptyPromptSet extends MediaSet implements ContentListener
packages/apps/Gallery/src/com/android/gallery3d/data/FilterTypeSet.java:22:public class FilterTypeSet extends MediaSet implements ContentListener {

packages/apps/Gallery/src/com/android/gallery3d/data/LocalMergeAlbum.java:36:public class LocalMergeAlbum extends MediaSet implements ContentListener {
packages/apps/Gallery/src/com/android/gallery3d/data/LocalAlbum.java:42:public class LocalAlbum extends MediaSet {
packages/apps/Gallery/src/com/android/gallery3d/data/LocalAlbumSet.java:38:public class LocalAlbumSet extends MediaSet

packages/apps/Gallery/src/com/android/gallery3d/data/ClusterAlbum.java:22:public class ClusterAlbum extends MediaSet implements ContentListener {
packages/apps/Gallery/src/com/android/gallery3d/data/ClusterAlbumSet.java:28:public class ClusterAlbumSet extends MediaSet implements ContentListener {


packages/apps/Gallery/src/com/android/gallery3d/data/ComboAlbum.java:26:public class ComboAlbum extends MediaSet implements ContentListener {
packages/apps/Gallery/src/com/android/gallery3d/data/ComboAlbumSet.java:26:public class ComboAlbumSet extends MediaSet implements ContentListener {

重点查看下 LocalAlbum 和 LocalAlbumSet

1.3.1 MediaSet.LocalAlbum

继承于MediaSet,代表一个bucket(目录)下的所有的media items。提供MediaItem的查询,删除等操作。LocalAlbum是AlbumPage的单位

package com.android.gallery3d.data;

public class LocalAlbum extends MediaSet {

}
1.3.1 MediaSet.LocalAlbumSet

继承于MediaSet,是所有图片和视频专辑的集合。其内部定义了三个path,分别是PATH_ALL,PATH_IMAGE,PATH_VIDEO.其内部定义了mAlbums用来保存专辑列表。LocalAlbumSet是AlbumSetPage的单位

package com.android.gallery3d.data;

public class LocalAlbumSet extends MediaSet
        implements FutureListener<ArrayList<MediaSet>> {

            
}

2. 数据源-MediaSource

Gallery2中引入数据源的概念,由DataManager负责管理,目的是在不同的显示界面,能通过DataManager获得一个合适的数据源来初始化自己的数据。Gallery2中主要定义的数据源有ComboSource(组合源), PicasaSource(Picasa源),LocalSource(本地源), ClusterSource(簇源), UriSource(URL源),FilterSource(过滤源)。这些数据源有一个共同的基类MediaSource, MediaSource是对数据源的抽象,它里面主要定义了数据源的基本组成,如定义了数据源的唯一标识prefix, prefix后面会讲到。

package com.android.gallery3d.data;

public abstract class MediaSource {
}

查看所有数据源

ytw012@rom:~/Android_Build_CS/android$ grep -irn "extends MediaSource" packages/apps/Gallery/

packages/apps/Gallery/src_pd/com/android/gallery3d/picasasource/PicasaSource.java:34:public class PicasaSource extends MediaSource {
packages/apps/Gallery/src/com/android/gallery3d/data/ComboSource.java:21:class ComboSource extends MediaSource {
packages/apps/Gallery/src/com/android/gallery3d/data/SnailSource.java:20:public class SnailSource extends MediaSource {
packages/apps/Gallery/src/com/android/gallery3d/data/FilterSource.java:21:public class FilterSource extends MediaSource {
packages/apps/Gallery/src/com/android/gallery3d/data/ClusterSource.java:21:class ClusterSource extends MediaSource {
packages/apps/Gallery/src/com/android/gallery3d/data/LocalSource.java:33:class LocalSource extends MediaSource {
packages/apps/Gallery/src/com/android/gallery3d/data/SecureSource.java:21:public class SecureSource extends MediaSource {
packages/apps/Gallery/src/com/android/gallery3d/data/UriSource.java:31:class UriSource extends MediaSource {

图库中支持的数据源有:

  1. LocalSource(本地数据)
  2. PicasaSource(Gmail同步)
  3. ComboSource(混合类型数据)
  4. FilterSource(过滤后的数据)
  5. SecureSource(安全数据)
  6. UriSource(Uri数据)
  7. ClusterSource(簇源)
  8. SnailSource

3.数据管理-DataManager

DataManager是用来管理整个系统中的所有media sets(集合)和media item。DataManager在Gallery Application启动时就创建并且初始化,可以通过GalleryAppImpl的getDataManager方法来获得DataManager实例,DataManager的初始化做了以下事情

package com.android.gallery3d.data;

public class DataManager implements StitchingChangeListener {

    public synchronized void initializeSourceMap() {
        if (!mSourceMap.isEmpty()) return;

        // the order matters, the UriSource must come last
        addSource(new LocalSource(mApplication));// 本地数据
        addSource(new PicasaSource(mApplication)); // Gmail同步,Picasa 源
        addSource(new ComboSource(mApplication));// 组合源
        addSource(new ClusterSource(mApplication));// 簇源
        addSource(new FilterSource(mApplication));// 过滤源
        addSource(new SecureSource(mApplication)); // 安全数据
        addSource(new UriSource(mApplication)); // URL源
        addSource(new SnailSource(mApplication));

        if (mActiveCount > 0) {
            for (MediaSource source : mSourceMap.values()) {
                source.resume();
            }
        }
    }

}

DataManager实例化的同时也创建了所有数据源实例,并把它们加入自身维护的一个SourceMap中,提供存取操作。SourceMap中保存的索引是上面讲到的prefix。Prefix是数据源的唯一标识,在数据源的构造方法中赋值。如LocalSource的prefix为”local”, ComboSource的prefix为”combo”。

package com.android.gallery3d.data;

public abstract class MediaSource {

    protected MediaSource(String prefix) {
        mPrefix = prefix;
    }

}

package com.android.gallery3d.data;

class LocalSource extends MediaSource {

    public LocalSource(GalleryApp context) {
        super("local");
    }
}

public class PicasaSource extends MediaSource {

    public PicasaSource(GalleryApp application) {
        super("picasa");
        ...
    }
}


class ComboSource extends MediaSource {

    public ComboSource(GalleryApp application) {
        super("combo");
        ...
    }

    ...

DataManager不仅提供了丰富的数据操作接口,同时定义了一组代表数据集合的PATH:

package com.android.gallery3d.data;

    public class DataManager implements StitchingChangeListener {
    
    TOP_SET_PATH = "/combo/{/local/all,/picasa/all}";//表示用户能看到的最顶端的数据集合
    
    TOP_IMAGE_SET_PATH = "/combo/{/local/image,/picasa/image}";//表示用户能看到的最顶端的图片数据集合
    
    TOP_VIDEO_SET_PATH = "/combo/{/local/video,/picasa/video}";//表示用户能看到的最顶端的视频数据集合
    
    TOP_LOCAL_SET_PATH = "/local/all";//表示用户能看到的最顶端的本地数据集合
    
    TOP_LOCAL_IMAGE_SET_PATH = "/local/image";//表示用户能看到的最顶端的本地图片集合
    
    TOP_LOCAL_VIDEO_SET_PATH = "/local/video";//表示用户能看到的最顶端的本地视频集合
}

通过上述路径查看,数据范围的比较如下:

  • TOP_SET_PATH > TOP_IMAGE_SET_PATH = TOP_VIDEO_SET_PATH > TOP_LOCAL_SET_PATH > TOP_LOCAL_IMAGE_SET_PATH = TOP_LOCAL_VIDEO_SET_PATH
3.1 以LocalSource本地数据源为例

LocalSource表示本地存储器中的所有Media数据源,负责管理Local Media数据集。从它的createMediaObject方法(继承于MediaSource)可以看出,它可以根据传入的path路径,创建出LocalAlbumSet,LocalAlbum,LocalMergeAlbum,LocalImage,LocalVideo所有本地媒体数据相关的数据集合以及单个媒体文件。

package com.android.gallery3d.data;

class LocalSource extends MediaSource {
    @Override
    public MediaObject createMediaObject(Path path) {
        GalleryApp app = mApplication;
        switch (mMatcher.match(path)) {
            case LOCAL_ALL_ALBUMSET:
            case LOCAL_IMAGE_ALBUMSET:
            case LOCAL_VIDEO_ALBUMSET:
                return new LocalAlbumSet(path, mApplication);
            case LOCAL_IMAGE_ALBUM:
                return new LocalAlbum(path, app, mMatcher.getIntVar(0), true);
            case LOCAL_VIDEO_ALBUM:
                return new LocalAlbum(path, app, mMatcher.getIntVar(0), false);
            case LOCAL_ALL_ALBUM: {
                int bucketId = mMatcher.getIntVar(0);
                DataManager dataManager = app.getDataManager();
                MediaSet imageSet = (MediaSet) dataManager.getMediaObject(
                        LocalAlbumSet.PATH_IMAGE.getChild(bucketId));
                MediaSet videoSet = (MediaSet) dataManager.getMediaObject(
                        LocalAlbumSet.PATH_VIDEO.getChild(bucketId));
                Comparator<MediaItem> comp = DataManager.sDateTakenComparator;
                return new LocalMergeAlbum(
                        path,mApplication, comp, new MediaSet[] {imageSet, videoSet}, bucketId, false);
            }
            case LOCAL_IMAGE_ITEM:
                return new LocalImage(path, mApplication, mMatcher.getIntVar(0));
            case LOCAL_VIDEO_ITEM:
                return new LocalVideo(path, mApplication, mMatcher.getIntVar(0));
            default:
                throw new RuntimeException("bad path: " + path);
        }
    }
}

那么LocalSource是怎样根据传入的path来生成AlbumSet,还是Album呢?首先我们先来看看LocalSource的构造方法:

答案在 xxx_ALBUMSET 和 xxx_IMAGE_ITEM 中

    public LocalSource(GalleryApp context) {
        super("local");
        mApplication = context;
        mMatcher = new PathMatcher();
        mMatcher.add("/local/image", LOCAL_IMAGE_ALBUMSET);
        mMatcher.add("/local/video", LOCAL_VIDEO_ALBUMSET);
        mMatcher.add("/local/all", LOCAL_ALL_ALBUMSET);

        mMatcher.add("/local/image/*", LOCAL_IMAGE_ALBUM);
        mMatcher.add("/local/video/*", LOCAL_VIDEO_ALBUM);
        mMatcher.add("/local/all/*", LOCAL_ALL_ALBUM);
        mMatcher.add("/local/image/item/*", LOCAL_IMAGE_ITEM);
        mMatcher.add("/local/video/item/*", LOCAL_VIDEO_ITEM);

        mUriMatcher.addURI(MediaStore.AUTHORITY,
                "external/images/media/#", LOCAL_IMAGE_ITEM);
        mUriMatcher.addURI(MediaStore.AUTHORITY,
                "external/video/media/#", LOCAL_VIDEO_ITEM);
        mUriMatcher.addURI(MediaStore.AUTHORITY,
                "external/images/media", LOCAL_IMAGE_ALBUM);
        mUriMatcher.addURI(MediaStore.AUTHORITY,
                "external/video/media", LOCAL_VIDEO_ALBUM);
        mUriMatcher.addURI(MediaStore.AUTHORITY,
                "external/file", LOCAL_ALL_ALBUM);
    }

LocalSource的构造方法中实例化了PathMatcher,并将所有代表local资源相关的path及其类型添加到PathMatcher实例中。这里PathMatcher的作用是维护一个树结构,用于保存path以及匹配path类型。PathMatcher类内部定义一个Node(节点),代表树的一个节点。Node由HashMap以及一个整型kind组成,其中HashMap用来保存路径子段和Node的映射,而整型kind用来保存该节点的类型,如(LOCAL_IMAGE_ALBUMSET/LOCAL_VIDEO_ALBUMSET)等。先来说一下PathMatcher的实现过程,在PathMatcher的构造方法中,首先创建了一个名为Root的树的根节点,这个Root的根节点作为match操作的入口。另外,PatchMatcher通过add方法,先将传入的path路径以”/”为分割符创建segments数组,然后通过segments数组的元素构造树结构,并给最后一个节点的kind类型赋值,表示从根节点到该节点生成的path代表哪个类型的媒体结构。

资源图片.png

匹配的过程如下:

  • path = “/local/image/item/10001”

序列为:

  • [local][image][item][10001]

二叉树查询:

  • Kind=LOCAL_IMAGE_ITEM, 生成LocalImage, id=10001
3.2 Media 数据的加载过程

我们从点击Gallery2图标进入图片专辑页面这个过程为例,描述一下Local数据的加载过程。

首先点击图库图标进入GalleryActivity(旧版本或者命名为Gallery),这个Activity是整个图库程序的入口,非外部ACTION_VIEW调用下,调用startDefaultPage启动AlbumSetPage(就是我们打开Gallery2后见到的第一个专辑页面),这时传入给AlbumSetPage一个名为media-path的参数,media-path值为"/combo/{/local/all,/picasa/all}",这个是一个combo类型的path,表示需要显示local以及picasa两个组合的所有的媒体文件.

    public static final String KEY_MEDIA_PATH = "media-path";

    public static final int INCLUDE_IMAGE = 1;
    public static final int INCLUDE_VIDEO = 2;
    public static final int INCLUDE_ALL = INCLUDE_IMAGE | INCLUDE_VIDEO;

    public void startAlbumPage() {
        PicasaSource.showSignInReminder(this);
        Bundle data = new Bundle();
        int clusterType = FilterUtils.CLUSTER_BY_ALBUM;
        
        data.putString(AlbumSetPage.KEY_MEDIA_PATH, getDataManager()
                .getTopSetPath(DataManager.INCLUDE_ALL));
        ...
        getStateManager().startState(AlbumSetPage.class, data);
        ...
    }
    
    // This is the path for the media set seen by the user at top level.
    private static final String TOP_SET_PATH = "/combo/{/local/all,/picasa/all}";
    
    public String getTopSetPath(int typeBits) {
        ...
        switch (typeBits) {
            ...
            case INCLUDE_ALL:
                // "/combo/{/local/all,/picasa/all}"
                // combo 类型的 path,表示需要显示local以及picasa两个组合的所有的媒体文件
                return TOP_SET_PATH; 
        }
        ...
    }

具体的解析步骤如下:

  1. 在AlbumSetPage的 initializeData 方法取出 media-path,mediaPath = /combo/{/local/all,/picasa/all}
private void initializeData(Bundle data) {
    // mediaPath = /combo/{/local/all,/picasa/all}
    String mediaPath = data.getString(AlbumSetPage.KEY_MEDIA_PATH);
}
  1. AlbumSetPage通过DataManager实例解析出由两个segments组成的url
  • Segments[0]:combo
  • Segments[1]:{/local/all,/picasa/all}
    mMediaSet = getDataManager().getMediaSet(mediaPath);

    public MediaObject getMediaObject(String s) {
        return getMediaObject(Path.fromString(s));
    }
    
    // /combo/{/local/all,/picasa/all}
    public static Path fromString(String s) {
        synchronized (Path.class) {
            // Segments[0]:combo
            // Segments[1]:{/local/all,/picasa/all}
            String[] segments = split(s);
            Path current = sRoot;
            for (int i = 0; i < segments.length; i++) {
                current = current.getChild(segments[i]);
            }
            return current;
        }
    }
  1. 第二部解析出来path的prefix(前缀)是 combo,DataManager通过这个prefix取得对应的数据源,这里获得的数据源是 ComboSource
public class Path {
    private IdentityCache<String, Path> mChildren;
    
    public Path getChild(String segment) {
        synchronized (Path.class) {
            if (mChildren == null) {
                mChildren = new IdentityCache<String, Path>();
            } else {
                Path p = mChildren.get(segment);
                if (p != null) return p;
            }

            Path p = new Path(this, segment);
            mChildren.put(segment, p);
            return p;
        }
    }
    
    private Path(Path parent, String segment) {
        mParent = parent;
        mSegment = segment;
    }
    
    public String getPrefix() {
        if (this == sRoot) return "";
        return getPrefixPath().mSegment;
    }
}

    public MediaSet getMediaSet(Path path) {
        return (MediaSet) getMediaObject(path);
    }
    
    public MediaObject getMediaObject(Path path) {
        synchronized (LOCK) {
            MediaObject obj = path.getObject();
            if (obj != null) return obj;

            // 解析出来path的prefix(前缀)是 combo,故 MediaSource 为 ComboSource
            MediaSource source = mSourceMap.get(path.getPrefix());
            ...
            // ComboSource 查找 {/local/all,/picasa/all},对应的数据
            MediaObject object = source.createMediaObject(path);
            ...
        }
    }
  1. DataManager调用ComboSource的createMediaObject方法来初始化ComboAlbumSet实例返回到AlbumSetPage,与此同时,在构造ComboAlbumSet时,继续分拆大括号里的/local/all和/picasa/all,生成 LocalSource 实例和 EmptySource 实例。
class ComboSource extends MediaSource {

    public ComboSource(GalleryApp application) {
        super("combo");
        mApplication = application;
        mMatcher = new PathMatcher();
        mMatcher.add("/combo/*", COMBO_ALBUMSET);
    }
    
    @Override
    public MediaObject createMediaObject(Path path) {
        // {/local/all,/picasa/all}
        String[] segments = path.split();
        if (segments.length < 2) {
            throw new RuntimeException("bad path: " + path);
        }

        DataManager dataManager = mApplication.getDataManager();
        switch (mMatcher.match(path)) {
            case COMBO_ALBUMSET:
                // 继续分拆大括号里的/local/all和/picasa/all
                // LocalSource 实例和 EmptySource 实例
                return new ComboAlbumSet(path, mApplication,
                        dataManager.getMediaSetsFromString(segments[1]));
            ....
        }
        ...
    }

4.1 分拆/local/all,创建 LocalSource 数据源,生成 LocalAlbumSet 实例。

Segments[0]:local

Segments[1]:all

  • current path=/local/all Prefix=local
    public LocalSource(GalleryApp context) {
        super("local");
        ...
        mMatcher.add("/local/all", LOCAL_ALL_ALBUMSET);
        ...
    }

    @Override
    public MediaObject createMediaObject(Path path) {
        GalleryApp app = mApplication;
        switch (mMatcher.match(path)) {
            case LOCAL_ALL_ALBUMSET:
                return new LocalAlbumSet(path, mApplication);
        ...
    }

4.2 分拆/picasa/all,创建 PicasaSource 数据源,生成EmptyAlbumSet实例

Segments[0]:picasa

Segments[1]:all

  • current path=/picasa/all Prefix=picasa
    public PicasaSource(GalleryApp application) {
        super("picasa");
        mApplication = application;
        mMatcher = new PathMatcher();
        mMatcher.add("/picasa/all", PICASA_ALBUMSET);
        mMatcher.add("/picasa/image", PICASA_ALBUMSET);
        mMatcher.add("/picasa/video", PICASA_ALBUMSET);
    }

    @Override
    public MediaObject createMediaObject(Path path) {
        switch (mMatcher.match(path)) {
            case PICASA_ALBUMSET:
                return new EmptyAlbumSet(path, MediaObject.nextVersionNumber());
            default:
                throw new RuntimeException("bad path: " + path);
        }
    }

(1)~(4)是AlbumSetPage中initializeData调用获取mMediaSet所做的事情,这里返回的mMediaSet实例就是指向一个由ComboSource数据源创建的ComboAlbumSet实例,后面这个mMediaSet的所有实现都可以在ComboAlbumSet中找到。获得mMediaSet后,initializeData里继续做的事情有,用mMediaSet初始化mSelectionManager来管理这个界面的所有选择操作;用mMediaSet初始化AlbumSetDataLoader实例,作为数据适配器,数据取自mMediaSet,这里生成的实例名叫mAlbumSetDataAdapter,也很形象,数据源和适配器都准备完毕后,mAlbumSetView通过setMode方法将适配器传入渲染器中。

资源图片.png

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值