每日一问04-加载图片对性能的影响

知道了那么多关于iOS上界面渲染的理论知识后,终于可以回归最开始的问题,将一张 png/jpg 格式的图片渲染到页面上显示有哪些流程?先来区分几个东西。

1.PNG,JPG的区别。

JPG

JPEG是一种针对相片影像而广泛使用的一种失真压缩标准方法。JPEG的压缩方式通常是破坏性资料压缩(lossy compression),意即在压缩过程中图像的品质会遭受到可见的破坏。JPEG不支持动画,不支持半透明。

PNG

便携式网络图片(Portable Network Graphics),简称PNG,是一种无损数据压缩位图图形文件格式。PNG格式是无损数据压缩的,允许使用类似于GIF格式的调色板技术,支持真彩色图像,并具备Alpha(半透明)等特性。

最基本的区别就是jpg图片不支持透明,并且是有损压缩。而png图片支持透明度,无损压缩

2.iOS加载本地图片的方式

imageNamed:
+ (nullable UIImage *)imageNamed:(NSString *)name;      // load from main bundle

这个方法用一个指定的名字在系统缓存中查找并返回一个图片对象如果它存在的话。如果缓存中没有找到相应的图片,这个方法从指定的文档中加载然后缓存并返回这个对象。
特点:
1.imageNamed加载图片会进行缓存
2.这个方法会在加载图片之后立刻进行解压

imageWithContentsOfFile:
+ (nullable UIImage *)imageWithContentsOfFile:(NSString *)path;

仅加载图片,图像数据不会缓存。因此对于较大的图片以及使用情况较少时,那就可以用该方法,降低内存消耗。

3.图片加载的工作流

进入正题,当我们从磁盘中加载一张图片,并将它显示到频幕上,中间的主要流程如下:

1.假设我们使用 +imageWithContentsOfFile: 方法从磁盘中加载一张图片,这个时候的图片并没有解压缩;
2.然后将生成的 UIImage 赋值给 UIImageView ;
3.接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化;
4.在主线程的下一个 run loop 到来时,Core Animation 提交了这个隐式的 transaction ,这个过程可能会对图片进行 copy 操作,而受图片是否字节对齐等因素的影响,这个 copy 操作可能会涉及以下部分或全部步骤:
·分配内存缓冲区用于管理文件 IO 和解压缩操作;
·将文件数据从磁盘读到内存中;
·将压缩的图片数据解码成未压缩的位图形式,这是一个非常·耗时的 CPU 操作
·最后 Core Animation 使用未压缩的位图数据渲染 UIImageView 的图层。

在上面的步骤中,图片的解压缩是一个非常耗时的 CPU 操作,并且它默认是在主线程中执行的。那么当需要加载的图片比较多时,就会对我们应用的响应性造成严重的影响,尤其是在快速滑动的列表上,这个问题会表现得更加突出。

为什么需要解压缩
什么是位图

位图就是一个像素数组,数组中的每个像素就代表着图片中的一个点。我们在应用中经常用到的 JPEG 和 PNG 图片就是位图。
事实上,不管是 JPEG 还是 PNG 图片,都是一种压缩的位图图形格式。只不过 PNG 图片是无损压缩,并且支持 alpha 通道,而 JPEG 图片则是有损压缩,可以指定 0-100% 的压缩比。
因此,在将磁盘中的图片渲染到屏幕之前,必须先要得到图片的原始像素数据,才能执行后续的绘制操作,这就是为什么需要对图片解压缩的原因。
明白了,我们总不能直接把压缩包拿来用吧。。。

补充:压缩前与压缩后的大小关系

一张 PNG 图片,像素为 30 × 30 ,文件大小为 843B,使用以下代码:

UIImage *image = [UIImage imageNamed:@"check_green"];
CFDataRef rawData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));

就可以获取到这个图片的原始像素数据,大小为 3600B

