Glide之旅 —— Registry

前言

glide是谷歌推荐的Android图片加载框架,其优秀的缓存策略、Activity的生命周期的继承、GIF图片的支持等都是为人所称道的地方。下面是用glide加载一张图片的调用。

private void loadImage() {
    Glide.with(this)
         .load("http://pic2.orsoon.com/2017/0118/20170118011032176.jpg")
         .into(ivTest);
}

那么,该框架是如何实际运作的呢,我会通过“Glide之旅”系列博客尽可能详细地将我的心得记录下来。“Glide之旅”系列文章汇总:

概述

Registry(com.bumptech.glide.Registry)是用来注册管理任务执行对象的管理类,可以简单理解为:Registry是一个工厂,而其中所有注册的对象都是一个工厂员工,当任务分发时,根据当前任务的性质,分发给相应员工进行处理。

分类

Registry是用来负责管理整个glide需要用到的任务执行对象,具体又分为七个注册分支:

  • com.bumptech.glide.load.model.ModelLoaderRegistry

  • com.bumptech.glide.provider.EncoderRegistry

  • com.bumptech.glide.provider.ResourceDecoderRegistry

  • com.bumptech.glide.provider.ResourceEncoderRegistry

  • com.bumptech.glide.load.data.DataRewinderRegistry

  • com.bumptech.glide.load.resource.transcode.TranscoderRegistry

  • com.bumptech.glide.provider.ImageHeaderParserRegistry

注册对象

glide初始化的时候,所有的需求对象就已经被注册了:

package com.bumptech.glide;

...

public class Glide implements ComponentCallbacks2 {

