戳我
戳我
文章目录
  1. NSString如何计算字符的个数
  2. NSUserDefaults性能优化
  3. 数据库为什么要创建索引
  4. Full Text Search为什么快
  5. imageNamed:与imageWithContentsOfFile:对比
  6. Image的加载以及内存计算
  7. ==, isEqual与hash
  8. NSDictionary 内部结构、实现原理

iOS开发总结系列-语言设计

这里主要是一些语言设计方面的常见问题.

NSString如何计算字符的个数

首先我们要知道字符串末尾会自动加上一个\0, 因此我们可以设计这么一个函数:

int string_length(char *s) {
    int c = 0;
    while (*s[c] != `\0`){
        c++:
    }
    return c;
}

通过指针偏移量来获取每个元素, 同\0比较, 相等时的指针偏移量就是字符串元素的个数.

NSUserDefaults性能优化

当使用NSUserDefaults写入文件后, 值会被存放到Library/Preferences/com.xxx.AppName.plist文件里(com.xxx.AppName是应用的标识符). 这个文件是个plist文件, 因此, 它可以存放plist可以存放的数据类型: NSData, NSString, NSNumber, NSDate, NSArray, 或者NSDictionary.
它是针对每个应用适用的, 只作用于自身应用内. sandbox的安全机制保证, 它不能修改其他应用的值, 也不会被其他应用修改.

NSUserDefaults是个单例, 而且它访问的文件也只有一个, NSUserDefaults帮我们做了一层优化. NSUserDefaults是带缓存的, NSUserDefaults会把访问到的key缓存到内存里, 下次再访问时, 如果内存中命中就直接访问, 如果未命中再从文件中载入. 应用会时不时调用[defaults synchronize]方法来保证内存与文件中的数据的一致性, 有时在写入一个值后也最好调用下这个方法来保证数据真正写入文件.

我们不能在一个地方大规模写入NSUserDefaults, 性能会有很大影响. 用plist文件读入内存在访问的瓶颈主要在读入的过程中, 如果文件很大会比较耗时, 但是一旦载好, 在内存中读取就很快; 但是, 对内存值的写入则会造成内存与文件数据不一致, 这时为了保证数据一致性就要写入文件, 写入后又要读入内存, 这就导致延迟.

NSUserDefaults对象发valueForKey:消息, 返回的数据永远是不可变类型的.

数据库为什么要创建索引

创建索引的优点:

  1. 通过创建唯一性索引, 可以保证数据库表中每一行数据的唯一性
  2. 可以加快数据的检索速度
  3. 可以加速表与表之间的链接
  4. 可以显著减少查询中分组和排序的时间
  5. 使用索引在查询过程中使用优化隐藏器, 提高系统性能

创建索引的缺点:

  1. 创建索引和维护索引要耗费时间, 这种时间随着数据量增加而增加
  2. 创建索引需要占用物理空间
  3. 对表的数据增删改查时, 索引也要动态的维护

因此我们经常在主键列, 常用在连接的列, 经常根据范围搜索的列, 经常排序的列, 经常使用在WHERE子句中的列上面创建索引.
而那些在查询中很少用的列, 数据值很少的列, text, image和bit数据类型的列(因为要么数据量大要么取值少), 修改性能远远大于检索性能时不应该创建索引.

Full Text Search为什么快

它的工作原理是计算机索引程序通过扫描文章中的每一个词, 对每一个词建立一个索引, 指明该词在文章中出现的次数和位置, 当用户查询时, 检索程序就根据事先建立的索引进行查找, 并将查找的结果反馈给用户的检索方式.

分为按字检索和按词检索: 按字检索是指对于文章中的每一个字都建立索引, 检索时将词分解为字的组合. 按词检索是指对文章中的词, 即语义单位建立索引, 检索时按词检索.

imageNamed:与imageWithContentsOfFile:对比

使用imageNamed这个方法生成的UIImage对象, 会在应用的main bundle中寻找图片, 如果找到则Cache到系统缓存中, 作为内存的cache, 而程序员是无法操作cache的, 只能由系统自动处理, 如果我们需要重复加载一张图片, 那这无疑是一种很好的方式, 因为系统能很快的从内存的cache找到这张图片. 但是试想, 如果加载很多很大的图片的时候, 内存消耗过大的时候, 就会会强制释放内存, 即会遇到内存警告(memory warnings). 由于在iOS系统中释放图片的内存比较麻烦, 所以冲易产生内存泄露.
经常使用在图片资源反复使用到, 而且占用内存少的场景下.

相比上面的imageNamed这个方法要写的代码多了几行, 使用imageWithContentsOfFile的方式加载的图片, 图片会被系统以数据的方式进行加载. 返回的对象不会保存在缓存中, 一旦对象销毁就会释放内存, 所以一般不会因为加载图片的方法遇到内存问题.
经常使用在图片资源较大, 加载到内存后, 比较耗费内存资源, 图片一般只使用一次的场景下.

