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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,565評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,115評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,577評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,514評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,234評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,621評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,641評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,822評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,380評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,128評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,319評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,879評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,548評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,970評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,229評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,048評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,285評論 2 376

推薦閱讀更多精彩內容