通过 URL 获取图片宽高优化

一张小图.png

前言

客户端研发时,有时会有这样的需求,需要根据图片链接地址获取图片的宽高来进行界面排版。

一般比较正规的做法,是服务端在返回数据时将图片的信息属性一起带回来,这也符合轻客户端设计规范。但是现实不是理想,有时就是会出现服务端没有返回,你却要知道图片宽高,所以本文,针对通过 URL 来获取图片宽高进行简单的介绍。

传统获取图片宽高方案:

最为常见也是最慢的一种方案,通过 URL 下载图片,得到图片数据后获取图片宽高。

这种方式 iOS 下有很多实现方案,可以使用三方工具进行图片下载,也可以直接自己写,该方法思想就是通过下载整个图片然后得到完整的图片数据信息,解析数据,再从中得到图片的宽高。 比如本文使用 CGImageSource 来通过 URL 图片信息,从而得到图片的宽高。代码如下:

// 获取图像属性
CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSourceRef, 0, NULL);

返回的 imageProperties 就已经从整个图片数据中获取到需要的信息了,我们只需要从中通过 KEY 来取出需要的值。

// 获取宽高
CFNumberRef widthNumberRef = CFDictionaryGetValue(imageProperties, kCGImagePropertyPixelWidth);
CFNumberRef heightNumberRef = CFDictionaryGetValue(imageProperties, kCGImagePropertyPixelHeight);

改进方案:

在传统方案里,获取图片的宽高的时候,下载了整个图片数据,但这里我们的需求只是获取图片的宽高信息,此时还不需要全部的图片信息,下载完整的图片数据,导致需要获取的数据量更大,从而增加的宽高获取到的时间,减少了我们的流畅性。

如果我们通过 URL 只下载我们需要的图片中的某些信息的话(比如宽高信息),就能减少此次请求的传输的数据量,从而加快信息的获取。那如何实现呢?

在图片数据中,不管什么格式,在表示该图片的数据中都有一段数据块表示着这个图片的描述信息,其中就包含着该图片的宽高大小。所以知道这点,我们只要通过 URL 请求获取该部分段信息,就可以从中解析出我们需要的图片宽高。

让我们先看下 PNG 图片的数据格式的大概模样,如下图:

PNG数据头数据图

图中我们只需要关注 PNG Signature 与标红的 WIDTH HEIGHT 段,PNG Signature标志着该图片是一张 PNG 图,知道它是 PNG 图数据后,如果图片数据的开头与这张图中一致,则代表该图片是一张 PNG 图。

在数据的固定位置处,即 WIDTH HEIGHT 所在的字节位置里存放的就是该图片的宽高信息,所以我们只需要从该处取出所存数据就知道图片宽高了。

同理针对其他格式的图片也是一样的,只是他们中数据的段格式以及位置有些不同,但都存在着这样一个数据段表示着图片的描述信息。(这里并不对所有的图片格式进行介绍,这里了解资料)

下面是 GIF 图的文件头格式图:

GIF头格式

