
1.介绍下 runtime 的内存模型(isa、对象、类、metaclass、结构体的存储信息等)
- 对象(实例)
OC 中的对象指向的是一个 objc_object 指针类型,typedef struct objc_object *id;从它的结构体中可以看出,它包括一个 isa 指针,指向的是这个对象的类对象,一个对象实例就是通过这个 isa 找到它自己的 Class,而这个 Class 中存储的就是这个实例的方法列表、属性列表、成员变量列表等相关信息的。 - 类(Class)
- 在 OC 中的类是用 Class 来表示的,实际上它指向的是一个 objc_class 的指针类型,typedef struct objc_class *Class.
- OC 的 Class 类型包括如下 元数据(metadata):super_class(父类类对象);name(类对象的名称);version、info(版本和相关信息);instance_size(实例内存大小);ivars(实例变量列表);methodLists(方法列表);cache(缓存);protocols(实现的协议列表);
- 当然也包括一个 isa 指针,这说明 Class 也是一个对象类型,所以我们称之为类对象,这里的 isa 指向的是元类对象(metaclass),元类中保存了创建类对象(Class)的类方法的全部信息。
- OC 对象、类、元类之间的关系
对象(实例)的 isa 指针指向类对象(Class),类对象的 isa 指针指向元类(meta Class), 元类的 isa 指针指向根元类(root meta Class). 根元类的 isa 指针指向他自己。
2.为什么要设计 metaclass?
- 类对象、元类对象能够复用消息发送流程机制;
- 单一职责原则
metaclass 代表的是类对象的对象,它存储了类的类方法,它的目的是将实例和类的相关方法列表以及构建信息区分开来,方便各司其职,符合单一职责设计原则。
3.class_copyIvarList & class_copyPropertyList 区别
- class_copyIvarList
会返回所有的成员变量,包括属性生成的成员变量。
获取类对象中的所有实例变量信息,从 class_ro_t 中获取。 - class_copyPropertyList
获取类对象中的属性信息, class_rw_t 的 properties,先后输出了 category / extension/ baseClass 的属性,而且仅输出当前的类的属性信息,而不会向上去找 superClass 中定义的属性。
1 | @interface ViewController () |
4.class_rw_t 和 class_ro_t 的区别
class_ro_t 存储了当前类在编译期就已经确定的属性、方法以及遵循的协议,里面是没有分类的方法的。 ro = read only
class_rw_t 运行时添加的方法将会存储在运行时生成的 class_rw_t 中。 rw = read write
5.category 如何被加载的,两个 category 的 load 方法的加载顺序,两个 category 的同名方法的加载顺序
+load 方法是 images 加载的时候调用,先调用父类的方法然后才是本类的方法。 category 的 +load 则是按照编译顺序来的,先编译的先调用,后编译的后调用,可在 Xcode 的 BuildPhase 中查看
分类添加到了 rw = cls->data() 中的 methods/properties/protocols 中,实际上并无覆盖,只是查找到就返回了,导致本类函数无法加载。同名方法调用后编译的。
6.initialize && Load
1 | + (void)load { |
load 方法会先调用,initialize 方法 当前对象第一次初始化创建时才会调用,如果该对象的父类的 initialize 方法还未调用,会先调用父类的方法。父类再初始化创建时不再调用。。这两个方法在 App 生命周期内仅调用一次。 category 的 load 方法按照编译顺序,先编译先调用。category 实现 initialize 方法那么本类的不会调用,多个 category 都实现调用最后编译的(这时走的是消息发送流程)。编译顺序在 BuildPhase 中查看。
不管是 load 还是 initialize 方法都是 runtime 底层自动调用的,如果开发自己手动进行了 [super load] ,那么会调用父类的 load 方法。
调用 [super initialize] 方法,那么调用父类的 initialize 方法。 实际上是走消息发送流程。
7.category & extension 区别,能给 NSObject 添加 Extension 吗,结果如何
- category 可以给类添加方法和属性(需要借助 runtime
objc_setAssociatedObject(self, &redXXKey, redXX, OBJC_ASSOCIATION_COPY_NONATOMIC);
及objc_getAssociatedObject(self, &redXXKey);
) - extension 可以给类添加成员变量和方法,但是是私有的在 .m 内(只有在.m 里才是私有的)。
- 不能给 NSObject 及系统类 添加 Extension,必须有源码才行 .m
8.在方法调用的时候,方法查询-> 动态解析-> 消息转发 之前做了什么
OC 中的方法调用,编译后的代码最终都会转成 objc_msgSend(id , SEL, ...)
方法进行调用。这个方法第一个参数是一个消息接收者对象,runtime 通过这个对象的 isa 指针找到这个对象的类对象,从类对象中的 cache 中查找(哈希查找,bucket 桶实现)是否存在 SEL 对应的 IMP,如果不存在,则会在 method_list 中查找(二分查找或者顺序查找),如果还是没找到,则会到 supper_class 中查找,仍然没找到的话,就会调用_objc_msgForward(id, SEL, ...)
进行消息转发。
9.IMP、SEL、Method 的区别和使用场景
IMP:IMP 定义为
id (*IMP) (id, SEL, …)
。这样说来,IMP 是一个指向函数的指针,这个被指向的函数包括 id(“self”指针),调用的 SEL(方法名),再加上一些其他参数.说白了 IMP 就是实现方法。SEL:是“selector”的一个类型,表示一个方法的名字
Method:(我们常说的方法)表示一种类型,这种类型与 selector 和实现(implementation)相关
10.load、initialize 方法的区别什么?在继承关系中他们有什么区别
load 会在程序启动后自动调用,initialize 会在当前对象初始化创建时调用。在 App 生命周期内都是仅调用一次。
继承中会先执行父类的 load 方法然后才是本类的,继承中如果本类的父类还未执行过 initialize 方法,父类会先调用 initialize 方法,然后才是本类。如果本类未实现 initialize 方法,会继承父类的并调用执行一次。
11.说说消息转发机制的优劣
- 优势:动态特性,可以动态的为对象添加方法,也可以将消息转发给其他对象去处理间接实现多继承。
- 劣势:当一个方法只声明没实现时,编译不会出错,运行时会崩溃。
消息转发三部曲
1. 第一步动态添加一个实现方法。 如果是实例对象会调用这个实例所属的类方法resolveInstanceMethod
,如果是类方法会调用+ (BOOL)resolveClassMethod:(SEL)sel;
我们可以动态的给该对象添加该方法。
1 | +(BOOL)resolveInstanceMethod:(SEL)sel { |
2. 消息转发,可以将消息转发给实现了该方法的对象。 系统会调用下面的这个方法。
1 | - (id)forwardingTargetForSelector:(SEL)aSelector { |
3. 完整的消息转发
先调用 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
获取一个方法签名,如果没有返回一个有效的签名则直接调用- (void)doesNotRecognizeSelector:(SEL)aSelector;
抛出异常。
如果获取到则将签名包装成NSInvocation
传给- (void)forwardInvocation:(NSInvocation *)anInvocation;
进行消息转发(也可以在这个方法里不进行处理,直接丢弃。但是该方法必须实现)。
1 | // 返回一个方法签名 |
12.weak 的实现原理?SideTable 的结构是什么样的
runtime 维护着一个 weak 表,这个表是 hash 表。以 weak 指向对象的内存地址为 key,
value 是存放着所有的 weak 指针地址的数组。当对象的引用计数为 0 被回收的时候,会在这个 hash 表中以对象的内存地址为 key 找到所有的 weak 指针置为 nil.
runtime 源码,objc-weak.m 的 arr_clear_deallocating 函数
weak 指针的使用涉及到 Hash 表的增删改查,有一定的性能开销.
SideTable 结构
1 | struct SideTable { |
13.关联对象的应用?系统如何实现关联对象的
- 应用:给 category 添加属性。
- 如何实现关联的?
首先系统中有一个全局 AssociationsManager,里面有个 AssociationsHashMap 哈希表,哈希表中的 key 是对象的内存地址,value 是 ObjectAssociationMap,也是一个哈希表。
ObjectAssociationMap 中的 key 是我们设置关联对象时所设置的唯一 key,value 是 ObjcAssociation,里面存放着关联对象设置的值和内存管理的策略。
以void objc_setAssociatedObject(id object, const void * key,id value, objc_AssociationPolicy policy)
为例,
首先会通过 AssociationsManager 获取 AssociationsHashMap,然后以 object 的内存地址为 key,从 AssociationsHashMap 中取出 ObjectAssociationMap,若没有,则新创建一个 ObjectAssociationMap,
然后通过 key 获取旧值,以及通过 key 和 policy 生成新值 ObjcAssociation(policy, new_value),把新值存放到 ObjectAssociationMap 中,
若新值不为 nil,并且内存管理策略为 retain,则会对新值进行一次 retain,若新值为 nil,则会删除旧值,若旧值不为空并且内存管理的策略是 retain,则对旧值进行一次 release. - 其被释放的时候需要手动将所有的关联对象的指针置空么?
对这个问题我的理解是:当对象被释放时,需要手动移除该对象所设置的关联对象吗? 不需要,因为在对象的 dealloc 中,若发现对象有关联对象时,会调用_object_remove_assocations
方法来移除所有的关联对象,并根据内存策略,来判断是否需要对关联对象的值进行 release
1 | @interface UIView (XX) |
14.关联对象的如何进行内存管理的?关联对象如何实现 weak 属性
在关联对象时又一个参数objc_AssociationPolicy policy
设置内存管理策略
OBJC_ASSOCIATION_ASSIGN 类型的关联对象和 weak 有一定差别,而更加接近于 unsafe_unretained,
即当目标对象遭到摧毁时,属性值不会自动清空。
然后内部封装一个 weak 变量持有;或者不用 weak,但是还是封装一层,但是在 dealloc 中进行置为 nil 操作。
15.Autoreleasepool 的原理?所使用的的数据结构是什么
- Autoreleasepool 是由多个 AutoreleasePoolPage 以双向链表的形式连接起来的。
- 释放时机: 当前 RunLoop 迭代结束时候释放。
- Autoreleasepool 的基本原理:在每个自动释放池创建的时候,会在当前的 AutoreleasePoolPage 中设置一个标记位,在此期间,当有对象调用 autorelsease 时,会把对象添加到 AutoreleasePoolPage 中,若当前页添加满了,会初始化一个新页,然后用双向量表链接起来,并把新初始化的这一页设置为 hotPage,当自动释放池 pop 时,从最下面依次往上 pop,调用每个对象的 release 方法,直到遇到标志位。
- AutoreleasePoolPage 结构如下
1 | class AutoreleasePoolPage { |
16.ARC 的实现原理?ARC 下对 retain & release 做了哪些优化
- Automatic Reference Counting,自动引用计数,即 ARC,ARC 会自动帮你插入 retain 和 release 语句,
ARC 编译器有两部分,分别是前端编译器和优化器 - 前端编译器:前端编译器会为“拥有的”每一个对象插入相应的 release 语句。如果对象的所有权修饰符是
__strong
,那么它就是被拥有的。如果在某个方法内创建了一个对象,前端编译器会在方法末尾自动插入 release 语句以销毁它。而类拥有的对象(实例变量/属性)会在 dealloc 方法内被释放。事实上,你并不需要写 dealloc 方法或调用父类的 dealloc 方法,ARC 会自动帮你完成一切。此外,由编译器生成的代码甚至会比你自己写的 release 语句的性能还要好,因为编辑器可以作出一些假设。在 ARC 中,没有类可以覆盖 release 方法,也没有调用它的必要。ARC 会通过直接使用 objc_release 来优化调用过程。而对于 retain 也是同样的方法。ARC 会调用 objc_retain 来取代保留消息 - ARC 优化器: 虽然前端编译器听起来很厉害的样子,但代码中有时仍会出现几个对 retain 和 release 的重复调用。ARC 优化器负责移除多余的 retain 和 release 语句,确保生成的代码运行速度高于手动引用计数的代码
17.ARC 下哪些情况会造成内存泄漏
- 循环引用
- 注册通知,不移除
18. Method Swizzle 注意事项
如果直接替换,相当于交换了父类这个方法的实现,但这个新的实现是在子类中的,父类的实例调用这个方法时,会崩溃。建议先添加:class_addMethod
1 | AFNetworking 源码涉及代码 |
19. iOS 中内省的几个方法有哪些?内部实现原理是什么
对象在运行时获取其类型的能力称为内省
1 | -(BOOL) isKindOfClass: 判断是否是这个类或者这个类的子类的实例 |
20. 属性修饰符 atomic 的内部实现是怎么样的?能保证线程安全吗
实现机制:atomic 是 property 的修饰词之一,表示是原子性的,使用方式为@property(atomic)int age;,此时编译器会自动生成 getter/setter 方法,最终会调用 objc_getProperty 和 objc_setProperty 方法来进行存取属性。若此时属性用 atomic 修饰的话,在这两个方法内部使用 os_unfair_lock 来进行加锁,来保证读写的原子性。锁都在 PropertyLocks 中保存着(在 iOS 平台会初始化 8 个,mac 平台 64 个),在用之前,会把锁都初始化好,在需要用到时,用对象的地址加上成员变量的偏移量为 key,去 PropertyLocks 中去取。因此存取时用的是同一个锁,所以 atomic 能保证属性的存取时是线程安全的。注:由于锁是有限的,不用对象,不同属性的读取用的也可能是同一个锁
不能保证:atomic 在 getter/setter 方法中加锁,仅保证了存取时的线程安全,假设我们的属性是 @property(atomic)NSMutableArray *array;可变的容器时,无法保证对容器的修改是线程安全的
在编译器自动生产的 getter/setter 方法,最终会调用 objc_getProperty 和 objc_setProperty 方法存取属性,在此方法内部保证了读写时的线程安全的,当我们重写 getter/setter 方法时,就只能依靠自己在 getter/setter 中保证线程安全
os_unfair_lock(互斥锁) 锁,iOS 10 之前使用 OSSpinLock(自旋锁)不能完全保证。
OSSpinLock 忙等 是会一直循环等待,循环等待的时候会消耗 cpu 的性能
os_unfair_lock 线程休眠,cpu 线程调度的时候会消耗 cpu 性能
21. class、objc_getClass、object_getclass 方法有什么区别?
- object_getClass:获得的是 isa 的指向
- self.class:当 self 是实例对象的时候,返回的是类对象,否则则返回自身。
- 类方法 class,返回的是 self,所以当查找 meta class 时,需要对类对象调用 object_getClass 方法
22.NSNotification
1. 通知的发送时同步的,还是异步的
同步发送,所有接受者处理完后,才会走发送后下边的代码。
2. NSNotificationCenter 接受消息和发送消息是在一个线程里吗?如何异步发送消息
NSNotificationQueue
3.NSNotificationQueue 是异步还是同步发送?在哪个线程响应
通知队列,用于异步发送消息,这个异步并不是开启线程,而是把通知存到双向链表实现的队列里面,等待某个时机触发时调用 NSNotificationCenter 的发送接口进行发送通知,这么看 NSNotificationQueue 最终还是调用 NSNotificationCenter 进行消息的分发。
- 依赖 runloop,所以如果在其他子线程使用 NSNotificationQueue,需要开启 runloop
- 最终还是通过 NSNotificationCenter 进行发送通知,所以这个角度讲它还是同步的
- 所谓异步,指的是非实时发送而是在合适的时机发送,并没有开启异步线程
4.如何保证通知接收的线程在主线程
使用 addObserverForName: object: queue: usingBlock 方法注册通知,指定在 mainqueue 上响应 block
5.下面的方式能接收到通知吗?为什么
1 | // 发送通知 |
不会
存储是以 name 和 object 为维度的,即判定是不是同一个通知要从 name 和 object 区分,如果他们都相同则认为是同一个通知,后面包括查找逻辑、删除逻辑都是以这两个为维度的
23.runloop
1.app 如何接收到触摸事件的
1.系统响应阶段 (SpringBoad.app 是一个系统进程,可以理解为桌面系统,可以统一管理和分发系统接收到的触摸事件。)
- 指触碰屏幕,屏幕感应到触碰后,将事件交由 IOKit 处理。
- IOKit 将触摸事件封装成一个 IOHIDEvent 对象,并通过 mach port 传递给 SpringBoad 进程。
- SpringBoard 进程因接收到触摸事件,将触摸事件交给前台 app 进程来处理。
2.APP 响应阶段
- APP 进程的 mach port 接受到 SpringBoard 进程传递来的触摸事件,主线程的 runloop 被唤醒,触发了 source1 回调。
- source1 回调又触发了一个 source0 回调,将接收到的 IOHIDEvent 对象封装成 UIEvent 对象,此时 APP 将正式开始对于触摸事件的响应。
- source0 回调内部将触摸事件添加到 UIApplication 对象的事件队列中。事件出队后,UIApplication 开始一个寻找最佳响应者的过程,这个过程又称 hit-testing,另外,此处开始便是与我们平时开发相关的工作了。
- 寻找到最佳响应者后,接下来的事情便是事件在响应链中的传递及响应了。
- 触摸事件历经坎坷后要么被某个响应对象捕获后释放,要么致死也没能找到能够响应的对象,最终释放。至此,这个触摸事件的使命就算终结了。runloop 若没有其他事件需要处理,也将重归于眠,等待新的事件到来后唤醒。
2.事件响应者链
这个传递的过程叫做 hit-Testing
- 事件是自上而下传递的即
UIApplication -> UIWindow -> 子试图 -> ..->子试图中的子试图 - 后加试图响应程度更高,即最靠近我们的试图。
- 首先调用当前视图的 pointInside:withEvent:方法判断触摸点是否在当前视图内
- 若 pointInside:withEvent:方法返回 NO,说明触摸点不在当前视图内,则当前视图的 hitTest:withEvent:返回 nil
- 若 pointInside:withEvent:方法返回 YES,说明触摸点在当前视图内,则遍历当前视图的所有子视图(subviews),调用子视图的 hitTest:withEvent:方法重复前面的步骤,子视图的遍历顺序是从 top 到 bottom,即从 subviews 数组的末尾向前遍历,直到有子视图的 hitTest:withEvent:方法返回非空对象或者全部子视图遍历完毕。
- 若第一次有子视图的 hitTest:withEvent:方法返回非空对象,则当前视图的 hitTest:withEvent:方法就返回此对象,处理结束
- 若所有子视图的 hitTest:withEvent:方法都返回 nil,则当前视图的 hitTest:withEvent:方法返回当前视图自身(self)
1 | //判断点击的位置是不是在视图内 |
1 | - (UIView _)hitTest:(CGPoint)point withEvent:(UIEvent _)event{ |
3.为什么只有主线程的 runloop 是开启的
- 主线程需要维持一份 RunLoop,保持 App 在 Main 后不会直接退出。
- 其他线程默认并没有调用 NSRunLoop *runloop = [NSRunLoop currentRunLoop]
4.runloop 的 mode 作用是什么?
mode 主要是用来指定事件在运行循环中的优先级分为:
- NSDefaultRunLoopMode:默认,空闲状态
- UITrackingRunLoopMode: ScrollView 滑动
- UIInitializationRunloopMode: 启动时
- NSRunloopCommonModes:Mode 集合
苹果公开的有 2 个:
NSDefaultRunLoopMode
NSRunLoopCommonModes
定时在 scrollview 滑动时通过添加到 NSRunLoopCommonModes[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
5.为什么只在主线程刷新 UI
- UIKit 并不是一个 线程安全 的类,UI 操作涉及到渲染访问各种 View 对象的属性
- 如果异步操作下会存在读写问题,而为其加锁则会耗费大量资源并拖慢运行速度
- 另一方面因为整个程序的起点 UIApplication 是在主线程进行初始化,所有的用户事件都是在主线程上进行传递(如点击、拖动),所以 view 只能在主线程上才能对事件进行响应。而在渲染方面由于图像的渲染需要以 60 帧的刷新率在屏幕上 同时 更新,在非主线程异步化的情况下无法确定这个处理过程能够实现同步更新。
6.PerformSelector 和 runloop 的关系
perform 有几种方式,如 [self performSelector:@selector(perform) withObject:nil] 同步执行的,等同于 objc_msgSend 方法执行调用方法。
[self performSelector:@selector(perform) withObject:nil afterDelay:0]
则是会在当前 runloop 中起一个 timer,如果当前线程没有起 runloop(也就是上面说的没有调用[NSRunLoop currentRunLoop]
方法的话),则不会有输出
7.如何使线程保活
线程保活就是不让线程退出,所以往简单说就是搞个 “while(1)” 自己实现一套处理流程,事件派发就可以了。
runloop 线程保活前提就是有事情要处理,这里指 timer,source0,source1 事件。
1 | // Timer |
1 | // Port |
8.runloop 和线程有什么关系?
runloop 为线程而生,没有线程他就没有存在的必要。
runloop 是线程的基础架构部分,runloop 和线程是一一对应关系
主线程的 runloop 是默认启动的,其他线程的 runloop 是没有启动的,
如果你需要更多的线程交互则可以手动配置和启动。
24.KVO、KVC
1.KVO 实现原理
当你观察一个对象时,系统会动态的创建一个以 NSKVONotifying_
为前缀的类。
然后将被观察对象的 isa 指针指向这个新创建的类。
这个类继承自该对象的原本类,并重写了被观察属性的 setter 方法。
同时也会重写 class 方法,返回原先类对象,这样外部就无感知了。
重写所有要观察属性的 setter 方法,统一会走一个方法,
内部实现是 willChangeValueForKey 和 didChangevlueForKey 方法,
然后就是 observeValueForKey:ofObject:change:context:
通知所有观察对象值的更改。
2.如何手动关闭 KVO
1 | +(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { |
3.通过 KVC 修改属性会触发 KVO 么
会触发 KVO 操作,KVC 时候会先查询对应的 getter 和 setter 方法。
4.KVC
KVC(key-value-codeing)键值编码 是通过一种字符串间接访问对象的方式(即给属性赋值)
KVC 调用 getter 流程:getKEY,KEY,isKEY, _KEY,接着是实例变量 _KEY,_isKEY, KEY, isKEY;
KVC 调用 setter 流程:setKEY 和 _setKEY,实例变量顺序 _KEY,_isKEY, KEY, isKEY,没找到就调用 setValue: forUndefinedKey: 当一个对象调用 setValue 方法时,方法内部会做以下操作
- 检查是否存在相应的 key 的 set 方法,存在就调用 set 方法
- 如果 set 方法不存在就找带下划线的成员变量,如果有就直接给成员变量属性赋值。
- 如果没有找到_key 就会找相同属性名的 key,有就直接赋值
- 如果还没找到,就调用 valueForUndefinedKey:和 setValue:forUndefinedKey:方法。这些方法就抛出异常崩溃了。
5.哪些情况下使用 kvo 会崩溃,怎么防护崩溃
- 添加了观察者,但未实现 observeValueForKeyPath:ofObject:change:context:方法,导致崩溃;
- 添加或者移除时 keypath == nil,导致崩溃;
- 多次重复移除同一个属性,移除了未注册的观察者
6.KVO/KVC 的优缺点?
- KVC
优点:没有 property 的变量(私有)也能通过 KVC 进行设置,或者简化代码(多级属性)
缺点:如果 key 只写错,编写的时候不会报错,但是运行的时候会报错 - KVO 优点:
能够提供一种简单的方法实现两个对象的同步;
能够对内部对象的状态改变作出响应,而且不需要改变内部对象的实现;
能够提供被观察者属性的最新值和之前的值;
使用 key Path 来观察属性,因此可以观察嵌套对象;
完成了对观察对象的抽象,因为不需要额外的代码来允许观察者被观察。 - KVO 缺点:
KVO 只能检测类中的属性,并且属性名都是通过 NSString 来查找,编译器不会补全(编译时不会出现警告),容易写错;
对属性重构,将导致观察代码不可用;
复杂的 “if” 语句要求对象正在观察多个值,是因为所有的观察代码通过一个方法来指向;
25. Block
1.block 的内部结构和作用
1 | struct Block_layout { |
通过上面的结构, 可以看出一个 block 实例的构成实际上有 6 个部分:
- isa 指针: 所有对象都有该指针,用于实现对象相关的功能。
- flags: 附加标识位, 在 copy 和 dispose 等情况下可以用到。
- reserved:保留变量。
- invoke: 函数指针,指向 block 的实现代码, 也可以说是函数调用地址。
- descriptor: 表示该 block 的附加描述信息,主要是 size,以及 copy 和 dispose 函数的指针。这两个辅助函数在拷贝及丢弃块对象时运行, 其中会执行一些操作, 比方说, 前者要保留捕获的对象,而后者则将之释放。
- variables: 捕获的变量,block 能够访问它外部的局部变量,就是因为将这些变量复制到了结构体中。
2.block 的类型
block 其实是有类型的, 且一共有 3 种类型, 全局块, 栈块, 堆块:
__NSGlobalBlock__
: 存储在全局/静态的 block,不会捕获任何外部变量。__NSStackBlock__
: 存储在栈中的 block,当函数返回时会被销毁。__NSMallocBlock__
: 存储在堆中的 block,当引用计数为 0 时会被销毁。
- NSGlobalBlock
这种块不会捕捉任何变量, 运行时也无须有状态来参与。全局块声明在全局内存里, 在编译期已经完全确定了。所以, 无论是 ARC 还是 MRC 下, 如下代码中的 block 都是全局静态的。
1 | // NSGlobalBlock |
- NSStackBlock 或 NSMallocBlock
ARC 下为 NSMallocBlock(堆区), MRC 下为 NSStackBlock(栈区)
1 | // ARC下为NSMallocBlock(堆区), MRC下为NSStackBlock(栈区) |
- NSMallocBlock
要问 MRC 下有没有存储于堆区的 block, 当然有了。但 block 默认会分配在栈区, 需要保留的话, 也可以手动改到堆区, 这样它就是堆块了
1 | // MRC下为NSMallocBlock(堆区), ARC下为NSMallocBlock(堆区) |
block 在 ARC 和 MRC 下都是存储于堆区的, 所以其类型是 NSMallocBlock 的。
为了解决栈块在其变量作用域结束之后被释放的问题,我们需要把 block copy 到堆中,延长其生命周期。在开启 ARC 时,编译器会判断其是不是全局块, 若不是全局块则需要将 block 从栈 copy 到堆中,并自动生成相应代码。所以, 上面的例子中, 本不用手动添加 copy 代码的, ARC 会帮我们来做这个事情。
- NSStackBlock
创建的 block 没有被持有的时候,编译器就不会做出将其拷贝到堆区的操作,所以这种情况下,它还在栈区。
1 | - (void)stackBlockInARC { |
2.block 类型总结
总结一下, 在 MRC 中, 可能有三种 block, 就是全局块, 栈块和堆块。 但是在 ARC 中, 一版情况下只有两种 block, 即全局块和堆块。由于 ARC 已经能很好地处理对象的生命周期的管理, 所以都放到堆上管理, 不再使用栈区管理了, 所以就栈块的情况就很少了。
而且捕获了变量的 block 默认会分配在栈区, 在 MRC 中需要保留的话, 可以手动改到堆区; 在 ARC 中, block 也是在栈区的, 但编译器会并自动将其 copy 到堆中, 所以会存储在堆区。所以每一个堆块都是由栈块 copy 而来的。
在 ARC 下, 当你所创建的 block 没有被指针所持有的时候,编译器就不会做出将其拷贝到堆区的操作。在这种情况下,block 就是一个直接的栈块。
3.block 捕获局部变量
不加 __block
捕获的是值,加了捕获的是指针
1 | - (void)captureVariable { |
上面的例子 不加__block
在构建 block 的时候, 传入的捕获变量是变量 a 的值(即传入 a)**。 所以对于 block 捕获的变量, block 默认是将其复制到其数据结构中来实现访问的, **且 block 捕获的变量是在 block 内部进行修改是不会影响外部变量的。
1 | - (void)capture__blockVariable { |
上面的例子使用 __block
修饰 在 ARC 下, block 内部和 block 后的地址是相同的都存在于堆中, 且与 block 前的地址不同。在构建 block 时, 传入捕获变量 a 的地址(即传入&a)。所以对于 block 捕获的__block
修饰的变量,block 是复制其引用地址来实现访问的。自然就可以在 block 内部修改变量从而影响外部的变量了, 且 block 内外打印其地址都是同一个地址。
这里的 copy, 都是浅拷贝, 就是所谓的指针拷贝, 所以 a 指针指向的内存地址还是之前定义对象 a 的某块堆区区域。
4.block 可以用 strong 修饰吗
ARC 是可以的 strong 和 copy 的操作都是将栈上 block 拷贝到堆上。
5.block 在修改 NSMutableArray,需不需要添加__block
不需要,本身 block 内部就捕获了 NSMutableArray 指针,除非你要修改指针指向的对象,而这里明显只是修改内存数据,这个可以类比 NSMutableString。
6.怎么进行内存管理的
_NSConcreteGlobalBlock
:是设置在程序的全局数据区域(.data 区)中的 Block 对象。在全局声明实现的 block 或者 没有用到自动变量的 block 为_NSConcreteGlobalBlock
,生命周期从创建到应用程序结束。_NSConcreteStackBlock
是设置在栈上的 block 对象,生命周期由系统控制的,一旦所属作用域结束,就被系统销毁了。
7.block 发生 copy 时机
- 调用 Block 的 copy 方法
- 将 Block 作为函数返回值时
- 将 Block 赋值给__strong 修饰的变量或 Block 类型成员变量时
- 向 Cocoa 框架含有 usingBlock 的方法或者 GCD 的 API 传递 Block 参数时
26.多线程
1.iOS 开发中有多少类型的多线程?分别对比
1. Pthread,较少使用。
2. NSThread,每个 NSThread 对象对应一个线程,量级较轻,通常我们会起一个 runloop 保活,然后通过添加自定义 source0 源或者 perform onThread 来进行调用。
- 优点 轻量级,使用简单,
- 缺点:需要自己管理线程的生命周期,保活,另外还会线程同步,加锁、睡眠和唤醒。
3. GCD:Grand Central Dispatch(派发) 是基于 C 语言的框架,可以充分利用多核,是苹果推荐使用的多线程技术
- 优点:GCD 更接近底层,而 NSOperationQueue 则更高级抽象,所以 GCD 在追求性能的底层操作来说,是速度最快的,有待确认
- 缺点:操作之间的事务性,顺序行,依赖关系。GCD 需要自己写更多的代码来实现
4. NSOperation 基于 GCD 更高一层封装
- 优点: 使用者的关注点都放在了 operation 上,而不需要线程管理。
支持在操作对象之间依赖关系,方便控制执行顺序。
支持可选的完成块,它在操作的主要任务完成后执行。
支持使用 KVO 通知监视操作执行状态的变化。
支持设定操作的优先级,从而影响它们的相对执行顺序。
支持取消操作,允许您在操作执行时暂停操作。 - 缺点:高级抽象,性能方面相较 GCD 来说不足一些;
2.GCD 有哪些队列,默认提供哪些队列
主队列(main queue )【串行】
保证所有的任务都在主线程执行,而主线程是唯一用于 UI 更新的线程。此外还用于发送消息给视图或发送通知。四个全局调度队列(high、default、low、background【并发】
Apple 的接口也会使用这些队列,所以你添加的任何任务都不会是这些队列中唯一的任务自定义队列
- 多个任务以串行方式执行,但又不想在主线程中
- 多个任务以并行方式执行,但不希望队列中有其他系统的任务干扰。
3.GCD 主线程 & 主队列的关系
队列其实就是一个数据结构体,主队列由于是串行队列,所以入队列中的 task 会逐一派发到主线程中执行;但是其他队列也可能会派发到主线程执行
4.如何实现同步,有多少方式就说多少
- dispatch_sync
- @synchronized
- dispatch_group,
- dispatch_semaphore
- NSLock/NSRecursiveLock
- pthread_mutex_t 互斥锁、递归锁等
5.有哪些类型的线程锁,分别介绍下作用和使用场景
- @synchronized 性能最差,SD 和 AFN 等框架内部有使用这个.
- NSRecursiveLock 和 NSLock :建议使用前者,避免循环调用出现死锁
- OSSpinLock 自旋锁,存在的问题是:优先级反转问题,破坏了 spinlock
- dispatch_semaphore 信号量 : 保持线程同步为线程加锁
27.通过[UIImage imageNamed:]生成的对象什么时候被释放?
- 建议针对小图标/场景出现较多图片(此类方式加载,会缓存到内存)
- @autoreleasepool 如果没有使用局部释放池,并且在主线程,则是当前主线程 Runloop 一次循环结束前释放。
- imageWithContentsOfFile : 加载适用于大图片,不常用的图片,一般无引用时候,会释放
imageName & imageWithContentsOfFile 区别
- 如果图片较小,并且使用频繁的图片使用 imageNamed:方法来加载。相同的图片是不会重复加载的
- 如果图片较大,并且使用较少,使用 imageWithContentOfFile:来加载。加载:imageWithContentsOfFile 只能加载 mainBundle 中图片。
- 当你不需要重用该图像,或者你需要将图像以数据方式存储到数据库,又或者你要通过网络下载一个很大的图像时,使用 imageWithContentsOfFile;
- 如果在程序中经常需要重用的图片,比如用于 UITableView 的图片,那么最好是选择 imageNamed 方法。这种方法可以节省出每次都从磁盘加载图片的时间;
28.UIView & CALayer 的区别
- UIView 为 CALayer 提供内容,以及负责处理触摸等事件,参与响应链;
- CALayer 负责显示内容 contents
layoutsubviews
- init 初始化不会触发 layoutSubviews。
- addSubview 会触发 layoutSubviews。
- 改变一个 UIView 的 Frame 会触发 layoutSubviews,当然前提是 frame 的值设置前后发生了变化。
- 滚动一个 UIScrollView 引发 UIView 的重新布局会触发 layoutSubviews。
- 旋转 Screen 会触发父 UIView 上的 layoutSubviews 事件。
- 直接调用 setNeedsLayout 或者 layoutIfNeeded。
- setNeedsLayout 标记为需要重新布局,异步调用 layoutIfNeeded 刷新布局,不立即刷新,在下一轮 runloop 结束前刷新,对于这一轮 runloop 之内的所有布局和 UI 上的更新只会刷新一次,layoutSubviews 一定会被调用。
- layoutIfNeeded 如果有需要刷新的标记,立即调用 layoutSubviews 进行布局(如果没有标记,不会调用 layoutSubviews)。
drawrect
- 如果在 UIView 初始化时没有设置 frame,会导致 drawRect 不被自动调用
- sizeToFit 后会调用。这时候可以先用 sizeToFit 中计算出 size,然后系统自动调用 drawRect 方法
- 通过设置 contentMode 为.redraw 时,那么在每次设置或更改 frame 的时候自动调用 drawRect
- 直接调用 setNeedsDisplay,或者 setNeedsDisplayInRect 会触发 drawRect
29. 图片是什么时候解码的,如何优化
当你用 UIImage 或 CGImageSource 的那几个方法创建图片时,图片数据并不会立刻解码。图片设置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。
图片加载
- 假设我们使用 +imageWithContentsOfFile: 方法从磁盘中加载一张图片,这个时候的图片并没有解压缩
- 然后将生成的 UIImage 赋值给 UIImageView ;
- 接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化;
- 在主线程的下一个 run loop 到来时,Core Animation 提交了这个隐式的 transaction ,这个过程可能会对图片进行 copy 操作,而受图片是否字节对齐等因素的影响,这个 copy 操作可能会涉及以下部分或全部步骤:
- 分配内存缓冲区用于管理文件 IO 和解压缩操作;
- 将文件数据从磁盘读到内存中;
- 将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作;
- 最后 Core Animation 使用未压缩的位图数据渲染 UIImageView 的图层。
图片解码
解码操作是比较耗时的,并且没有 GPU 硬解码,只能通过 CPU,iOS 默认会在主线程对图像进行解码。解码过程是一个相当复杂的任务,需要消耗非常长的时间。60FPS ≈ 0.01666s per frame = 16.7ms per frame,这意味着在主线程超过 16.7ms 的任务都会引起掉帧。很多库都解决了图像解码的问题,不过由于解码后的图像太大,一般不会缓存到磁盘,SDWebImage 的做法是把解码操作从主线程移到子线程,让耗时的解码操作不占用主线程的时间。
当加载图片的时候,iOS 通常会延迟解压图片的时间,直到加载到内存之后。因为需要在绘制之前进行解压,这就会在准备绘制图片的时候影响性能。
iOS 通常会延时解压图片,等到图片在屏幕上显示的时候解压图片。解压图片是非常耗时的操作。
30.性能优化
1.如何做启动优化,如何监控
- 合并或者删减一些 OC 类和函数;可以使用 AppCode 清理项目中没用到的类,属性等。
- 将不必须在+load 方法中做的事情延迟到+initialize 中
- 类和方法名不要太长:iOS 每个类和方法名都在__cstring 段里都存了相应的字符串值,所以类和方法名的长短也是对可执行文件大小是有影响的;因还是 object-c 的动态特性,因为需要通过类/方法名反射找到这个类/方法进行调用,object-c 对象模型会把类/方法名字符串都保存下来;
- 在设计师可接受的范围内压缩图片的大小,启动的时候大大小小的图片加载个十来二十个是很正常的,图片小了,IO 操作量就小了,启动当然就会快了,比较靠谱的压缩算法是 TinyPNG。
- 减少依赖不必要的库,不管是动态库还是静态库;如果可以的话,把动态库改造成静态库;如果必须依赖动态库,则把多个非系统的动态库合并成一个动态库;
- 删减一些无用的静态变量,没有被调用到或者已经废弃的方法。
2.如何做卡顿优化,如何监控
CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。
卡顿原因
标准情况下,页面滑动流畅是 60FPs ,就是每一秒有 60 帧的画面刷新,每 16.7ms(1/60 秒)有一帧数据。上图两个 VSync 之间的时间就是 16.7ms。
如果 CPU 和 GPU 加起来的处理时间超过了 16.7ms,就会造成掉帧甚至卡顿。当 FPs 帧数低于 30 时,人的肉眼就能感觉到画面明显的卡顿。
卡顿监控
- 思路一:监控一秒钟内的帧数是否经常低于或远低于 60FPs。
- 思路二:监控每一帧的时长是否超时。
思路一实现方法:用 CADisplayLinker 来计数
CADisplayLink 可以以屏幕刷新的频率调用指定 selector,iOS 系统中正常的屏幕刷新率为 60 次/秒,只要在这个方法里面统计每秒这个方法执行的次数,通过次数/时间就可以得出当前屏幕的刷新率了。
思路二实现方法:通过子线程监测主线程的 RunLoop,判断两个状态 RunLoop 的状态区域之间的耗时是否达到一定阈值。
开启子线程,实时计算这两个状态区域之间的耗时是否到达某个阀值,便能揪出这些性能杀手,假定连续 6 次超时 50ms 认为卡顿(当然也包含了单次超时 300ms)
卡顿优化
图像显示的工作是由 CPU 和 GPU 协同完成的, 那么优化的方向和思路就是尽量减少他们的处理时长。
对 CPU 处理的优化:
在子线程中进行对象的创建,调整和销毁,节省一部分 CPU 的时间
在子线程中预排版(布局计算,文本计算),让主线程有更多的时间去响应用户的交互
对文本等异步绘制,图片编解码等内容进行 预渲染、预排版对 GPU 处理的优化
尽量避免使用 CALayer 的 Border、corner、shadow、mask 等技术,这样能少触发离屏渲染
尽可能将多张图片合成为一张进行显示,减轻视图层级
31.MVVM 和 MVC 的区别
MVC
M:数据模型, V:视图, C:控制器
controller 层拿到 model 更新 view,
view 事件传递到 controller 层,更新 model
弊端:C 控制器层 代码逻辑较多
MVVM
M:数据模型,V:视图、控制器,VM:处理逻辑、网络
- View 引用 ViewModel,但 ViewModle 不能引用 View 视图、控制器。
- ViewModel 可以引用 Model, Model 不能引用 ViewModel
- viewController 尽量不涉及业务逻辑,让 viewModel 去做这些事情。
- viewController 只是一个中间人,接收 view 的事件、调用 viewModel 的方法、响应 viewModel 的变化。
优势:
- 低耦合:View 可以独立于 Model 变化和修改,一个 viewModel 可以绑定到不同的 View 上
- 可重用性:可以把一些视图逻辑放在一个 viewModel 里面,让很多 view 重用这段视图逻辑
- 独立开发:开发人员可以专注于业务逻辑和数据的开发 viewModel,设计人员可以专注于页面设计
- 可测试:通常界面是比较难于测试的,而 MVVM 模式可以针对 viewModel 来进行测试
弊端:
- 对于过大的项目,数据绑定和数据转化需要花费更多的内存(成本)。主要成本在于:
- 数据绑定使得 Bug 很难被调试。你看到界面异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得一个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。
32.用过设计模式介绍下
单例模式: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
- 优点: 1. 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。2. 避免对资源的多重占用(比如写文件操作)。
- 缺点: 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
工厂模式: 定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。
- 优点: 1、一个调用者想创建一个对象,只要知道其名称就可以了。 2、扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。 3、屏蔽产品的具体实现,调用者只关心产品的接口。
- 缺点:每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。
观察者模式: 系统的 KVO
33.描述一个 ViewController 的生命周期
- 当我们调用 UIViewControlller 的 view 时,
- 系统首先判断当前的 UIViewControlller 是否存在 view,如果存在直接返回 view,
- 如果不存在的话,会调用 loadview 方法,
- 然后判断 loadview 方法是否是自定义方法,
- 如果是自定义方法,就执行自定义方法,
- 如果不是自定义方法,判断当时视图控制器是否有 xib、stroyboard。
- 如果有 xib、stroyboard 就加载 xib、stroyboard。
- 如果没有创建一个空白的 view。
- 调用 viewDidLoad 方法。
- 最后返回 view
- Post title:Runtime 相关知识
- Post author:xxxixxxx
- Create time:2021-03-08 22:35:00
- Post link:https://xxxixxx.github.io/2021/03/08/6666-001-runtime/
- Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.