SDWebImage学习总结

      SDWebImage是一个开源的第三方库,它提供了UIImageView的一个分类,以支持从远程服务器下载并缓存图片的功能。它具有以下功能

1 提供UIImageView的一个分类,以支持网络图片的加载与缓存管理

2 一个异步的图片加载器

3 一个异步的内存+磁盘图片缓存

4 支持GIF图片

5 支持WebP图片

6 后台图片解压缩处理

7 确保同一个URL的图片不被下载多次(操作队列)

8 确保虚假的URL不会被反复加载

9 确保下载及缓存时,主线程不被阻塞(写到磁盘时采用异步)   

  

     SDWebImage底层实现有沙盒缓存机制,主要由三块组成:1、内存图片缓存,2、内存操作缓存,3、磁盘沙盒缓存


一、SDWebImage主要类的介绍

1. SDWebImageManager

 SDWebImageManager是UIImageView+WebCache类别中的类,它连接了异步下载和图片存储


SDWebImageManager *manager = [SDWebImageManager sharedManager];

[manager downloadImageWithURL:imageURL

                      options:0

                     progress:nil

                    completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {

                        if (image) {

                            // do something with image

                        }

                    }];

2.SDImageCache

SDImageCache处理内存缓存以及可选的磁盘缓存,磁盘缓存的写入是异步执行的

(1)initWithNamespace初始化命名空间,在该函数中主要进行变量等的初始化,并且当收到内存警告时清理内存 调用clearMemory,程序中断时清理磁盘,调用 cleanDisk函数,而cleanDisk函数内调用cleanDiskWithCompletionBlock块分两轮清理内存,第一轮以7天为限,清理过期的文件,第二轮以设定文件大小的一半为标准,清理超过设定内存一半的文件(先排序,清理时间最久的文件);程序进入后台时,调用backgroundCleanDisk函数,后台长期执行cleanDiskWithCompletionBlock

(2)storeImage保存图片,如果内存能用,缓存到内存;存到磁盘,

(3)queryDiskCacheForKey 返回一个NSOperation操作,queryDiskCacheForKey:done:,如果此方法返回nil,则说明缓存中现在还没有这张照片,因此你需要得到并缓存这张图片。缓存key是缓存图片的程序唯一的标识符,一般使用图片的完整URL。  downloadImageWithURL调用,根据option策略寻找图片进而决定是否下载


3.SDWebImageDownloader

    异步下载器SDWebImageDownload利用它的单例对象sharedDownloader,可以很好的对图片的下载过程进行配置。

  (1)downloadImageWithURL:整个下载器对于下载请求的管理,管理下载进度回调、完成回调以及取消操作

  (2)下载的核心是SDWebImageDownloaderOperation中用NSURLSession完成的



二、SDWebImage原理

(一)、下载

在SDWebImage中,图片的下载是由SDWebImageDownloader类来完成的。它是一个异步下载器,并对图像加载做了优化处理。下面我们就来看看它的具体实现。

1、下载选项

   在下载的过程中,程序会根据设置的不同的下载选项,而执行不同的操作。下载选项由枚举SDWebImageDownloaderOptions定义,具体如下


typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {

  SDWebImageDownloaderLowPriority = 1 << 0,

  SDWebImageDownloaderProgressiveDownload = 1 << 1,

  // <span style="background-color: #ffff00;">默认情况下请求不使用NSURLCache,如果设置该选项,则以默认的缓存策略来使用NSURLCache</span>

  SDWebImageDownloaderUseNSURLCache = 1 << 2,

  // 如果从NSURLCache缓存中读取图片,则使用nil作为参数来调用完成block

  SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,

  // 在iOS 4+系统上,允许程序进入后台后继续下载图片。该操作通过向系统申请额外的时间来完成后台下载。如果后台任务终止,则操作会被取消

  SDWebImageDownloaderContinueInBackground = 1 << 4,

  // 通过设置NSMutableURLRequest.HTTPShouldHandleCookies = YES来处理存储在NSHTTPCookieStore中的cookie

  SDWebImageDownloaderHandleCookies = 1 << 5,

  // 允许不受信任的SSL证书。主要用于测试目的。

  SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,

  // 将图片下载放到高优先级队列中

  SDWebImageDownloaderHighPriority = 1 << 7,

};

 

可以看出,这些选项主要涉及到下载的优先级、缓存、后台任务执行、cookie处理以认证几个方面。