代码实现:

  1. 方式1:通过设置 HTTP 请求头 Range 字段来获取数据的某位置段数据;

    比如,此时有一张 PNG 图的链接地址,想要知道其宽高,代码如下:

     NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
     [request setValue:@"bytes=16-23" forHTTPHeaderField:@"Range"];
     NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:nil
                                                      error:nil];
     CGSize size = CGSizeZero;
     if (data.length >= 8) {
         int w = 0, h = 0;
         [data getBytes:&w range:NSMakeRange(0, 4)];
         [data getBytes:&h range:NSMakeRange(4, 4)];
         w = CFSwapInt32BigToHost(w);
         h = CFSwapInt32BigToHost(h);
         size = CGSizeMake(w, h);
     }
    

    16-23字节位置处,前4字节代表着宽,后4字节代表着高,由此我们就完成了图片的宽高获取,相对于传统方式,不管图片真实大小多大,我们只下载了仅仅 8 字节的数据,无疑加快了速度和节省了流量,其他格式图代码可见文件

  2. 方式2:直接下载,在网络回调中解析数据,得到足够数据后,解析出宽高,提前停止请求。
    在第一种方式中,虽然速度很快但存在一个问题,下载前必须先知道图片宽高数据存储位置,对于 PNG 和 GIF 图片来说是没有问题,但在 JPG 格式图时,由于其数据段并不是在文件的头部,也不再固定的位置,可能在中间的任何一段地方,所以通过提前指定请求头 的 Range 范围是无法有效获取到信息的,此时我们只能通过一边下载图片数据,一边在解析得到的数据,如果检测到了图片的描述信息段,则开始解析,解析成功后提前结束网络请求,这样在速度和流量方面相对于传统的依然是有一定的提升。下图为 JPG 图数据格式:

    image

    其中 FFCO段为描述段信息开头,我们在代码中通过While 来在一个个数据段中寻找该描述段,找到了它就找到了宽高。
    代码如下:


    - (CGSize)fetchHWFromJPGData:(NSData *)data {
    CGSize size = CGSizeZero;
    // FF D8 FF E0 (XX XX 这两字节为长度) ('JF' 'TF' 转为ascll码值)
    UInt8 word0 = 0x0, word1 = 0x0, word2 = 0x0, word3 = 0x0;
    [data getBytes:&word0 range:NSMakeRange(0, 1)];
    [data getBytes:&word1 range:NSMakeRange(1, 1)];
    [data getBytes:&word2 range:NSMakeRange(2, 1)];
    [data getBytes:&word3 range:NSMakeRange(3, 1)];
    if (word0 == 0xFF && word1 == 0xD8 && word2 == 0xFF && word3 == 0xE0) {
        UInt8 c0 = 0, c1 = 0, c2 = 0, c3 = 0;
        [data getBytes:&c0 range:NSMakeRange(6, 1)];
        [data getBytes:&c1 range:NSMakeRange(7, 1)];
        [data getBytes:&c2 range:NSMakeRange(8, 1)];
        [data getBytes:&c3 range:NSMakeRange(9, 1)];
        if (c0 == 'J' && c1 == 'F' && c2 == 'I' && c3 == 'F') {
            UInt16 block_length = 0;
            [data getBytes:&block_length range:NSMakeRange(4, 2)];
            block_length = XCSSwapWebIntToInt16(block_length);
            int i = 4;
            do {
                i += block_length;
                if (i > data.length) {
                    break;
                }
                UInt8 aW = 0x0;
                [data getBytes:&aW range:NSMakeRange(i, 1)];
                if (aW != 0xFF) {
                    break;
                }
                
                UInt8 ca = 0x0;
                [data getBytes:&ca range:NSMakeRange(i+1, 1)];
                if (ca >= 0xC0 && ca<= 0xC3) {
                    /**
                     图片信息段 FF CO (xx xx 该段长度) XX(抛弃字节,不用) (HH HH 高度) (WW WW 宽度) ...后面是其他信息。
                     这里宽高段和 png gif 等都不一样,jpg 是高在前
                     */
                    UInt16 w = 0, h = 0;
                    [data getBytes:&h range:NSMakeRange(i + 5, 2)];
                    [data getBytes:&w range:NSMakeRange(i + 7, 2)];
                    w = XCSSwapWebIntToInt16(w);
                    h = XCSSwapWebIntToInt16(h);
                    size = CGSizeMake(w, h);
                    break;
                }else {
                    i += 2;
                    [data getBytes:&block_length range:NSMakeRange(i, 2)];
                    block_length = XCSSwapWebIntToInt16(block_length);
                }
            } while (i < data.length);
        }
        
    }
    return size;
}

更多详情代码,可见DEMO

最后结果代码

XCSImagePrefetcher 通过传入 URL 来获取图片的宽高。

@interface XCSImagePrefetcher : NSObject

@property (nonatomic, strong, readonly) NSMutableData *downloadData;
@property (nonatomic, strong, readonly) NSURL *imageUrl;

- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithUrl:(NSURL *)url NS_DESIGNATED_INITIALIZER;
- (CGSize)fetchImageSize;

@end

使用方式如下:


    XCSImagePrefetcher *fetcher = [[XCSImagePrefetcher alloc] initWithUrl:[NSURL URLWithString:JPG_IMG_URL]];
    NSLog(@"%@", NSStringFromCGSize([fetcher fetchImageSize]));

更多测试代码,可见DEMO

总结

  1. 在数据的提取过程中,需要注意大小端问题导致的数据解析出来不对(相关知识);
  2. 即使通过这种方式进行优化,获取图片大小问题仍然因为需要发送网络请求而变的速度不够稳定,所以真正的解决方案,还是需要服务端配合添加上图片数据宽高的记录;
  3. 实际应用中需要和缓存配合来达到最佳效果。

相关资料

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

推荐阅读更多精彩内容

  • 第一部分 HTML&CSS整理答案 1. 什么是HTML5? 答:HTML5是最新的HTML标准。 注意:讲述HT...
    kismetajun阅读 27,400评论 1 45
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,082评论 1 32
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,350评论 0 17
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • 写在最后 普拉克西特利斯是公元前4世纪的雕塑家,传说芙丽涅是他的情人,若说芙丽涅除了美貌之外还有什么才能,那大概就...
    艺萃阅读 2,131评论 0 6