    ...

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    Glide(Context context,
          Engine engine,
          MemoryCache memoryCache,
          BitmapPool bitmapPool,
          ArrayPool arrayPool,
          ConnectivityMonitorFactory connectivityMonitorFactory,
          int logLevel,
          RequestOptions defaultRequestOptions) {
        this.engine = engine;
        this.bitmapPool = bitmapPool;
        this.arrayPool = arrayPool;
        this.memoryCache = memoryCache;
        this.connectivityMonitorFactory = connectivityMonitorFactory;

        DecodeFormat decodeFormat = defaultRequestOptions.getOptions().get(Downsampler.DECODE_FORMAT);
        bitmapPreFiller = new BitmapPreFiller(memoryCache, bitmapPool, decodeFormat);

        final Resources resources = context.getResources();

        registry = new Registry();
        registry.register(new DefaultImageHeaderParser());

        Downsampler downsampler = new Downsampler(registry.getImageHeaderParsers(),
                resources.getDisplayMetrics(), bitmapPool, arrayPool);
        ByteBufferGifDecoder byteBufferGifDecoder =
                new ByteBufferGifDecoder(context, registry.getImageHeaderParsers(), bitmapPool, arrayPool);
        registry.register(ByteBuffer.class, new ByteBufferEncoder())
                .register(InputStream.class, new StreamEncoder(arrayPool))
                .append(ByteBuffer.class, Bitmap.class, new ByteBufferBitmapDecoder(downsampler))
                .append(InputStream.class, Bitmap.class, new StreamBitmapDecoder(downsampler, arrayPool))
                .append(ParcelFileDescriptor.class, Bitmap.class, new VideoBitmapDecoder(bitmapPool))
                .register(Bitmap.class, new BitmapEncoder())
                .append(ByteBuffer.class, BitmapDrawable.class, new BitmapDrawableDecoder<>(resources, bitmapPool, new ByteBufferBitmapDecoder(downsampler)))
                .append(InputStream.class, BitmapDrawable.class, new BitmapDrawableDecoder<>(resources, bitmapPool, new StreamBitmapDecoder(downsampler, arrayPool)))
                .append(ParcelFileDescriptor.class, BitmapDrawable.class, new BitmapDrawableDecoder<>(resources, bitmapPool, new VideoBitmapDecoder(bitmapPool)))
                .register(BitmapDrawable.class, new BitmapDrawableEncoder(bitmapPool, new BitmapEncoder()))
                .prepend(InputStream.class, GifDrawable.class, new StreamGifDecoder(registry.getImageHeaderParsers(), byteBufferGifDecoder, arrayPool))
                .prepend(ByteBuffer.class, GifDrawable.class, byteBufferGifDecoder)
                .register(GifDrawable.class, new GifDrawableEncoder())
                .append(GifDecoder.class, GifDecoder.class, new UnitModelLoader.Factory<GifDecoder>())
                .append(GifDecoder.class, Bitmap.class, new GifFrameResourceDecoder(bitmapPool))
                .register(new ByteBufferRewinder.Factory())
                .append(File.class, ByteBuffer.class, new ByteBufferFileLoader.Factory())
                .append(File.class, InputStream.class, new FileLoader.StreamFactory())
                .append(File.class, File.class, new FileDecoder())
                .append(File.class, ParcelFileDescriptor.class, new FileLoader.FileDescriptorFactory())
                .append(File.class, File.class, new UnitModelLoader.Factory<File>())
                .register(new InputStreamRewinder.Factory(arrayPool))
                .append(int.class, InputStream.class, new ResourceLoader.StreamFactory(resources))
                .append(int.class, ParcelFileDescriptor.class, new ResourceLoader.FileDescriptorFactory(resources))
                .append(Integer.class, InputStream.class, new ResourceLoader.StreamFactory(resources))
                .append(Integer.class, ParcelFileDescriptor.class, new ResourceLoader.FileDescriptorFactory(resources))
                .append(String.class, InputStream.class, new DataUrlLoader.StreamFactory())
                .append(String.class, InputStream.class, new StringLoader.StreamFactory())
                .append(String.class, ParcelFileDescriptor.class, new StringLoader.FileDescriptorFactory())
                .append(Uri.class, InputStream.class, new HttpUriLoader.Factory())
                .append(Uri.class, InputStream.class, new AssetUriLoader.StreamFactory(context.getAssets()))
                .append(Uri.class, ParcelFileDescriptor.class, new AssetUriLoader.FileDescriptorFactory(context.getAssets()))
                .append(Uri.class, InputStream.class, new MediaStoreImageThumbLoader.Factory(context))
                .append(Uri.class, InputStream.class, new MediaStoreVideoThumbLoader.Factory(context))
                .append(Uri.class, InputStream.class, new UriLoader.StreamFactory(context.getContentResolver()))
                .append(Uri.class, ParcelFileDescriptor.class, new UriLoader.FileDescriptorFactory(context.getContentResolver()))
                .append(Uri.class, InputStream.class, new UrlUriLoader.StreamFactory())
                .append(URL.class, InputStream.class, new UrlLoader.StreamFactory())
                .append(Uri.class, File.class, new MediaStoreFileLoader.Factory(context))
                .append(GlideUrl.class, InputStream.class, new HttpGlideUrlLoader.Factory())
                .append(byte[].class, ByteBuffer.class, new ByteArrayLoader.ByteBufferFactory())
                .append(byte[].class, InputStream.class, new ByteArrayLoader.StreamFactory())
                .register(Bitmap.class, BitmapDrawable.class, new BitmapDrawableTranscoder(resources, bitmapPool))
                .register(Bitmap.class, byte[].class, new BitmapBytesTranscoder())
                .register(GifDrawable.class, byte[].class, new GifDrawableBytesTranscoder());

        ImageViewTargetFactory imageViewTargetFactory = new ImageViewTargetFactory();
        glideContext = new GlideContext(context, registry, imageViewTargetFactory, defaultRequestOptions, engine, this, logLevel);
    }

