SDWebImage是在iOS开发中被广泛使用的一个第三方开源框架,对于网络图片的请求使用非常方便。
一、总体概述
它实现的大致思路是:首先根据LRU去缓存中查找,如果缓存中没有,去磁盘中查找,找到了,直接读取image,然后展示;如果没找到,将从网络上下载,下载完成后,拿到image展示,并添加到缓存中。如下图:
总体类图如下:
二、核心代码解读:
SDWebImageManager类核心方法解读:- (id)loadImageWithURL:(nullableNSURL*)url options:(SDWebImageOptions)options progress:(nullableSDWebImageDownloaderProgressBlock)progressBlock completed:(nullableSDInternalCompletionBlock)completedBlock;步骤:
1、判断url是否为无效链接;
2、创建管理队列SDWebImageCombinedOperation,并加入队列数组
3、从缓存SDImageCache(包括磁盘)中读取;
4、根据读取缓存结果,判断是否需要从网络下载;
5、通过SDWebImageDownloader从网络中下载image,下载成功,写到缓存;
6、回调,移除管理队列数组。
具体代码如下:
- (id)loadImageWithURL:(nullableNSURL*)url
options:(SDWebImageOptions)options
progress:(nullableSDWebImageDownloaderProgressBlock)progressBlock
completed:(nullableSDInternalCompletionBlock)completedBlock {
// Invoking this method without a completedBlock is pointless
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
// Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode won't
// throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
if ([url isKindOfClass:NSString.class]) {
url = [NSURLURLWithString:(NSString*)url];
}
// Prevents app crashing on argument type error like sending NSNull instead of NSURL
if (![url isKindOfClass:NSURL.class]) {
url =nil;
}
///管理队列,包含读写缓存缓存的队列,下载的队列
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager=self;
///判断下载url是否是无效链接
BOOLisFailedUrl =NO;
if(url) {
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLscontainsObject:url];
}
}
if(url.absoluteString.length==0|| (!(options &SDWebImageRetryFailed) && isFailedUrl)) {
[selfcallCompletionBlockForOperation:operationcompletion:completedBlockerror:[NSErrorerrorWithDomain:NSURLErrorDomaincode:NSURLErrorFileDoesNotExistuserInfo:nil]url:url];
returnoperation;
}
///将operationd管理队列添加到数组保存
@synchronized (self.runningOperations) {
[self.runningOperationsaddObject:operation];
}
///将url转换为key,后面缓存相关需要用到
NSString*key = [selfcacheKeyForURL:url];
///确定读取缓存策略
SDImageCacheOptionscacheOptions =0;
///异步读取
if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
///同步读取
if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
__weakSDWebImageCombinedOperation*weakOperation = operation;
///读取缓存中的数据,并返回读取缓存的队列
operation.cacheOperation= [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage*cachedImage,NSData*cachedData,SDImageCacheType cacheType) {
__strong__typeof(weakOperation) strongOperation = weakOperation;
if(!strongOperation || strongOperation.isCancelled) {
[selfsafelyRemoveOperationFromRunning:strongOperation];
return;
}
// Check whether we should download image from network是否需要从网络下载图片
BOOLshouldDownload = (!(options &SDWebImageFromCacheOnly))
&& (!cachedImage || options &SDWebImageRefreshCached)
&& (![self.delegaterespondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegateimageManager:selfshouldDownloadImageForURL:url]);
if(shouldDownload) {///需要从网络中下载
if(cachedImage && options &SDWebImageRefreshCached) {
// If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
// AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
[selfcallCompletionBlockForOperation:strongOperationcompletion:completedBlockimage:cachedImagedata:cachedDataerror:nilcacheType:cacheTypefinished:YESurl:url];
}
// download if no image or requested to refresh anyway, and download allowed by delegate
///下载策略
SDWebImageDownloaderOptionsdownloaderOptions =0;
if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
if(cachedImage && options &SDWebImageRefreshCached) {
// force progressive off if image already cached but forced refreshing
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
// ignore image read from NSURLCache if image if cached but force refreshing
downloaderOptions |=SDWebImageDownloaderIgnoreCachedResponse;
}
// `SDWebImageCombinedOperation` -> `SDWebImageDownloadToken` -> `downloadOperationCancelToken`, which is a `SDCallbacksDictionary` and retain the completed block below, so we need weak-strong again to avoid retain cycle
__weaktypeof(strongOperation) weakSubOperation = strongOperation;
///下载图片,并返回下载队列信息
strongOperation.downloadToken= [self.imageDownloaderdownloadImageWithURL:urloptions:downloaderOptionsprogress:progressBlockcompleted:^(UIImage*downloadedImage,NSData*downloadedData,NSError*error,BOOLfinished) {
__strongtypeof(weakSubOperation) strongSubOperation = weakSubOperation;
if(!strongSubOperation || strongSubOperation.isCancelled) {///下载队列被取消,不做处理
// Do nothing if the operation was cancelled
// See #699 for more details
// if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data
}elseif(error) {///下载失败
[selfcallCompletionBlockForOperation:strongSubOperationcompletion:completedBlockerror:errorurl:url];
BOOLshouldBlockFailedURL;
// Check whether we should block failed url
if([self.delegaterespondsToSelector:@selector(imageManager:shouldBlockFailedURL:withError:)]) {
shouldBlockFailedURL = [self.delegateimageManager:selfshouldBlockFailedURL:urlwithError:error];
}else{
shouldBlockFailedURL = ( error.code!=NSURLErrorNotConnectedToInternet
&& error.code!=NSURLErrorCancelled
&& error.code!=NSURLErrorTimedOut
&& error.code!=NSURLErrorInternationalRoamingOff
&& error.code!=NSURLErrorDataNotAllowed
&& error.code!=NSURLErrorCannotFindHost
&& error.code!=NSURLErrorCannotConnectToHost
&& error.code!=NSURLErrorNetworkConnectionLost);
}
///添加到下载失败数组中
if(shouldBlockFailedURL) {
@synchronized(self.failedURLs) {
[self.failedURLsaddObject:url];
}
}
}
else{///下载成功,且队列没被取消
if((options &SDWebImageRetryFailed)) {
@synchronized(self.failedURLs) {
[self.failedURLsremoveObject:url];
}
}
BOOLcacheOnDisk = !(options &SDWebImageCacheMemoryOnly);
// We've done the scale process in SDWebImageDownloader with the shared manager, this is used for custom manager and avoid extra scale.
if(self!= [SDWebImageManagersharedManager] &&self.cacheKeyFilter&& downloadedImage) {
downloadedImage = [selfscaledImageForKey:keyimage:downloadedImage];
}
if(options &SDWebImageRefreshCached&& cachedImage && !downloadedImage) {
// Image refresh hit the NSURLCache cache, do not call the completion block
}elseif(downloadedImage && (!downloadedImage.images|| (options &SDWebImageTransformAnimatedImage)) && [self.delegaterespondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
UIImage*transformedImage = [self.delegateimageManager:selftransformDownloadedImage:downloadedImagewithURL:url];
if(transformedImage && finished) {
BOOLimageWasTransformed = ![transformedImageisEqual:downloadedImage];
NSData*cacheData;
// pass nil if the image was transformed, so we can recalculate the data from the image
if(self.cacheSerializer) {
cacheData =self.cacheSerializer(transformedImage, (imageWasTransformed ?nil: downloadedData), url);
}else{
cacheData = (imageWasTransformed ?nil: downloadedData);
}
[self.imageCachestoreImage:transformedImageimageData:cacheDataforKey:keytoDisk:cacheOnDiskcompletion:nil];
}
[selfcallCompletionBlockForOperation:strongSubOperationcompletion:completedBlockimage:transformedImagedata:downloadedDataerror:nilcacheType:SDImageCacheTypeNonefinished:finishedurl:url];
});
}else{
if(downloadedImage && finished) {///根据不同方式写缓存
if(self.cacheSerializer) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0), ^{
NSData*cacheData =self.cacheSerializer(downloadedImage, downloadedData, url);
[self.imageCachestoreImage:downloadedImageimageData:cacheDataforKey:keytoDisk:cacheOnDiskcompletion:nil];
});
}else{
[self.imageCachestoreImage:downloadedImageimageData:downloadedDataforKey:keytoDisk:cacheOnDiskcompletion:nil];
}
}
///回调
[selfcallCompletionBlockForOperation:strongSubOperationcompletion:completedBlockimage:downloadedImagedata:downloadedDataerror:nilcacheType:SDImageCacheTypeNonefinished:finishedurl:url];
}
}
if(finished) {
[selfsafelyRemoveOperationFromRunning:strongSubOperation];
}
}];
}elseif(cachedImage) {///存在缓存图片
///回调
[selfcallCompletionBlockForOperation:strongOperationcompletion:completedBlockimage:cachedImagedata:cachedDataerror:nilcacheType:cacheTypefinished:YESurl:url];
[selfsafelyRemoveOperationFromRunning:strongOperation];
}else {///缓存中不存在且不让下载
///回调
// Image not in cache and download disallowed by delegate
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
[selfsafelyRemoveOperationFromRunning:strongOperation];
}
}];
returnoperation;
}
SDWebImageDownloader的核心方法解读- (nullableSDWebImageDownloadToken*)downloadImageWithURL:(nullableNSURL*)url options:(SDWebImageDownloaderOptions)options progress:(nullableSDWebImageDownloaderProgressBlock)progressBlock completed:(nullableSDWebImageDownloaderCompletedBlock)completedBlock;步骤:
1、根据url创建对应的网络请求NSURLRequest;
2、根据NSURLRequest等创建下载管理队列SDWebImageDownloaderOperation;
3、创建下载token,下载唯一标志结构体,包含下载队列,下载相关回调,urle等相关信息;
4、将SDWebImageDownloaderOperation添加到downloadQueue中,根据优先级开始下载。
注意:下载相关回调在SDWebImageDownloaderOperation类中处理。
具体代码如下:
- (nullableSDWebImageDownloadToken*)downloadImageWithURL:(nullableNSURL*)url
options:(SDWebImageDownloaderOptions)options
progress:(nullableSDWebImageDownloaderProgressBlock)progressBlock
completed:(nullableSDWebImageDownloaderCompletedBlock)completedBlock {
__weak SDWebImageDownloader *wself = self;
return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
__strong__typeof(wself) sself = wself;
///设置超时时间,默认15秒
NSTimeIntervaltimeoutInterval = sself.downloadTimeout;
if(timeoutInterval ==0.0) {
timeoutInterval =15.0;
}
// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
///创建下载请求
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:cachePolicy
timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if(sself.headersFilter) {
request.allHTTPHeaderFields= sself.headersFilter(url, [sselfallHTTPHeaderFields]);
}
else{
request.allHTTPHeaderFields= [sselfallHTTPHeaderFields];
}
///下载管理队列
SDWebImageDownloaderOperation*operation = [[sself.operationClassalloc]initWithRequest:requestinSession:sself.sessionoptions:options];
operation.shouldDecompressImages = sself.shouldDecompressImages;
///是否需要账号密码
if(sself.urlCredential) {
operation.credential= sself.urlCredential;
}elseif(sself.username&& sself.password) {
operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
}
///设置优先级
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
}else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[sself.lastAddedOperationaddDependency:operation];
sself.lastAddedOperation= operation;
}
returnoperation;
}];
}
///缓存相关回调
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullableNSURL*)url
createCallback:(SDWebImageDownloaderOperation*(^)(void))createCallback {
// The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
if(url ==nil) {
if(completedBlock !=nil) {
completedBlock(nil,nil,nil,NO);
}
returnnil;
}
LOCK(self.operationsLock);
///下载队列
SDWebImageDownloaderOperation *operation = [self.URLOperations objectForKey:url];
if(!operation) {
operation = createCallback();
__weaktypeof(self) wself =self;
///下载完成后的处理
operation.completionBlock= ^{
__strongtypeof(wself) sself = wself;
if(!sself) {
return;
}
LOCK(sself.operationsLock);
[sself.URLOperationsremoveObjectForKey:url];
UNLOCK(sself.operationsLock);
};
///保存队列
[self.URLOperationssetObject:operationforKey:url];
// Add operation to operation queue only after all configuration done according to Apple's doc.
// `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
///开始下载
[self.downloadQueueaddOperation:operation];
}
UNLOCK(self.operationsLock);
///下载相关回调
iddownloadOperationCancelToken = [operationaddHandlersForProgress:progressBlockcompleted:completedBlock];
///下载token,唯一标志,包含下载队列,下载相关回调,urle等相关信息
SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
token.downloadOperation= operation;
token.url= url;
token.downloadOperationCancelToken= downloadOperationCancelToken;
returntoken;
}
SDImageCache核心方法解读- (nullableNSOperation*)queryCacheOperationForKey:(nullableNSString*)key options:(SDImageCacheOptions)options done:(nullableSDCacheQueryCompletedBlock)doneBlock :
1、从内存中读;
2、根据读取结果和策略SDImageCacheOptions判断是否需要从磁盘中读取;
3、创建读取管理队列NSOperation,返回给manager;
4、创建从磁盘读取任务,根据读取策略,同步读取或者一步读取。
具体代码如下:
- (nullableNSOperation*)queryCacheOperationForKey:(nullableNSString*)key options:(SDImageCacheOptions)options done:(nullableSDCacheQueryCompletedBlock)doneBlock {
if(!key) {
if(doneBlock) {
doneBlock(nil,nil,SDImageCacheTypeNone);
}
returnnil;
}
// First check the in-memory cache...从内存中读取
UIImage *image = [self imageFromMemoryCacheForKey:key];
BOOLshouldQueryMemoryOnly = (image && !(options &SDImageCacheQueryDataWhenInMemory));
if(shouldQueryMemoryOnly) {///只从内存中读
if(doneBlock) {
doneBlock(image,nil,SDImageCacheTypeMemory);
}
returnnil;
}
///创建读取队列
NSOperation*operation = [NSOperationnew];
void(^queryDiskBlock)(void) = ^{///从硬盘中读
if(operation.isCancelled) {
// do not call the completion if cancelled
return;
}
@autoreleasepool {
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage*diskImage;
SDImageCacheTypecacheType =SDImageCacheTypeDisk;
if(image) {
// the image is from in-memory cache
diskImage = image;
cacheType =SDImageCacheTypeMemory;
}elseif(diskData) {
// decode image data only if in-memory cache missed
diskImage = [selfdiskImageForKey:keydata:diskData];
if(diskImage &&self.config.shouldCacheImagesInMemory) {
NSUIntegercost =SDCacheCostForImage(diskImage);
[self.memCachesetObject:diskImageforKey:keycost:cost];
}
}
if(doneBlock) {
if(options &SDImageCacheQueryDiskSync) {
doneBlock(diskImage, diskData, cacheType);
}else{
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}
}
};
if (options & SDImageCacheQueryDiskSync) {
queryDiskBlock();///同步从硬盘中读取
}else{
///异步从硬盘中读取
dispatch_async(self.ioQueue, queryDiskBlock);
}
returnoperation;
}
图片解码SDWebImageImageIOCoder的核心方法:- (nullable UIImage *)sd_decompressedAndScaledDownImageWithImage:(nullable UIImage *)image
- (nullableUIImage*)sd_decompressedAndScaledDownImageWithImage:(nullableUIImage*)image {
if(![[self class] shouldDecodeImage:image]) {
returnimage;
}
if(![[self class] shouldScaleDownImage:image]) {
return [self sd_decompressedImageWithImage:image];
}
CGContextRef destContext;
// autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
// on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
@autoreleasepool {
CGImageRef sourceImageRef = image.CGImage;
CGSize sourceResolution =CGSizeZero;
sourceResolution.width=CGImageGetWidth(sourceImageRef);
sourceResolution.height=CGImageGetHeight(sourceImageRef);
float sourceTotalPixels = sourceResolution.width* sourceResolution.height;
// Determine the scale ratio to apply to the input image
// that results in an output image of the defined size.
// see kDestImageSizeMB, and how it relates to destTotalPixels.
float imageScale =kDestTotalPixels/ sourceTotalPixels;
CGSize destResolution =CGSizeZero;
destResolution.width= (int)(sourceResolution.width*imageScale);
destResolution.height= (int)(sourceResolution.height*imageScale);
// current color space
CGColorSpaceRef colorspaceRef = [[self class] colorSpaceForImageRef:sourceImageRef];
// kCGImageAlphaNone is not supported in CGBitmapContextCreate.
// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
// to create bitmap graphics contexts without alpha info.
destContext =CGBitmapContextCreate(NULL,
destResolution.width,
destResolution.height,
kBitsPerComponent,
0,
colorspaceRef,
kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
if(destContext ==NULL) {
returnimage;
}
CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);
// Now define the size of the rectangle to be used for the
// incremental blits from the input image to the output image.
// we use a source tile width equal to the width of the source
// image due to the way that iOS retrieves image data from disk.
// iOS must decode an image from disk in full width 'bands', even
// if current graphics context is clipped to a subrect within that
// band. Therefore we fully utilize all of the pixel data that results
// from a decoding opertion by achnoring our tile size to the full
// width of the input image.
CGRect sourceTile =CGRectZero;
sourceTile.size.width= sourceResolution.width;
// The source tile height is dynamic. Since we specified the size
// of the source tile in MB, see how many rows of pixels high it
// can be given the input image width.
sourceTile.size.height= (int)(kTileTotalPixels/ sourceTile.size.width);
sourceTile.origin.x=0.0f;
// The output tile is the same proportions as the input tile, but
// scaled to image scale.
CGRect destTile;
destTile.size.width= destResolution.width;
destTile.size.height= sourceTile.size.height* imageScale;
destTile.origin.x=0.0f;
// The source seem overlap is proportionate to the destination seem overlap.
// this is the amount of pixels to overlap each tile as we assemble the ouput image.
float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
CGImageRef sourceTileImageRef;
// calculate the number of read/write operations required to assemble the
// output image.
intiterations = (int)( sourceResolution.height/ sourceTile.size.height);
// If tile height doesn't divide the image height evenly, add another iteration
// to account for the remaining pixels.
intremainder = (int)sourceResolution.height% (int)sourceTile.size.height;
if(remainder) {
iterations++;
}
// Add seem overlaps to the tiles, but save the original tile height for y coordinate calculations.
float sourceTileHeightMinusOverlap = sourceTile.size.height;
sourceTile.size.height+= sourceSeemOverlap;
destTile.size.height+=kDestSeemOverlap;
for(inty =0; y < iterations; ++y ) {
@autoreleasepool {
sourceTile.origin.y= y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
destTile.origin.y= destResolution.height- (( y +1) * sourceTileHeightMinusOverlap * imageScale +kDestSeemOverlap);
sourceTileImageRef =CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
if( y == iterations -1&& remainder ) {
float dify = destTile.size.height;
destTile.size.height=CGImageGetHeight( sourceTileImageRef ) * imageScale;
dify -= destTile.size.height;
destTile.origin.y+= dify;
}
CGContextDrawImage( destContext, destTile, sourceTileImageRef );
CGImageRelease( sourceTileImageRef );
}
}
CGImageRef destImageRef =CGBitmapContextCreateImage(destContext);
CGContextRelease(destContext);
if(destImageRef ==NULL) {
returnimage;
}
UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
CGImageRelease(destImageRef);
if(destImage ==nil) {
return image;
}
return destImage;
}
}
三、线程模型:
总体讲,SDWebImage有四个后台队列,一个是下载队列SDWebImageDownloaderOperation,一个是读取缓存队列(从磁盘中异步读取在后台,同步读取在主队列),一个是下载完成后需要序列化后写缓存的全局队列(其他的在下载回调主队列写),最后一个是编解码的队列。具体看代码:
1、下载队列:
SDWebImageDownloaderOperation继承NSOperation
2、读取缓存的队列:
3、写缓存队列:
4、图片解码队列:
以上,欢迎指正。