首先我们来看iOS加载一张图片所经历的过程:(下面所讲述的代码基本以      imageWithContentsOfFile 方法来举例)

数据加载

  1. 我们优先创建UIImageView,把获得的图像数据赋值UIImageView
  2. 识别到我们缓冲区没有数据,就会去从磁盘拷贝数据到缓冲区
  3. 然后加载我们的图片
  4. 拿到了图片,下面到了视图渲染

视图渲染

  1. 图片数据在CoreAnimation流水线中,执行如下流程
  2. 优先计算视图Frame,进行视图构建和图片格式转换
  3. 如果图像未解码,则优先解码成位图数据
  4. 进行打包处理,主线程Runloop将其提交给Render Server
  5. 在提交之前,如果数据没有字节对齐,CoreAnimation会再拷贝一份数据,进行字节对齐
  6. 然后经过GPU图形渲染管线处理以后将渲染结果放入帧缓冲区
  7. 然后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,给显示器显示

从上述渲染过程中,我寻找其可优化点。

  1. 图像解码
  2. 内存加载
  3. 对齐字节

1.图像解码

当你用 UIImage 或 CGImageSource 的那几个方法创建图片时,图片数据并不会立刻解码。图片设置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。那么当需要加载的图片比较多时,就会对我们应用的响应性造成严重的影响,尤其是在快速滑动的列表上,这个问题会表现得更加突出。

iOS默认会在主线程对图像进行解码。解码过程是一个相当复杂的任务,需要消耗非常长的时间。由于在主线程超过16.7ms的任务会引起掉帧,所以我们把解码操作从主线程移到子线程,让耗时的解码操作不占用主线程的时间,解码的核心方法如下:

CGContextRef CGBitmapContextCreate(
void * data, //这块内存用于存储被绘制的图形,这块内存的size最小不能小于bytesPerRow*height(图形每行的字节数乘以图形的高度),传递NULL意味着由这个函数来管理图形的内存,这可以减少内存泄漏的问题;
size_t width, //图形的width
size_t height,//图形的height
size_t bitsPerComponent, //像素的每个颜色分量使用的 bit 数,在 RGB 颜色空间下指定 8 即可
size_t bytesPerRow,//位图的每一行使用的字节数,大小至少为 width * bytes per pixel 字节。有意思的是,当我们指定 0 时,系统不仅会为我们自动计算,而且还会进行 cache line alignment 的优化
CGColorSpaceRef  _Nullable space, //就是我们前面提到的颜色空间,一般使用 RGB 即可;
uint32_t bitmapInfo//是一个枚举,
)

异步解码上代码

 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{NSString *imagePath = self.imagePaths;UIImage *image = [UIImage imageWithContentsOfFile:imagePath];UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, YES, 0);[image drawInRect:imageView.bounds];image = UIGraphicsGetImageFromCurrentImageContext();UIGraphicsEndImageContext();dispatch_async(dispatch_get_main_queue(), ^{});});

虽说能够正常解压,但是我们也会发现一个问题,就是大图片的解压,所以这个地方安装苹果和各大三方代码中的提示要分为2种情况讨论:

1.对于小于60M的图片我们直接对图片解码,下面是SD的代码


+ (UIImage *)decodedImageWithImage:(UIImage *)image {if (![self shouldDecodeImage:image]) {return image;}CGImageRef imageRef = [self CGImageCreateDecoded:image.CGImage];if (!imageRef) {return image;}UIImage *decodedImage = [[UIImage alloc] initWithCGImage:imageRef scale:image.scale orientation:image.imageOrientation];CGImageRelease(imageRef);SDImageCopyAssociatedObject(image, decodedImage);decodedImage.sd_isDecoded = YES;return decodedImage;
}

2.对于大于60M的图片,会对原图片进行缩放以减少占用内存空间,并且解码图片时会把原始的图片数据分成多个tail进行解码,下面是SD的代码