2、下载顺序

SDWebImage的下载操作是按一定顺序来处理的,它定义了两种下载顺序,如下所示

typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {

  // 以队列的方式,按照先进先出的顺序下载。这是默认的下载顺序

  SDWebImageDownloaderFIFOExecutionOrder,

  // 以栈的方式,按照后进先出的顺序下载。(以添加操作依赖的方式实现)

  SDWebImageDownloaderLIFOExecutionOrder

};


3、下载管理器

      SDWebImageDownloader下载管理器是一个单例类,它主要负责图片的下载操作的管理。图片的下载是放在一个NSOperationQueue操作队列中来完成的,其声明如下:

@property (strong, nonatomic) NSOperationQueue *downloadQueue;

默认情况下,队列最大并发数是6。如果需要的话,我们可以通过SDWebImageDownloader类的  maxConcurrentDownloads 属性来修改。 

所有下载操作的网络响应序列化处理是放在一个自定义的并行调度队列中来处理的,其声明及定义如下:

@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t barrierQueue;

- (id)init {

  if ((self = [super init])) {

    ...

    _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);

    ...

  }

  return self;

}

 

每一个图片的下载都会对应一些回调操作,如下载进度回调,下载完成回调等,这些回调操作是以block形式来呈现,为此在SDWebImageDownloader.h中定义了几个block,如下所示:

// 下载进度

typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);

// 下载完成

typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);

// Header过滤

typedef NSDictionary *(^SDWebImageDownloaderHeadersFilterBlock)(NSURL *url, NSDictionary *headers);

图片下载的这些回调信息存储在SDWebImageDownloader类的  URLCallbacks 属性中,该属性是一个字典,key是图片的URL地址,value则是一个数组,包含每个图片的多组回调信息。由于我们允许多个图片同时下载,因此可能会有多个线程同时操作URLCallbacks属性。为了保证URLCallbacks操作(添加、删除)的线程安全性,SDWebImageDownloader将这些操作作为一个个任务放到barrierQueue队列中,并设置屏障来确保同一时间只有一个线程操作URLCallbacks属性,我们以添加操作为例,如下代码所示: 

- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {

    ...

    // 1. 以dispatch_barrier_sync操作来保证同一时间只有一个线程能对URLCallbacks进行操作

    dispatch_barrier_sync(self.barrierQueue, ^{

  ...

  // 2. 处理同一URL的同步下载请求的单个下载

  NSMutableArray *callbacksForURL = self.URLCallbacks[url];

  NSMutableDictionary *callbacks = [NSMutableDictionarynew];

  if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlockcopy];

  if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlockcopy];

  [callbacksForURL addObject:callbacks];

  self.URLCallbacks[url] = callbacksForURL;

  ...

    });

}

 

整个下载管理器对于下载请求的管理都是放在downloadImageWithURL:options:progress:completed:方法里面来处理的,该方法调用了上面所提到的addProgressCallback:andCompletedBlock:forURL:createCallback:方法来将请求的信息存入管理器中,同时在创建回调的block中创建新的操作,配置之后将其放入downloadQueue操作队列中,最后方法返回新创建的操作。其具体实现如下:

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {

  ...

  [self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{

    ...

    // 1. 创建请求对象,并根据options参数设置其属性

    // 为了避免潜在的重复缓存(NSURLCache + SDImageCache),如果没有明确告知需要缓存,则禁用图片请求的缓存操作

    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ?NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];

    ...

    // 2. 创建SDWebImageDownloaderOperation操作对象,并进行配置

    // 配置信息包括是否需要认证、优先级

    operation = [[wself.operationClass alloc] initWithRequest:request

                              options:options

                             progress:^(NSInteger receivedSize,NSInteger expectedSize) {

                               // 3. 从管理器的callbacksForURL中找出该URL所有的进度处理回调并调用

                               ...

                               for (NSDictionary *callbacks in callbacksForURL) {

                                 SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];

                                 if (callback) callback(receivedSize, expectedSize);

                               }

                             }

                            completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {

                               // 4. 从管理器的callbacksForURL中找出该URL所有的完成处理回调并调用,

                               // 如果finished为YES,则将该url对应的回调信息从URLCallbacks中删除

                              ...

                              if (finished) {

                                [sself removeCallbacksForURL:url];

                              }

                              for (NSDictionary *callbacks in callbacksForURL) {

                                SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];

                                if (callback) callback(image, data, error, finished);

                              }

                            }

                            cancelled:^{

                              // 5. 取消操作将该url对应的回调信息从URLCallbacks中删除

                              SDWebImageDownloader *sself = wself;

                              if (!sself) return;

                              [sself removeCallbacksForURL:url];

                            }];

    ...

    // 6. 将操作加入到操作队列downloadQueue中

    // 如果是LIFO顺序,则将新的操作作为原队列中最后一个操作的依赖,然后将新操作设置为最后一个操作

    [wself.downloadQueue addOperation:operation];

    if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {

      [wself.lastAddedOperation addDependency:operation];

      wself.lastAddedOperation = operation;

    }

  }];

  return operation;

}

 

