導語:圖片的縮放、裁剪和壓縮等處理,總是在不經意間遇到,如果在考慮不周全的情況下,寫出的圖片處理代碼一不小心就埋下了坑(性能損耗或達不到理想效果)。
圖片處理的目標
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