    ...

}
ModelLoaderRegistry中的注册对象
01. new MultiModelLoaderFactory.Entry(GifDecoder.class, GifDecoder.class, new UnitModelLoader.Factory<GifDecoder>())
02. new MultiModelLoaderFactory.Entry(File.class, ByteBuffer.class, new ByteBufferFileLoader.Factory())
03. new MultiModelLoaderFactory.Entry(File.class, InputStream.class, new FileLoader.StreamFactory())
04. new MultiModelLoaderFactory.Entry(File.class, ParcelFileDescriptor.class, new FileLoader.FileDescriptorFactory())
05. new MultiModelLoaderFactory.Entry(File.class, File.class, new UnitModelLoader.Factory<File>())
06. new MultiModelLoaderFactory.Entry(int.class, InputStream.class, new ResourceLoader.StreamFactory(resources))
07. new MultiModelLoaderFactory.Entry(int.class, ParcelFileDescriptor.class, new ResourceLoader.FileDescriptorFactory(resources))
08. new MultiModelLoaderFactory.Entry(Integer.class, InputStream.class, new ResourceLoader.StreamFactory(resources))
09. new MultiModelLoaderFactory.Entry(Integer.class, ParcelFileDescriptor.class, new ResourceLoader.FileDescriptorFactory(resources))
10. new MultiModelLoaderFactory.Entry(String.class, InputStream.class, new DataUrlLoader.StreamFactory())
11. new MultiModelLoaderFactory.Entry(String.class, InputStream.class, new StringLoader.StreamFactory())
12. new MultiModelLoaderFactory.Entry(String.class, ParcelFileDescriptor.class, new StringLoader.FileDescriptorFactory())
13. new MultiModelLoaderFactory.Entry(Uri.class, InputStream.class, new HttpUriLoader.Factory())
14. new MultiModelLoaderFactory.Entry(Uri.class, InputStream.class, new AssetUriLoader.StreamFactory(context.getAssets()))
15. new MultiModelLoaderFactory.Entry(Uri.class, ParcelFileDescriptor.class, new AssetUriLoader.FileDescriptorFactory(context.getAssets()))
16. new MultiModelLoaderFactory.Entry(Uri.class, InputStream.class, new MediaStoreImageThumbLoader.Factory(context))
17. new MultiModelLoaderFactory.Entry(Uri.class, InputStream.class, new MediaStoreVideoThumbLoader.Factory(context))
18. new MultiModelLoaderFactory.Entry(Uri.class, InputStream.class, new UriLoader.StreamFactory(context.getContentResolver()))
19. new MultiModelLoaderFactory.Entry(Uri.class, ParcelFileDescriptor.class, new UriLoader.FileDescriptorFactory(context.getContentResolver()))
20. new MultiModelLoaderFactory.Entry(Uri.class, InputStream.class, new UrlUriLoader.StreamFactory())
21. new MultiModelLoaderFactory.Entry(URL.class, InputStream.class, new UrlLoader.StreamFactory())
22. new MultiModelLoaderFactory.Entry(Uri.class, File.class, new MediaStoreFileLoader.Factory(context))
23. new MultiModelLoaderFactory.Entry(GlideUrl.class, InputStream.class, new HttpGlideUrlLoader.Factory())
24. new MultiModelLoaderFactory.Entry(byte[].class, ByteBuffer.class, new ByteArrayLoader.ByteBufferFactory())
25. new MultiModelLoaderFactory.Entry(byte[].class, InputStream.class, new ByteArrayLoader.StreamFactory())
EncoderRegistry中的注册对象
1. new EncoderRegistry.Entry(ByteBuffer.class, new ByteBufferEncoder)
2. new EncoderRegistry.Entry(InputStream.class, new StreamEncoder(arrayPool))
ResourceDecoderRegistry中的注册对象
+) new ResourceDecoderRegistry.Entry(ByteBuffer.class, GifDrawable.class, byteBufferGifDecoder)
+) new ResourceDecoderRegistry.Entry(InputStream.class, GifDrawable.class, new StreamGifDecoder(byteBufferGifDecoder, arrayPool))
1. new ResourceDecoderRegistry.Entry(ByteBuffer.class, Bitmap.class, new ByteBufferBitmapDecoder(downsampler))
2. new ResourceDecoderRegistry.Entry(InputStream.class, Bitmap.class, new StreamBitmapDecoder(downsampler,arrayPool))
3. new ResourceDecoderRegistry.Entry(ParcelFileDescriptor.class, Bitmap.class, new VideoBitmapDecoder(bitmapPool))
4. new ResourceDecoderRegistry.Entry(ByteBuffer.class, BitmapDrawable.class, new BitmapDrawableDecoder<>(resources, bitmapPool, new ByteBufferBitmapDecoder(downsampler)))
5. new ResourceDecoderRegistry.Entry(InputStream.class, BitmapDrawable.class, new BitmapDrawableDecoder<>(resources, bitmapPool, new StreamBitmapDecoder(downsampler, arrayPool))
6. new ResourceDecoderRegistry.Entry(ParcelFileDescriptor.class, BitmapDrawable.class, new BitmapDrawableDecoder<>(resources, bitmapPool, new VideoBitmapDecoder(bitmapPool)))
7. new ResourceDecoderRegistry.Entry(GifDecoder.class, Bitmap.class, new GifFrameResourceDecoder(bitmapPool))
8. new ResourceDecoderRegistry.Entry(File.class, File.class, new FileDecoder())
ResourceEncoderRegistry中的注册对象
1. new ResourceEncoderRegistry.Entry(Bitmap.class, new BitmapEncoder())
2. new ResourceEncoderRegistry.Entry(BitmapDrawable.class, new BitmapDrawableEncoder(bitmapPool, new BitmapEncoder()))
3. new ResourceEncoderRegistry.Entry(GifDrawable.class, new GifDrawableEncoder())
DataRewinderRegistry中的注册对象
*) <ByteBuffer.class, new ByteBufferRewinder.Factory()>
*) <InputStream.class, new InputStreamRewinder.Factory(arrayPool)>
TranscoderRegistry中的注册对象
1. new TranscoderRegistry.Entry(Bitmap.class, BitmapDrawable.class, new BitmapDrawableTranscoder(resources, bitmapPool))
2. new TranscoderRegistry.Entry(Bitmap.class, byte[].class, new BitmapBytesTranscoder())
3. new TranscoderRegistry.Entry(GifDrawable.class, byte[].class, new GifDrawableBytesTranscoder())
ImageHeaderParserRegistry中的注册对象
1. new DefaultImageHeaderParser()

