iOS 面试知识点(三)
Objective-C的内存管理
引用计数机制
Objective-C 主要采用引用计数机制来管理对象的内存。
运行时系统维护有一个哈希表SideTablesMap
,SideTablesMap
中存储着许多个SideTable
,每个SideTable
由1个自旋锁、1个引用计数表(RefcountMap
)和1个弱引用表(weak_table_t
)构成,引用计数表和弱引用表都是哈希表。引用计数表中存储着对象的引用计数,弱引用表中存储着指向对象的 weak 指针数组,它们的 key 都是对象的内存地址。
新建一个对象,该对象的初始引用计数值为1。
当调用对象的retain
方法时,会根据对象的内存地址到SideTablesMap
中查找该对象对应的SideTable
,再根据对象的内存地址到SideTable
的引用计数表中查找对象的引用计数,并对该引用计数执行加1操作。
调用对象的release
方法时,同样会经过两次哈希查找找出对象的引用计数,并对该引用计数执行减1操作。
当对象的引用计数为0时,会调用对象的dealloc
方法来销毁对象。dealloc
方法内部会调用_objc_rootDealloc
函数,_objc_rootDealloc
函数会调用object_dispose
函数,object_dispose
函数会先调用objc_destructInstance
函数,然后调用free
函数释放对象占用的内存空间。
objc_destructInstance
函数内部首先会判断当前对象是否包含 C++ 内容,如果包含,则销毁 C++ 内容。然后,判断当前对象的属性是否有关联对象,如果有,则从关联哈希表中移除当前对象所关联的所有对象。接着,会调用clearDeallocating
函数。clearDeallocating
函数首先会将指向当前对象的所有 weak 指针指向nil
,并从弱引用表中移除当前对象的所有 weak 指针,然后再从引用计数表中移除当前对象的引用计数。
Tagged Pointer
NSNumber
、NSDate
这类小对象是使用 Tagged Pointer 来管理内存的。
NSNumber
对象包含一个long
类型的成员变量和一个void *
类型的isa
指针。在32位架构下,long
类型和void *
类型各占 4 个字节,NSNumber
对象总共占用 8 个字节的内存空间。但是在64位架构下,long
类型和void *
类型各占 8 个字节,NSNumber
对象总共占用了 16 个字节的内存空间,其内存占用翻倍了。
而NSNumber
和NSDate
对象的值需要占用的内存通常不用 8 个字节,因为 4 个字节所能表示的整数值可以达到40亿,这已经可以满足绝大多数需求了。
为了改进NSNumber
和NSDate
在64位设备上的内存占用,苹果引入了 Tagged Pointer,它被拆成两部分,一部分直接保存数值,另一部分作为特俗标记,表示这是一个特别的指针,不指向任何地址。
如果 8 个字节可以承载NSNumber
或NSDate
的值,那么在创建NSNumber
或NSDate
对象时,就会直接生成一个 Tagged Pointer。这样,NSNumber
和NSDate
在 64 位设备上的内存占用就还是 8 个字节。同时,还大大提高了读写它们的效率。
由于 Tagged Pointer 的值不是内存地址,而是真正的值,所以NSNumber
和NSDate
对象是一个伪对象。它们不是存储在堆区,不需要手动分配和释放内存,所以不需要使用引用计数来管理内存。如果 tagged pointer 是一个局部变量,则它是存储在栈区的,函数运行结束时会被系统自动销毁;如果 tagged pointer 是一个对象的属性值,那么它会在对象被销毁时,一起被销毁。
当 Tagged Pointer 存放不下值时,就还是会以原来的方式返回一个对象指针。
NONPOINTER_ISA(非指针型的isa)
在64位架构下,isa
指针是占64个比特位(1 byte = 8 bit)的,实际上33或44个比特位(arm64为33个,x86_64为44个)就已经够用了。为了提高内存利用率,NONPOINTER_ISA 除了存储有isa
指针指向的内存地址,剩余的比特位还存储了内存管理相关的数据内容,第1
个比特位用来标识这个isa
指针是否为 NONPOINTER_ISA,第2
个比特位用来标识对象是否有关联对象,第3
个比特位标识对象是否有使用 C++ 析构函数。
arm64 架构下,第4
到第36
个比特位是对象的内存,第37
到第42
个比特位用来标识对象是真的对象还是一个没有初始化的内存空间,第43
个比特位用来标识对象是否有弱引用指针,第44
个比特位用来标识对象是否正在被释放,第45
个比特位用来标识是是否有超出的引用计数存储在引用计数表中,第46到64
的比特位是对象的引用计数。
x86_64 架构下,第4
到第47
个比特位是对象的内存,第48
到第53
个比特位用来标识对象是真的对象还是一个没有初始化的内存空间,第54
个比特位用来标识对象是否有弱引用指针,第55
个比特位用来标识对象是否正在被释放,第56
个比特位用来标识是是否有超出的引用计数存储在引用计数表中,第57到64
的比特位是对象的引用计数。
什么是MRC?什么是ARC?
MRC,手动引用计数,由开发者手动调用retain
、release
方法来管理对象的引用计数。
ARC,自动引用计数,是通过由编译器自动在代码中插入retain
、release
操作,并使用运行时系统管理弱引用指针,来实现自动管理对象的引用计数的。
什么是循环引用?
在 ARC 模式下,只有当一个对象的引用计数为0并被释放时,才会对该对象所持有的属性执行一次release
操作。当两个对象相互直接或间接持有对方作为自己的属性时,这两个对象会一直等待对方先被释放后,其引用计数才能为0并被释放。这样就导致两个对象永远无法被释放,从而产生循环引用。
AutoreleasePool的实现原理
@autoreleasepool {
...
}
// 以上代码被转化为
void * atautoreleasepoolobj = objc_autoreleasePoolPush();
...
objc_autoreleasePoolPop(atautoreleasepoolobj);
// 栈节点的数据结构
class AutoreleasePoolPage {
id *next; // 栈中下一个可填充的位置
AutoreleasePoolPage * const parent; // 父节点
AutoreleasePoolPage *child; // 子节点
pthread_t const thread; // 所在线程
... // 还有其他参数没有列出
};
自动释放池是一个以AutoreleasePoolPage
对象为节点的双向链表,AutoreleasePoolPage
对象是一个先进后出的栈。
在线程进入 runloop 时,会调用objc_autoreleasePoolPush()
函数。该函数会获取当前线程绑定的AutoreleasePoolPage
对象,如果获取到的AutoreleasePoolPage
对象为nil
,则会创建一个AutoreleasePoolPage
对象,并将哨兵对象nil
添加到AutoreleasePoolPage
中,然后将新建的AutoreleasePoolPage
与当前线程绑定起来;如果AutoreleasePoolPage
对象不为nil
并且还有可用空间,则直接将哨兵对象nil
添加到AutoreleasePoolPage
中;如果AutoreleasePoolPage
对象不为nil
,但是却没有可用空间了,则会新建一个AutoreleasePoolPage
节点,并将哨兵对象nil
添加到这个AutoreleasePoolPage
中,然后将新建的AutoreleasePoolPage
与当前线程绑定起来。
当调用对象的autorelease
方法时,会获取与当前线程绑定的AutoreleasePoolPage
。如果AutoreleasePoolPage
还有可用空间,则直接将该对象添加到这个AutoreleasePoolPage
中;如果AutoreleasePoolPage
没有可用空间了,则新建一个AutoreleasePoolPage
节点,并将这个对象添加到新建的AutoreleasePoolPage
中,然后将新建的AutoreleasePoolPage
与当前线程绑定起来。
在 runloop 进入休眠状态前,会调用objc_autoreleasePoolPop()
函数。该函数会获取当前线程绑定的AutoreleasePoolPage
,然后以先进后出的顺序移除AutoreleasePoolPage
中的对象,并对对象执行一次release
操作。当这个AutoreleasePoolPage
被清空后,会继续移除上一个AutoreleasePoolPage
中的对象,并对对象执行一次release
操作。当遇到哨兵对象nil
时,会移除nil
对象并终止移除操作。 最后,销毁所有已经清空的AutoreleasePoolPage
。
接着,又会重新调用objc_autoreleasePoolPush()
函数。然后,runloop 进入休眠状态。
weak的实现原理
运行时系统维护着一个SideTablesMap
哈希表,其 key 是对象的内存地址,value 是对象对应的SideTable
。SideTable
中包含一个自旋锁,一个引用计数表RefcountMap
和一个弱引用表weak_table_t
。weak_table_t
是一个哈希表,其 key 是对象的内存地址,value 是指向该对象的弱引用指针数组weak_entry_t
。
对 weak 变量的赋值操作会被转换为objc_initWeak()
函数的调用,并传递弱引用指针的地址和值对象作为该函数的参数。
NSObject *obj = [[NSObject alloc] init];
__weak id weakObj = obj;
// `__weak id weakObj = obj;`代码会被编译器转换为
__weak id weakObj;
objc_initWeak(&weakObj, obj);
objc_initWeak()
函数内部会调用storeWeak()
函数,在storeWeak()
函数内部实现中,如果弱引用指针已经有指向一个旧值对象,则会获取该旧值对象,并在SideTablesMap
中查找旧值对象对应的SideTable
,并调用weak_unregister_no_lock
函数,该函数会在旧值对象对应的SideTable
的弱引用表weak_table_t
中查找旧值对象对应的weak_entry_t
数组,并从weak_entry_t
中移除该弱引用指针。
如果弱引用指针将要指向的新值对象不为nil
,则会在SideTablesMap
中查找新值对象对应的SideTable
,并调用weak_register_no_lock
函数,该函数会在新值对象对应的SideTable
的弱引用表weak_table_t
中查找新值对象对应的weak_entry_t
数组,并将该弱引用指针添加到weak_entry_t
中。最后,将弱引用指针指向新值对象;如果新值对象为nil
,则会直接将弱引用指针指向nil
。
当调用dealloc
方法释放值对象时,会调用sidetable_clearDeallocating()
函数,该函数会在SideTablesMap
中查找值对象对应的SideTable
。接着,调用weak_clear_no_lock()
函数,该函数会在SideTable
的weak_table_t
中查找对应的weak_entry_t
数组,然后遍历该数组,将所有弱引用指针指向nil
。最后,调用weak_entry_remove()
函数从weak_table_t
中删除该值对象的weak_entry_t
。
__strong的实现原理
在 ARC 模式下,在某个对象持有的 block 中访问该对象时,会产生循环引用。我们通常在 block 中使用__weak
指针来打破循环引用,__weak
指针不会使其所指对象的引用计数加1。但是,在异步执行 block 的过程中,如果__weak
指针所指对象在此时释放了,会引发异常。为了解决这个问题,我们可以在 block 开始执行时,在 block 中使用__strong
指针来强引用一次__weak
指针所指对象,使其引用计数加1,这样对象就不会在 block 执行过程中释放了。同时,由于__strong
指针在其作用域被释放时,也会一起被自动释放,从而使该对象的引用计数减1,也就不会产生循环引用了。
属性和成员变量之间有什么联系?
编译器会自动为属性生成对应的成员变量,并生成 set 和 get 方法来读写成员变量的值。使用.
语法访问属性时,实际上访问的是与其对应的成员变量。
如何给分类添加“成员变量”?
分类中添加的属性,编译器是不会为这些属性生成对应的成员变量的,可以通过关联对象来让分类的属性具有与类的属性相同的效果。
static NSString *nameKey = @"nameKey";
- (void)setName:(NSString *)name
{
objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY);
}
- (NSString *)name
{
return objc_getAssociatedObject(self, &nameKey);
}
关联对象的实现原理
运行时系统使用AssociationsManager
对象(非唯一)维护着一个唯一的AssociationsHashMap
,这个哈希表的 key 是属性所属对象的内存地址,value 是一个ObjectAssociationMap
,其 key 是属性标识符,value 是一个ObjcAssociation
对象,ObjcAssociation
对象中封装着关联对象和属性关联策略。
调用objc_setAssociatedObject
函数时,会首先根据传递的关联对象和属性关联策略创建一个ObjcAssociation
对象。
如果关联对象不为nil
,运行时系统会根据属性所属对象的内存地址到AssociationsHashMap
中查找是否存在与属性所属对象对应的ObjectAssociationMap
。如果不存在,则会创建一个ObjectAssociationMap
,并将这个ObjectAssociationMap
存到AssociationsHashMap
中。然后以属性标识符为 key,将ObjcAssociation
对象存到ObjectAssociationMap
中
如果关联对象为nil
,运行时系统会根据属性所属对象的内存地址到AssociationsHashMap
中读取对应的ObjectAssociationMap
。然后根据属性标识符到这个ObjectAssociationMap
中查找对应的ObjcAssociation
对象,如果有,则会从ObjectAssociationMap
中移除这个ObjcAssociation
对象。
扩展(Extension)
用处
- 声明私有属性;
- 声明私有方法;
- 声明私有成员变量。
特点
- 编译时决议
- 只是以声明的形式存在,多数情况下寄生于宿主类的 .m 文件中;
- 不能为系统类添加扩展。
与分类的区别
分类是在运行时决议,可以有自己的 .h 和 .m 文件,能为系统类添加分类。
AFNetworking源码分析
-
AFNetworkReachabilityManager
是一个独立的类,用于监听域名,以及 WWAN 和 WiFi 网络接口地址的可访问性。 -
AFSecurityPolicy
使用证书和公钥,通过安全连接来验证服务器是否可信。 -
AFHTTPRequestSerializer
封装了为 HTTP 请求设置请求头(HTTP Header),并将传递给服务端的参数编码为查询字符串(Query String)或者请求体(HTTP Body)的逻辑。 -
AFHTTPResponseSerializer
用于验证服务器的响应是否有效,如果响应无效的话,其会返回相应的错误信息。 -
AFJSONResponseSerializer
是AFHTTPResponseSerializer
的子类,其在验证服务器的响应是否有效后,会将服务器返回的 JSON 数据反序列化为 Objective-C 对象。 -
AFURLSessionManager
是 AFNetworking 框架的核心类,它创建并管理一个NSURLSession
对象。 -
AFHTTPSessionManager
是AFURLSessionManager
的子类,封装了用于发送 HTTP 请求的便捷方法。
创建并初始化一个AFHTTPSessionManager
对象,AFHTTPSessionManager
对象会创建并持有一个NSURLSession
对象、一个AFSecurityPolicy
对象、一个AFNetworkReachabilityManager
对象、一个AFHTTPRequestSerializer
对象和一个AFJSONResponseSerializer
对象。
调用AFHTTPSessionManager
对象的相关方法发送一个 HTTP 请求时,其持有的AFHTTPRequestSerializer
对象会根据给定的url创建一个NSMutableRequest
对象,并为NSMutableRequest
对象设置请求头。如果该请求是一个 GET 请求,则AFHTTPRequestSerializer
对象还会将需要传递给服务端的参数编码为查询字符串,并将此查询字符串拼接到NSMutableRequest
对象的 url 后面;如果该请求是一个 POST 请求,AFHTTPRequestSerializer
对象还会将需要传递的参数编码为二进制数据,并将该二进制数据设置为NSMutableRequest
对象的请求体(HTTP Body)。
接着,AFHTTPSessionManager
对象持有的NSURLSession
对象会根据前面的NSMutableRequest
对象创建一个NSURLSessionDataTask
对象来向服务器发送 HTTP 请求。
在启用NSURLSessionDataTask
对象发出 HTTP 请求之前,AFHTTPSessionManager
对象会创建一个AFURLSessionManagerTaskDelegate
对象来保存外部传递的在请求完成时执行的 block,AFURLSessionManagerTaskDelegate
对象还会弱引用AFHTTPSessionManager
对象。
AFHTTPSessionManager
对象将创建的AFURLSessionManagerTaskDelegate
对象保存到一个字典中,并以与其对应的NSURLSessionDataTask
对象的taskIdentifier
作为 key。
如果这是一个 HTTPS 请求,AFHTTPSessionManager
对象在收到 SSL 服务器信任质询代理回调时,会使用其持有的AFSecurityPolicy
对象来验证证书和公钥。如果验证失败,则断开连接。
AFHTTPSessionManager
对象是其创建并管理的NSURLSession
对象的delegate
,启用NSURLSessionDataTask
对象发送 HTTP 请求之后,AFHTTPSessionManager
对象会收到相关代理回调。
AFHTTPSessionManager
对象接收到服务器返回二进制数据的代理回调后,会将数据传递给对应的AFURLSessionManagerTaskDelegate
对象保存。
AFHTTPSessionManager
对象接收到请求完成代理回调后,会从字典中取出对应的AFURLSessionManagerTaskDelegate
对象来处理其保存的从服务器返回的二进制数据。
AFURLSessionManagerTaskDelegate
对象在处理服务器返回的二进制数据时,会使用AFJSONResponseSerializer
对象来验证服务器的响应是否有效,并将服务器返回的二进制数据反序列化为数组、字典或者字符串对象,并在指定的调度队列中调用其保存的 block 来传递该结果对象。
AFNetworking的图片缓存是如何处理的
AFNetworking使用NSURLCache
来自动管理图片的硬盘缓存。当客户端从服务器下载图片时,NSURLCache
会自动将图片缓存到客户端的硬盘中。当客户端再次请求从服务器下载同一张图片时,会直接返回保存在硬盘中的图片。
AFNetworking使用自定义的AFAutoPurgingImageCache
来管理图片的内存缓存。
AFAutoPurgingImageCache
使用并行调度队列和 GCD 栅栏函数来实现数据的多读单写。
AFAutoPurgingImageCache
有一个最大缓存容量,当图片缓存超过最大容量时,会删除近期未被使用的图片。其缓存淘汰算法实现原理为:
- 图片下载完成并被解码显示后,使用解码后的图片创建一个
AFCachedImage
对象。AFCachedImage
对象中保存着解码后的图片,并记录着图片的最近访问日期; - 使用一个字典来存储
AFCachedImage
对象,以 URL 作为 Key。每次从字典中取出AFCachedImage
对象使用时,会更新图片的访问日期; - 定义了一个常量属性来记录当前图片缓存大小;
- 每次往字典中添加新的
AFCachedImage
对象之后,会更新记录的当前图片缓存大小。如果当前图片缓存大小超过了最大缓存容量,则根据字典中的AFCachedImage
对象创建一个数组,并根据AFCachedImage
对象中记录的最近访问日期对数组中的AFCachedImage
对象排序。然后遍历排序后的数组,从字典中删除对应的AFCachedImage
对象,直到当前图片缓存大小达到设定的缓存容量。
如何对AFNetworking进行再次封装的?
- 定义一个网络请求类,由该类来负责创建并管理一个
AFHTTPSessionManager
对象。 - 由于在应用程序运行期间,需要多次发送网络请求,所以需要为该类实现一个单例构造方法。
- 在网络请求类的初始化方法中,创建一个
AFHTTPSessionManager
对象,并初始化AFHTTPSessionManager
对象的某些属性,例如responseSerializer
(JSON 或者 XML 解析,默认 JSON 解析),securityPolicy
(是否使用证书验证服务器的可信度,默认不使用),requestSerializer
(可设置超时时长)。 - 封装 GET,POST,Upload,Download 请求的便捷方法。
SDWebImage源码分析
从远程下载图片
-
SDWebImageDownloader
对象创建并管理着一个NSURLSession
对象和一个downloadOperationQueue
,并且SDWebImageDownloader
对象是NSURLSession
对象的代理。 -
SDWebImageDownloaderOperation
对象是一个自定义的并发操作对象,其是用于从本地发送HTTP请求到远程下载图片数据的,它还封装了用于响应NSURLSession
对象HTTP请求相关代理方法的逻辑。
当需要从给定的URL下载图片数据时,SDWebImageDownloader
对象会创建一个相应的SDWebImageDownloaderOperation
对象。
SDWebImageDownloaderOperation
对象会弱引用SDWebImageDownloader
对象创建的NSURLSession
对象,还会保存外部传递的图片下载完成后执行的block回调。
SDWebImageDownloaderOperation
对象会被添加到一个并行操作队列中,当并行操作队列调度SDWebImageDownloaderOperation
对象到线程上执行时,SDWebImageDownloaderOperation
对象会使用弱引用的NSURLSession
对象来发送HTTP请求。
当SDWebImageDownloader
对象收到NSURLSession
对象的服务器已返回数据代理回调时,会将每次接收的图片数据传递给对应的SDWebImageDownloaderOperation
对象保存。
当SDWebImageDownloader
对象收到NSURLSession
对象的请求完成代理回调时,会从并行操作队列中取出对应的未完成的SDWebImageDownloaderOperation
对象来处理已下载的图片数据。
SDWebImageDownloaderOperation
对象会根据已接收的图片数据创建一个UIImage
并且纠正该UIImage
的方向。接着,其使用SDWebImageDecoder
中实现的方法来解压缩UIImage
。最后,SDWebImageDownloaderOperation
对象将其自身的isFinished
属性标记为YES
。
图片解码(解压缩)
SDWebImageDecoder
是UIImage
的一个分类,封装了实现解压缩普通图片和高分辨率大图片的方法。
其解压缩图片的原理是:将图片绘制到一个位图图形上下文中,绘图系统在绘制该图片时,会解压缩图片。接着,使用这个位图图形上下文来生成一张位图,并根据这个位图创建一个UIImage
,然后返回这个UIImage
。
图片缓存
SDImageCache
用于管理图片缓存,图片缓存由内存缓存和硬盘缓存组成。
当图片被使用一次后,将其缓存到内存中后,在应用程序运行期间,下一次再使用时,就不用再从硬盘中去读取图片数据了,提高了程序运行效率。
当从远程下载完图片后,将其保存到硬盘缓存中后,下次再使用该图片时,可以直接从硬盘中读取,而不用再从远程下载,节省了客户端数据流量。
SDImageCache
使用NSCache
来管理图片的内存缓存,当应用程序的可用内存不足时,NSCache
会自动清理数据。
SDImageCache
使用NSFileManager
来管理图片的硬盘缓存,当应用程序将要终止运行或者进入到后台时,会读取图片缓存目录下所有文件的地址、最后修改日期和文件大小信息,并遍历所有文件信息,计算出总的缓存文件大小。在遍历过程中,如果文件的最后修改日期已经超过了硬盘缓存有效期(默认为一周),则会删除该文件。接着,如果总的缓存文件大小超过了最大硬盘缓存大小(默认为不设限),则会将文件信息按照其最后修改日期距离此时最远到最近的顺序排列,并依次删除文件,直到剩余的总的文件缓存大小小于设置的最大硬盘缓存大小。
图片加载逻辑
SDWebImageManager
对象创建并管理着SDImageCache
和SDWebImageDownloader
对象。
调用UIImageView
的sd_setImageWithURL:
方法设置图片时,SDWebImageManager
对象加载图片的逻辑为:
- 首先使用
SDImageCache
对象根据 url 在内存缓存中查找是否存在图片。如果存在,则返回该图片去显示; - 如果内存缓存中不存在图片,则在子线程异步在硬盘缓存中查找是否存在图片。如果存在,则将图片缓存到内存中并返回该图片去显示;
- 如果缓存中不存在图片,或者缓存中存在图片但是需要更新缓存,则会使用
SDWebImageDownloader
对象从远程下载图片; - 图片下载完成后,会在子线程异步解码图片;
- 解码完成后,先将图片写入到内存缓存中,然后在将图片异步写入到硬盘缓存中。最后,返回该图片去显示。
SDWebImage 是如何解决 Cell 重用时图片加载错乱问题的?
SDWebImage 会为UIImageView
创建一个字典来保存加载图片的 operation ,在使用 SDWebImage 加载图片时,会从字典中取出还未完成的 operation ,并取消这个 operation。
为什么要对图片进行解码(解压缩)?
计算机屏幕在显示图像时,是以单个像素点来代表图像中的某个点,对一组像素点进行排列和染色就能构成图像了。
由像素点组成的图像,叫做位图,又称为点阵图。
由于 PNG、JPEG 图片格式是压缩之后的位图图像格式,所以在将 PNG、JPEG 图片显示到屏幕上时,必须先对 PNG、JPEG 格式的图像数据进行解压缩,从而得到图像的原始像素点数据。
SDWebImage的图片解码(解压缩)是如何做的?
从远程下载或者本地加载的图片一般都是 PNG 或 JPEG 格式,使用它们所创建的UIImage
对象的图片数据是还没有经过解压缩的。
使用UIImageView
显示UIImage
时,系统会在主线程对UIImage
的图片数据进行解压缩。而图片的解压缩操作是比较耗时的,如果同时有多个UIImage
需要显示,那么就会在主线程多次执行图片的解压缩操作,这样就会导致主线程的执行效率降低,使程序变得卡顿。
SDWebImage 为了提高应用程序运行效率,会在图片数据下载完成之后,在子线程强制解压缩图片数据。其强制解压缩图片数据的原理是:将未解码的图片绘制到一个位图图形上下文中,绘图系统在绘制该图片时,会解压缩图片。接着,使用这个位图图形上下文来生成一张位图,并根据这个位图创建一个新的UIImage
,然后返回这个UIImage
。
使用UIImageView
显示由 SDWebImage 返回的UIImage
时,就不需要再执行解压缩图片数据的操作了。
由于UIImage
的图片数据已经被解压缩,因此会导致UIImage
所占用的内存增大,这是一种以空间换时间的做法。
SDWebImage对高分辨大图片的解码(解压缩)是如何处理的?
PNG、JPEG 格式是压缩后的位图图像格式,解压缩 PNG、JPEG 图片获取图像的原始像素点数据时,内存消耗会暴涨。而 iOS 系统为每个应用程序分配的内存是有限的,当解压缩高分辨大图片时,极有可能出现内存溢出,从而导致应用程序崩溃。
SDWebImage 解码高分辨率大图片时使用的策略是将大图片切割成一个又一个的小图块,并将这些小图块依次压缩绘制到同一个位图图形上下文中。绘制小图块到位图图形上下文中时,绘图系统会解压缩小图块。相比一次就解压缩完整的图片数据,多次解压缩图片的部分数据会大大降低内存消耗的峰值。当所有的小图块都压缩绘制到同一个位图图形上下文中后,使用这个位图图形上下文生成一个位图,然后根据这个位图创建一个解码后的UIImage
。由于是压缩绘制,所以解码后的图片的分辨率会降低。这是一种以时间换空间的做法。
将大图片切割成小图块时,由于 iOS 系统检索图像数据的方式是一行一行的检索像素点数据,并且,在将小图块绘制到位图图形上下文时,即使压缩后的小图块的宽度小于位图图形上下文的设定的宽度,iOS 系统也必须以位图图形上下文中设定的宽度来解码图像,因此将大图片横向切割成多个小图块,保证小图块的宽度和大图片的宽度一致,可以大大提高程序执行效率。为了避免小图块合成完整图像后,图像出现细线,在横向切割大图片的时候,还要为小图块在其上方多切割1~2
行像素点来覆盖上一个图块。
计算压缩之后的图片分辨率:
单个像素点所占字节(byte)数为:kBytesPerPixel = 4 Byte
1 MB = 1024 KB = 1024 * 1024 Byte = 1024 * 1024 * 8 Bit
图片的原始数据总大小为:width(分辨率高度) * height(分辨率宽度) * 4 Byte
图片包含的总像素点个数:kSourceTotalPixels = 分辨率高度 * 分辨率宽度
需要将图片解码后的原始数据总大小压缩至:kDestImageSizeMB = 20 MB(根据自己的需要去设置该值)
压缩后的图片包含的像素点个数:kDestTotalPixels = (kDestImageSizeMB * 1024 * 1024)/ 4
根据以上已知数据可以计算出:
- 图片的压缩比例为:imageScale = kDestTotalPixels / kSourceTotalPixels
- 压缩后的图片分辨率宽度为:width * imageScale
- 压缩后的图片分辨率高度为:height * imageScale
有哪些压缩图片的方式?
使用 UIKit 框架提供的UIImagePNGRepresentation
函数压缩图片,这种方式不会改变图片的分辨率,但压缩是有限度的。例如,我们想将图片数据的大小压缩成原来的二分之一,但实际可能最多只能压缩到原来的三分之二。该方式压缩的大小与图片本身有关,所以每个图片的压缩结果各不相同。
使用UIImageJPEGRepresentation
函数压缩图片,这种方式会改变图片的分辨率,但可以将图片压缩到任意大小。
使用UIImage
对象的drawInRect:
方法以指定的分辨率将图片绘制到图像上下文中,然后从图像上下文中获取一张新的图片。这种方式可以缩小图片到任意大小,但会改变图片的分辨率,会大大降低图片的质量。
另外,对于高分辨率的大图片,如果使用drawInRect:
方法以指定的分辨率重新绘制的话,在绘图系统解压缩图片时,极有可能出现内存溢出(OOM),从而导致应用程序崩溃。可以创建一个位图图形上下文,然后使用CGImageCreateWithImageInRect
函数将高分率的大图片切割成一个又一个的小图块,并使用CGContextDrawImage
函数依次将这些小图块压缩绘制到位图图形上下文中。所有小图块绘制完毕后,再使用CGBitmapContextCreateImage
函数根据该位图图形上下文创建一个位图,然后根据位图创建一个已经被压缩和解码的UIImage
。最后,调用UIImageJPEGRepresentation
函数将这个UIImage
压缩成 JPEG 格式的UIImage
。
压缩高分率大图片的另外一种方式是使用更底层的 ImageIO 接口,避免将图片解码成位图,具体代码可以参看这篇Blog。
如何在不改变图片分辨率的情况下显示一张超清大图片?
JPEG/PNG 图像格式是压缩后的位图图像格式,在加载 JPEG/PNG 格式的图片时,需要先将图片解码成位图,图片的分辨率越高,解码操作的内存消耗就越大。而 iOS 系统分配给每个应用程序的内存是有限的,所以对高分辨率大图片的解码操作极有可能导致系统强制杀死应用程序。
苹果官方提供了一个CATiledLayer
类来解决加载大图片引起的性能问题,CATiledLayer
会将大图片切割成一个又一个的小图块,并在主线程之外的其他线程异步绘制这些图块。原本需要一次性在主线程解码的高分辨率大图片,现在被分为多次异步解码低分辨率的小图块,当每个小图块被渲染到屏幕后,就会立即释放掉,这样就能分散内存的压力。同时,由于CATiledLayer
异步绘制这些小图块,应用程序的主线程不会被阻塞。
CATiledLayer
不能直接使用,需要子类化UIView
,并重写UIView
的+(Class)layerClass
方法来将UIView
的图层设置为CATiledLayer
,还要重写UIView
的drawRect:
方法来将图片绘制到UIView
关联的图形上下文中。
[UIImage imageNamed:@"name"]方法和[UIImage imageWithContentsOfFile:file]方法有什么区别?
使用imageNamed:
方法创建UIImage
对象时,会首先到系统缓存中查找给定名称的UIImage
对象,如果存在,则直接返回该对象。否则,该方法会从与给定名称对应的本地文件中加载图像数据,并将其缓存,然后返回结果对象。
使用imageWithContentsOfFile:
方法创建UIImage
对象时,会直接从指定的本地文件中加载图片数据,然后返回结果对象。
当图片资源需要被反复使用时,使用imageNamed:
方法能提高加载效率。例如,UIButton
的背景图片。
如果图片只是偶尔使用一次或者图片文件体积较大,则使用imageWithContentsOfFile:
方法,这样可以节省内存空间。
会导致App崩溃的场景有哪些?
- 野指针类型的 crash:对象已经被释放了,还继续给对象发送消息;
- unrecognized selector crash:给对象发送一个其不能响应的消息;
- container crash:数组越界,字典插入
nil
; - KVO crash:观察者对象被释放时,没有移除其自身来取消注册观察者。被观察对象继续发送通知时,会产生野指针类型的 crash;
- NSNotification crash:对象被释放时,没有移除通知。当下一个通知触发时,会产生野指针类型的 crash。
[NSArray array] 和 [[NSArray alloc] init] 有什么区别?
在 MRC 模式下,使用[NSArray array]
方法或者@[obj1,obj2,obj3]
方式创建数组对象时,会对数组对象发送一个autorelease
消息,在结束使用该数组对象时,就不用再手动对该数组对象执行一次release
操作了,该数组对象会在其栈帧被释放时一同被释放。而使用[NSArray new]
或者[[NSArray alloc] init]
方法创建的数组对象,在结束使用该数组对象时,需要我们手动执行一次release
操作来释放它。否则,会出现内存泄漏。
@2x图片和@3x图片的区别是什么?
相同的屏幕开发尺寸在不同 iOS 设备屏幕上对应的像素尺寸是不同的,在 iPhone 4 之前,屏幕开发尺寸中的1个点就等于1个像素,但 iPhone 4 以及之后的设备开始使用像素密度更高的 Retina 屏幕,这些设备的屏幕上的1个点可能等于2个像素,也可能等于3个像素。例如,iPhone 11 的屏幕的1个点等于2个像素,而 iPhone 11 pro 的屏幕的1个点等于3个像素。
一张分辨率为 120x120 的图片在 iPhone 11 的屏幕上以 60x60 的开发尺寸显示是没有任何问题的,但如果在 iPhone 11 pro 屏幕上以 60x60 的开发尺寸显示就会模糊,这是因为 iPhone 11 pro 屏幕上 60x60 的开发尺寸对应的像素尺寸是 180x180,120x120 的图片放到 180x180 的像素尺寸上会失真。所以在开发过程中,应该使用分辨率不同的 @2x 和 @3x 图片来适配不同的设备。
如何使用SQLite数据库?
- 导入 libsqlite3.0.tbd 框架;
- 使用
NSSearchPathForDirectoriesInDomains
函数创建一个.sqlite
类型的数据库文件; - 调用
sqlite3_open
函数打开数据库; - 使用
sqlite3_exec
函数创建一个表格,创建表格的 SQL 语句为create table if not exists t_student (id integer primary key, name text, sex text, age integer)
;-
create table
,创建表格; -
if not exists
,告知系统当这个表格不存在时才创建,可以不用写; -
t_student
,表格名称; -
()
里的语句是表格中所包含的字段,每一个字段用,
隔开; -
integer
表示整数,real
表示浮点数,text
表示字符串,blob
表示二进制数据。
-
- 使用
sqlite3_exec
函数向表格中插入数据,插入语句为insert into t_student (name, sex, age) values ('tom', 'man', 12)
;-
insert into
,插入数据;
-
- 使用
sqlite3_exec
函数更新表格数据,更新语句为update t_student set name = 'tom', sex = 'man', age = 15 where id = 6
;-
update
,更新数据; -
set
,要更新的字段; -
where
,根据条件更新,多条件用and
组合。
-
- 使用
sqlite3_exec
函数删除数据,删除语句为delete from t_student where id = 4
;-
delete from
,删除数据。
-
- 使用
sqlite3_prepare_v2
函数查询数据,查询语句为select * from t_student where name = 'tom'
;-
select * from
,查询数据; -
*
,表示查询所有字段。
-
FMDB源码分析
FMDB 有三个主要的类:
-
FMDatabase
:FMDatabase
对象维护着一个 SQLite 数据库,用来执行 SQL 语句; -
FMResultSet
:使用FMDatabase
对象执行查询操作后返回的结果集; -
FMDatabaseQueue
:FMDatabaseQueue
对象维护着一个FMDatabase
对象和一个串行队列,其使用dispatch_sync
函数向串行队列中同步添加数据库操作。
FMDB 将除查询(query
)以外的所有操作都归为更新(update
)操作:create
、update
、delete
、insert
。
LRU算法
LRU 算法根据数据的历史访问记录来淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:
- 新数据插入到链表头部;
- 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
- 当链表满的时候,将链表尾部的数据丢弃。
iOS 开发中可以使用字典和数组配合来实现 LRU 算法。