也就是说,这张文件大小为 843B 的 PNG 图片解压缩后的大小是 3600B ,是原始文件大小的 4.27 倍。那么这个 3600B 是怎么得来的呢?与图片的文件大小或者像素有什么必然的联系吗?事实上,解压缩后的图片大小与原始文件大小之间没有任何关系,而只与图片的像素有关:

解压缩后的图片大小 = 图片的像素宽 30 * 图片的像素高 30 * 每个像素所占的字节数 4

为了加深理解,我把这个图片拖进 Sublime Text 2 中,得到了这个图片的二进制数据,大小与原始文件大小一致,为 843B

强制解压缩的原理

我们前面已经提到了,当未解压缩的图片将要渲染到屏幕时,系统会在主线程对图片进行解压缩,而如果图片已经解压缩了,系统就不会再对图片进行解压缩。因此,也就有了业内的解决方案,在子线程提前对图片进行强制解压缩

而强制解压缩的原理就是对图片进行重新绘制,得到一张新的解压缩后的位图。其中,用到的最核心的函数是 CGBitmapContextCreate

CGBitmapContextCreate.png

这个函数用于创建一个位图上下文,用来绘制一张宽 width 像素,高 height 像素的位图。

好吧,这个函数除了前三个,后面的我基本都不知道。下面挨个儿来说说参数代表什么。

像素格式

我们前面已经提到了,位图其实就是一个像素数组,而像素格式则是用来描述每个像素的组成格式,它包括以下信息:

Bits per component :一个像素中每个独立的颜色分量使用的 bit 数;
Bits per pixel :一个像素使用的总 bit 数;
Bytes per row :位图中的每一行使用的字节数。

颜色空间

在 Quartz 中,一个颜色是由一组值来表示的,比如 0, 0, 1 。而颜色空间则是用来说明如何解析这些值的,离开了颜色空间,它们将变得毫无意义。比如,下面的值都表示蓝色:


blue_color.png
位图布局信息 CGBitmapInfo:
CGBitmapInfo.png

它主要提供了三个方面的布局信息:

alpha 的信息;
颜色分量是否为浮点数;
像素格式的字节顺序。

其中,alpha 的信息由枚举值 CGImageAlphaInfo 来表示:

CGImageAlphaInfo.png

它同样也提供了三个方面的 alpha 信息:

·是否包含 alpha ;
·如果包含 alpha ,那么 alpha 信息所处的位置,在像素的最低有效位,比如 RGBA ,还是最高有效位,比如 ARGB ;
·如果包含 alpha ,那么每个颜色分量是否已经乘以 alpha 的值,这种做法可以加速图片的渲染时间,因为它避免了渲染时的额外乘法运算。比如,对于 RGB 颜色空间,用已经乘以 alpha 的数据来渲染图片,每个像素都可以避免 3 次乘法运算,红色乘以 alpha ,绿色乘以 alpha 和蓝色乘以 alpha 。

根据 Which CGImageAlphaInfo should we use 和官方文档中对 UIGraphicsBeginImageContextWithOptions
函数的讨论:

You use this function to configure the drawing environment for rendering into a bitmap. The format for the bitmap is a ARGB 32-bit integer pixel format using host-byte order. If the opaque parameter is YES, the alpha channel is ignored and the bitmap is treated as fully opaque (kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host). Otherwise, each pixel uses a premultipled ARGB format (kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host).

我们可以知道,当图片不包含 alpha 的时候使用 kCGImageAlphaNoneSkipFirst ,否则使用 kCGImageAlphaPremultipliedFirst 。另外,这里也提到了字节顺序应该使用 32 位的主机字节顺序 kCGBitmapByteOrder32Host

至于颜色分量是否为浮点数,直接逻辑或 kCGBitmapFloatComponents 就可以了。

然后是字节顺序,它是由枚举值 CGImageByteOrderInfo 来表示的:


A20B879B-76AA-46E2-B7D6-BDC0AACBB213.png

它主要提供了两个方面的字节顺序信息:
小端模式还是大端模式
数据以 16 位还是 32 位为单位。