自定义注册对象

并不是说在glide内部就已经固定注册对象而不能对已注册的对象进行更替了。通过实现GlideModule(com.bumptech.glide.module.GlideModule)接口可以增加或替换注册对象,比如glide中对okhttp的支持。

package com.bumptech.glide.integration.okhttp3;

...

public class OkHttpGlideModule implements GlideModule {
    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
        // Do nothing.
    }

    @Override
    public void registerComponents(Context context, Registry registry) {
        registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory());
    }
}

获取注册对象

对象注册了,当然是方便工程运作时方便随时调用,Registry提供了以下接口对注册对象的获取。

  • getLoadPath(Class<Data>, Class<TResource>, Class<Transcode>)

  • getRegisteredResourceClasses(Class<Model>, Class<TResource>, Class<Transcode>)

  • isResourceEncoderAvailable(Resource<?>)

  • getResultEncoder(Resource<X>)

  • getSourceEncoder(X)

  • getRewinder(X)

  • getModelLoaders(Model)

  • getImageHeaderParsers()

getModelLoaders

1.在ModelLoaderRegistry数据集合中找出Entry第一个传参为String(因为请求的图片地址是一个字符串格式)的数据项,通过上面的数据集合可以知道,符合条件的有:

10. new MultiModelLoaderFactory.Entry(String.class, InputStream.class, new DataUrlLoader.StreamFactory())
11. new MultiModelLoaderFactory.Entry(String.class, InputStream.class, new StringLoader.StreamFactory())
12. new MultiModelLoaderFactory.Entry(String.class, ParcelFileDescriptor.class, new StringLoader.FileDescriptorFactory())

2.但由于DataUrlLoader#handles(String)大多数情况可能返回false,实际符合条件的也就11和12两项,那么该方法返回的数据集合就由两个MultiModelLoader组成。
3.而在StringLoader.StreamFactory#build(MultiModelLoaderFactory)中调用了MultiModelLoaderFactory.build(Uri.class, InputStream.class),那么,在ModelLoaderRegistry数据集合中找出Entry第一个传参为Uri,第二个传参为InputStream的数据项,符合条件的有:

13. new MultiModelLoaderFactory.Entry(Uri.class, InputStream.class, new HttpUriLoader.Factory())
14. new MultiModelLoaderFactory.Entry(Uri.class, InputStream.class, new AssetUriLoader.StreamFactory(context.getAssets()))
16. new MultiModelLoaderFactory.Entry(Uri.class, InputStream.class, new MediaStoreImageThumbLoader.Factory(context))
17. new MultiModelLoaderFactory.Entry(Uri.class, InputStream.class, new MediaStoreVideoThumbLoader.Factory(context))
18. new MultiModelLoaderFactory.Entry(Uri.class, InputStream.class, new UriLoader.StreamFactory(context.getContentResolver()))
20. new MultiModelLoaderFactory.Entry(Uri.class, InputStream.class, new UrlUriLoader.StreamFactory())

