一. 项目需求
二. 项目架构
HobenImageManager
提供图片下载、处理接口HobenImageCache
用于下载与缓存HobenImageProcessManager
用于图片处理
三. 图片下载、处理接口管理
1. 下载、处理图片
对于暴露给外界的接口,根据图片的URL地址来向图片下载缓存管理
请求返回image和下载进度,获得image后再向图片处理管理
请求处理图片,请求完毕后根据回调返回结果给调用接口的方法。
/**
*
根据处理类型下载图片
@param url 图片的URL地址
@param processType 图片处理类型
@param progressBlock 包含CGFloat类型的下载进度回调
@param completedBlock 包含UIImage类型的下载完成回调
*
**/
- (void)requestImageWithUrl:(NSString *)url
processType:(HobenImageProcessType)processType
progressBlock:(HobenImageProgressBlock)progressBlock
completedBlock:(HobenImageCompletedBlock)completedBlock {
[[HobenImageCache sharedInstance] requestImageWithUrl:url progressBlock:progressBlock completedBlock:^(UIImage * _Nullable image) {
[[HobenImageProcessManager sharedInstance] processImage:image processType:processType completedBlock:completedBlock];
}];
}
2. 清除缓存
清除缓存主要向图片下载缓存管理
请求清除缓存,清除缓存完成后会返回一个完成的回调。
/**
*
清除图片缓存
@param completionBlock 清除缓存成功后的回调
*
**/
- (void)removeCacheWithCompletionBlock:(void (^)(void))completionBlock {
[[HobenImageCache sharedInstance] removeCacheWithCompletionBlock:completionBlock];
}
3. 预加载图片
预加载图片主要向图片下载缓存管理
请求预加载图片
/**
*
预加载图片
@param urlStringArray 图片的URL地址数组
*
**/
- (void)prefetchImageWithUrlStringArray:(NSArray <NSString *> *)urlStringArray {
[[HobenImageCache sharedInstance] prefetchImageWithUrlStringArray:urlStringArray];
}
4. 取消预加载
取消预加载图片主要向图片下载缓存管理
请求取消预加载图片
/**
*
停止预加载图片
*
**/
- (void)cancelPrefetchingImage {
[[HobenImageCache sharedInstance] cancelPrefetchingImage];
}
四. 图片下载、缓存管理
1. 图片的下载和缓存
流程图如下,为确保图片不被重复下载,该模块使用单例模式,且保存了正在请求图片的URL,如果存在缓存,则直接回调,如果不存在,则调用下载方法,下载完成后缓存该图片,并及时回调图片和进度。
图片的缓存和下载主要使用了第三方库SDWebImage
。
1) 读取缓存图片
以URL为key,直接返回SDImageCache
存储的图片。
- (UIImage *)_imageWithUrl:(NSString *)url {
return [[SDImageCache sharedImageCache] imageFromCacheForKey:url];
}
2) 下载图片和回调进度
调用SDWebImage
的loadImageWithURL:options:progress:completed:
方法,加载完成后,第三方库会自动将该图片缓存起来。
- (void)_requestImageWithUrl:(NSString *)url
progressBlock:(HobenImageProgressBlock)progreeBlock
completedBlock:(HobenImageCompletedBlock)completedBlock {
WEAK_SELF_DECLARED
[[SDWebImageManager sharedManager] loadImageWithURL:[NSURL URLWithString:url] options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
CGFloat progress = receivedSize * 1.f / expectedSize;
if (progreeBlock) {
progreeBlock(progress);
}
} completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
STRONG_SELF_BEGIN
if (image) {
[strongSelf _cacheImageWithUrl:url];
[strongSelf.requestList removeObject:url];
if (completedBlock) {
completedBlock(image);
}
} else {
NSLog(@"Cache ERROR in URL:%@", url);
[strongSelf.requestList removeObject:url];
if (completedBlock) {
completedBlock(nil);
}
}
STRONG_SELF_END
}];
}
2. 清除图片缓存
调用SDImageCache
的相应方法以清除缓存。
- (void)removeCacheWithCompletionBlock:(void (^)(void))completionBlock {
[self.requestList removeAllObjects];
[[SDImageCache sharedImageCache] clearMemory];
[[SDImageCache sharedImageCache] clearDiskOnCompletion:completionBlock];
}
3. 预加载图片
调用SDWebImagePrefetcher
的prefetchURLs:
进行预加载
- (void)prefetchImageWithUrlStringArray:(NSArray <NSString *> *)urlStringArray {
NSMutableArray *urlArray = [NSMutableArray arrayWithCapacity:urlStringArray.count];
for (NSString *urlString in urlStringArray) {
[urlArray addObject:[NSURL URLWithString:urlString]];
}
self.prefetchingList = [NSMutableArray arrayWithArray:urlStringArray];
[self.requestList addObjectsFromArray:urlStringArray];
WEAK_SELF_DECLARED
[[SDWebImagePrefetcher sharedImagePrefetcher] prefetchURLs:urlArray progress:nil completed:^(NSUInteger noOfFinishedUrls, NSUInteger noOfSkippedUrls) {
STRONG_SELF_BEGIN
[strongSelf.requestList removeObjectsInArray:urlStringArray];
[strongSelf.prefetchingList removeAllObjects];
STRONG_SELF_END
}];
}
4. 停止预加载
调用SDWebImagePrefetcher
的cancelPrefetching
停止预加载
- (void)cancelPrefetchingImage {
[[SDWebImagePrefetcher sharedImagePrefetcher] cancelPrefetching];
[self.requestList removeObjectsInArray:self.prefetchingList];
[self.prefetchingList removeAllObjects];
}
五. 图像处理
图像处理提供的方法主要有以下三种:
typedef NS_ENUM(NSUInteger, HobenImageProcessType) {
HobenImageProcessTypeCommon = 0, // 不处理
HobenImageProcessTypeGaussian, // 高斯模糊
HobenImageProcessTypeWatermark, // 水印
};
1. 高斯模糊处理
在iOS绘图系列:图像模糊之均值模糊详细描述了相关原理,在此不赘述。
- (void)_processGaussianImage:(UIImage *)image completedBlock:(void (^)(UIImage * _Nullable image))completeBlock {
CGFloat blur = 0.5f;
int boxSize = (int)(blur * 40);
// 确保为奇数
boxSize = boxSize - (boxSize % 2) + 1;
CGImageRef imageRef = image.CGImage;
vImage_Buffer inBuffer, outBuffer;
vImage_Error error;
void *pixelBuffer;
// 从CGImage中获取数据
CGDataProviderRef inProvider = CGImageGetDataProvider(imageRef);
CFDataRef inBitmapData = CGDataProviderCopyData(inProvider);
// 设置从CGImage获取对象的属性
pixelBuffer = malloc(CGImageGetBytesPerRow(imageRef) * CGImageGetHeight(imageRef));
outBuffer.width = inBuffer.width = CGImageGetWidth(imageRef);
outBuffer.height = inBuffer.height = CGImageGetHeight(imageRef);
outBuffer.rowBytes = inBuffer.rowBytes = CGImageGetBytesPerRow(imageRef);
inBuffer.data = (void *)CFDataGetBytePtr(inBitmapData);
outBuffer.data = pixelBuffer;
// 均值模糊
error = vImageBoxConvolve_ARGB8888(&inBuffer,
&outBuffer,
NULL,
0,
0,
boxSize,
boxSize,
NULL,
kvImageEdgeExtend);
if (error) {
NSLog(@"Error from convolution %ld", error);
}
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(outBuffer.data, outBuffer.width, outBuffer.height, 8, outBuffer.rowBytes, colorSpace, CGImageGetBitmapInfo(imageRef));
CGImageRef resultImageRef = CGBitmapContextCreateImage(context);
UIImage *resultImage = [UIImage imageWithCGImage:resultImageRef];
CGColorSpaceRelease(colorSpace);
free(pixelBuffer);
CFRelease(inBitmapData);
CGColorSpaceRelease(colorSpace);
CGImageRelease(resultImageRef);
if (completeBlock) {
completeBlock(resultImage);
}
}
2. 水印处理
在iOS绘图系列:Core Graphics绘图提到了相关原理,在此不赘述。
- (void)_processWatermarkImage:(UIImage *)image text:(NSString *)text completedBlock:(HobenImageCompletedBlock)completeBlock {
UIGraphicsBeginImageContextWithOptions(image.size, NO, 0);
[image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)];
NSDictionary *dictionary = @{NSForegroundColorAttributeName: [UIColor whiteColor], NSFontAttributeName: [UIFont boldSystemFontOfSize:image.size.height / 10]};
[text drawAtPoint:CGPointZero withAttributes:dictionary];
UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
if (completeBlock) {
completeBlock(resultImage);
}
}
六. 导出Framework
之前学习过如何封装Framework,主要的步骤如下:
- 选择暴露的头文件
- 分别在真机环境和模拟器环境下导出Framework
- 通过
lipo -create
方法将两个Framework,合并成一个Framework后output
出来 - 将得到的Framework导入到需要用的文件中
这次使用shell脚本来导出Framework,步骤如注释所示:
#工作路径
WORKSPACE="HobenImageLib.xcworkspace"
CONFIG="Release"
USER_HOME=$(eval echo ~${SUDO_USER})
#project名字
FMK_NAME=${PROJECT_NAME}
OUTPUT_DIR=${USER_HOME}/Desktop/Framework_build_output
INSTALL_DIR=${OUTPUT_DIR}/${FMK_NAME}.framework
DEVICE_DIR=${BUILD_DIR}/${CONFIG}-iphoneos/${FMK_NAME}.framework
TEMP_DEVICE_DIR=${OUTPUT_DIR}/TEMP_DEIVCE_DIR/${FMK_NAME}.framework
SIMULATOR_DIR=${BUILD_DIR}/${CONFIG}-iphonesimulator/${FMK_NAME}.framework
if [ -d "${INSTALL_DIR}" ]
then
rm -rf "${INSTALL_DIR}"
fi
#XCode导出真机Framework,注意要先clean再build
xcodebuild -workspace "${WORKSPACE}" -configuration "Release" -scheme "${FMK_NAME}" -sdk iphoneos clean build OBJROOT="${OBJROOT}/DependentBuilds"
mkdir -p "${TEMP_DEVICE_DIR}"
cp -R "${DEVICE_DIR}/" "${TEMP_DEVICE_DIR}/"
#XCode导出模拟器Framework,注意要先clean再build
xcodebuild -workspace "${WORKSPACE}" -configuration "Release" -scheme "${FMK_NAME}" -sdk iphonesimulator clean build OBJROOT="${OBJROOT}/DependentBuilds"
mkdir -p "${INSTALL_DIR}"
cp -R "${TEMP_DEVICE_DIR}/" "${INSTALL_DIR}/"
#将两个Framework合并且导出
lipo -create "${TEMP_DEVICE_DIR}/${FMK_NAME}" "${SIMULATOR_DIR}/${FMK_NAME}" -output "${INSTALL_DIR}/${FMK_NAME}"
rm -rf "${TEMP_DEVICE_DIR}"
open "${INSTALL_DIR}/../"
七. 导入Framework
在Demo中导入Framework并引用头文件,使用Framework里面暴露的头文件方法:
#import <HobenImageLib/HobenImageLib.h>
...
WEAK_SELF_DECLARED
void (^progressBlock)(CGFloat progress) = ^(CGFloat progress) {
dispatch_async(dispatch_get_main_queue(), ^{
STRONG_SELF_BEGIN
NSLog(@"progress: %lf", progress);
[strongSelf setProgressLabelValue:progress];
strongSelf.progressView.progress = progress;
STRONG_SELF_END
});
};
void (^completedBlock)(UIImage * _Nullable image) = ^(UIImage * _Nullable image) {
STRONG_SELF_BEGIN
PreviewController *vc = [[PreviewController alloc] initWithImage:image];
[strongSelf.navigationController pushViewController:vc animated:YES];
STRONG_SELF_END
};
[[HobenImageManager sharedInstance] requestImageWithUrl:url
processType:self.processType
progressBlock:progressBlock
completedBlock:completedBlock];
八. 成果展示
为了使得加载进度效果显示出来,在模拟器模拟了弱网效果。
成果展示如下:
九. Demo
我的Demo在这里,欢迎star~
更新于5.16
反馈问题修正:
1. 修改水印功能
新增水印位置和水印内容参数:
- (void)processWatermarkImage:(UIImage *)image
text:(NSString *)text
position:(HobenImageWatermarkPosition)position
completedBlock:(HobenImageCompletedBlock)completeBlock {
UIGraphicsBeginImageContextWithOptions(image.size, NO, 0);
[image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)];
UIFont *font = [UIFont boldSystemFontOfSize:image.size.height / 10];
NSDictionary *dictionary = @{NSForegroundColorAttributeName: [UIColor whiteColor], NSFontAttributeName: font};
CGFloat watermarkX = 0.f;
CGFloat watermarkY = 0.f;
// (计算水印的位置的时候,虽然不知道那个乘以4是怎么来的,但是一点点试出来效果之后发现挺管用)
CGFloat fontSize = font.pointSize * 4;
CGFloat fontHeight = font.pointSize;
switch (position) {
case HobenImageWatermarkPositionUpLeft: {
watermarkX = 0.f;
watermarkY = 0.f;
}
break;
case HobenImageWatermarkPositionUpRight: {
watermarkX = image.size.width - fontSize;
watermarkY = 0.f;
}
break;
case HobenImageWatermarkPositionCenter: {
watermarkX = image.size.width / 2 - fontSize / 2;
watermarkY = image.size.height / 2 - fontHeight / 2;
}
break;
case HobenImageWatermarkPositionDownLeft: {
watermarkX = 0.f;
watermarkY = image.size.height - fontHeight;
}
break;
case HobenImageWatermarkPositionDownRight: {
watermarkX = image.size.width - fontSize;
watermarkY = image.size.height - fontHeight;
}
break;
}
[text drawAtPoint:CGPointMake(watermarkX, watermarkY) withAttributes:dictionary];
UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
if (completeBlock) {
completeBlock(resultImage);
}
}
2. 加入ErrorBlock,同时在遇到请求中的URL时及时返回Block
- (void)_loadImageWithUrl:(NSString *)url
progressBlock:(HobenImageProgressBlock)progreeBlock
completedBlock:(HobenImageCompletedBlock)completedBlock
errorBlock:(HobenImageErrorBlock)errorBlock {
if ([self.requestList containsObject:url]) {
if (progreeBlock) {
progreeBlock(0.f);
}
if (completedBlock) {
completedBlock(nil);
}
if (errorBlock) {
errorBlock(@"图片加载请求重复");
}
return;
}
[self.requestList addObject:url];
[self _requestImageWithUrl:url progressBlock:progreeBlock completedBlock:completedBlock errorBlock:errorBlock];
}
3. 加入加载中视图,加载时禁用按钮
//设置小菊花颜色
self.activityIndicator.color = [UIColor blackColor];
//设置背景颜色
self.activityIndicator.backgroundColor = [UIColor whiteColor];
......
//点击按钮时
[self.activityIndicator startAnimating];
[[UIApplication sharedApplication] beginIgnoringInteractionEvents];
.....
//图片读取完成时
[self.activityIndicator stopAnimating];
[[UIApplication sharedApplication] endIgnoringInteractionEvents];
4. 分开接口
已将水印和高斯模糊接口分开,可以同时存在。