iOS图片加载渲染的优化
首先我们来看iOS加载一张图片所经历的过程:(下面所讲述的代码基本以 imageWithContentsOfFile 方法来举例)
数据加载
- 我们优先创建UIImageView,把获得的图像数据赋值UIImageView
- 识别到我们缓冲区没有数据,就会去从磁盘拷贝数据到缓冲区
- 然后加载我们的图片
- 拿到了图片,下面到了视图渲染
视图渲染
- 图片数据在CoreAnimation流水线中,执行如下流程
- 优先计算视图Frame,进行视图构建和图片格式转换
- 如果图像未解码,则优先解码成位图数据
- 进行打包处理,主线程Runloop将其提交给Render Server
- 在提交之前,如果数据没有字节对齐,CoreAnimation会再拷贝一份数据,进行字节对齐
- 然后经过GPU图形渲染管线处理以后将渲染结果放入
帧缓冲区
然后
视频控制器
会按照VSync
信号逐行读取帧缓冲区
的数据,给显示器显示
从上述渲染过程中,我寻找其可优化点。
- 图像解码
- 内存加载
- 对齐字节
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图片加载渲染的优化相关推荐
- QML的图片加载,内存优化研究(一)
QML的图片加载,内存优化研究(一) QML加载图片的两个控件 Image控件及其相关属性 Image加载图片的内存问题 代码一: 代码二: 代码三: 代码四: 代码五: 通过源码来分析 QML加载图 ...
- ios 图片加载内存尺寸_iOS内存分析上-图片加载内存分析
简介 对于大多数App来说,内存占用主要就是图片.本文将从实用的角度分析,iOS图片的内存占用.测量.优化等. iOS内存-有什么影响 在移动操作系统设备中,是不能像PC一样进行内存swap的,而随着 ...
- 如何利用 webp 进行小程序图片加载速度的优化
导语 最近很长一段时间没有更新博客,一方面是自己最近参与了小程序的开发,另一方面也是自己略有些怠惰,给自己记个过~那么现在既然回到学校那么还是要分享一些知识的. 前一阵子参与微信小程序开发时遇到了一个 ...
- android学习之路(六)---- 图片加载库的优化、封装
封装Image-Loader 一.背景 universal-image-loader是一项伟大的开源项目,作者在其中运用到的软件工程解决办法让人印象深刻,在本篇文章的开篇,首先向uni ...
- html 图片显示一块一块加载失败,页面中图片加载失败的优化方法
网站当中经常会遇到图片加载失败的问题,img中有地址,但是地址打开是错误的.情况如下: 不同浏览器处理错误图片是不一样的,有的干脆就显示差号,例如IE,有的显示一张破碎的图片,有的则是给一张高度比较大 ...
- ios 图片加载内存尺寸_iOS加载超清大图内存暴涨问题解决
加载超清大图是会引起内存爆表的问题,最近一直困扰着我. SDWebImage在加载大图时做的不是很好,加载大图内存爆表.YYWebImage会好一点,但还是不行. 当不要求图片质量的情况下,最好是在上 ...
- iOS开发学无止境 - 异步图片加载优化与常用开源库分析
作者:罗轩(@luoyibu) 网址:http://www.jianshu.com/p/3b2c95e1404f 1. 网络图片显示大体步骤: 下载图片 图片处理(裁剪,边框等) 写入磁盘 从磁盘 ...
- iOS关于加载图片的几种方式选择
最近在开发过程中遇到一些性能优化的东西,这次来说说关于图片加载的性能优化和选择. 大家都知道创建UIImage常用以下几种方式 + (nullable UIImage *)imageNamed:(NS ...
- iOS网络加载图片缓存策略之ASIDownloadCache缓存优化
iOS网络加载图片缓存策略之ASIDownloadCache缓存优化 在我们实际工程中,很多情况需要从网络上加载图片,然后将图片在imageview中显示出来,但每次都要从网络上请求,会严重影响用户体 ...
最新文章
- Gartner发布2021年重要战略科技趋势
- Spring 4 使用Freemarker模板发送邮件添加附件
- 虚拟局域网vlan实验报告_网络交换机如何规划,VLAN原理介绍
- uipath sequence传递参数_多孔材料测试及声学参数识别(中)_多孔材料声学参数正向识别...
- NYOJ 1075 (递推 + 矩阵快速幂)
- Android 一个对sharedpreferences 数据进行加密的开源库
- Google的“机器人情结”:两次合计36亿美元的人工智能收购
- 浅谈dup和dup2的用法
- Spring Cloud 未来发展方向
- python 等差数列list_Python3基础 list range+for 等差数列
- qt linux编程思路,关于QT编程入门的那些事
- echarts全解析及其用法详解
- 外显子bed文件获取
- 科学计算机符号大全,计算机符号代码大全
- 你需要TrustedInstaller提供的权限才能对此文件进行更改
- HCIE-Routing Switching V3.0 资料分享
- php工作p7,广告服务端PHP高级工程师(P6-P7)职位描述与岗位职责任职要求
- 自然语言处理-中文分词相关算法(MM、RMM、BMM、HMM)
- LeetCode221210_135、剑指 Offer 58 - II. 左旋转字符串
- 练习:银行复利计算(用 for 循环解一道初中小题)