ios面试题整理

iOS 面试知识点(三)

2019-08-29  本文已影响0人  渐z

Objective-C的内存管理

引用计数机制

Objective-C 主要采用引用计数机制来管理对象的内存。

运行时系统维护有一个哈希表SideTablesMapSideTablesMap中存储着许多个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

NSNumberNSDate这类小对象是使用 Tagged Pointer 来管理内存的。

NSNumber对象包含一个long类型的成员变量和一个void *类型的isa指针。在32位架构下,long类型和void *类型各占 4 个字节,NSNumber对象总共占用 8 个字节的内存空间。但是在64位架构下,long类型和void *类型各占 8 个字节,NSNumber对象总共占用了 16 个字节的内存空间,其内存占用翻倍了。

NSNumberNSDate对象的值需要占用的内存通常不用 8 个字节,因为 4 个字节所能表示的整数值可以达到40亿,这已经可以满足绝大多数需求了。

为了改进NSNumberNSDate在64位设备上的内存占用,苹果引入了 Tagged Pointer,它被拆成两部分,一部分直接保存数值,另一部分作为特俗标记,表示这是一个特别的指针,不指向任何地址。

如果 8 个字节可以承载NSNumberNSDate的值,那么在创建NSNumberNSDate对象时,就会直接生成一个 Tagged Pointer。这样,NSNumberNSDate在 64 位设备上的内存占用就还是 8 个字节。同时,还大大提高了读写它们的效率。

由于 Tagged Pointer 的值不是内存地址,而是真正的值,所以NSNumberNSDate对象是一个伪对象。它们不是存储在堆区,不需要手动分配和释放内存,所以不需要使用引用计数来管理内存。如果 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,手动引用计数,由开发者手动调用retainrelease方法来管理对象的引用计数。

ARC,自动引用计数,是通过由编译器自动在代码中插入retainrelease操作,并使用运行时系统管理弱引用指针,来实现自动管理对象的引用计数的。

什么是循环引用?

在 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 是对象对应的SideTableSideTable中包含一个自旋锁,一个引用计数表RefcountMap和一个弱引用表weak_table_tweak_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()函数,该函数会在SideTableweak_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)

用处

特点

与分类的区别

分类是在运行时决议,可以有自己的 .h 和 .m 文件,能为系统类添加分类。

AFNetworking源码分析

创建并初始化一个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有一个最大缓存容量,当图片缓存超过最大容量时,会删除近期未被使用的图片。其缓存淘汰算法实现原理为:

如何对AFNetworking进行再次封装的?

SDWebImage源码分析

从远程下载图片

当需要从给定的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

图片解码(解压缩)

SDWebImageDecoderUIImage的一个分类,封装了实现解压缩普通图片和高分辨率大图片的方法。

其解压缩图片的原理是:将图片绘制到一个位图图形上下文中,绘图系统在绘制该图片时,会解压缩图片。接着,使用这个位图图形上下文来生成一张位图,并根据这个位图创建一个UIImage,然后返回这个UIImage

图片缓存

SDImageCache用于管理图片缓存,图片缓存由内存缓存和硬盘缓存组成。

当图片被使用一次后,将其缓存到内存中后,在应用程序运行期间,下一次再使用时,就不用再从硬盘中去读取图片数据了,提高了程序运行效率。

当从远程下载完图片后,将其保存到硬盘缓存中后,下次再使用该图片时,可以直接从硬盘中读取,而不用再从远程下载,节省了客户端数据流量。

SDImageCache使用NSCache来管理图片的内存缓存,当应用程序的可用内存不足时,NSCache会自动清理数据。

SDImageCache使用NSFileManager来管理图片的硬盘缓存,当应用程序将要终止运行或者进入到后台时,会读取图片缓存目录下所有文件的地址、最后修改日期和文件大小信息,并遍历所有文件信息,计算出总的缓存文件大小。在遍历过程中,如果文件的最后修改日期已经超过了硬盘缓存有效期(默认为一周),则会删除该文件。接着,如果总的缓存文件大小超过了最大硬盘缓存大小(默认为不设限),则会将文件信息按照其最后修改日期距离此时最远到最近的顺序排列,并依次删除文件,直到剩余的总的文件缓存大小小于设置的最大硬盘缓存大小。

图片加载逻辑

SDWebImageManager对象创建并管理着SDImageCacheSDWebImageDownloader对象。

调用UIImageViewsd_setImageWithURL:方法设置图片时,SDWebImageManager对象加载图片的逻辑为:

  1. 首先使用SDImageCache对象根据 url 在内存缓存中查找是否存在图片。如果存在,则返回该图片去显示;
  2. 如果内存缓存中不存在图片,则在子线程异步在硬盘缓存中查找是否存在图片。如果存在,则将图片缓存到内存中并返回该图片去显示;
  3. 如果缓存中不存在图片,或者缓存中存在图片但是需要更新缓存,则会使用SDWebImageDownloader对象从远程下载图片;
  4. 图片下载完成后,会在子线程异步解码图片;
  5. 解码完成后,先将图片写入到内存缓存中,然后在将图片异步写入到硬盘缓存中。最后,返回该图片去显示。

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,还要重写UIViewdrawRect:方法来将图片绘制到UIView关联的图形上下文中。

[UIImage imageNamed:@"name"]方法和[UIImage imageWithContentsOfFile:file]方法有什么区别?

使用imageNamed:方法创建UIImage对象时,会首先到系统缓存中查找给定名称的UIImage对象,如果存在,则直接返回该对象。否则,该方法会从与给定名称对应的本地文件中加载图像数据,并将其缓存,然后返回结果对象。

使用imageWithContentsOfFile:方法创建UIImage对象时,会直接从指定的本地文件中加载图片数据,然后返回结果对象。

当图片资源需要被反复使用时,使用imageNamed:方法能提高加载效率。例如,UIButton的背景图片。

如果图片只是偶尔使用一次或者图片文件体积较大,则使用imageWithContentsOfFile:方法,这样可以节省内存空间。

会导致App崩溃的场景有哪些?

[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数据库?

FMDB源码分析

FMDB 有三个主要的类:

FMDB 将除查询(query)以外的所有操作都归为更新(update)操作:createupdatedeleteinsert

LRU算法

LRU 算法根据数据的历史访问记录来淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:

  1. 新数据插入到链表头部;
  2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
  3. 当链表满的时候,将链表尾部的数据丢弃。

iOS 开发中可以使用字典和数组配合来实现 LRU 算法。

上一篇下一篇

猜你喜欢

热点阅读