4.由以上可知,会用一个MultiModelLoader存储ModelLoader实现类的集合,集合数据有:

(1) com.bumptech.glide.load.model.stream.HttpUriLoader
(2) com.bumptech.glide.load.model.AssetUriLoader
(3) com.bumptech.glide.load.model.stream.MediaStoreImageThumbLoader
(4) com.bumptech.glide.load.model.stream.MediaStoreVideoThumbLoader
(5) com.bumptech.glide.load.model.UriLoader
(6) com.bumptech.glide.load.model.UrlUriLoader

5.同理,在StringLoader.FileDescriptorFactory#build(MultiModelLoaderFactory)中调用了MultiModelLoaderFactory.build(Uri.class, ParcelFileDescriptor.class),那么,在ModelLoaderRegistry数据集合中找出Entry第一个传参为Uri,第二个传参为ParcelFileDescriptor的数据项,符合条件的有:

15. new MultiModelLoaderFactory.Entry(Uri.class, ParcelFileDescriptor.class, new AssetUriLoader.FileDescriptorFactory(context.getAssets()))
19. new MultiModelLoaderFactory.Entry(Uri.class, ParcelFileDescriptor.class, new UriLoader.FileDescriptorFactory(context.getContentResolver()))

6.由以上可知,会再用一个MultiModelLoader存储ModelLoader实现类的集合,集合数据有:

(1) com.bumptech.glide.load.model.AssetUriLoader
(2) com.bumptech.glide.load.model.UriLoader

7.这样一来,该方法最终返回一个包含两个MultiModelLoader的集合项。

getSourceEncoder

将远程资源解码成数据流(InputStream)后,调用该方法,那么就在EncoderRegistry的数据集合中找出Entry第一个传参为InputStream.class的数据项,通过上面的数据集合可以知道,符合条件的有

2. new EncoderRegistry.Entry(InputStream.class, new StreamEncoder(arrayPool))

而StreamEncoder对象则是该方法的返回项。

getLoadPath

该方法需要返回一个LoadPath对象,当获取缓存文件的ByteBuffer时,调用此方法

getLoadPath(ByteBuffer.class, Object.class, Drawable.class)

其中,第二个参数取决于com.bumptech.glide.request.BaseRequestOptions#resourceClass,而默认为Object;第三个参数取决于com.bumptech.glide.RequestManager#as(Class<ResourceType>)中的传参,而当没主动设置的情况下,默认有以下实现

package com.bumptech.glide;

...

public class RequestManager implements LifecycleListener {

    ...

    public RequestBuilder<Drawable> asDrawable() {
            return as(Drawable.class).transition(new DrawableTransitionOptions());
        }
    }

    public RequestBuilder<Drawable> load(@Nullable Object model) {
        return asDrawable().load(model);
    }

    ...

}

也就是说,默认第三个参数为Drawable类型,那么获取返回流程如下:
1.在ResourceDecoderRegistry数据集合中找出Entry第一二个传参的基类分别为ByteBuffer和Object的数据项,通过上面的数据集合可以知道,符合条件的有以下三项:

+) new ResourceDecoderRegistry.Entry(ByteBuffer.class, GifDrawable.class, byteBufferGifDecoder)
1. new ResourceDecoderRegistry.Entry(ByteBuffer.class, Bitmap.class, new ByteBufferBitmapDecoder(downsampler))
4. new ResourceDecoderRegistry.Entry(ByteBuffer.class, BitmapDrawable.class, new BitmapDrawableDecoder<>(resources, bitmapPool, new ByteBufferBitmapDecoder(downsampler)))

将这三个Entry的第二个参数作为一个集合(为了区分,简称resource集合)

1. GifDrawable.class
2. Bitmap.class
3. BitmapDrawable.class

