内存结构与内存管理
NSCache 是一个容器,通过 key-value 形式存储和查询值,用于临时存储对象。 NSCache 胜过 NSDictionary 之处在于,当系统资源将要耗尽时,它可以自动删减缓存。 NSCache 是线程安全的,NSDictionary 不是。
Designated Initializer 即指定初始化函数。
NS_DESIGNATED_INITIALIZER 是一个宏定义其含义如下:
可以看到 "NS_DESIGNATED_INITIALIZER" 其实是给初始化函数声明的后面加上了一个编译器可见的标记,这个标记可以使编译器在编译时以警告的方式找出潜在的问题。
使用规则: 1、 类的初始化过程是从子类到父类依次调用 Designated Initializer。 2、如果子类指定了新的初始化器,那么在这个初始化器内部必须调用父类的 Designated Initializer,并且需要重写父类的 Designated Initializer,将其指向子类新的初始化器。 3、若调用父类的类的 Designated Initializer 方法时,要调用直接父类的 Designated Initializer。 4、多个 Secondary initializers(次要初始化器),它们之间可以任意调用,但最后必须指向 Designated Initializer。在 Secondary initializers 内不能直接调用父类的初始化器。 5、如果有多个不同数据源的 Designated Initializer,那么不同数据源下的 Designated Initializer 应该调用相应的 [super (designated initializer)]。如果父类没有实现相应的方法,则需要根据实际情况来决定是给父类补充一个新的方法还是调用父类其他数据源的 Designated Initializer。
1、顺序存储方式 顺序存储方式就是在一块连续的存储区域一个接着一个的存放数据,把逻辑上相连的结点存储在物理位置上相邻的存储单元里.线性存储方式主要用于线性逻辑结构的数据存放.而对于图和树等非线性逻辑结构则不适用. 2、链接存储方式 链接存储方式比较灵活,逻辑上相邻的结点在物理位置上未必相邻.结点间的逻辑关系由附加的引用字段表示,一个结点的引用字段会指向下一个结点的存放位置.一般在原数据项中增加应用类型来表示结点之间的位置关系. 3、索引存储方式 索引存储方式是采用附加索引表的方式来存储结点信息的一种存储方式.索引表由若干个索引项组成.索引存储方式中索引项的一般形式为:(关键字、地址).其中,关键字是能够唯一标识一个结点的数据项. 索引存储方式还可以细分为如下两类: 稠密索引(Dense Index):这种方式中每个结点在索引表中都有一个索引项.其中,索引项的地址指示结点所在的的存储位置. 稀疏索引(Spare Index):这种方式中一组结点在索引表中只对应一个索引项.其中,索引项的地址指示一组结点的起始存储位置. 4、散列存储方式 散列存储方式是根据结点的关键计算出该结点的存储地址的一种存储的方式.
coreData 是苹果对 sqlite 的封装,不用操作 sqlite 语句。它提供了对象关系映射功能,能将 OC 对象转化成数据,保存在 sqlite 中,也能将保存的数据还原成 OC 对象。 CoreData 中的 NSManagedObjectContext 在多线程中不安全.多线程访问 CoreData 最好的方法是一个线程一个 NSManagedObjectContext,每个 NSManagedObjectContext 对象实例使用同一个 NSPersistentStoreCoordinator 实例,这个实例可以很安全的顺序访�问永久存储,这是因为 NSManagedObjectContext 会在使用 NSPersistentStoreCoordinator 前上锁.
1、链地址法 指把所有的冲突关键字存储在一个线性链表中,这个链表由其散列地址唯一标识. 2、开放定址法 通常有三种方法:线性探测,二次探测,再哈希法. 2.1、线性探测 线性探测方法就是线性探测空白单元.哈希表越来越满时会产生非常长的探测长度,后续的数据插入将会非常费时.线性探测就是使用算术取余的方法计算余数,当产生冲突时就通过线性递增的方法进行探测,一直到数组的位置为空,插入数据项即可. 2.2、二次探测 二次探测的过程是 x+1,x+4,x+9 等以此类推,二次探测的步数是原始位置相隔的步数的平方.二次探测可以消除在线性探测中产生的聚集问题,但是二次探测也会产生一种更明确更细的聚集. 2.3、再哈希法 再哈希是把关键字用不同的哈希函数再做一遍哈希化,用这个结果作为步长.对指定的关键字,探测的步长是不变的,可以说不同的关键字可以使用不同的步长,并且步长可以控制. 3、再散列法 当发生冲突时,利用另一个哈希函数再次计算一个地址,直到冲突不再发生. 4、建立公共溢出区 一旦由哈希函数得到的地址冲突,就都填入溢出表.
Swizzling 要用 dispatch_once 函数避免代码被重复执行. Swizzling 要在 +load 中执行,要在被交换方法调用之前调用.且不要调用 [super load],可能会出现“Swizzle无效”的假象.
方法签名的作用是在支持方法重载的语言中标示一个方法的唯一性.(方法名,参数和返回值类型)
库本质上讲是一种可执行的二进制格式,可以载入内存中执行。是程序代码的集合,共享代码的一种方式。静态库是闭源库,不公开源代码,都是编译后的二进制文件,不暴露具体实现。静态库一般都是以 .a 或者 .framework 形式存在。静态库编译的文件比较大,因为整个函数库的数据都会被整合到代码中,这样的好处就是编译后的程序不需要外部的函数库支持,不好的一点就是如果改变静态函数库,就需要程序重新编译。多次使用就有多份冗余拷贝。 使用静态库的好处:模块化分工合作、可重用、避免少量改动导致大量的重复编译链接。 有静态库自然就有动态库了。这里所谓的静态和动态是相对编译期和运行期的。 静态库在程序编译时会被链接到代码中,程序运行时将不再需要改静态库,而动态库在编译时不会被链接到代码中,只有程序运行时才会被载入,所以做插件都是运用了 runtime 机制,然后动态库注入修改的。 制作 .a 文件时候,要注意 CPU 架构的支持,i386、X86_64、 armv7、armv7s。 查看可以通过命令 “ lipo -info 静态库名称” 。模拟器 .a 文件和真机 .a 文件合并可以通过 "lipo -create 模拟器静态库1名 真机静态库2名 -output 新静态库名称" 使用静态库注意事项 命名要规范。 framework中用到了NSClassFromString,但是转换出来的class 一直为nil。 解决方法:在主工程的【Other linker Flags】需要添加参数[-ObjC]即可。 如果Xcode找不到框架的头文件,你可能是忘记将它们声明为public了。 解决方法:进入target的Build Phases页,展开Copy Headers项,把需要public的头文件从Project或Private部分拖拽到Public部分。 尽量不要用 xib 。由于静态框架采用静态链接,linker会剔除所有它认为无用的代码。不幸的是,linker不会检查xib文件,因此如果类是在xib中引用,而没有在OC代码中引用,linker将从最终的可执行文件中删除类。这是linker的问题,不是框架的问题(当你编译一个静态库时也会发生这个问题)。苹果内置框架不会发生这个问题,因为他们是运行时动态加载的,存在于iOS设备固件中的动态库是不可能被删除的。 有两个解决的办法: 1、 让框架的最终用户关闭linker的优化选项,通过在他们的项目的Other linker Flags中添加-ObjC和-all_load。 2、 在框架的另一个类中加一个该类的代码引用。例如,假设你有个MyTextField类,被linker剔除了。假设你还有一个MyViewController,它在xib中使用了MyTextField,MyViewController并没有被剔除。你应该这样做: 在MyTextField中: +(void)forcelinkerLoad_ {} 在MyViewController中: +(void)initialize {[MyTextField forcelinkerLoad_];} 他们仍然需要添加-ObjC到linker设置,但不需要强制all_load了。 第2种方法需要你多做一点工作,但却让最终用户避免在使用你的框架时关闭linker优化(关闭linker优化会导致object文件膨胀)。 动态库和静态库
主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode.这两个 Mode 都已经被标记为”Common”属性.DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态.当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也会影响到滑动操作. 设置Timer 的 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
单链表: 用快指针(fast)和慢指针(slow),快指针每次走两个节点,慢指针每次走一个节点,若有环,则快慢指针必然会重合. 两个链表 先判断两个链表是否带环; 1> 若两个都不带环 将一个链表的头节点加在另一个链表的尾节点上,若有环,必相交. 2> 一个带环一个不带环,则肯定不相交. 3> 两个都带环:找到两个入环点r1,r2,环1的入环点为r1,从r1开始遍历一圈,每个结点和r2比较.
SEL:类成员方法的指针,方法编号. IMP:函数指针,指向方法的地址. SEL(方法编号)最终会通过 Dispatch table(存放SEL和IMP的对应) 表寻找到对应的IMP(函数指针)并执行函数.
KVO&KVC
究其原因是2.x和3.x版本分别使用的是NSURLConnection和NSURLSession. 在2.x版本的AFN中,使用的是 NSURLCollection 进行封装.NSURLConnection 的网络请求是异步发起的,结果的回调在原来线程的 RunLoop 中进行. 在3.x版本的AFN中,由于 NSURLSession 不需要再当前线程等待网络请求得回调结果,所以说在AFN中就不需要常驻线程等待回调,而是交给 NSOperationQueue 来处理,让系统自己来决定是否需要开启新的线程.并且设置 maxConcurrentOperationCount 为1,让回调串行执行.
GPU屏幕渲染有两种方式:
On-Screen Rendering (当前屏幕渲染) :指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区进行. Off-Screen Rendering (离屏渲染):指的是GPU在当前屏幕缓冲区以外开辟一个缓冲区进行渲染操作.
为什么用到离屏渲染?
有些效果不能直接呈现于屏幕,而需要在别的地方做额外的处理预合成.图层属性的混合体没有预合成之前不能直接在屏幕中绘制,所以就需要离屏渲染.
离屏渲染的代价
创建新的缓冲区:要想进行离屏渲染则首先要先创建新的缓冲区. 上下文切换:离屏渲染的整个过程,需要多次切换上下文环境.先是从当前屏幕切换到离屏,等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上时又需要将上下文环境从离屏切换到当前屏幕.而上下文环境的切换是要付出很大代价的. 离屏渲染会增加GPU的工作量,从而使一个绘制周期内不能完成CPU+GPU的工作. 离屏渲染
方案一:基于Runloop 主线程绝大部分计算或者绘制任务都是以 Runloop 为单位发生.单次 Runloop 如果时长超过16ms,就会导致UI体验的卡顿.Runloop 的生命周期及运行机制虽然不透明,但苹果提供了一些API去检测部分行为.我们可以通过如下代码监听Runloop每次进入的事件:
用 kCFRunLoopExit 减去 kCFRunLoopEntry 的时间,即为一次 Runloop 所耗费的时间. 方案二 用一个 worker 线程每隔一小段时间(delta)ping 一下主线程(发送一个NSNotification),如果主线程此时有空.必然能接收到这个通知,并回复pong(发送另一个NSNotification),如果worker线程超过 delta 时间没有收到 pong 的回复,那么可以推测UI线程必然在处理其他任务了.
常见锁
自旋锁:会不断的进行尝试请求.如:OSSpinLock. 互斥锁:对于某一资源同时只允许有一个访问,无论读写,会进入休眠.如:NSLock. 读写锁:对于某一资源同时只允许有一个写访问或多个读访问或读写只能一个.如:pthread_rwlock,dispatch_barrier_async. 条件锁:在满足某个条件的时候进行加锁或者解锁.如:NSConditionLock. 递归锁:可以被一个线程多次获得,而不会引起死锁.它记录了成功获得锁的次数,每一次成功的获得锁,必须有一个配套的释放锁和其对应,这样才不会引起死锁.只有当所有的锁被释放之后,其他线程才可以获得锁.如:NSRecursiveLock
性能对比
按照元素和元素间的关系 若元素之间没有任何关系则为集合结构, 若元素之间只和另外一个元素有关系则为线性结构, 若一个元素同时和其他几个元素有关系则为树形结构, 若多个元素之间相互有关系则为图形结构。
排序算法
网络协议
设计模式 >>>
因为OC的消息机制,名字查找发生在运行时,而不是编译时,不能解决多个基类的二义性.
分类与扩展
Autoreleasepool 底层是使用了AutoreleasePoolPage 来管理.AutoreleasePoolPage 是一个双向的链表,每个 AutoreleasePoolPage 都有4096个字节,用来存放 autorelease 对象的地址. 在 Autoreleasepool 开始的时候,会调用 AutorelasePoolPage 的 push 方法,会将一个标识 POOL_BOUNDARY 添加到 AutoreleasePoolPage 对象里面并且返回 POOL_BOUNDARY 的地址r. 当对像进行 relase 的时候,会将对象的地址依次添加到当前 AutorelasePoolPage 里. 当 Autoreleasepool 作用域结束的时候,会调用 AutorelasePoolPage 的 pop(r),AutorelasePoolPage 则会将里面保存的对象从最后一个开始进行 release 操作,当碰到 r 的时候,标识当前 Autoreleasepool 里面所有的对象都进行了一次 release 操作. @autoreleasepool 底层原理
性能优化简介
Runtime 概述
1、数据源同步,如删除列表中的某一行。
磁盘管理模块:管理图片在磁盘存储时的逻辑。
网络管理模块:管理图片下载逻辑。
图片解码管理模块:图片解码、图片压缩/解压缩。 图片解码:应用策略模式对不同图片格式进行解码;在磁盘读取后/网络请求返回后放入内存前进行解码。
5、阅读时长统计
怎么处理记录丢失? 1、定时写磁盘。 2、设置缓存条数,超过该条数后写入磁盘。
原因分析: 1、异步函数全局队列执行的速率是很快的,这是因为,系统开启了子线程来执行函数。1、2、6 的执行顺序只是受到调用顺序的影响,它们三者的等效的。 2、GCD 的延迟执行和异步函数主队列执行的方法要慢于直接调用的 [self performSelector:@selector(XXX) withObject:XXX afterDelay:XXX] 方法。GCD 是 C 语言的方法,而后者是系统的方法,(推测)系统方法要比较快一点。 3、异步函数主队列执行方法还是要快于延迟执行的方法的,(推测)虽然它们都是在主线程上执行,但是前者还是开启了一条线程来执行函数的。
8、一个 NSObject 对象占用多少内存?
系统分配了 16 个字节给 NSObject 对象,但是 NSObject 对象内部只使用了 8 个字节。
9、OC 中的对象有哪些?
10、OC 的类信息存放在哪里?
对象方法、属性、成员变量、协议信息,存放在 class 对象中; 类方法,存放在 meta-class 对象中; 成员变量的具体值,存放在 instance 对象。
11、isa 指针
isa 指针
12、创建一个实例对象至少需要多少内存?实际上分配了多少内存?
8 和 16 注1: 可以通过 #import <objc/runtime.h> class_getInstanceSize([NSObject class]);来查看需要多少内存。 可以通过#import <malloc/malloc.h> malloc_size((__bridge const void *)obj);来查看实际分配了多少内存。
注 2:实时查看内存数据 Debug --> Debug Workfllow --> View Memory (Shift + Command + F)
注 3:常用的 LLDB 指令 1、print、p 打印 2、po 打印对象 3、读取内存: memory read/数量格式字节数 内存地址 x/数量格式字节数 内存地址 例:x/3xw 0x10010 注: x 是16进制,f 是浮点,d 是10进制 b:byte 1 字节,h:half word 2 字节,w:word 4 字节,g:giant word 8 字节 4、修改内存中的值 memory write 内存地址 数值 例:memory write 0x0000010 10
13、代码编译流程?
1、OC --> CC++ --> 汇编语言 --> 机器语言。 2、机器语言是计算机唯一可以使用的语言。 3、OC 的对象、类主要是基于 CC++ 的结构体实现的。 4、可以通过 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件 这样的方法将 Objective-C 代码转换为 CC++ 代码。如果需要链接其他框架,使用 -framework 参数(比如:-framework UIKit)。
14、Block 的原理和本质
封装了函数调用以及调用环境的 OC 对象,它内部也存在 isa 指针。
15、__block 的作用是什么?有什么使用注意点?
__block 可以用于解决 block 内部无法修改 auto 变量值的问题。编译器会将 __block 变量包装成一个对象。 __block 不能修饰全局变量、静态变量(static)。
16、Block 对对象类型的 auto 变量内存管理
1、如果 block 是在栈上,将不会对对象类型的 auto 变量产生强引用。 2、如果 block 被拷贝到堆上,会调用 block 内部的 copy 函数。copy 函数内部会调用 _Block_object_assign 函数,_Block_object_assign 函数会根据对象类型的 auto 变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用或者弱引用。 3、如果 block 从堆上移除,会调用 block 内部的 dispose 函数。dispose 函数内部会调用 _Block_object_dispose 函数,_Block_object_dispose 函数会自动释放引用的对象类型的 auto 变量。
17、Block 在修改 NSMutableArray 时需要添加 __block 吗?
不需要。当在 Block 内部向可变数组中添加数据的时候,是在使用可变数组的指针而不是修改可变数组的指针。当在 Block 内部修改可变数组的指针的时候,才需要添加 __block。