+ (UIImage *)decodedAndScaledDownImageWithImage:(UIImage *)image limitBytes:(NSUInteger)bytes {if (![self shouldDecodeImage:image]) {return image;}if (![self shouldScaleDownImage:image limitBytes:bytes]) {return [self decodedImageWithImage:image];}CGFloat destTotalPixels;CGFloat tileTotalPixels;if (bytes == 0) {bytes = kDestImageLimitBytes;}destTotalPixels = bytes / kBytesPerPixel;tileTotalPixels = destTotalPixels / 3;CGContextRef destContext;// autorelease the bitmap context and all vars to help system to free memory when there are memory warning.// on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];@autoreleasepool {CGImageRef sourceImageRef = image.CGImage;CGSize sourceResolution = CGSizeZero;sourceResolution.width = CGImageGetWidth(sourceImageRef);sourceResolution.height = CGImageGetHeight(sourceImageRef);CGFloat sourceTotalPixels = sourceResolution.width * sourceResolution.height;// Determine the scale ratio to apply to the input image// that results in an output image of the defined size.// see kDestImageSizeMB, and how it relates to destTotalPixels.CGFloat imageScale = sqrt(destTotalPixels / sourceTotalPixels);CGSize destResolution = CGSizeZero;destResolution.width = MAX(1, (int)(sourceResolution.width * imageScale));destResolution.height = MAX(1, (int)(sourceResolution.height * imageScale));// device color spaceCGColorSpaceRef colorspaceRef = [self colorSpaceGetDeviceRGB];BOOL hasAlpha = [self CGImageContainsAlpha:sourceImageRef];// iOS display alpha info (BGRA8888/BGRX8888)CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;// kCGImageAlphaNone is not supported in CGBitmapContextCreate.// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipFirst// to create bitmap graphics contexts without alpha info.destContext = CGBitmapContextCreate(NULL,destResolution.width,destResolution.height,kBitsPerComponent,0,colorspaceRef,bitmapInfo);if (destContext == NULL) {return image;}CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);// Now define the size of the rectangle to be used for the// incremental bits from the input image to the output image.// we use a source tile width equal to the width of the source// image due to the way that iOS retrieves image data from disk.// iOS must decode an image from disk in full width 'bands', even// if current graphics context is clipped to a subrect within that// band. Therefore we fully utilize all of the pixel data that results// from a decoding operation by anchoring our tile size to the full// width of the input image.CGRect sourceTile = CGRectZero;sourceTile.size.width = sourceResolution.width;// The source tile height is dynamic. Since we specified the size// of the source tile in MB, see how many rows of pixels high it// can be given the input image width.sourceTile.size.height = MAX(1, (int)(tileTotalPixels / sourceTile.size.width));sourceTile.origin.x = 0.0f;// The output tile is the same proportions as the input tile, but// scaled to image scale.CGRect destTile;destTile.size.width = destResolution.width;destTile.size.height = sourceTile.size.height * imageScale;destTile.origin.x = 0.0f;// The source seem overlap is proportionate to the destination seem overlap.// this is the amount of pixels to overlap each tile as we assemble the output image.float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);CGImageRef sourceTileImageRef;// calculate the number of read/write operations required to assemble the// output image.int iterations = (int)( sourceResolution.height / sourceTile.size.height );// If tile height doesn't divide the image height evenly, add another iteration// to account for the remaining pixels.int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;if(remainder) {iterations++;}// Add seem overlaps to the tiles, but save the original tile height for y coordinate calculations.float sourceTileHeightMinusOverlap = sourceTile.size.height;sourceTile.size.height += sourceSeemOverlap;destTile.size.height += kDestSeemOverlap;for( int y = 0; y < iterations; ++y ) {@autoreleasepool {sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, 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 );}}CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);CGContextRelease(destContext);if (destImageRef == NULL) {return image;}
#if SD_MACUIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:kCGImagePropertyOrientationUp];
#elseUIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
#endifCGImageRelease(destImageRef);if (destImage == nil) {return image;}SDImageCopyAssociatedObject(image, destImage);destImage.sd_isDecoded = YES;return destImage;}
}

上述代码中,我们看对原始图片进行了缩放,并且对把原始图片分成多块进行批量解码,并且添加了自动释放池,保证了内存的释放操作,由于操作了底层相关的东西,也进行了手动内存的释放,这点是要注意的。当然子线程解码我们也要控制子线程数量,线程的数量控制最好合CPU核心数保持一致。针对大文件做缓存的图像体积也大,这个时候使用内存映射读取文件优势很大,内存拷贝的量少,拷贝后占用用户内存也不高,文件越大内存映射优势越大。

下面我们再看一下苹果官方提供的降低采样率方案:

swift版


func downsample(imageAt imageURL: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage {let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionarylet imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, imageSourceOptions)!let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scalelet downsampleOptions = [kCGImageSourceCreateThumbnailFromImageAlways: true,kCGImageSourceShouldCacheImmediately: true,kCGImageSourceCreateThumbnailWithTransform: true,kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels] as CFDictionarylet downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0,         downsampleOptions)!return UIImage(cgImage: downsampledImage)
}

对应重写的OC版