Image的加载以及内存计算

iOS从磁盘加载一张图片,使用UIImageVIew显示在屏幕上,需要经过以下步骤:

  1. 从磁盘拷贝数据到内核缓冲区.
  2. 从内核缓冲区复制数据到用户空间.
  3. 生成UIImageView, 把图像数据赋值给UIImageView.
  4. 如果图像数据为未解码的PNG/JPG, 解码为位图数据.
  5. CATransaction捕获到UIImageView layer树的变化.
  6. 主线程Runloop提交CATransaction, 开始进行图像渲染.
    • 分配内存缓冲区用于管理文件 IO 和解压缩操作
    • 将文件数据从磁盘读到内存中
    • 将压缩的图片数据解码成未压缩的位图形式, 这是一个非常耗时的 CPU 操作.
    • 如果数据没有字节对齐, Core Animation会再拷贝一份数据, 进行字节对齐.
    • GPU处理位图数据, 进行渲染.

位图就是一个像素数组, 数组中的每个像素就代表着图片中的一个点. 我们在应用中经常用到的 JPEG 和 PNG 图片就是位图.

不管是 JPEG 还是 PNG 图片, 都是一种压缩的位图图形格式. 只不过 PNG 图片是无损压缩, 并且支持 alpha 通道, 而 JPEG 图片则是有损压缩. 因此, 在将磁盘中的图片渲染到屏幕之前, 必须先要得到图片的原始像素数据, 才能执行后续的绘制操作, 这就是为什么需要对图片解压缩的原因.

解压缩后的图片大小 = 图片的像素宽 图片的像素高 每个像素所占的字节数. 因为位图不用解压缩, 所以位图的内存大小就是图片的实际大小. 非位图的图片要解压缩, 所以非位图的图片在内存中的大小就是上面的公式.

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

==, isEqual与hash

  • 对于基本类型, ==运算符比较的是值; 对于对象类型, ==运算符比较的是对象的地址(即是否为同一对象)
  • 常见类型的isEqual方法还有NSString isEqualToString / NSDate isEqualToDate / NSArray isEqualToArray / NSDictionary isEqualToDictionary / NSSet isEqualToSet. isEqual比较对象值是否相等, 如果是自定义对象, 那么对象所有的属性也要像等.
  • hash 值决定了该对象在 hash 表中存储的位置. hash值是对象判等的必要非充分条件, 集成成员的hash值和目标hash值相等, 并且进行对象判等为true, 则hash返回true.

    • 作为判等的结果 hash方法只在对象被添加至NSSet和设置为NSDictionary的key时会调用.
    • 基于hash值索引的Hash Table查找某个成员的过程就是: 通过hash值直接找到查找目标的位置, 如果目标位置上有多个相同hash值得成员, 此时再按照数组方式进行查找.

NSDictionary 内部结构、实现原理

NSDictionary(字典)是使用 hash表来实现key和value之间的映射和存储的, hash函数设计的好坏影响着数据的查找访问效率.

Objective-C 中的字典 NSDictionary 底层其实是一个哈希表, 实际上绝大多数语言中字典都通过哈希表实现. 哈希表的本质是一个数组, 数组中每一个元素称为一个箱子(bin), 箱子中存放的是键值对.

  1. 根据 key 计算出它的哈希值 h.
  2. 假设箱子的个数为 n, 那么这个键值对应该放在第 (h % n) 个箱子中.
  3. 如果该箱子中已经有了键值对, 那么就会有冲突, 如何解决冲突:
    • 开放寻址法.
    • 链地址法. 每个箱子其实是一个链表, 属于同一个箱子的所有键值对都会排列在链表中
    • 再哈希法. 当发生冲突时, 使用第二个, 第三个, 哈希函数计算地址, 直到无冲突. 缺点: 计算时间增加. 比如对字符串首字母进行哈希, 如果产生冲突可以按照字符串字母第二位进行哈希, 再冲突, 第三位, 直到不冲突为止.
    • 建立一个公共溢出区. 假设哈希函数的值域为[0,m-1], 则设向量HashTable[0..m-1]为基本表, 另外设立存储空间向量OverTable[0..v]用以存储发生冲突的记录.

负载因子(load factor), 它用来衡量哈希表的 空/满 程度, 一定程度上也可以体现查询的效率, 计算公式为: 负载因子 = 总键值对数 / 箱子个数. 负载因子越大, 意味着哈希表越满, 越容易导致冲突, 性能也就越低. 因此, 一般来说, 当负载因子大于某个常数(可能是 1, 或者 0.75 等)时, 哈希表将自动扩容.
哈希表在自动扩容时, 一般会创建两倍于原来个数的箱子, 因此即使 key 的哈希值不变, 对箱子个数取余的结果也会发生改变, 因此所有键值对的存放位置都有可能发生改变, 这个过程也称为重哈希(rehash).