另外,每个下载操作的超时时间可以通过downloadTimeout属性来设置,默认值为15秒。

4、下载操作

     每个图片的下载都是一个Operation操作。我们在上面分析过这个操作的创建及加入操作队列的过程。现在我们来看看单个操作的具体实现。

SDWebImage定义了一个协议,即  SDWebImageOperation 作为图片下载操作的基础协议。它只声明了一个cancel方法,用于取消操作。协议的具体声明如下: 

@protocol SDWebImageOperation <NSObject>

- (void)cancel;

@end

SDWebImage自定义了一个Operation类,即  SDWebImageDownloaderOperation,它继承自NSOperation,并采用了SDWebImageOperation协议。除了继承而来的方法,该类只向外暴露了两个方法,即上面所用到的初始化方法

- (id)initWithRequest:(NSURLRequest *)request

            inSession:(NSURLSession *)session

              options:(SDWebImageDownloaderOptions)options

             progress:(SDWebImageDownloaderProgressBlock)progressBlock

            completed:(SDWebImageDownloaderCompletedBlock)completedBlock

            cancelled:(SDWebImageNoParamsBlock)cancelBlock;

- (id)initWithRequest:(NSURLRequest *)request

              options:(SDWebImageDownloaderOptions)options

             progress:(SDWebImageDownloaderProgressBlock)progressBlock

            completed:(SDWebImageDownloaderCompletedBlock)completedBlock

            cancelled:(SDWebImageNoParamsBlock)cancelBlock 

对于图片的下载,SDWebImageDownloaderOperation完全依赖于URL加载系统中的NSURLSession类

    我们先来分析一下SDWebImageDownloaderOperation类中对于图片实际数据的下载处理,即NSURLsession各代理方法的实现。

首先,SDWebImageDownloaderOperation在分类中采用了NSURLSessionTaskDelegate协议和NSURLSessionDataDelegate协议,并实现了该协议的以下几个方法:

- (void)URLSession:(NSURLSession *)session

          dataTask:(NSURLSessionDataTask *)dataTask

didReceiveResponse:(NSURLResponse *)response

 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler通知代理受到服务器的回调,如果服务器返回的状态吗是304,取消操作返回内存缓存中的图片

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data 通知代理接收到期望数据

- (void)URLSession:(NSURLSession *)session

          dataTask:(NSURLSessionDataTask *)dataTask

 willCacheResponse:(NSCachedURLResponse *)proposedResponse

 completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler询问代理是否将数据任务的response存储在缓存中



- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error 通知代理任务完成

