SDWebImage源码阅读(二)NSData+ImageContentType
NSData+ImageContentType 是NSData 的分类(Category)。
创建分类的步骤:
在Xcode 工程页面,按command + N ,在iOS -> Source 选择Objective-C File ,点击Next ,File Type 选择 Category ,file 框里输入分类名字,Calss 为要添加分类的类,可以是系统的类也可以是自定义的类,当然这里出现的主要是系统的类。
分类的描述:
无论一个类设计的如何完美,都不可避免的会遇到没有预测到的需求,那怎么扩展现有的类呢?当然,继承是个不错的选择。但是Objective-C 提供了一种特别的方式来扩展类,叫Catagory,可以动态的为已经存在的类添加新的行为。这样可以保证类在原来的基础上,较小的改动就可以增加需要的功能。使用Category 对类进行扩展时,不需要访问其源代码,也不需要创建子类,这样我们就可以扩展系统提供的类。Category使用简单的方式,实现了类的相关方法的模块化,把不同的类方法分配到不同的分类文件中。
使用Objective-C 中的分类Category,利用OC 的动态运行时分配机制,是一种编译时的手段,允许我们给一个已经存在的类添加方法来扩充它(但是通过category不能添加新的实例变量),并且我们不需要访问原有类中的代码就可以做到。
分类的功能作用:
- 即使在你不知道一个类的源码的情况下,可以在不修改原来类的基础上向这个类添加扩展方法,主要用来给系统自带的类扩展方法
- 将类的实现分散到多个不同文件或多个不同框架中。
- 创建对私有方法的前向引用。(实现对基类中私有方法(就是没在.h 文件中声明)的访问,在基类中有一个私有方法 a(),直接访问是不行的,就需要在分类中.h 文件中声明一下这个方法,然后就可以调用了)
- 向对象添加非正式协议。
- 使用分类为类添加方法,通过在.h 文件中声明一个额外的方法并且在.m 文件中实现方法即可。分类的名字(也就是括号括起来的自定义字符串)表示的是:对于声明于其他地方的某个类,在此处添加的是他的额外的方法,而不是表示这是一个新的类。
- Category 只能添加“方法”,不能增加成员变量,Category 中可以访问原来类中的成员变量,但是只能访问@protected 和 @public 形式的变量,如果想要访问本类中的私有变量,分类和子类一样,只能通过方法来访问。如果分类中声明了一个属性,那么分类只会生成这个属性的set、get方法声明,也就是不会生成set 、get 方法的实现。(如果想添加实例变量,那就通过继承创建子类来实现)
- Category可以重新定义新方法,可以重载原始类的方法,不推荐这么做,这样会覆盖掉原始类的方法。如果确实要重载,那就通过继承创建子类来实现。
类属性修饰符延伸:
Objective-C 中,类的实例化变量的范围有@private、@protected、@public。他们代表的意思和C++中相同,只是前面添加了一个@符号。下面介绍一下他们代表的范围:
- @pribate :作用范围只能在自身类
- @protected :作为范围在自身类和继承自己的子类,什么都不写的时候默认就是此属性
- @public :作用范围最大,在任何地方
分类的执行优先级:
- 在本类和分类有相同的方法时,优先调用分类的方法再调用本类的方法。
- 如果有两个分类,他们都实现了相同的方法,如何判断谁先执行?分类执行顺序可以通过TARGETS -> Build Phases -> Compile Sources 进行调节,注意执行顺序是从上到下的。(只有两个分类含有相同的方法名)
类扩展(class Extensions)
类扩展(Extension) 是Category 的一个特例,有时候也被称为匿名分类。他的作用是为一个类添加一些私有的成员变量和方法。和分类不同,类扩展即可以声明成员变量又可以声明方法。
类扩展(Extension) 只有接口文件(.h),没有实现文件(.m) 定义的方法通通在类里边实现。类扩展听上去很复杂,但其实我们早就认识他了,当我们创建一个继承自UIViewControll的子类时,在它的.m 文件里面会自动生成类扩展:
@interface ExtensionsViewController ()
@end
我们可以在里面添加私有属性和私有方法,私有方法需要在下面的@implementation 里面实现。
当然类扩展可以定义在.m 文件中,这种扩展方式中定义的变量都是私有的,也可以定义在.h 文件中,这样定义的代码就是公有的,类扩展在.m 文件中声明私有方法是非常好的方式。类扩展中添加的方法,一定要实现,不然会有编译警告。
1 #import "ExtensionsViewController.h" 2 3 @interface ExtensionsViewController () 4 5 @end 6 7 @implementation ExtensionsViewController 8 9 ...
类扩展的作用:
- 能为某个类附加额外的属性,成员变量,方法声明
- 一般的类扩展写到.m文件中
- 一般的私有属性写到类扩展
使用格式:
1 #import "ViewController.h" 2 3 @interface ViewController () 4 5 // 属性 6 // 方法 7 8 @end
分类和类扩展的相似之处是:都可以为类添加一个额外的方法。
不同之处在于:
- 分类的括号里面必须有名字:
1 // .h 2 @interface 类名(分类名字) 3 4 /*方法声明*/ 5 6 @end 7 8 // .m 9 @implementation类名(分类名字) 10 11 /*方法实现*/ 12 13 @end
- 分类只能扩充方法,不能扩展属性和成员变量(如果包含成员变量会直接报错)。
- 再说一下我们为什么不能包含类的.m 文件,因为这样会重复包含另一类的实现文件。
参考链接:http://blog.csdn.net/jiajiayouba/article/details/21104987
http://www.jianshu.com/p/18d48e7f2aad
http://www.cnblogs.com/sgdkg/p/3142647.html
http://blog.csdn.net/kindazrael/article/details/8062784
http://www.cocoachina.com/ios/20161018/17784.html
http://www.jb51.net/article/80246.htm
http://www.2cto.com/kf/201504/387630.html
http://blog.csdn.net/qq_30513483/article/details/52068424
http://www.tuicool.com/articles/AV7fiy
下面接着看NSData 的分类ImageContentType:
NSData+ImageContentType.h 文件中:
首先定义了一个SDImageFormat 枚举:
1 typedef NS_ENUM(NSInteger, SDImageFormat) { 2 SDImageFormatUndefined = -1, 3 SDImageFormatJPEG = 0, 4 SDImageFormatPNG, 5 SDImageFormatGIF, 6 SDImageFormatTIFF, 7 SDImageFormatWebP 8 };
用于表示不同的图片格式,我们平时最常见的就是 JPEG/PNG/GIF 格式的图片,TIFF 和WebP 格式的图片较为少见。
分类里面定义的方法:
1 /** 2 * Return image format 3 * 4 * @param data the input image data 5 * 6 * @return the image format as `SDImageFormat` (enum) 7 */ 8 + (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data;
用于根据图片的data 数据判断返回图片的格式。这里主要是根据不同的图片格式对应的图片的NSData 数据的文件头是不一样的作为判断依据,来获得图片的格式的。首先对“文件头”的相关内容进行学习:
文件头的定义:
文件头是位于文件开头的一段承担一定任务的数据,一般都在开头的部分。
文件头的解释:
别看这个文件头和C 语言中的头文件读起来很像,但这两个东西其实根本没有一点关系,头文件是一种包含功能函数,数据接口声明的载体文件,而文件头则是直接位于文件中的一段数据,是文件的一部分。
计算机中的文件有很多种类型,即使相同类型的文件一般还会多多少少有一些不同的地方。文件的类型有EXE(Windows 应用程序)、COM、BMP(图像)、GIF(动态图像)、WMV(音频)、APE(音频)、RMVB(视频)、FLV(视频)、SWF、TXT(文本)、CPP(Windows mobile 应用程序)、ASM 等等,当然有的文件根本就没有文件头,比如说TXT,它除了这个文本文件本身的字符外,你不会看到任何其他的数据。不同类型的文件,只要后缀拓展名不同,操作系统就可以识别这个文件,并用相应的应有程序来打开这个文件,比如JPEG 文件,它的拓展名一般式jpg,当双击这种类型的文件时,操作系统会自动选择默认打开图像的程序来打开这个文件,maxOS 下比如用预览打开。但值得注意的是,相同类型的文件也是有不同的地方的。
比如拿BMP 来举例,大家可以想象一下不同的图片之间最大的不同是什么,有人可能会说是内容,其实不是,它们最大的不同是文件头,不知道有没有在看到各种不同大小的图像的时候想过图像浏览软件是怎样识别图像的大小的,这其实就是文件头的功劳。BMP 图像数据的前32 个字节是它的文件头,对应BMP 文件的文件结构 18 - 21 字节是它的宽度数据,22 - 25 字节是它的高度数据,图像浏览软件就是根据它们获得图像的大小的。
文件头就是为了描述一个文件的一些重要的属性,它告诉了打开并处理该文件的程序这些属性,比如上面说的BMP 的文件头将长宽像素值告诉了图像浏览程序,图像浏览程序会根据文件头中的数据以及图像数据的正文把图像显示出来。
常见文件的文件头(10进制):
这里和图片相关的文件头:
- JPEG (jpg),文件头:FFD8FFE1
- PNG (png),文件头:89504E47
- GIF (gif),文件头:47494638
- TIFF tif;tiff 0x49492A00
- TIFF tif;tiff 0x4D4D002A
- RAR Archive (rar),文件头:52617221
- WebP : 524946462A73010057454250
可以看出我们只要获取图片的NSData 数据的第一个字节就能判断出除WebP 以外的所有的图片类型。
WebP 这种格式很特别,它的文件头是由12 个字节组成的,如果把这些字节通过ASCII 编码后会可获得下面的字符串:
Hex: 52 49 46 46 2A 73 01 00 57 45 42 50
ASCII: R I F F * S SOH NUL W E B P
NSData+ImageContentType.m 文件:
+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data; 方法实现:
1 + (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data { 2 if (!data) { 3 return SDImageFormatUndefined; 4 } 5 6 uint8_t c; 7 [data getBytes:&c length:1]; // 取出data中指定长度的字节存入buffer 这个提前声明的无类型指针(无类型指针即可以指向任何数据类型的指针)中 - (void)getBytes:(void *)buffer length:(NSUInteger)length;
8 switch (c) { 9 case 0xFF: 10 return SDImageFormatJPEG; 11 case 0x89: 12 return SDImageFormatPNG; 13 case 0x47: 14 return SDImageFormatGIF; 15 case 0x49: 16 case 0x4D: 17 return SDImageFormatTIFF; 18 case 0x52: 19 // R as RIFF for WEBP 20 if (data.length < 12) { 21 return SDImageFormatUndefined; 22 } 23 24 NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding]; 25 if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) { 26 return SDImageFormatWebP; 27 } 28 } 29 return SDImageFormatUndefined; 30 }
这里:
1 uint8_t c; 2 [data getBytes:&c length:1]; // 取出data 中指定长度的字节存入buffer 这个提前声明的无类型指针(无类型指针即可以指向任何数据类型的指针)中 - (void)getBytes:(void *)buffer length:(NSUInteger)length;
使用了一个void 指针,下面对void 和void 指针在C/C++ 中的运用进行一些学习:
指针概述:
指针有两个属性:指向变量(/对象)的地址和长度,但是指针只存储地址,长度则取决于指针指向的数据类型。编译器根据指针指向的类型从指针指向的地址开始向后寻址,指针指向的类型不同则寻址范围也不同。比如:int * 从指定地址开始向后寻找4个字节作为变量的存储单元,double * 从指定地址开始向后寻找8个字节作为变量的存储单元 。
void 的含义:
void即“无类型”,void * 则为“无类型指针”,即可以指向任何数据类型的指针。
void 指针是一种特别的指针,说它特别是因为它没有指定类型,或者说这个类型不能判断出指向变量(/对象)的长度
void 指针使用规范:
- void 指针可以指向任意类型的变量(/对象),亦即可用任意数据类型的指针对void 指针赋值。
1 int *pint; 2 void *pvoid; 3 pvoid = pint; // 不过不能 pint= pvoid;
如果要将pvoid赋给其他类型指针,则需要强制类型转换如:
1 pint = (int *)pvoid;
- 在ANSIC标准中,不允许对void指针进行算术运算。如:pvoid++ 或 pvoid+=1 等,而在GNU 中则允许,因为在缺省情况下,GNU 认为void * 与char * 一样。
1 sizeof(*pvoid ) == sizeof( char);
void 作用:
- 对函数返回的限定
- 对函数参数的限定
当函数不需要返回值时,必须使用void限定。例如:void func(int, int);
当函数不允许接受参数时,必须使用void限定。例如:int func(void);
由于void指针可以指向任意类型的数据,亦即可用任意数据类型的指针对void 指针赋值,因此还可以用void 指针来作为函数形参,这样函数就可以接受任意数据类型的指针作为参数。例如:
1 void * memcpy( void *dest, const void *src, size_t len ); 2 void * memset( void *buffer, int c, size_t num);
void几乎只有“注释”和限制程序的作用,因为从来没有人会定义一个void 变量。
如果指针p1 和p2 的类型相同,那么我们可以直接在p1 和p2 间互相赋值;如果p1 和p2 指向不同的数据类型,则必须使用强制类型转换运算符把赋值运算符右边的指针类型转换为左边指针的类型。例如:
1 float *p1; 2 int *p2; 3 p1 = p2;
其中p1 = p2语句会编译出错,提示“ ’=’:cannotconvertfrom’int *’to’float *’ ”,必须改为:
1 p1 = (float *)p2;
而void*则不同,任何类型的指针都可以直接赋值给它,无需进行强制类型转换:
1 void *p1; 2 int *p2; 3 p1 = p2;
但这并不意味着,void * 也可以无需强制类型转换地赋给其它类型的指针。因为“无类型”可以包容“有类型”,而“有类型”则不能包容“无类型”。
下面的语句编译会出错:
1 void *p1; 2 int *p2; 3 p2 = p1; 4 5 // 提示“ ’=’:cannotconvertfrom’void*’to’int*’ ”。
参考链接:http://blog.csdn.net/geekcome/article/details/6249151
http://blog.chinaunix.net/uid-22197900-id-359211.html
http://blog.csdn.net/maiwc/article/details/50679162
下面接着看 + (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data; 方法实现:
根据参数data 传入的图像NSData 数据截取第一个字节的数据赋值给 uint8_t c; 这个变量。(uint8_t: typedef unsigned char uint8_t;)
1 switch (c) { 2 case 0xFF: 3 return SDImageFormatJPEG; 4 case 0x89: 5 return SDImageFormatPNG; 6 case 0x47: 7 return SDImageFormatGIF; 8 case 0x49: 9 case 0x4D: 10 return SDImageFormatTIFF; 11 case 0x52: 12 // R as RIFF for WEBP 13 if (data.length < 12) { 14 return SDImageFormatUndefined; 15 } 16 17 NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding]; 18 if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) { 19 return SDImageFormatWebP; 20 } 21 }
根据截取的c 和图片的文件头很容易区分出 JPEG/PNG/GIF/TIFF 格式的图片,当第一个字节是52 时,如果data 数据长度小于12 则直接返回Undefined ,因为如果data 第一个字节是52 ,且如果是Webp 格式的图片,而Webp 格式的图片其NSData 数据的文件头的长度就是12 ,所以如果长度小于12 则第一个字节虽是52 ,但也必不是Webp 格式的图片,就直接返回Undefined 即可。
如果c 是Ox52 且data 长度大于12,则截取前长度为12 的NSData 数据,并根据NSASCIIStringEncoding ,encoding 编码为NSString ,如果前缀有 "RIFF" 后缀有 "WEBP" (RIFF/WEBP 对应前面的:WebP 格式的图片,它的文件头是由12 个字节组成的,如果把这些字节通过ASCII 编码后会可获得 "RIFF*SSOHNULWEBP" 的字符串),则当前的图片就是Webp 格式的图片。
END