项目3:制作图片下载、缓存、预加载、图像处理的iOS静态库

一. 项目需求

二. 项目架构

  • 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) 下载图片和回调进度

调用SDWebImageloadImageWithURL: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. 预加载图片

调用SDWebImagePrefetcherprefetchURLs:进行预加载

- (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. 停止预加载

调用SDWebImagePrefetchercancelPrefetching停止预加载

- (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,主要的步骤如下:

  1. 选择暴露的头文件
  2. 分别在真机环境和模拟器环境下导出Framework
  3. 通过lipo -create方法将两个Framework,合并成一个Framework后output出来
  4. 将得到的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. 分开接口

已将水印和高斯模糊接口分开,可以同时存在。

5. 新效果

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,753评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,668评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,090评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,010评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,054评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,806评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,484评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,380评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,873评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,021评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,158评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,838评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,499评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,044评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,159评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,449评论 3 374
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,136评论 2 356