对于 iPhone 来说,采用的是小端模式,但是为了保证应用的向后兼容性,我们可以使用系统提供的宏,来避免 Hardcoding

系统宏

根据前面的讨论,我们知道字节顺序的值应该使用的是 32 位的主机字节顺序 kCGBitmapByteOrder32Host ,这样的话不管当前设备采用的是小端模式还是大端模式,字节顺序始终与其保持一致。

好了,了解完这些相关知识后,我们再回过头来看看 CGBitmapContextCreate
函数中每个参数所代表的具体含义:

data:如果不为 NULL,那么它应该指向一块大小至少为 bytesPerRow * height字节的内存;如果 为 NULL ,那么系统就会为我们自动分配和释放所需的内存,所以一般指定 NULL、即可;
width和 height:位图的宽度和高度,分别赋值为图片的像素宽度和像素高度即可;
bitsPerComponent :像素的每个颜色分量使用的 bit 数,在 RGB 颜色空间下指定 8 即可;
bytesPerRow:位图的每一行使用的字节数,大小至少为 width * bytes per pixel 字节。有意思的是,当我们指定 0 时,系统不仅会为我们自动计算,而且还会进行 cache line alignment 的优化
space :就是我们前面提到的颜色空间,一般使用 RGB 即可;
bitmapInfo:就是我们前面提到的位图的布局信息。

具体的例子

参照YY大神的YYKit中的使用代码:

CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) {
    ...

    if (decodeForDisplay) { // decode with redraw (may lose some precision)
        CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask;

        BOOL hasAlpha = NO;
        if (alphaInfo == kCGImageAlphaPremultipliedLast ||
            alphaInfo == kCGImageAlphaPremultipliedFirst ||
            alphaInfo == kCGImageAlphaLast ||
            alphaInfo == kCGImageAlphaFirst) {
            hasAlpha = YES;
        }

        // BGRA8888 (premultiplied) or BGRX8888
        // same as UIGraphicsBeginImageContext() and -[UIView drawRect:]
        CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
        bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;

        CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, YYCGColorSpaceGetDeviceRGB(), bitmapInfo);
        if (!context) return NULL;

        CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); // decode
        CGImageRef newImage = CGBitmapContextCreateImage(context);
        CFRelease(context);

        return newImage;
    } else {
        ...
    }
}

它接受一个原始的位图参数 imageRef ,最终返回一个新的解压缩后的位图 newImage ,中间主要经过了以下三个步骤:

使用 CGBitmapContextCreate 函数创建一个位图上下文;
使用 CGContextDrawImage 函数将原始位图绘制到上下文中;
使用 CGBitmapContextCreateImage 函数创建一张新的解压缩后的位图。

小结:图片加载到界面,其实就是从磁盘加载到内存解压后再进行渲染。这里我们知道了解压这个过程在主线程并且非常消耗CPU,往往卡顿都是因为它。为了解决这个问题,我们需要在子线程强行解压图片。强行解压的方法其实就是对图片进行重新绘制。

相关文章:
谈谈 iOS 中图片的解压缩
加载和潜伏

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 绘制像素到屏幕上 answer-huang22 Mar 2014 分享文章 一个像素是如何绘制到屏幕上去的?有很多...
    阿狸旅途T恤阅读 1,616评论 0 7
  • 卷首语 欢迎来到 objc.io 的第三期! 这一期都是关于视图层的。当然视图层有很多方面,我们需要把它们缩小到几...
    评评分分阅读 1,753评论 0 18
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,395评论 25 707
  • 谁吃掉我们的CPU: 方法CA::Render::create_image_from_provider 图片预解码...
    神采飞扬_2015阅读 3,183评论 2 6
  • 冬天来了 秋姑娘说 再去那里看看 某城 某天 某一瞬间 每一个秋姑娘 都有一个被温暖的寒冬 和多个荒芜的盛夏与春耕
    蜗壳先生阅读 146评论 0 0