×

iOS 加载高清大图片

96
飘金
2017.10.18 12:36* 字数 1127

苹果官方加载大图片例子

首先得知道图片加载到内存中的大小怎么计算

  • 如果是位图,则位图多大则加载到内存中所占用的大小就是多大。

  • 如果是非位图的图片比如jpg/png,则需要解码成位图,可以参考iOS图片加载速度极限优化,解码出来的位图多大也就是意味着该jpg/png占用内存多大。位图的大小计算公式:

图片width x heigth x 4(ARGB)

比如一张1000 x 1000的png图片,则其解压出来的位图的占用大小为1000 x 1000 x 4(即3.81MB左右),也就是说这张图片会占用3.81MB左右的内存。

  • 由于图片占用的内存大小主要是看图片转成位图时的大小,所以一个图片占用内存的大小与图片的分辨率有关,与图片的占用磁盘大小无关(经过多张图片测试,均验证正确,图片数量有限可能有误)。

加载大图片到内存在低设备上动不动就内存挂掉,很是麻烦。显示时会挂,压缩时也会挂,决绝方案可以参考iOS压缩高清大图片

下载大图片

这时候大图片的下载不能够使用SDWebImage,内存肯定吃不消,可以通过AFNetWorking把大图片下载写入沙盒中而不是内存中,大图片的下载就可以搞定了,可以参考iOS大文件下载、断点下载

上传大图片

有时候需要上传大图片到服务器,这时候如果把大图片压缩(且不说压缩过程就会内存暴增,并且容易挂掉)成NSData形式,会占用很多的内存,然后在上传,很容易就会挂掉。故,上传大图片显然不能够一下子全部压缩上传,不过可以大文件分片上传和断点续传。把大图片保存到沙盒里面(不用把图片一下子全部压缩),分片去上传,这样大图片上传问题就搞定了。

加载显示大图片

如果大图片直接全部加载显示,低设备基本是会挂。不过找到了苹果官方加载大图片的例子,苹果官方例子改变了图片的渲染方式,利用GPU 进行渲染,有效降低内存,不改变图片的质量,亲测加载47M图片都没问题,最好不要用于加载太小的图片,因为没意义,比较适合用于加载大图(大概1M以上)。

苹果官方加载大图片的原理

  • 把大图片进行分片加载,不同的设备分片时的大小不一样,根据设备来设定。先计算好原图片的每个分片CGRect,然后在根据设备获取到对应的缩放后的分片CGRect,缩放的比例也是根据设备来设定。

  • 根据原图的分片CGRect获取到原图的一个小分片p1,根据缩放后的分片CGRect和 p1生成缩放后的小分片,并且绘制到屏幕,这样一直循环把原图片全部的每个小分片全部生成对应缩放的小分片,然后绘制。

  • 把原图的缩放后的小分片全部绘制到屏幕,发小图片质量并没有变化很大,但是占用内存缺少了很多。大概思路是这样,读者可以自行阅读苹果官方例子源码。

  • 不过苹果官方的这种方式其实是缩放了原图的大小加分片处理图片,以达到控制内存的目的,也就意味着最终加载出来的图片质量上虽然没有太大变化,但是图片分辨率(即大小width x heigth)变小了。所以如果大图片的显示可以这么处理,上传原图的话则要走大文件分片上传和断点续传方式。

附上代码:

LargeImageDownsizingViewController.h

/*
     File: LargeImageDownsizingViewController.h 
 Abstract: The primary view controller for this project. 
  Version: 1.1
 Copyright (C) 2014 Apple Inc. All Rights Reserved. 
  
*/

#import <UIKit/UIKit.h>

@class ImageScrollView;

@interface LargeImageDownsizingViewController : UIViewController {
    // The input image file
    UIImage* sourceImage;
    // output image file
    UIImage* destImage;
    // sub rect of the input image bounds that represents the 
    // maximum amount of pixel data to load into mem at one time.
    CGRect sourceTile;
    // sub rect of the output image that is proportionate to the
    // size of the sourceTile. 
    CGRect destTile;
    // the ratio of the size of the input image to the output image.
    float imageScale;
    // source image width and height
    CGSize sourceResolution;
    // total number of pixels in the source image
    float sourceTotalPixels;
    // total number of megabytes of uncompressed pixel data in the source image.
    float sourceTotalMB;
    // output image width and height
    CGSize destResolution;
    // the temporary container used to hold the resulting output image pixel 
    // data, as it is being assembled.
    CGContextRef destContext;
    // the number of pixels to overlap tiles as they are assembled.
    float sourceSeemOverlap;
    // an image view to visualize the image as it is being pieced together
    UIImageView* progressView;
    // a scroll view to display the resulting downsized image
    ImageScrollView* scrollView;
}

