1、nil 、 NSNull
nil:针对对象,而空对象不是说不占用空间,相当于一个“洗白”,回到初始状态。
我们给对象赋值时一般会使用object = nil,表示我想把这个对象释放掉;或者对象由于某种原因,经过多次release,于是对象引用计数器为0了,系统将这块内存释放掉,这个时候这个对象为nil,我称它为“空对象”。(注意:我这里强调的是“空对象”,下面我会拿它和“值为空的对象”作对比!!!)
所以对于这种空对象,所有关于retain的操作都会引起程序崩溃,例如字典添加键值或数组添加新原素等
NSNull:针对指针,对对象指针和非对象指针都有效,Null不会占用空间。
NSNull和nil的区别在于,nil是一个空对象,已经完全从内存中消失了,而如果我们想表达“我们需要有这样一个容器,但这个容器里什么也没有”的观念时,我们就用到NSNull,我称它为“值为空的对象”。如果你查阅开发文档你会发现NSNull这个类是继承NSObject,并且只有一个“+ (NSNull *) null;”类方法。这就说明NSNull对象拥有一个有效的内存地址,所以在程序中对它的任何引用都是不会导致程序崩溃的。
2、沙盒包有含三个目录:Documents、Library、temp
1)Documents :这个目录用于存储用户数据。该路径可通过配置实现iTunes共享文件。iTunes或iCloud会对其进行备份。
2)Library :这个目录下有两个子目录:
Preferences :包含应用程序的偏好设置文件。iTunes或iCloud会对其进行备份。
Caches :存放缓存数据,可以重新下载或生成的数据,同时没有这些数据不会影响用户离线使用。缓存数据在设备低存储空间时可能被删除。iTunes或iCloud不会对其进行备份。
3)tmp :这个目录用于存放临时文件,保存应用程序再次启动过程中不需要的信息。系统会不定期删除其中的文件。该路径下的文件不会被iTunes备份。
3、事件传递链
先判断点是否在View内部,然后遍历subViews
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
判断点是否在这个View内部
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
// default returns YES if point is in bounds
A: 流程
1:先判断该层级是否能够响应(1.alpha>0.01 2.userInteractionEnabled == YES 3.hidden = NO)
2:判断改点是否在view内部,
3:如果在那么遍历子view继续返回可响应的view,直到没有。
B:常见问题
父view设置为不可点击,子view可以点击吗
不可以,hit test 到父view就截止了
子view设置view不可点击不影响父类点击
同父view覆盖不影响点击
手势对responder方法的影响
C:实际用法
点一一个圆形控件,如何实现只点击圆形区域有效
重载pointInside。此时可将外部的点也判断为内部的点,反之也可以
4、进程和线程
1)线程在进程下行进(单纯的车厢无法运行)
2)一个进程可以包含多个线程(一辆火车可以有多个车厢)
3)不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
4)同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
5)进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
6)进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
7)进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
8)进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-"互斥锁"
9)进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”
GCD的队列分为2大类型:
1、并发队列
可以让多个任务并发执行(自动开启多个线程同时执行任务)
并发功能只有在异步函数下才有效
2、串行队列
任务一个接一个的执行(一个任务执行完毕后执行下一个任务)
同步、异步、并发、串行
同步和异步主要影响:能不能开启新的线程
同步:在当前线程中执行任务,不具备开启线程的能力
异步:在新的线程中执行任务,具备开启新线程的能力
并发和串行主要影响:任务的执行方式
并发:多个任务同时执行
串行:一个任务执行完毕后,在执行下一个任务
5、objc_getClass与object_getClass
1)Class objc_getClass(const char *aClassName)
1> 传入字符串类名
2> 返回对应的类对象
2)Class object_getClass(id obj)
1> 传入的obj可能是instance对象、class对象、meta-class对象
2> 返回值
a) 如果是instance对象,返回class对象
b) 如果是class对象,返回meta-class对象
c) 如果是meta-class对象,返回NSObject(基类)的meta-class对象
3)- (Class)class、+ (Class)class
1> 返回的就是类对象
- (Class) {
return self->isa;
}
+ (Class) {
return self;
}
6、反射
反射可以理解为类名、方法名、属性名等和字符串在运行时相互转化的一种机制
实质 - 发送了一个消息给Runtime,然后Runtime再根据这个Class的字符串名和这个方法的字符串名,去匹配真正相应的方法地址,然后再执行。同样反射就是利用字符串去动态的检测,从而实现运行时的转化。
优点:
解耦合,消除类与类之间的依赖
缺点:
代码可读性降低,将原有逻辑复杂化了,不利于维护
性能较差。使用反射匹配字符串间接命中内存比直接命中内存的方式要慢。当然,这个程度取决于使用场景,如果只是作为程序中很少涉及的部分,这个性能上的影响可以忽略不计。但是,如果在性能很关键的应用核心逻辑中使用反射,性能问题就尤其重要了)
7、CADisplayLink 与 NSTimer
CADisplayLink
正常情况下会在每次刷新结束都被调用,精确度相当高
使用场合相对专一,适合做UI的不停重绘
NSTimer
精确度就显得低了点,比如NSTimer的触发时间到的时候,runloop如果在阻塞状态,触发时间就会推迟到下一个runloop周期
使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用
8、iOS---------消息转发机制
消息转发机制的原理 其实就是在内部做了三次的补救机会
1)动态解析 利用runtime动态添加实现代码
resolveInstanceMethod:
resolveClassMethod:
2)快速转发 也就是重定向接受者 它会去找其他的类 将消息转发给可以响应该消息的对象进行处理
- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil;
}
3)完整转发 指定选择器 IMP指向实现代码
forwardInvocation
_objc_msgForward 函数是做什么的?直接调用会发生什么问题?
当对象没有实现某个方法 ,会调用这个函数进行方法转发。 (某方法对应的IMP没找到,会返回这个函数的IMP去执行)
1.调用resolveInstanceMethod:方法,允许用户在此时为该Class动态添加实现。如果有实现了,则调用并返回。如果仍没实现,继续下面的动作。
2.调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接转发给它。如果返回了nil,继续下面的动作。
3.调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。
4.调用forwardInvocation:方法,将地3步获取到的方法签名包装成Invocation传入,如何处理就在这里面了。
如果直接调用这个方法,就算实现了想调用的方法,也不会被调用,会直接走消息转发步骤。
9、UI刷新在主线程
UIKit并不是一个线程安全的类,UI操作涉及到渲染访问各种View对象的属性,如果异步操作下会存在读写问题,而为其加锁则会耗费大量资源并拖慢运行速度。
整个程序的起点UIApplication是在主线程进行初始化,所有的用户事件都是在主线程上进行传递(如点击、拖动),所以view只能在主线程上才能对事件进行响应。
渲染方面由于图像的渲染需要以60帧的刷新率在屏幕上同时更新,在非主线程异步化的情况下无法确定这个处理过程能够实现同步更新。在子线程中如果要对UI 进行更新,必须等到该子线程运行结束才能把UI的更新提交给渲染服务。
10、NSObject和id
id是一个指针。
NSObject *是NSObject类型的指针。
Objective-C中并非所有的类都继承自NSObject,还有NSProxy类,故NSObject *的范围小于id。
11、锁
自旋锁、互斥锁比较 -
代码执行频率高,CPU充足,可以使用互斥锁,
频率低,代码复杂则需要互斥锁。
自旋锁 - 等待锁的进程会处于忙等(busy-wait)状态,一直占用着CPU资源,目前已经不安全,可能会出现优先级翻转问题
自旋锁在等待时间比较短的时候比较合适
临界区代码经常被调用,但竞争很少发生
CPU不紧张
多核处理器
互斥锁 - 等待锁的线程会处于休眠状态
预计线程等待时间比较长
单核处理器
临界区IO操作
临界区代码比较多、复杂,或者循环量大
临界区竞争非常激烈
递归锁 - 同一个线程可以加锁N次而不会引发死锁
mutex :pthread_mutex_t 互斥锁,等待锁的线程会处于休眠状态。
NSLock :对mutex普通锁的封装。
NSRecursiveLock :对mutex递归锁的封装,递归锁可以对相同的线程进行反复加锁。
NSCondition :对mutex和cond的封装,优点是可以让线程之间形成依赖,缺点是没有明确的条件。
NSConditionLock :NSCondition的进一步封装
可以实现多个子线程进行线程间的依赖,A依赖于B执行完成,B依赖于C执行完毕则可以使用NSConditionLock来解决问题
dispatch_queue :特殊的锁,直接使用GCD的串行队列,也是可以实现线程同步的。串行队列其实就是线程的任务在队列中按照顺序执行,达到了锁的目的。
dispatch_semaphore :信号量控制并发数量,可以控制并发线程的数量,当设置为1时,可以作为同步锁来用,设置多个的时候,就是异步并发队列。
@synchronized :锁的是对象obj,使用该锁的时候,底层是对象计算出来的值作为key,生成一把锁,不同的资源的读写可以使用不同obj作为锁对象。
atmoic :原子操作,给属性添加atmoic修饰,可以保证属性的setter和getter都是原子性操作,也就保证了setter和getter的内部是线程同步的。atomic读取频率高的时候会导致线程都在排队,浪费CPU时间
读写锁:pthread_rwlock(c语言封装的读写锁) / 异步栅栏调用 dispatch_barrier_async
性能从高到低排序
os_unfair_lock
OSSpinLock
dispatch_semaphore
pthread_mutex
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSCondition
pthread_mutex(recursive)
NSRecursiveLock
NSConditionLock
@synchronized
平时简单使用的话没有很大的区别,还是推荐使用NSLock和信号量,最简单的是@synchronized,不用声明和初始化,直接拿来就用。
总结
普通线程锁本质就是同步执行
atomic原子操作只限制setter和getter方法,不限制成员变量
读写锁高性能可以使用pthread_rwlock_t和dispatch_barrier_async
死锁的4个必要条件
1、互斥: 某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
2、占有且等待: 一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
3、不可抢占:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
4、循环等待: 存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。
12、优化
CPU
尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用CALayer取代UIView
不要频繁地调用UIView的相关属性,比如frame、bounds、transform等属性,尽量减少不必要的修改
尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性
Autolayout会比直接设置frame消耗更多的CPU资源
图片的size最好刚好跟UIImageView的size保持一致
控制一下线程的最大并发数量
尽量把耗时的操作放到子线程
文本处理(尺寸计算、绘制)
图片处理(解码、绘制)
GPU
尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示
GPU能处理的最大纹理尺寸是4096x4096,一旦超过这个尺寸,就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸
尽量减少视图数量和层次
减少透明的视图(alpha<1),不透明的就设置opaque为YES
尽量避免出现离屏渲染
合理选择 imageNamed 和 imageWithContentsOfFile
imageNamed 会对图片进行缓存,适合多次使用某张图片
imageWithContentsOfFile 从bundle中加载图片文件,不会进行缓存,适用于加载一张较大的并且只使用一次的图片,例如引导图等
13、[self class] [super class]
1、前两个方法是给self发送消息, 消息名称是class和superclass, 结果很明显就是查看自己的类型和父类的类型
2、后两个方法也是给self发送消息, 只不过是从父类开始查询class和superclass方法, 而这两个方法都存在于NSObject中
3、消息接收者同样是self, 调用的方法也是相同, 所以结果就是自己的类型和父类的类型
14、RunLoop的基本作用
1、保持程序的持续运行
2、处理app中的各种事件(比如触摸事件、定时器事件、selector事件
3、节省CPU资源,提高程序性能,有事情就做事情,没事情就休息
Mode作用 - 指定事件在运行循环(Loop)中的优先级。 线程的运行需要不同的模式,去响应各种不同的事件,去处理不同情境模式。(比如可以优化tableview的时候可以设置UITrackingRunLoopMode下不进行一些操作)
15、dealloc包括以下几个步骤
1、c++析构函数调用(C++析构函数的作用是用来完成对象被删除前的一些清理工作,也就是专门的扫尾工作)
2、关联对象(例如使用runtime在分类中关联变量)移除,是一个hash表来存储
3、这里进行弱引用表sidetable的相关释放操作,包括表的释放以及引用计数,即weak指针置为nil的操作就在这里
注意 - dealloc方法是对象引用计数在哪个线程为0,则在哪个线程调用dealloc方法,所以不一定在主线程执行
16、SideTables 、 SideTable
在runtime内存空间中,SideTables是一个8个元素长度(长度为64)的hash数组,里面存储了SideTable。SideTables的hash键值就是一个对象obj的address。一个 obj,对应了一个SideTable。但是一个SideTable,会对应多个obj。因为SideTable的数量只有64个,所以会有很多obj共用同一个SideTable
在一个SideTable中,成员:
spinlock_t slock ; //自旋锁,用于上锁/解锁 SideTable。
RefcountMap refcnts; // 对象引用计数相关 map
(hash map,其key是obj的地址,而value,则是obj对象的引用计数)
(仅在未开启isa优化 或 在isa优化情况下isa_t的引用计数溢出时才会用到)
weak_table_t weak_table; // 对象弱引用相关 table
(存储了弱引用obj的指针的地址,其本质是一个以obj地址为key,弱引用obj的指针的地址作为value的hash表)
Runtime 维护了一个 weak表,用于存储指向某个对象的所有weak指针。weak表 其实是一个 hash(哈希)表,Key 是所指对象的地址,Value是 weak指针 的地址(这个地址的值是所指对象指针的地址)数组。
1、初始化时:runtime 会调用 objc_initWeak函数,初始化一个新的 weak指针 指向对象的地址。
2、添加引用时:objc_initWeak函数 会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
3、释放时,调用 clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有 weak指针地址的数组,然后遍历这个数组把其中的数据设为 nil,最后把这个 entry 从 weak表中删除,最后清理对象的记录。
17、load 和 initialize
+load
1、+load方法会在runtime加载类、分类时调用
2、每个类、分类的+load,在程序运行过程中只调用一次
3、调用顺序
1)先调用类的+load
按照编译先后顺序调用(先编译,先调用)
2)调用子类的+load之前会先调用父类的+load
3)再调用分类的+load
按照编译先后顺序调用(先编译,先调用)
+initialize
1、 +initialize方法会在类第一次接收到消息时调用
2、调用顺序
1、先调用父类的+initialize,再调用子类的+initialize
(先初始化父类,再初始化子类,每个类只会初始化1次)
区别:
+initialize是通过objc_msgSend进行调用的,所以有以下特点:
1、如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次)
2、如果分类实现了+initialize,就覆盖类本身的+initialize调用
18、Category
实现原理
1、Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
2、在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)
Category和Class Extension的区别
1、Class Extension在编译的时候,它的数据就已经包含在类信息中
2、Category是在运行时,才会将数据合并到类信息中
19、能否想向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?
1.不能向编译后得到的类增加实例变量
2.能向运行时创建的类中添加实例变量
解释:
1.编译后的类已经注册在runtime中,类结构体中的objc_ivar_list实例变量的链表和instance_size实例变量的内存大小已经确定, runtime会调用 class_setvarlayout 或 class_setWeaklvarLayout来处理 strong weak引用.所以不能向存在的类中添加实例变量
2.运行时创建的类是可以添加实例变量,调用class_addIvar函数.但是的在调用objc_allocateClassPair之后, objc_registerClassPair之前,原因同上.
20、页面加载速率
viewcontroller从viewdidload的第一行到viewdidappear的最后一行所用的时间
21、AutoreleasePool AutoreleasePage
一个AutoreleasePoolPage属于一个线程,一个线程中可以有多个AutoreleasePoolPage
我们可以知道AutoreleasePoolPage底层结构如下:
AutoreleasePoolPage是以栈为结点通过双向链表的形式组合而成;遵循先进后出规则,整个自动释放池由一系列的AutoreleasePoolPage组成的,而AutoreleasePoolPage是以双向链表的形式连接起来。
自动释放池与线程一一对应;
每个AutoreleasePoolPage对象占用4096字节内存,其中56个字节用来存放它内部的成员变量,剩下的空间(4040个字节)用来存放autorelease对象的地址。要注意的是第一页只有504个对象,因为在创建page的时候会在next的位置插入1个POOL_SENTINEL。
POOL_BOUNDARY为哨兵对象,入栈时插入,出栈时释放对象到此传入的哨兵对象
每一页里都存储了next指针,指向下次新添加的autoreleased对象的位置。
每一页里都包含父节点和子节点,分别指向上一页和下一页。第一页的父节点为nil,最后一页的子节点为nil。
每一页都有一个深度标记,第一页深度值为0,后面的页面递增1。
每一页里还包当前线程、最大入栈数量。
AutoreleasePool的内存结构如上图所示,特点如下:
1)自动释放池是一个栈的结构,是一个以AutoreleasePoolPage为结点的双向链表,根据需要来动态添加或删除页面。
2)每一页AutoreleasePoolPage的大小为4096字节,地址从低到高依次存储page自身成员、哨兵、对象指针。其中,自身成员占用56字节,且哨兵作为对象指针的边界,在释放池里只会有一个,因此:
第一页,内部存放:page成员 + 1个哨兵 + 504个对象指针。
其它页,内部存放:page成员 + 505个对象指针。
3)已存满的页面被标记为full page,当前正在操作的页被标记为hot page。
4)AutoreleasePoolPage继承自AutoreleasePoolPageData,内部成员情况如下:
magic:用来校验AutoreleasePoolPage的结构是否完整。
next :下次新添加的autoreleased对象的位置,初始化时指向begin()。
thread:当前线程,说明自动释放池和线程有关联。
parent :指向父节点,即上一个页面,第一个页面的parent值为nil。
child:指向子节点,即下一个页面,最后一个页面的child值为nil。
depth :表示页面深度,从0开始,往后递增1。
hiwat :即high water mark,表示最大入栈数量标记
POOL_BOUNDARY:
只是nil的别名。前世叫做POOL_SENTINEL,称为哨兵对象或者边界对象;
POOL_BOUNDARY用来区分不同的自动释放池,以解决自动释放池嵌套的问题
每当创建一个自动释放池,就会调用push()方法将一个POOL_BOUNDARY入栈,并返回其存放的内存地址;
当往自动释放池中添加autorelease对象时,将autorelease对象的内存地址入栈,它们前面至少有一个POOL_BOUNDARY;
当销毁一个自动释放池时,会调用pop()方法并传入一个POOL_BOUNDARY,会从自动释放池中最后一个对象开始,依次给它们发送release消息,直到遇到这个POOL_BOUNDARY
22、ARC
在即将超出作用域的时候,编译器会给所有__strong标识的变量调用一次release
weak也称为弱引用,弱引用表示并不持有对象,当所引用的对象销毁了,这个变量就自动设为nil
即使是使用alloc/new/copy/mutableCopy创建的对象,也不持有,结果就是这个对象没人要,所以一出来就销毁了
注意 - 通过非这4个方法创建的对象,并不会因为__weak标识已创建就销毁,而是要等到超出autoreleasepool的时候才会销毁。
23、sizeThatFits、sizeToFit
sizeThatFits: 会计算出最优的 size ,但是不会改变 自己的 size;
sizeToFit: 会计算出最优的 size 而且会改变自己的 size.
Label 文本较长以至于不能单行显示时,两者也是有区别的
24、SEL IMP
SEL - 选择器,代表方法名/函数名,底层结构和char *类似
可以通过@selector()和sel_registerName()获得
可以通过sel_getName()和NSStringFromSelector()转成字符串
不同类中相同名字的方法名,所对应的方法选择器是相同的
IMP - 函数的具体实现
25、安装包瘦身
ipa主要由可执行文件、资源组成
1、资源(图片、音频、视频等)
采取无损压缩
去除没有用到的资源: https://github.com/tinymind/LSUnusedResources
2、可执行文件瘦身
编译器优化
Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default设置为YES
去掉异常支持
Enable C++ Exceptions、Enable Objective-C Exceptions设置为NO, Other C Flags添加-fno-exceptions
利用AppCode(https://www.jetbrains.com/objc/)检测未使用的代码:菜单栏 -> Code -> Inspect Code
编写LLVM插件检测出重复代码、未被调用的代码