(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler通知代理Task层次收到了授权,证书等问题

注:缩放操作可以查看SDWebImageCompat文件中的SDScaledImageForKey函数;解压缩操作可以查看SDWebImageDecoder文件+decodedImageWithImage方法


5、小结

      下载的核心其实就是利用NSURLsession对象来加载数据。每个图片的下载都由一个Operation操作来完成,并将这些操作放到一个操作队列中。这样可以实现图片的并发下载。

 

(二)、缓存

     为了减少网络流量的消耗,我们都希望下载下来的图片缓存到本地,下次再去获取同一张图片时,可以直接从本地获取,而不再从远程服务器获取。这样做的另一个好处是提升了用户体验,用户第二次查看同一幅图片时,能快速从本地获取图片直接呈现给用户。

SDWebImage提供了对图片缓存的支持,而该功能是由SDImageCache类来完成的。该类负责处理内存缓存及一个可选的磁盘缓存。其中磁盘缓存的写操作是异步的,这样就不会对UI操作造成影响。

1、内存缓存及磁盘缓存

    内存缓存的处理是使用NSCache对象来实现的。NSCache是一个类似于集合的容器。它存储key-value对,这一点类似于NSDictionary类。我们通常用使用缓存来临时存储短时间使用但创建昂贵的对象。重用这些对象可以优化性能,因为它们的值不需要重新计算。另外一方面,这些对象对于程序来说不是紧要的,在内存紧张时会被丢弃。

磁盘缓存的处理则是使用NSFileManager对象来实现的。图片存储的位置是位于Cache文件夹。另外,SDImageCache还定义了一个串行队列,来异步存储图片。

      SDImageCache提供了大量方法来缓存、获取、移除及清空图片。而对于每个图片,为了方便地在内存或磁盘中对它进行这些操作,我们需要一个key值来索引它。在内存中,我们将其作为NSCache的key值,而在磁盘中,我们用这个key作为图片的文件名。对于一个远程服务器下载的图片,其url是作为这个key的最佳选择了。我们在后面会看到这个key值的重要性。

2、存储图片

      我们先来看看图片的缓存操作,该操作会在内存中放置一份缓存,而如果确定需要缓存到磁盘,则将磁盘缓存操作作为一个task放到串行队列中处理。在iOS中,会先检测图片是PNG还是JPEG,并将其转换为相应的图片数据,最后将数据写入到磁盘中(文件名是对key值做MD5摘要后的串)。缓存操作的基础方法是-storeImage:recalculateFromImage:imageData:forKey:toDisk,它的

3、查询图片

如果我们想在内存或磁盘中查询是否有key指定的图片,则可以分别使用以下方法:

- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;

- (UIImage *)imageFromDiskCacheForKey:(NSString *)key;

而如果只是想查看本地是否存在key指定的图片,则不管是在内存还是在磁盘上,则可以使用以下方法:

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock

 

4、移除图片

图片的移除操作则可以使用以下方法:

- (void)removeImageForKey:(NSString *)key;

- (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion;

- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk;

- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;

我们可以选择同时移除内存及磁盘上的图片。

5、清理图片(磁盘)

磁盘缓存图片的清理操作可以分为完全清空和部分清理。完全清空操作是直接把缓存的文件夹移除,清空操作有以下两个方法:

- (void)clearDisk;

- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;

部分清理则是根据我们设定的一些参数值来移除一些文件,这里主要有两个指标:文件的缓存有效期及最大缓存空间大小。文件的缓存有效期可以通过maxCacheAge属性来设置,默认是1周的时间。如果文件的缓存时间超过这个时间值,则将其移除。而最大缓存空间大小是通过maxCacheSize属性来设置的,如果所有缓存文件的总大小超过这一大小,则会按照文件最后修改时间的逆序,以每次一半的递归来移除那些过早的文件,直到缓存的实际大小小于我们设置的最大使用空间。清理的操作在-cleanDiskWithCompletionBlock:方法中,其实现如下:

+ View Code

 

6、小结

以上分析了图片缓存操作,当然,除了上面讲的几个操作,SDImageCache类还提供了一些辅助方法。如获取缓存大小、缓存中图片的数量、判断缓存中是否存在某个key指定的图片。另外,SDImageCache类提供了一个单例方法的实现,所以我们可以将其当作单例对象来处理。

汇总一些常用接口、属性:

(1)-getSize  :获得硬盘缓存的大小

(2)-getDiskCount : 获得硬盘缓存的图片数量

(3)-clearMemory  : 清理所有内存图片

(4)- removeImageForKey:(NSString *)key  系列的方法 : 从内存、硬盘按要求指定清除图片

(5)maxMemoryCost  :  保存在存储器中像素的总和

(6)maxCacheSize  :  最大缓存大小 以字节为单位。默认没有设置,也就是为0,而清理磁盘缓存的先决条件为self.maxCacheSize > 0,所以0表示无限制。

(7)maxCacheAge : 在内存缓存保留的最长时间以秒为单位计算,默认是一周

 

 

(三)、SDWebImageManager

    在实际的运用中,我们并不直接使用SDWebImageDownloader类及SDImageCache类来执行图片的下载及缓存。为了方便用户的使用,SDWebImage提供了SDWebImageManager对象来管理图片的下载与缓存。而且我们经常用到的诸如UIImageView+WebCache等控件的分类都是基于SDWebImageManager对象的。该对象将一个下载器和一个图片缓存绑定在一起,并对外提供两个只读属性来获取它们,如下代码所示:

@interface SDWebImageManager : NSObject


@property (weak, nonatomic) id <SDWebImageManagerDelegate> delegate;


@property (strong, nonatomic, readonly) SDImageCache *imageCache;

@property (strong, nonatomic, readonly) SDWebImageDownloader *imageDownloader;



@end

从上面的代码中我们还可以看到有一个delegate属性,其是一个id<SDWebImageManagerDelegate>对象。SDWebImageManagerDelegate声明了两个可选实现的方法,如下所示:

// 控制当图片在缓存中没有找到时,应该下载哪个图片

- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;


// 允许在图片已经被下载完成且被缓存到磁盘或内存前立即转换

- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;

这两个代理方法会在SDWebImageManager的-downloadImageWithURL:options:progress:completed:方法中调用,而这个方法是SDWebImageManager类的核心所在。对于这个方法,我们没有做过多的解释。其主要就是下载图片并根据操作选项来缓存图片。上面这个下载方法中的操作选项参数是由枚举SDWebImageOptions来定义的,这个操作中的一些选项是与SDWebImageDownloaderOptions中的选项对应的。

 

大家在看-downloadImageWithURL:options:progress:completed:,可以看到两个SDWebImageOptions与SDWebImageDownloaderOptions中的选项是如何对应起来的,在此不多做解释。

 

SDWebImageManager的几个方法

(1)- (void)cancelAll   : 取消runningOperations中所有的操作,并全部删除

(2)- (BOOL)isRunning  :检查是否有操作在运行,这里的操作指的是下载和缓存组成的组合操作

(3) - downloadImageWithURL:options:progress:completed:   核心方法

(4)- (BOOL)diskImageExistsForURL:(NSURL *)url  :指定url的图片是否进行了磁盘缓存

 

四、视图扩展

我在使用SDWebImage的时候,使用得最多的是UIImageView+WebCache中的针对UIImageView的扩展方法,这些扩展方法将UIImageView与WebCache集成在一起,来让UIImageView对象拥有异步下载和缓存远程图片的能力。其中最核心的方法是-sd_setImageWithURL:placeholderImage:options:progress:completed:,其使用SDWebImageManager单例对象下载并缓存图片,完成后将图片赋值给UIImageView对象的image属性,以使图片显示出来

除了扩展UIImageView之外,SDWebImage还扩展了UIView、UIButton、MKAnnotationView等视图类,大家可以参考源码。

当然,如果不想使用这些扩展,则可以直接使用SDWebImageManager来下载图片,这也是很OK的。

 

五、技术点

SDWebImage的主要任务就是图片的下载和缓存。为了支持这些操作,它主要使用了以下知识点:

1dispatch_barrier_sync函数:该方法用于对操作设置顺序,确保在执行完任务后才会执行后续操作。该方法常用于确保类的线程安全性操作。

2 NSMutableURLRequest:用于创建一个网络请求对象,我们可以根据需要来配置请求报头等信息。

3 NSOperation及NSOperationQueue:操作队列是Objective-C中一种高级的并发处理方法,现在它是基于GCD来实现的。相对于GCD来说,操作队列的优点是可以取消在任务处理队列中的任务,另外在管理操作间的依赖关系方面也容易一些。对SDWebImage中我们就看到了如何使用依赖将下载顺序设置成后进先出的顺序。

4    一套新的网络请求接口,即NSURLSession类。

5 开启一个后台任务。

6 NSCache类:一个类似于集合的容器。它存储key-value对,这一点类似于NSDictionary类。我们通常用使用缓存来临时存储短时间使用但创建昂贵的对象。重用这些对象可以优化性能,因为它们的值不需要重新计算。另外一方面,这些对象对于程序来说不是紧要的,在内存紧张时会被丢弃。

7 清理缓存图片的策略:特别是最大缓存空间大小的设置。如果所有缓存文件的总大小超过这一大小,则会按照文件最后修改时间的逆序,以每次一半的递归来移除那些过早的文件,直到缓存的实际大小小于我们设置的最大使用空间。

8 对图片的解压缩操作:这一操作可以查看SDWebImageDecoder.m中+decodedImageWithImage方法的实现。

9 对GIF图片的处理

10对WebP图片的处理

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员的修养

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值