//缩略图核心代码
+ (UIImage *)thumbnailWithImageWithoutScale:(UIImage *)image size:(CGSize)asize{UIImage *newimage;if (nil == image) {newimage = nil;}else{CGSize oldsize = image.size;CGRect rect;if (asize.width/asize.height > oldsize.width/oldsize.height) {rect.size.width = asize.height*oldsize.width/oldsize.height;rect.size.height = asize.height;rect.origin.x = (asize.width - rect.size.width)/2;rect.origin.y = 0;}else{rect.size.width = asize.width;rect.size.height = asize.width*oldsize.height/oldsize.width;rect.origin.x = 0;rect.origin.y = (asize.height - rect.size.height)/2;}UIGraphicsBeginImageContext(asize);CGContextRef context = UIGraphicsGetCurrentContext();CGContextSetFillColorWithColor(context, [[UIColor clearColor] CGColor]);UIRectFill(CGRectMake(0, 0, asize.width, asize.height));//clear background[image drawInRect:rect];newimage = UIGraphicsGetImageFromCurrentImageContext();UIGraphicsEndImageContext();}return newimage;
}

我们在上文的写缩略图生成过程中,已经对图片进行解码操作

2.内存加载

用过FastImageCache的同学,都知道其使用了虚拟内存,进行文件映射,进行读写文件。

void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);start:映射开始地址,设置NULL则让系统决定映射开始地址;
length:映射区域的长度,单位是Byte;
prot:映射内存的保护标志,主要是读写相关,是位运算标志;(记得与下面fd对应句柄打开的设置一致)
flags:映射类型,通常是文件和共享类型;
fd:文件句柄;
off_toffset:被映射对象的起点偏移;

我们使用NSData与mmap之间的关系可以获取到映射的数据,如下

+ (id)dataWithContentsOfFile:(NSString *)path options:(NSDataReadingOptions)readOptionsMask error:(NSError **)errorPtr;

针对NSDataReadingOptions的几种类型有如下解释:

NSDataReadingMappedIfSafe 提示显示文件应该映射到虚拟内存,如果可能和安全
NSDataReadingUncached 提示显示文件不应该存储在文件系统缓存。数据读取一次,丢弃,这个选项可以提高性能
NSDataReadingMappedAlways 在如果可能提示映射文件

我们使用NSDataReadingMappedIfSafe,能够保证安全。

我们在SDWebImage中也可以看到相应的代码:

- (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key {if (!key) {return nil;}NSData *data = [self.diskCache dataForKey:key];if (data) {return data;}// Addtional cache path for custom pre-load cacheif (self.additionalCachePathBlock) {NSString *filePath = self.additionalCachePathBlock(key);if (filePath) {data = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil];}}return data;
}

经过分析,SD中SDImageCache缓存文件中默认是NSDataReadingMappedIfSafe。

说白了就是用mmap把文件映射到用户空间里的虚拟内存,文件中的位置在虚拟内存中有了对应的地址,可以像操作内存一样操作这个文件,相当于已经把整个文件放入内存,但在真正使用到这些数据前却不会消耗物理内存,也不会有读写磁盘的操作,只有真正使用这些数据时,也就是图像准备渲染在屏幕上时,虚拟内存管理系统VMS才根据缺页加载的机制从磁盘加载对应的数据块到物理内存,再进行渲染。这样的文件读写文件方式少了数据从内核缓存到用户空间的拷贝,效率很高。

3.对齐字节

我们都知道CoreAnimation在图像数据非字节对齐的情况下渲染前会先拷贝一份图像数据。

我们从堆栈中也能看得出系统使用了这个copy_image,进行图像数据拷贝。

字节对齐是为了提高读取的性能。因为处理器读取内存中的数据不是一个一个字节读取的,而是一块一块读取的一般叫做cache lines。如果一个不对齐的数据放在了2个数据块中,那么处理器可能要执行两次内存访问。当这种不对齐的数据非常多的时候,就会影响到读取性能了。这样可能会牺牲一些储存空间,但是提升了内存的读取性能。

我们在使用CGBitmapContextCreate创建绘图上下文的时候,目前我们使用的机器基本上是处理器的是64byte,所以可以指定bytesPerRow为64的整数倍,这样就可以减少这部分是耗时,提升性能。

我们能做的优化远不止于此,不断的探索才是我们的目标,加油!!!骚年

iOS图片加载渲染的优化相关推荐

  1. QML的图片加载,内存优化研究(一)

    QML的图片加载,内存优化研究(一) QML加载图片的两个控件 Image控件及其相关属性 Image加载图片的内存问题 代码一: 代码二: 代码三: 代码四: 代码五: 通过源码来分析 QML加载图 ...

  2. ios 图片加载内存尺寸_iOS内存分析上-图片加载内存分析

    简介 对于大多数App来说,内存占用主要就是图片.本文将从实用的角度分析,iOS图片的内存占用.测量.优化等. iOS内存-有什么影响 在移动操作系统设备中,是不能像PC一样进行内存swap的,而随着 ...

  3. 如何利用 webp 进行小程序图片加载速度的优化

    导语 最近很长一段时间没有更新博客,一方面是自己最近参与了小程序的开发,另一方面也是自己略有些怠惰,给自己记个过~那么现在既然回到学校那么还是要分享一些知识的. 前一阵子参与微信小程序开发时遇到了一个 ...

  4. android学习之路(六)---- 图片加载库的优化、封装

    封装Image-Loader 一.背景         universal-image-loader是一项伟大的开源项目,作者在其中运用到的软件工程解决办法让人印象深刻,在本篇文章的开篇,首先向uni ...

  5. html 图片显示一块一块加载失败,页面中图片加载失败的优化方法

    网站当中经常会遇到图片加载失败的问题,img中有地址,但是地址打开是错误的.情况如下: 不同浏览器处理错误图片是不一样的,有的干脆就显示差号,例如IE,有的显示一张破碎的图片,有的则是给一张高度比较大 ...

  6. ios 图片加载内存尺寸_iOS加载超清大图内存暴涨问题解决

    加载超清大图是会引起内存爆表的问题,最近一直困扰着我. SDWebImage在加载大图时做的不是很好,加载大图内存爆表.YYWebImage会好一点,但还是不行. 当不要求图片质量的情况下,最好是在上 ...

  7. iOS开发学无止境 - 异步图片加载优化与常用开源库分析

    作者:罗轩(@luoyibu) 网址:http://www.jianshu.com/p/3b2c95e1404f 1. 网络图片显示大体步骤:   下载图片 图片处理(裁剪,边框等) 写入磁盘 从磁盘 ...

  8. iOS关于加载图片的几种方式选择

    最近在开发过程中遇到一些性能优化的东西,这次来说说关于图片加载的性能优化和选择. 大家都知道创建UIImage常用以下几种方式 + (nullable UIImage *)imageNamed:(NS ...

  9. iOS网络加载图片缓存策略之ASIDownloadCache缓存优化

    iOS网络加载图片缓存策略之ASIDownloadCache缓存优化 在我们实际工程中,很多情况需要从网络上加载图片,然后将图片在imageview中显示出来,但每次都要从网络上请求,会严重影响用户体 ...

最新文章

  1. Gartner发布2021年重要战略科技趋势
  2. Spring 4 使用Freemarker模板发送邮件添加附件
  3. 虚拟局域网vlan实验报告_网络交换机如何规划,VLAN原理介绍
  4. uipath sequence传递参数_多孔材料测试及声学参数识别(中)_多孔材料声学参数正向识别...
  5. NYOJ 1075 (递推 + 矩阵快速幂)
  6. Android 一个对sharedpreferences 数据进行加密的开源库
  7. Google的“机器人情结”:两次合计36亿美元的人工智能收购
  8. 浅谈dup和dup2的用法
  9. Spring Cloud 未来发展方向
  10. python 等差数列list_Python3基础 list range+for 等差数列
  11. qt linux编程思路,关于QT编程入门的那些事
  12. echarts全解析及其用法详解
  13. 外显子bed文件获取
  14. 科学计算机符号大全,计算机符号代码大全
  15. 你需要TrustedInstaller提供的权限才能对此文件进行更改
  16. HCIE-Routing Switching V3.0 资料分享
  17. php工作p7,广告服务端PHP高级工程师(P6-P7)职位描述与岗位职责任职要求
  18. 自然语言处理-中文分词相关算法(MM、RMM、BMM、HMM)
  19. LeetCode221210_135、剑指 Offer 58 - II. 左旋转字符串
  20. 练习:银行复利计算(用 for 循环解一道初中小题)

热门文章

  1. android官网m魅族15,魅族15亮相安卓官网 圆形Home键设计
  2. DOTA的常用礼仪用语及英文缩写
  3. 一、用Python从零实现横向联邦图像分类
  4. Norbit多波束(iWBMS)和前视声纳(WBMS FLS)的使用简介
  5. 压缩感知——沃尔什-哈达玛(WHT)变换与逆变换的Matlab代码实现
  6. 医药知识图谱项目搭建注意事项(QASystemOnMedicalKG)
  7. 比尔总动员技师职业详解
  8. 半导体显示|LG显示器宣布在韩国投资26亿美元 生产OLED面板
  9. 2021最新大厂Java面试集合,顺利拿到offer
  10. 【转发】IDM fabless foundry