@property (strong) UIImage* destImage;

-(void)downsize:(id)arg;
-(void)updateScrollView:(id)arg;
-(void)initializeScrollView:(id)arg;
-(void)createImageFromContext;

@end

LargeImageDownsizingViewController.m

/*
     File: LargeImageDownsizingViewController.m 
 Abstract: The primary view controller for this project. 
  Version: 1.1
 Copyright (C) 2014 Apple Inc. All Rights Reserved. 
  
*/

#import "LargeImageDownsizingViewController.h"
#import <QuartzCore/QuartzCore.h>
#import "ImageScrollView.h"
#define kImageFilename @"large_leaves_70mp.jpg" // 7033x10110 image, 271 MB uncompressed

#define IPAD1_IPHONE3GS
#ifdef IPAD1_IPHONE3GS
#   define kDestImageSizeMB 60.0f // The resulting image will be (x)MB of uncompressed image data. 
#   define kSourceImageTileSizeMB 20.0f // The tile size will be (x)MB of uncompressed image data. 
#endif

/* 这些常量为iPad2和iphone4提供了初始值 */
//#define IPAD2_IPHONE4
#ifdef IPAD2_IPHONE4
#   define kDestImageSizeMB 120.0f // The resulting image will be (x)MB of uncompressed image data. 
#   define kSourceImageTileSizeMB 40.0f // The tile size will be (x)MB of uncompressed image data. 
#endif

/* 这些常量为iPhone3G、iPod2和早期设备提供了初始值 */
//#define IPHONE3G_IPOD2_AND_EARLIER
#ifdef IPHONE3G_IPOD2_AND_EARLIER
#   define kDestImageSizeMB 30.0f // The resulting image will be (x)MB of uncompressed image data. 
#   define kSourceImageTileSizeMB 10.0f // The tile size will be (x)MB of uncompressed image data. 
#endif

#define bytesPerMB 1048576.0f  //1024 * 1024
//每个像素占用4字节
#define bytesPerPixel 4.0f
//每个像素占用1/MB数
#define pixelsPerMB ( bytesPerMB / bytesPerPixel ) // 262144 pixels, for 4 bytes per pixel.
#define destTotalPixels kDestImageSizeMB * pixelsPerMB
#define tileTotalPixels kSourceImageTileSizeMB * pixelsPerMB
#define destSeemOverlap 2.0f // the numbers of pixels to overlap the seems where tiles meet.

@implementation LargeImageDownsizingViewController

@synthesize destImage;