2.依次比对集合中的项的基类是否是Drawable(getLoadPath的第三个传参),可知GifDrawable.class和BitmapDrawable.class满足条件,那么分别将getLoadPath的第三个参数即Drawable.class存下来。
3.Bitmap.class不满足条件,那么就在TranscoderRegistry数据集中找出Entry第一二个传参的基类分别为Bitmap和Drawable的数据项,符合条件的显然有一项

1. new TranscoderRegistry.Entry(Bitmap.class, BitmapDrawable.class, new BitmapDrawableTranscoder(resources, bitmapPool))

那么,将Entry的第二个参数即BitmapDrawable.class存下来,也就是说,第2、3两步的存储集合(为了区分,简称transcode集合)为

1.Drawable.class
2.BitmapDrawable.class
3.Drawable.class

4.重复第1步操作,只是第二个传参不再是Object.class,而是resource集合中的每个项,而这次需要收集的是ResourceDecoderRegistry.Entry的第三个参数所对应的集合,那么,不难得出集合(为了区分,简称decoder集合)为

byteBufferGifDecoder
new ByteBufferBitmapDecoder(downsampler)
new BitmapDrawableDecoder<>(resources, bitmapPool, new ByteBufferBitmapDecoder(downsampler))

5.在TranscoderRegistry数据集合中找出Entry第一二个传参的基类分别为resource集合的对象和transcode集合的对象的数据项,如果第一个传参是第二个传参的基类,那么就返回UnitTranscoder,否则返回TranscoderRegistry.Entry的第三个参数,那么所对应的集合(为了区分,简称transcoder集合)为

UnitTranscoder
new BitmapDrawableTranscoder(resources, bitmapPool)
UnitTranscoder

6.那么,根据上面的集合,创建一个DecodePath的集合(为了区分,简称decodePaths)

1. new DecodePath(ByteBuffer, GifDrawable, Drawable, [ByteBufferGifDecoder], UnitTranscoder, exceptionListPool)
2. new DecodePath(ByteBuffer, Bitmap, BitmapDrawable, [ByteBufferBitmapDecoder], BitmapDrawableTranscoder, exceptionListPool)
3. new DecodePath(ByteBuffer, BitmapDrawable, Drawable, [BitmapDrawableDecoder], UnitTranscoder, exceptionListPool)

7.现在就可以提取返回值了

new LoadPath<>(ByteBuffer, Object, Drawable, decodePaths, exceptionListPool);
getRewinder

当获取缓存文件的ByteBuffer时,调用此方法

getRewinder(ByteBuffer.class)

DataRewinderRegistry的数据集中查找以ByteBuffer为键的项,不难发现,有

*) <ByteBuffer.class, new ByteBufferRewinder.Factory()>

然后执行com.bumptech.glide.load.resource.bytes.ByteBufferRewinder.Factory#build(ByteBuffer),其执行结果就是创建一个ByteBufferRewinder的实例,源码如下:

package com.bumptech.glide.load.resource.bytes;

...

public class ByteBufferRewinder implements DataRewinder<ByteBuffer> {
    private final ByteBuffer buffer;

    public ByteBufferRewinder(ByteBuffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public ByteBuffer rewindAndGet() throws IOException {
        buffer.position(0);
        return buffer;
    }

    @Override
    public void cleanup() {
        // Do nothing.
    }

    public static class Factory implements DataRewinder.Factory<ByteBuffer> {

        @Override
        public DataRewinder<ByteBuffer> build(ByteBuffer data) {
            return new ByteBufferRewinder(data);
        }

        @Override
        public Class<ByteBuffer> getDataClass() {
            return ByteBuffer.class;
        }
    }
}

而这个返回的实例就是该方法的返回值。

getResultEncoder

当通过com.bumptech.glide.load.resource.bitmap.ByteBufferBitmapDecoder#decode(ByteBuffer, int, int, Options)获取到一个Resource<Bitmap>,那么这时调用getResultEncoder的话,就需要从ResourceEncoderRegistry数据集合中找出Entry第一个参数为Bitmap的数据项集,那么符合条件的有

1. new ResourceEncoderRegistry.Entry(Bitmap.class, new BitmapEncoder())

那么第二个参数new BitmapEncoder()就是该方法需要返回的值。

isResourceEncoderAvailable

该方法就是判断getResultEncoder的返回结果是否为空

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值