iOS实录5:iOS中本地图片的缩放、裁剪和压缩
导语:图片的缩放、裁剪和压缩等处理,总是在不经意间遇到,如果在考虑不周全的情况下,写出的图片处理代码一不小心就埋下了坑(性能损耗或达不到理想效果)。
图片处理的目标
1、 iOS性能优化中希望UIImageView设置的图片不要超出UIImageView的大小,这时候最好缩放处理一下。
2、 iOS性能优化中常提到设置圆角会引发离屏渲染,较好的方案一般是自己裁剪出圆角图片。
3、图片上传时候,后台希望上传的图片小,产品要求上传的图片够清晰。较好的方案是有节制的压缩。
一、图片的缩放处理
1、在UIImage的分类中,提供了四个相关接口
其中最重要的是scaleImageWithSize: 方法,其他三个方法是通过根据参数计算出Size,然后调用scaleImageWithSize处理的。接口如下:
/**
缩放图片到指定Size
*/
- (UIImage *)scaleImageWithSize:(CGSize)size;
/**
按比例缩放图片,scale就是缩放比例
*/
- (UIImage *)scaleImageWithScale:(CGFloat)scale;
/**
缩放图片到指定宽
*/
- (UIImage *)scaleImageToTargetWidth:(CGFloat)targetW;
/**
缩放图片到指定高
*/
- (UIImage *)scaleImageToTargetHeight:(CGFloat)targetH;
2、scaleImageWithSize的具体代码实现
/**
缩放图片到指定Size
*/
- (UIImage *)scaleImageWithSize:(CGSize)size{
//创建上下文
UIGraphicsBeginImageContextWithOptions(size, YES, self.scale);
//绘图
[self drawInRect:CGRectMake(0, 0, size.width, size.height)];
//获取新图片
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
3、UIGraphicsBeginImageContextWithOptions方法说明(含性能损耗的坑)
//UIGraphicsBeginImageContextWithOptions函数原型
void UIGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale)
-
UIGraphicsBeginImageContextWithOptions有三个函数参数,这三个参数的含义分别是:
参数1:表示所要创建的图片的尺寸;
参数2:透明通道的开关,用来指定所生成图片的背景是否为不透明,如果使用YES,表示不透明,我们得到的图片背景将会是黑色,使用NO,表示透明,图片背景色正常
参数3:指定生成图片的缩放因子,传入0则表示让图片的缩放因子根据屏幕的分辨率而变化,所以我们得到的图片不管是在单分辨率还是视网膜屏上看起来都会很好。 -
UIGraphicsBeginImageContextWithOptions引发性能原因就是参数2(背景是否为不透明)设置不当。因为透明通道的存在,GPU会去计算图层堆叠后像素点的真实颜色,这样会引起性能损耗。如果设置为不包含透明通道,虽然不会引起性能损耗,但是图片的背景色是黑的。在图片裁剪的情形下,被裁剪去的部分是黑色的,很难看(解决办法后面说)。
-
因为是图片压缩和缩放操作,不会有被裁剪去的部分,所以通常UIGraphicsBeginImageContextWithOptions的参数2(背景是否为不透明)设置为YES,表明不需要透明通道,避开不必要的性能损耗。
二、图片的裁剪处理
1、在UIImage的分类中,提供了三个相关接口
其中clipImageWithCornerRadius:bgColor:是最常用的,通常可以用来设置UIImageView的圆角图片。它是设置好圆角Path,然后调用clipImageWithPath:bgColor:实现的。接口如下:
/**
根据贝塞尔路径来裁剪
*/
- (UIImage *)clipImageWithPath:(UIBezierPath *)path bgColor:(UIColor *)bgColor;
/**
裁剪出圆角矩形
@param cornerRadius 圆角半径
@param bgColor 背景色
*/
- (UIImage *)clipImageWithCornerRadius:(CGFloat)cornerRadius bgColor:(UIColor *)bgColor;
/**
从指定的rect裁剪出图片
*/
- (UIImage *)clipImageWithRect:(CGRect)rect;
- 为了不让绘制图片时,背景为不透明的情况下,被裁剪去的部分显示黑色,在图片上下文中先画一层bgColor。不会出现黑色的情形。
- clipImageWithRect:是从图片中扣出矩形子图片,不需要传入背景色
2、代码实现
- (UIImage *)clipImageWithPath:(UIBezierPath *)path bgColor:(UIColor *)bgColor{
CGSize imageSize = self.size;
CGRect rect = CGRectMake(0, 0, imageSize.width, imageSize.height);
//创建位图上下文
UIGraphicsBeginImageContextWithOptions(rect.size, YES, self.scale);
if (bgColor) {
UIBezierPath *bgRect = [UIBezierPath bezierPathWithRect:rect];
[bgColor setFill];
[bgRect fill];
}
//裁剪
[path addClip];
//绘制
[self drawInRect:rect];
UIImage *clipImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return clipImage;
}
/**
裁剪圆角图片
*/
- (UIImage *)clipImageWithCornerRadius:(CGFloat)cornerRadius bgColor:(UIColor *)bgColor{
CGSize imageSize = self.size;
CGRect rect = CGRectMake(0, 0, imageSize.width, imageSize.height);
UIBezierPath *roundRectPath = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:cornerRadius];
return [self clipImageWithPath:roundRectPath bgColor:bgColor];
}
/**
从指定的rect裁剪出图片
*/
- (UIImage *)clipImageWithRect:(CGRect)rect{
CGFloat scale = self.scale;
CGImageRef clipImageRef = CGImageCreateWithImageInRect(self.CGImage,
CGRectMake(rect.origin.x * scale,
rect.origin.y * scale,
rect.size.width * scale,
rect.size.height * scale));
CGRect smallBounds = CGRectMake(0, 0, CGImageGetWidth(clipImageRef)/scale, CGImageGetHeight(clipImageRef)/scale);
UIGraphicsBeginImageContextWithOptions(smallBounds.size, YES, scale);
CGContextRef context = UIGraphicsGetCurrentContext();
// clipImage是将要绘制的UIImage图片(防止图片上下颠倒)
CGContextTranslateCTM(context, 0, smallBounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CGContextDrawImage(context, CGRectMake(0, 0, smallBounds.size.width, smallBounds.size.height), clipImageRef);
// CGContextDrawImage(context, smallBounds, clipImageRef);
UIImage* clipImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return clipImage;
}
3、CGContextDrawImage 引起的图片上下颠倒解决
1)上下颠倒的原因:坐标轴不同
- iOS SDK的核心UIKit框架,和传统的windows桌面一样,坐标系是y轴向下的;
- Core Graphics(Quartz)一个基于2D的图形绘制引擎,它的坐标系则是y轴向上的;
- OpenGL ES是iOS SDK的2D和3D绘制引擎,它使用左手坐标系,它的坐标系也是y轴向上的,如果不考虑z轴,在二维下它的坐标系和Quartz是一样的。
- 通过CGContextDrawImage绘制图片到一个context中时,如果传入的是UIImage的CGImageRef,因为UIKit和CG坐标系y轴相反,所以图片绘制将会上下颠倒。
解决办法:绘制到context前通过矩阵垂直翻转坐标系,代码如下:
CGContextTranslateCTM(context, 0, smallBounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CGContextDrawImage(context, CGRectMake(0, 0, smallBounds.size.width, smallBounds.size.height), clipImageRef);
4、clipImageWithCornerRadius:bgColor:解决的性能问题
-
在iOS中,使用UIImageView展示圆角图片时候,比较简单但损耗性能的做法是:
imageView.layer.cornerRadius = imageSize.height/2; //1 imageView.layer.masksToBounds = YES; //2
这样做,第2步masksToBounds = YES会引发会引发离屏渲染,性能也就损耗了。好的办法是自己去裁剪图片,利用clipImageWithCornerRadius:bgColor:将图片裁剪成圆角的,避免引发离屏渲染,从而降低损耗。
-
但是,通常情况下,图片显示的是网络上下载来的图片,网络的图片是通过SDWebImage来下载下来的。如何将图片的裁剪函数利用到UIImageView中去,且又要尽快能较少性能损耗(尽量减少裁剪操作,如一个图片每次展示都要裁剪)和接口的友好型。可以自行思考。
三、图片的压缩处理
1、在UIImage的分类中,提供了两个相关接口
一个接口是尺寸压缩,一个接口是质量压缩,一般情况下,图片的压缩是先尺寸压缩,把图片压缩到合适的宽高像素(如640px,1080px),然后再压缩质量,图片质量降低到合适的字节数。接口定义如下:
/**
压缩到指定像素px
*/
- (UIImage *)compressImageToTargetPx:(CGFloat)targetPx;
/**
压缩到指定千字节(kb)
*/
- (NSData *)compressImageToTargetKB:(NSInteger )numOfKB;
2、代码实现
/**
压缩到指定像素px
*/
- (UIImage *)compressImageToTargetPx:(CGFloat)targetPx{
UIImage *compressImage = nil;
CGSize imageSize = self.size;
CGFloat compressScale = 0; //压缩比例
//压缩后的目标size
CGSize targetSize = CGSizeMake(targetPx, targetPx);
//实际宽高比例
CGFloat factor = imageSize.width / imageSize.height;
if (imageSize.width < targetSize.width && imageSize.height < targetSize.height) {
//图片实际宽高 都小于 目标宽高,没必要压缩
compressImage = self;
}else if (imageSize.width > targetSize.width && imageSize.height > targetSize.height){
//图片实际宽高 都大于 目标宽高
if (factor <= 2) {
//宽高比例小于等于2,获取大的等比压缩
compressScale = targetPx / MAX(imageSize.width,imageSize.height);
}else{
//宽高比例大于2,获取小的等比压缩
compressScale = targetPx / MIN(imageSize.width,imageSize.height);
}
}else if(imageSize.width > targetSize.width && imageSize.height < imageSize.height){
//宽大于目标宽,高小于目标高
if (factor <= 2) {
compressScale = targetSize.width / imageSize.width;
}else{
compressImage = self;
}
}else if(imageSize.width < targetSize.width && imageSize.height > imageSize.height){
//宽小于目标宽,高大于目标高
if (factor <= 2) {
compressScale = targetSize.height / imageSize.height;
}else{
compressImage = self;
}
}
//需要压缩
if (compressScale > 0 && !compressImage) {
CGSize compressSize = CGSizeMake(self.size.width * compressScale, self.size.height * compressScale);
UIGraphicsBeginImageContextWithOptions(compressSize, YES, 1);
[self drawInRect:CGRectMake(0, 0, compressSize.width, compressSize.height)];
compressImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
if (!compressImage) {
compressImage = self;
}
return compressImage;
}
/**
压缩到指定千字节(kb)
*/
- (NSData *)compressImageToTargetKB:(NSInteger )numOfKB{
CGFloat compressionQuality = 0.9f;
CGFloat compressionCount = 0;
NSData *imageData = UIImageJPEGRepresentation(self,compressionQuality);
while (imageData.length >= 1000 * numOfKB && compressionCount < 15) { //15是最大压缩次数.mac中文件大小1000进制
compressionQuality = compressionQuality * 0.9;
compressionCount ++;
imageData = UIImageJPEGRepresentation(self, compressionQuality);
}
return imageData;
}
- compressImageToTargetPx中参考宽高比来计算压缩比例,主要是为了尺寸压缩后看起来的效果,尽量理想些。
- 考虑到compressImageToTargetKB是一个同步方法,设置了compressionCount(压缩次数,15是我自己拍脑袋解决的)是15,这是为了保证即时达不到压缩效果,经过一定次数压缩后,停止压缩,返回当时的压缩效果即可,避免压缩消耗太久时间。
四、图片压缩、裁剪和压缩方法的使用
注:因为这些方法都是放到UIImage的分类UIImage+Tool中,所有使用前,#import "UIImage+Tool.h"就可以使用相关方法了。
1、Controller中调用这些方法,并展示在UIImagView中
- (UIImage *)testScaleImage:(UIImage *)originImage{
UIImage *scaleImage = [originImage scaleImageWithSize:CGSizeMake(100, 100)];
// UIImage *scaleImage = [originImage scaleImageWithScale:0.5];
// UIImage *scaleImage = [originImage scaleImageToTargetWidth:100];
// UIImage *scaleImage = [originImage scaleImageToTargetHeight:100];
return scaleImage;
}
- (UIImage *)testClipImage:(UIImage *)originImage{
UIImage *clipImage = [originImage clipImageWithRect:CGRectMake(0, 0, originImage.size.width/2, originImage.size.height)];
return clipImage;
}
- (UIImage *)testClipRoundCornerImage:(UIImage *)originImage {
UIImage *roundImage = [originImage clipImageWithCornerRadius:originImage.size.height/2 bgColor:[UIColor whiteColor]];
return roundImage;
}
- (void)testCompressImage{
UIImage *image = [UIImage imageNamed:@"image1_3.6MB_4032 × 3024.jpeg"];
NSString *filePath = [[NSBundle mainBundle]pathForResource:@"image1_3.6MB_4032 × 3024" ofType:@"jpeg"];
NSString *lengthStr = [[NSData dataWithContentsOfFile:filePath] lengthString];
NSDate *startDate = [NSDate date];
NSLog(@"\n\n压缩前尺寸大小:%@ ,质量大小:%@ ,scale = %lf", NSStringFromCGSize(image.size),lengthStr,image.scale);
UIImage *newImage = [image compressImageToTargetPx:1080];
NSLog(@"尺寸压缩后 的 尺寸大小:%@ ,scale = %lf", NSStringFromCGSize(newImage.size),newImage.scale);
NSData *imageData = [newImage compressImageToTargetKB:100];
NSTimeInterval cost = [[NSDate date]timeIntervalSinceDate:startDate];
NSLog(@"质量压缩后 的 质量大小:%@,花费时间 = %.3lfs",[imageData lengthString],cost);
}
2、图片压缩前后,质量大小的说明
- 同一个文件,在MAC上显示文件的大小略大于Windows上显示的大小。这是因为 OSX和iOS系统中文件大小采用的是1000进制换算,而Windows常用的使用的是1024进制进行换算。
- 图片压缩前,不可以将UIImageJPEGRepresentation(image, 1.0)得到的二进制数据的大小代表图片的质量大小。因为这个大小是大于存储在存储介质中的图片质量大小的。应该使用 [[NSData dataWithContentsOfFile:filePath] lengthString]获得图片的质量大小。
- 图片压缩后,压缩后图片的大小就是:质量压缩后输出的NSData对象大小。因为这个NSData对象的大小 和 NSData对象保存成文件后的文件大小 是相等的。
为了方便计算图片对应的NSData大小,新增了一个NSData的方法lengthString,用来计算NSData对象代表的质量大小。
/**
获取大小描述
*/
- (NSString *)lengthString{
NSUInteger length = [self length];
//MAC上,文件大小采用的是1000进制换算
CGFloat scale = 1000.0f;
CGFloat fileSize = 0.0f;
NSString *unitStr = @"";
if(length >= pow(scale, 3)) {
fileSize = length * 1.0 / pow(scale, 3);
unitStr = @"GB";
}else if (length >= pow(scale, 2)) {
fileSize = length * 1.0 / pow(scale, 2);
unitStr = @"MB";
}else if (length >= scale) {
fileSize = length * 1.0 / scale;
unitStr = @"KB";
}else {
fileSize = length * 1.0;
unitStr = @"B";
}
NSString *fileSizeInUnitStr = [NSString stringWithFormat:@"%.2f %@",
fileSize,unitStr];
return fileSizeInUnitStr;
}
3、图片处理后的效果
图片处理展示效果图- 为模拟器开启了Color Blended Layers开关,图片所在区域都是绿色,说明没有出现像素混合的情况。GPU不需要做像素混合的计算,无疑是减少了GPU的工作,降低了性能损耗。
- 上下往下,第一张是原图,第二张是缩放的图,第三张是裁剪出子图(矩形)的图,第四张是裁剪圆角的图。
- 可以看到3.6MB,宽高像素4032 x 3024的图片压缩到97.7kb,宽高像素1080 x 810,花费时间0.156s,效果还是可以的。
五、图片处理其他####
图片的处理中还有些不在缩放、裁剪和压缩之列,但是亦在项目中常用到相关处理。
1、图片的拉伸和平铺函数
- (UIImage *)resizableImageWithCapInsets:(UIEdgeInsets)capInsets resizingMode:(UIImageResizingMode)resizingMode
-
该方法返回的是UIImage类型的对象,即返回经该方法拉伸后的图像。
-
参数1 capInsets是UIEdgeInsets类型的数据,即原始图像要被保护的区域(不拉伸或平铺部分),是被保护的区域到原始图像外轮廓的上部,左部,底部,右部的直线距离。
-
参数2 resizingMode是UIImageResizingMode枚举类型,表示图像拉伸时选用的拉伸模式。可选UIImageResizingModeTile(平铺) 或UIImageResizingModeStretch(拉伸)
2、UIImageView 的contentMode
-
contentMode是用来设置图片的显示方式,如居中、居右,是否缩放等,有以下几个常量可供设定:
UIViewContentModeScaleToFill UIViewContentModeScaleAspectFit UIViewContentModeScaleAspectFill UIViewContentModeRedraw UIViewContentModeCenter UIViewContentModeTop UIViewContentModeBottom UIViewContentModeLeft UIViewContentModeRight UIViewContentModeTopLeft UIViewContentModeTopRight UIViewContentModeBottomLeft UIViewContentModeBottomRight
-
contentMode默认是UIViewContentModeScaleToFill,当图片尺寸超过 ImageView尺寸时,整个图片填充整个ImageView的,如果图片比例和ImageView比例不同,图片会变形(比较常见的)。
-
UIViewContentModeScaleAspectFit, 这个图片全部显示view里面,并且图片比例不变, View会留下空白.
-
UIViewContentModeScaleAspectFill, 这个图片填充整个ImageView的,并且图片比例不变,图片显示不全
-
没有带Scale的,当图片尺寸超过 ImageView尺寸时,只有部分显示在ImageView中。
说明:setNeedsDisplay会调用自动调用drawRect方法,这样可以拿到 UIGraphicsGetCurrentContext,就可以画画了。而setNeedsLayout会默认调用layoutSubViews,方便处理子视图中的一些数据。
END
-
相关文章
-
我是南华coder,一名北漂的初级iOS程序猿。iOS实(践)录系列是我的一点开发心得,希望能够抛砖引玉。
-
源码直通车:QSUseImageDemo