- (void)viewDidLoad {
    [super viewDidLoad];
    progressView = [[UIImageView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:progressView];
    [NSThread detachNewThreadSelector:@selector(downsize:) toTarget:self withObject:nil];
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
    // Return YES for supported orientations
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}

-(void)downsize:(id)arg {
    @autoreleasepool {
    NSString *path_document = NSHomeDirectory();
    NSString *imagePath = [path_document stringByAppendingString:@"/Documents/bigImage1.png"];
    sourceImage = [[UIImage alloc] initWithContentsOfFile:imagePath];
    if( sourceImage == nil ) NSLog(@"input image not found!");
        //原图片size
    sourceResolution.width = CGImageGetWidth(sourceImage.CGImage);
    sourceResolution.height = CGImageGetHeight(sourceImage.CGImage);
        //原图片分辨率
    sourceTotalPixels = sourceResolution.width * sourceResolution.height;
        //原图片位图大小
    sourceTotalMB = sourceTotalPixels / pixelsPerMB;
        //各种设备上对应的缩放比例,各种设备上的destTotalPixels(预览图分辨率)值也不一样
    imageScale = destTotalPixels / sourceTotalPixels;
    destResolution.width = (int)( sourceResolution.width * imageScale );
    destResolution.height = (int)( sourceResolution.height * imageScale );
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
        //位图每行占用的字节数
    int bytesPerRow = bytesPerPixel * destResolution.width;
    // 分配足够的像素数据来保存输出预览图图像,位图占用的全部字节数。
    void* destBitmapData = malloc( bytesPerRow * destResolution.height );
    if( destBitmapData == NULL ) NSLog(@"failed to allocate space for the output image!");
    // create the output bitmap context
    destContext = CGBitmapContextCreate( destBitmapData, destResolution.width, destResolution.height, 8, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast );
    // remember CFTypes assign/check for NULL. NSObjects assign/check for nil.
    if( destContext == NULL ) {
        free( destBitmapData ); 
        NSLog(@"failed to create the output bitmap context!");
    }        
    // release the color space object as its job is done
    CGColorSpaceRelease( colorSpace );
    CGContextTranslateCTM( destContext, 0.0f, destResolution.height );
    CGContextScaleCTM( destContext, 1.0f, -1.0f );
        //加载原图时每次加载一小块,这样大图片内存不会一下子飙升很大
    sourceTile.size.width = sourceResolution.width;
    sourceTile.size.height = (int)( tileTotalPixels / sourceTile.size.width );     
    NSLog(@"source tile size: %f x %f",sourceTile.size.width, sourceTile.size.height);
    sourceTile.origin.x = 0.0f;
    destTile.size.width = destResolution.width;
    destTile.size.height = sourceTile.size.height * imageScale;        
    destTile.origin.x = 0.0f;
    NSLog(@"dest tile size: %f x %f",destTile.size.width, destTile.size.height);
    sourceSeemOverlap = (int)( ( destSeemOverlap / destResolution.height ) * sourceResolution.height );
    NSLog(@"dest seem overlap: %f, source seem overlap: %f",destSeemOverlap, sourceSeemOverlap);    
    CGImageRef sourceTileImageRef;
    int iterations = (int)( sourceResolution.height / sourceTile.size.height );
    int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
    if( remainder ) iterations++;
    float sourceTileHeightMinusOverlap = sourceTile.size.height;
    sourceTile.size.height += sourceSeemOverlap;
    destTile.size.height += destSeemOverlap;    
    NSLog(@"beginning downsize. iterations: %d, tile height: %f, remainder height: %d", iterations, sourceTile.size.height,remainder );
    for( int y = 0; y < iterations; ++y ) {
        // create an autorelease pool to catch calls to -autorelease made within the downsize loop.
        @autoreleasepool {
        NSLog(@"iteration %d of %d",y+1,iterations);
        sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap; 
        destTile.origin.y = ( destResolution.height ) - ( ( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + destSeemOverlap );
        sourceTileImageRef = CGImageCreateWithImageInRect( sourceImage.CGImage, sourceTile );
        if( y == iterations - 1 && remainder ) {
            float dify = destTile.size.height;
            destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
            dify -= destTile.size.height;
            destTile.origin.y += dify;
        }
         
        CGContextDrawImage( destContext, destTile, sourceTileImageRef );
        CGImageRelease( sourceTileImageRef );
        }
        if( y < iterations - 1 ) {            
            sourceImage = [[UIImage alloc] initWithContentsOfFile:imagePath];
            [self performSelectorOnMainThread:@selector(updateScrollView:) withObject:nil waitUntilDone:YES];
        }
    }
    NSLog(@"downsize complete.");
    [self performSelectorOnMainThread:@selector(initializeScrollView:) withObject:nil waitUntilDone:YES];
    CGContextRelease( destContext );
    }
}

-(void)createImageFromContext {
    // create a CGImage from the offscreen image context
    CGImageRef destImageRef = CGBitmapContextCreateImage( destContext );
    if( destImageRef == NULL ) NSLog(@"destImageRef is null.");
    // 在CGImage周围封装一个UIImage
    self.destImage = [UIImage imageWithCGImage:destImageRef scale:1.0f orientation:UIImageOrientationDownMirrored];
    // release ownership of the CGImage, since destImage retains ownership of the object now.
    CGImageRelease( destImageRef );
    if( destImage == nil ) NSLog(@"destImage is nil.");
}

-(void)updateScrollView:(id)arg {
    [self createImageFromContext];
    // display the output image on the screen.
    progressView.image = destImage;
}

-(void)initializeScrollView:(id)arg {
    [progressView removeFromSuperview];
    [self createImageFromContext];
    // create a scroll view to display the resulting image.
    scrollView = [[ImageScrollView alloc] initWithFrame:self.view.bounds image:self.destImage];
    [self.view addSubview:scrollView];
}

@end

ImageScrollView.h

/*
     File: ImageScrollView.h 
 Abstract: This scroll view allows the user to inspect the resulting image's levels of detail. 
  Version: 1.1
 Copyright (C) 2014 Apple Inc. All Rights Reserved. 
  
*/

#import <UIKit/UIKit.h>

@class TiledImageView;

@interface ImageScrollView : UIScrollView <UIScrollViewDelegate> {
    // The TiledImageView that is currently front most
    TiledImageView* frontTiledView;
    // The old TiledImageView that we draw on top of when the zooming stops
    TiledImageView* backTiledView;  
    // A low res version of the image that is displayed until the TiledImageView
    // renders its content.
    UIImageView *backgroundImageView;
    float minimumScale;
    // current image zoom scale
    CGFloat imageScale;
    UIImage* image;
}
@property (strong) UIImage* image;
@property (strong) TiledImageView* backTiledView;   

-(id)initWithFrame:(CGRect)frame image:(UIImage*)image;

@end

ImageScrollView.m

/*
     File: ImageScrollView.m 
 Abstract: This scroll view allows the user to inspect the resulting image's levels of detail. 
  Version: 1.1
 Copyright (C) 2014 Apple Inc. All Rights Reserved. 
  
*/

#import "ImageScrollView.h"
#import "TiledImageView.h"
#import <QuartzCore/QuartzCore.h>

@implementation ImageScrollView;

@synthesize image, backTiledView;

- (void)dealloc {
    self.backTiledView = nil;
}
-(id)initWithFrame:(CGRect)frame image:(UIImage*)img {
    if((self = [super initWithFrame:frame])) {      
        // Set up the UIScrollView
        self.showsVerticalScrollIndicator = NO;
        self.showsHorizontalScrollIndicator = NO;
        self.bouncesZoom = YES;
        self.decelerationRate = UIScrollViewDecelerationRateFast;
        self.delegate = self;
        self.maximumZoomScale = 5.0f;
        self.minimumZoomScale = 0.25f;
        self.backgroundColor = [UIColor colorWithRed:0.4f green:0.2f blue:0.2f alpha:1.0f];
        // determine the size of the image
        self.image = img;
        CGRect imageRect = CGRectMake(0.0f,0.0f,CGImageGetWidth(image.CGImage),CGImageGetHeight(image.CGImage));
        imageScale = self.frame.size.width/imageRect.size.width;
        minimumScale = imageScale * 0.75f;
        NSLog(@"imageScale: %f",imageScale);
        imageRect.size = CGSizeMake(imageRect.size.width*imageScale, imageRect.size.height*imageScale);
        // Create a low res image representation of the image to display before the TiledImageView
        // renders its content.
        UIGraphicsBeginImageContext(imageRect.size);        
        CGContextRef context = UIGraphicsGetCurrentContext();       
        CGContextSaveGState(context);
        CGContextDrawImage(context, imageRect, image.CGImage);
        CGContextRestoreGState(context);        
        UIImage *backgroundImage = UIGraphicsGetImageFromCurrentImageContext();     
        UIGraphicsEndImageContext();
        //模糊的背景图片,用于在加载好清晰图前显示,清晰图加载好后会覆盖掉模糊图,这样视觉效果比较好
        backgroundImageView = [[UIImageView alloc] initWithImage:backgroundImage];
        backgroundImageView.frame = imageRect;
        backgroundImageView.contentMode = UIViewContentModeScaleAspectFit;
        [self addSubview:backgroundImageView];
        //放到父视图的最后面显示
        [self sendSubviewToBack:backgroundImageView];
        // Create the TiledImageView based on the size of the image and scale it to fit the view.
        //显示的清晰图片
        frontTiledView = [[TiledImageView alloc] initWithFrame:imageRect image:image scale:imageScale];
        [self addSubview:frontTiledView];
    }
    return self;
}

#pragma mark -
#pragma mark Override layoutSubviews to center content

// We use layoutSubviews to center the image in the view
- (void)layoutSubviews {
    [super layoutSubviews];
    // center the image as it becomes smaller than the size of the screen
    CGSize boundsSize = self.bounds.size;
    CGRect frameToCenter = frontTiledView.frame;
    // center horizontally
    if (frameToCenter.size.width < boundsSize.width)
        frameToCenter.origin.x = (boundsSize.width - frameToCenter.size.width) / 2;
    else
        frameToCenter.origin.x = 0;
    // center vertically
    if (frameToCenter.size.height < boundsSize.height)
        frameToCenter.origin.y = (boundsSize.height - frameToCenter.size.height) / 2;
    else
        frameToCenter.origin.y = 0;
    frontTiledView.frame = frameToCenter;
    backgroundImageView.frame = frameToCenter;
    // to handle the interaction between CATiledLayer and high resolution screens, we need to manually set the
    // tiling view's contentScaleFactor to 1.0. (If we omitted this, it would be 2.0 on high resolution screens,
    // which would cause the CATiledLayer to ask us for tiles of the wrong scales.)
    frontTiledView.contentScaleFactor = 1.0;
}
#pragma mark -
#pragma mark UIScrollView delegate methods
// A UIScrollView delegate callback, called when the user starts zooming. 
// We return our current TiledImageView.
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
    return frontTiledView;
}
// A UIScrollView delegate callback, called when the user stops zooming.  When the user stops zooming
// we create a new TiledImageView based on the new zoom level and draw it on top of the old TiledImageView.
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale {
    // set the new scale factor for the TiledImageView
    imageScale *=scale;
    if( imageScale < minimumScale ) imageScale = minimumScale;
    CGRect imageRect = CGRectMake(0.0f,0.0f,CGImageGetWidth(image.CGImage) * imageScale,CGImageGetHeight(image.CGImage) * imageScale);
    // Create a new TiledImageView based on new frame and scaling.
    frontTiledView = [[TiledImageView alloc] initWithFrame:imageRect image:image scale:imageScale]; 
    [self addSubview:frontTiledView];
}

// A UIScrollView delegate callback, called when the user begins zooming.  When the user begins zooming
// we remove the old TiledImageView and set the current TiledImageView to be the old view so we can create a
// a new TiledImageView when the zooming ends.
- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view {
    // Remove back tiled view.
    [backTiledView removeFromSuperview];
    // Set the current TiledImageView to be the old view.
    self.backTiledView = frontTiledView;
}

@end

TiledImageView.h

/*
     File: TiledImageView.h 
 Abstract: View backed by CATiledLayer which is good for particularly large views. 
  Version: 1.1
 Copyright (C) 2014 Apple Inc. All Rights Reserved. 
  
*/

#import <UIKit/UIKit.h>

@interface TiledImageView : UIView {
    CGFloat imageScale;
    UIImage* image;
    CGRect imageRect;
}
@property (strong) UIImage* image;

-(id)initWithFrame:(CGRect)_frame image:(UIImage*)image scale:(CGFloat)scale;

@end

TiledImageView.m

/*
     File: TiledImageView.m 
 Abstract: View backed by CATiledLayer which is good for particularly large views. 
  Version: 1.1
 Copyright (C) 2014 Apple Inc. All Rights Reserved. 
  
*/

#import "TiledImageView.h"
#import <QuartzCore/QuartzCore.h>

@implementation TiledImageView

@synthesize image;

// Set the layer's class to be CATiledLayer.
+ (Class)layerClass {
    return [CATiledLayer class];
}

// Create a new TiledImageView with the desired frame and scale.
-(id)initWithFrame:(CGRect)_frame image:(UIImage*)img scale:(CGFloat)scale {
    if ((self = [super initWithFrame:_frame])) {
        self.image = img;
        imageRect = CGRectMake(0.0f, 0.0f, CGImageGetWidth(image.CGImage), CGImageGetHeight(image.CGImage));
        imageScale = scale;
        CATiledLayer *tiledLayer = (CATiledLayer *)[self layer];
        // levelsOfDetail and levelsOfDetailBias determine how
        // the layer is rendered at different zoom levels.  This
        // only matters while the view is zooming, since once the 
        // the view is done zooming a new TiledImageView is created
        // at the correct size and scale.
        tiledLayer.levelsOfDetail = 4;
        tiledLayer.levelsOfDetailBias = 4;
        tiledLayer.tileSize = CGSizeMake(512.0, 512.0); 
    }
    return self;
}

-(void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();    
    CGContextSaveGState(context);
    // Scale the context so that the image is rendered 
    // at the correct size for the zoom level.
    CGContextScaleCTM(context, imageScale,imageScale);  
    CGContextDrawImage(context, imageRect, image.CGImage);
    CGContextRestoreGState(context);    
}

@end

最后附上两张大图片

iOS OC系类
Web note ad 1