IOS 知识点整理

495 阅读36分钟

1. KVC 的工作原理

Key-Value Coding(键值编码)是由 NSKeyValueCoding 非正式协议启用的一种机制,对象采用这种机制来提供对其属性/成员变量的间接访问。

KVC设值原理: image.png KVC取值原理: image.png

通过KVC修改属性会触发KVO么?

是会触发的,通过KVC修改对象属性的值,无论是否调用了setKey方法,始终都会触发KVO。这是因为KVC修改对象属性的值过程中会调用对象的willChangeValueForKey:和didChangeValueForKey:方法,只要调用了这两个方法,就会触发KVO。

2. KVO 的工作原理

KVO (自动键值观察)是通过 isa-swizzling (交换)实现的。基本的流程就是编译器自动为被观察者对象创造一个派生类(此派生类的父类是被观察者对象所属的类),并将被观察者对象的 isa 指向这个派生类(类名是 NSKVONotifying_XXX)。如果用户注册了对此目标对象的某一个属性的观察,那么此派生类会重写这个属性的 setter 方法,并在其中添加进行通知的代码。Objective-C 在发送消息的时候,会通过 isa 指针找到当前对象所属的类对象。而类对象中保存着当前对象可调用的实例方法,因此在向此对象发送消息时候,实际上是发送到了派生类对象的方法。由于编译器对派生类的方法进行了重写,并添加了通知代码,因此会向注册的观察者对象发送通知。注意派生类只重写注册了观察者的属性方法。

3. iOS 中的方法缓存、快速查找、慢速查找、消息转发流程

struct objc_class {
  Class isa;
  Class superclass;
  cache_t cache; //方法缓存
  class_data_bits_t bits;//用于获取具体的类信息
};

Class内部结构中有一个方法缓存cache_t,用散列表(哈希表)来缓存之前调用过的方法,可以提高方法的查找速度.

struct  cache_t {
  struct bucket_t *_buckets;//散列表
  mask_t _mask;//散列表的长度 -1
  mask_t _occupied;//已经缓存的方法数量
};
struct bucket_t {
  cache_key_t _key;//SEL作为key
  IMP _imp;//函数的内存地址
}

objc_msgSend 是怎么实现的呢?

乍看它以为是一个 C/C++ 函数,但它其实是汇编实现的。 使用汇编的原因,除了 快速,方法的查找操作是很频繁的,汇编是相对底层的语言更容易被机器识别,节省中间的一些编译过程 还有一些重要的原因,用汇编实现,是为了应对不同的 “Calling convention”,把函数调用前的栈和寄存器的参数、状态设置,交给编译器去处理。 快速查找

image.png 慢速查找

image.png 消息转发流程 image.png

消息传递机制简化版 image.png image.png

iOS 响应链和事件传递

响应者链:由多个响应者组合起来的链条,就叫做响应者链。它表示了每个响应者之间的联系,并且可以使得一个事件可选择多个对象处理

image.png

如何判断上一个响应者:

  • 1、如果当前这个view是控制器的view,那么控制器就是上一个响应者
  • 2、如果当前这个view不是控制器的view,那么父控件就是上一个响应者 响应者链的事件传递过程:
  • 1、如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图
  • 2、在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理
  • 3、如果window对象也不处理,则其将事件或消息传递给UIApplication对象
  • 4、如果UIApplication也不能处理该事件或消息,则将其丢弃

事件传递过程:

  • iOS系统检测到手指触摸(Touch)操作时会将其打包成一个UIEvent对象,并放入当前活动Application的事件队列,单例的UIApplication会从事件队列中取出触摸事件并传递给单例的UIWindow来处理,UIWindow对象首先会使用hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图(最合适来处理的控件),这个过程称之为hit-test view。 两个重要的响应方法(UIView的)
  • hit-test view:事件传递给控件的时候, 就会调用该方法,去寻找最合适的view并返回看可以响应的view
  • pointInside: 该方法判断触摸点是否在控件身上,是则返回YES,否则返回NO,point参数必须是方法调用者的坐标系.

6. UIView和CALayer的区别

  • 每个 UIView 内部都有一个 CALayer 在背后提供内容的绘制和显示,并且 UIView 的尺寸样式都由内部的 Layer 所提供。两者都有树状层级结构,layer 内部有 SubLayers,View 内部有 SubViews.但是 Layer 比 View 多了个AnchorPoint
  • 在 View显示的时候,UIView 做为 Layer 的 CALayerDelegate,View 的显示内容由内部的 CALayer 的 display
  • CALayer 是默认修改属性支持隐式动画的,在给 UIView 的 Layer 做动画的时候,View 作为 Layer 的代理,Layer 通过 actionForLayer:forKey:向 View请求相应的 action(动画行为)
  • layer 内部维护着三分 layer tree,分别是 presentLayer Tree(动画树),modeLayer Tree(模型树), Render Tree (渲染树),在做 iOS动画的时候,我们修改动画的属性,在动画的其实是 Layer 的 presentLayer的属性值,而最终展示在界面上的其实是提供 View的modelLayer
  • 两者最明显的区别是 View可以接受并处理事件,而 Layer 不可以

7.进程和线程、并行和并发、同步和异步知识点

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存,是操作系统结构的基础。

线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

线程与进程的区别可以归纳为

  • 1、进程是一段正在执行的程序,是资源分配的基本单元,而线程是CPU调度的基本单元。
  • 2、进程间相互独立进程,进程之间不能共享资源,一个进程至少有一个线程,同一进程的各线程共享整个进程的资源(寄存器、堆栈、上下文)。
  • 3、线程的创建和切换开销比进程小。

并发:在同一个时间段内,两个或多个程序执行,有时间上的重叠(宏观上是同时,微观上仍是顺序执行)。在操作系统中,并发是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。

并行:当系统有一个以上 CPU 时,则线程的操作有可能非并发。当一个 CPU 执行一个线程时,另一个 CPU 可以执行另一个线程,两个线程互不抢占 CPU 资源,可以同时进行,这种方式我们称之为并行(Parallel)。

并发与并行的区别:

并发和并行是即相似又有区别的两个概念,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。

同步 sync:只能在当前线程按先后顺序依次执行任务,不具备开启新线程的能力。(阻塞当前线程,等待任务执行完成)

异步 async:在新的线程中执行任务,具备开启新线程的能力。(不阻塞当前线程,不等待任务执行完成)

7. iOS 中的线程锁都有哪些?

iOS 开发中使用到的锁,包括 spinlock_t、os_unfair_lock、pthread_mutex_t、NSLock、NSRecursiveLock、NSCondition、NSConditionLock、@synchronized、dispatch_semaphore、pthread_rwlock_t。

  • OSSpinLock(自旋锁): 不是一个线程安全的锁,可能会导致优先级反转
  • os_unfair_lock: 是一个低等级锁, 跟 OSSpinLock 不同,等待 os_unfair_lock 的线程会处于休眠状态(类似 run loop 那样),不是忙等(busy-wait)。
  • pthread_mutex_t: 是 C 语言下多线程互斥锁的方式,是跨平台使用的锁,等待锁的线程会处于休眠状态,可根据不同的属性配置把 pthread_mutex_t 初始化为不同类型的锁,例如:互斥锁、递归锁、条件锁。
  • NSLock:基于 mutex 基本锁的封装,更加面向对象,等待锁的线程会处于休眠状态。 继承自 NSObject 并遵循 NSLocking协议,NSLocking 协议中仅有两个方法 -(void)lock 和 -(void)unlock。
  • NSRecursiveLock: 是递归锁,和 NSLock 的区别在于,它可以在同一个线程中重复加锁也不会导致死锁。也是基于 mutex 的封装,遵守 NSLocking 协议.
  • NSCondition:对象实际上作为一个锁和一个线程检查器,可以根据条件决定是否继续运行线程,基于 mutex 基础锁和 cont 条件的封装,所以它是互斥锁且自带条件,等待锁的线程休眠
  • NSConditionLock 和 NSLock 类似,同样是继承自 NSObject 和遵循 NSLocking 协议,加解锁 try 等方法都类似,只是多了一个 condition 属性
  • @synchronized:是一把支持多线程递归的互斥锁。

objc_sync_enter关键流程分析:

获取当前线程的缓存链表结构,查看缓存链表是否存在对象object 如果存在,则执行lockCount++,更新缓存并结束流程 如果当前线程的缓存链表中未找到对象object缓存,则查看listp总链表结构 若总链表结构存在对象object,则threadCount++ 若总链表结构不存在对象object,则新建一个SyncData,且将lockCount、threadCount置为1,最后更新缓存

objc_sync_exit关键流程分析:

获取当前线程的缓存链表结构,查看缓存链表是否存在对象object 如果存在,则执行lockCount-- 如果当前的lockCount==0,则threadCount--,更新缓存并结束流程

  • dispatch_semaphore 是 GCD 用来同步的一种方式,与他相关的只有三个函数,一个是创建信号量(dispatch_semaphore_create),一个是等待信号量(dispatch_semaphore_wait),一个是发送信号(dispatch_semaphore_signal)。

 dispatch_semaphore_create(1) 方法可以创建一个 dispatch_semaphore_t 类型的信号量,设定信号量的初始化值为 1。注意,这里的传入参数必须大于等于 0,否则 dispatch_semaphore 会返回 NULL

synchronized 相关问题集

  • 锁是如何与你传入 @synchronized 的对象关联上的?

你调用 sychronized 的每个对象,Objective-C runtime 都会为其分配一个递归锁并存储在哈希表中。

  • @synchronized会保持(retain,增加引用计数)被锁住的对象么?

使用@synchronized不会导致此对象的引用计数增加

  • 假如传入 @synchronized 的对象在 @synchronized 的 block 里面被释放或者被赋值为 nil 将会怎么样?

如果在 sychronized 内部对象被释放或被设为 nil 看起来都 OK。不过这没在文档中说明,所以我不会再生产代码中依赖这条。

  • 如果传入@synchronized 的对象值为 nil 将会怎么样?

@synchronized(nil)不会有任何作用,hash计算为空,加锁失败,代码块不是线程安全的。你可以通过在 objc_sync_nil 上加断点来查看是否发生了这样的事情。

8、dispatch_once 执行原理

dispatch_once 保证任务只会被执行一次,即使同时多线程调用也是线程安全的。常用于创建单例、swizzeld method 等功能。

if (dispatch_atomic_cmpxchg(vval, NULL, &dow)) {
    _dispatch_client_callout(ctxt, func);
    tmp = dispatch_atomic_xchg(vval, DISPATCH_ONCE_DONE);
    tail = &dow;
    while (tail != tmp) {
        while (!tmp->dow_next) {
            _dispatch_hardware_pause();
        }
        sema = tmp->dow_sema;
        tmp = (struct _dispatch_once_waiter_s*)tmp->dow_next;
        _dispatch_thread_semaphore_signal(sema);
    }
} else {
    dow.dow_sema = _dispatch_get_thread_semaphore();
    for (;;) {
        tmp = *vval;
        if (tmp == DISPATCH_ONCE_DONE) {
            break;
        }
        dispatch_atomic_store_barrier();
        if (dispatch_atomic_cmpxchg(vval, tmp, &dow)) {
            dow.dow_next = tmp;
            _dispatch_thread_semaphore_wait(dow.dow_sema);
        }
    }
    _dispatch_put_thread_semaphore(dow.dow_sema);
}

image.png

dispatch_once用原子性操作block执行完成标记位,同时用信号量确保只有一个线程执行block,等block执行完再唤醒所有等待中的线程。

9.dispatch_semaphore 的实现原理

 dispatch_semaphore 是 GCD 中提供的一个很常用的操作,通常用于保证资源的多线程安全性和控制任务的并发数量。其本质实际上是基于 mach 内核的信号量接口来实现的。

dispatch_semaphore_t 是指向 dispatch_semaphore_s 结构体的指针。首先看一下基础的数据结构。

struct dispatch_queue_s;

DISPATCH_CLASS_DECL(semaphore, OBJECT);
struct dispatch_semaphore_s {
    DISPATCH_OBJECT_HEADER(semaphore);
    
    // 可看到上半部分的宏定义和其它的 GCD 类是相同的,毕竟大家都是继承自 dispatch_object_s,重点是下面两个新的成员变量,
    // dsema_value 和 dsema_orig 是信号量执行任务的关键,执行一次 dispatch_semaphore_wait 操作,dsema_value 的值就做一次减操作。
    
    long volatile dsema_value;
    long dsema_orig;
    _dispatch_sema4_t dsema_sema;
}; 

dispatch_semaphore_s 结构体中:dsema_orig 是信号量的初始值,dsema_value 是信号量的当前值,信号量的相关 API 正是通过操作 dsema_value 来实现其功能的。

  • dispatch_semaphore_create 用初始值(long value)创建新的计数信号量。

参数 value:信号量的起始值,传递小于零的值将导致返回 NULL。返回值 result:新创建的信号量,失败时为 NULL。

  • dispatch_semaphore_wait 等待(减少)信号量。 减少计数信号量,如果结果值小于零,此函数将等待信号出现,然后返回。(可以使总信号量减 1,信号总量小于 0 时就会一直等待(阻塞所在线程),否则就可以正常执行。)
  • dispatch_semaphore_signal 发信号(增加)信号量。如果先前的值小于零,则此函数在返回之前唤醒等待的线程。如果线程被唤醒,此函数将返回非零值。否则,返回零。  semaphore_signal 能够唤醒一个在 semaphore_wait 中等待的线程。如果有多个等待线程,则根据线程优先级来唤醒。

10.dispatch_group 的实现原理

image.png dispatch group是一个基于信号量的同步机制,主要提供了下面5个函数:enter,leave,wait,async,notify.

  • 1、dispatch group是GCD的一项特性,可以把任务分组。这组任务完成后时,调用者会收到通知

据此,可将要并发执行的多个任务合并为一组,这样调用者就可以知道这些任务何时能全部执行完

  • 2、创建dispatch group:

dispatch_group_t dispatchGroup = dispatch_group_create();

  • 3、将任务分组的两种方式:

方式一、用dispatch_group_async:

void dipatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block); //比dispatch_async多了group分组这个参数 方式二、用dispatch_group_enter和dispatch_group_leave:

void dispatch_group_enter(dispatch_group_t group); //使分组里正要执行的任务数递增

void dispatch_group_leave(dispatch_group_t group); //使分组里的任务数递减

dispatch_group_enter和dispatch_group_leave就相当于引用计数里的保留和释放操作,必须搭配使用,以防内存泄漏

  • 4、dispatch_group_wait:等待dispatch group执行完毕,会阻塞当前线程

void dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout); //第一个参数是要等待的组 //第二个参数是等待时间。group执行时间不超过timeout返回0,超时返回非0值。一般用DISPATCH_TIME_FOREVER一直等待

  • 5、dispatch_group_notify:等待dispatch group执行完之后执行块中的任务,不会阻塞当前线程

void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block); //第一个参数是要等待的group //第二个参数是block要在哪个队列中执行 //第三个参数是等待执行的代码块。将group执行完后需要处理的任务放block中

dipatch_group_notify和dispatch_group_wait都能实现等待group中的任务执行完之后再进行其他操作;不同的是dispatch_group_notify可以将要执行的任务放到block中,不会阻塞当前线程,dispatch_group_wait会阻塞当前线程

  • 6、dispatch_apply:需要反复执行某个任务时使用,会阻塞当前线程

void dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block) (size_t) ); //iterations表示要执行的次数 //queue可以使用并发队列,这样系统会根据资源情况来并发执行

11.dispatch_barrier_async 的实现原理

  • dispatch_barrier_async函数会等待栅栏函数前边追加到并发队列中的任务全部执行完毕之后,再将指定的任务追加到栅栏函数的并发队列中。然后在dispatch_barrier_async函数追加的任务执行完毕之后,并发队列才恢复为一般动作,接着追加任务到栅栏函数后的并发队列并开始执行。
  • 使用栅栏函数使用自己创建的并发队列,不要使用系统提供的全局并发队列

dispatch_barrier_sync和dispatch_barrier_async共同点

1、等待在它前面插入队列的任务先执行完

2、等待他们自己的任务执行完再执行后面的任务

dispatch_barrier_sync和dispatch_barrier_async不同点

1、dispatch_barrier_sync将自己的任务插入到队列的时候,需要等待自己的任务结束之后才会继续插入被写在它后面的任务,然后执行它们

2、dispatch_barrier_async将自己的任务插入到队列之后,不会等待自己的任务结束,它会继续把后面的任务插入到队列,然后等待自己的任务结束后才执行后面任务。

所以,dispatch_barrier_async的不等待(异步)特性体现在将任务插入队列的过程,它的等待特性体现在任务真正执行的过程。

栅栏实现多读单写

- (id)readDataForKey:(NSString *)key
{
    __block id result;
    
    dispatch_sync(_concurrentQueue, ^{
       
        result = [self valueForKey:key];
    });
    
    return result;
}
 
- (void)writeData:(id)data forKey:(NSString *)key
{
    dispatch_barrier_async(_concurrentQueue, ^{
       
        [self setValue:data forKey:key];
    });
} 

12.RunLoop

runloop内部实现逻辑

image.png

RunLoop与线程

  • 每条线程都有唯一的一个与之对应的RunLoop对象
  • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
  • 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
  • RunLoop会在线程结束时销毁
  • 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop

Source0 和 Source1 的区别

• Source1 :就是系统事件,基于mach_Port的,来自系统内核或者其他进程或线程的事件,可以主动唤醒休眠中的RunLoop(iOS里进程间通信开发过程中我们一般不主动使用)。mach_port大家就理解成进程间相互发送消息的一种机制就好。

• Source0 :就是应用层事件,非基于Port的 处理事件,什么叫非基于Port的呢?就是说你这个消息不是其他进程或者内核直接发送给你的。

timer 与 runloop 的关系?

  • NSTimer是由RunLoop来管理的,NSTimer其实就是CFRunLoopTimerRef,他们之间是 toll-free bridged 的,可以相互转换;
  • 如果我们在子线程上使用NSTimer,就必须开启子线程的RunLoop,否则定时器无法生效

解决定时器在滚动视图上面失效问题NSTimer添加到两种RunLoop中

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

NSTimer 和 CADisplayLink 存在的问题

  • 不准时:NSTime和CADisplayLink底层都是基于RunLoop的CFRunLoopTimerRef的实现的,也就是说它们都依赖于RunLoop。如果RunLoop的任务过于繁重,会导致它们不准时

解决方法:使用 GCD 的定时器。GCD 的定时器是直接跟系统内核挂钩的,而且它不依赖于RunLoop,所以它非常的准时

循环引用

1、苹果系统API可以解决(iOS10以上)

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:
(BOOL)repeats block:(void (^)(NSTimer *timer))block 
API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:
(BOOL)repeats block:(void (^)(NSTimer *timer))block 
API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

- (instancetype)initWithFireDate:(NSDate *)date interval:
(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block 
API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

2、使用block来解决

#import "NSTimer+PFSafeTimer.h"

@implementation NSTimer (PFSafeTimer)

+ (NSTimer *)PF_ScheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval block:(void(^)(void))block repeats:(BOOL)repeats {
    
    return [NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(handle:) userInfo:[block copy] repeats:repeats];
}

+ (void)handle:(NSTimer *)timer {
    
    void(^block)(void) = timer.userInfo;
    if (block) {
        block();
    }
}
@end

3、使用NSProxy来解决循环引用

#import "PFProxy.h"

@interface PFProxy()

@property (nonatomic, weak) id object;

@end
@implementation PFProxy

- (instancetype)initWithObjc:(id)object {
    
    self.object = object;
    return self;
}

+ (instancetype)proxyWithObjc:(id)object {
    
    return [[self alloc] initWithObjc:object];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    
    if ([self.object respondsToSelector:invocation.selector]) {
        
        [invocation invokeWithTarget:self.object];
    }
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    
    return [self.object methodSignatureForSelector:sel];
}
@end

Runloop与事件关系

  • 事件响应过程 当一个硬件事件(触摸、锁屏、旋转等)发生后,由IOKit.framework生成一个IOHIDEvent事件;事件由SpringBoard接收,通过mach_port分发至App进程,随后通过注册的source1回调将IOHIDEvent包装成UIEvent事件进行处理和分发。

  • 手势识别过程 在source1回调中识别到一个手势后,首先调用cancel打断touchBegin/Move/End系列事件,然后将识别到的手势标志为待处理。苹果注册了observer监听beforeWaiting事件,在这个回调中会拿取待处理的手势事件并进行相应的处理

runloop渲染

  • runloop渲染过程

当调用[UIView setNeedsDisplay]时,回调用UIView layer的setNeedsDisplay方法,相当于给layer打一个标志。这时并未直接进行绘制工作,而是到当前runloop的beforeWaiting才会进行绘制工作。调用[CALayer display]进行绘制工作。首先判断是否实现layer.delegate的方法displayer:,这个接口是异步绘制的入口;如若未实现,进行系统绘制流程,绘制结束。

image.png

  • 系统绘制流程 创建BackingStore,用于获取图形上下文,然后判断是否有Delegate。有,则调用[layer.delegate drawLayer:inContext:],并返回回调[UIView draw]给我们,让我们在系统绘制的基础上做一些其他事情;没有,则调用[CALayer drawInContext:]。以上两个分支都会将绘制存储到BackingStore中,然后提交到GPU,绘制结束。

image.png

  • 异步绘制 在异步绘制入口(上面提到的[layer.delegate displayer:])中使用子线程将所需要的内容绘制好,通过bitmap为layer.contents属性赋值

Runloop在AFNetworking2.0中的运用

AFNetworking在runloop启动前添加了一个NSMachPort,目的是为了runloop不退出。代码实现如下

autoreleasePool

原理:autoreleasePoolPage双向连接而成(双向链表,autoreleasePoolPage相当于一个node) 释放时机:App启动后,苹果会注册两个observer,它们的回调都是_wrapRunloopWithAutoreleasePoolHandler()。

  • a、第一个observer监听的是entry事件,其回调内会调用_objc_autoreleasePoolPush()创建释放池,它的优先级最高,保证它创建在最前面。

  • b、第二observer监听两个事件:

    1)beforeWaiting事件:beforeWaiting事件回调中,会调用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush()释放旧的释放池和创建新的释放池;

    2)exit事件:在exit事件回调中,调用_objc_autoreleasePoolPop()释放新的释放池,其优先级最低,保证它在最后执行

autoReleasePoolPage大致的基本结构如下图所示(大小:4096bytes=4k),AutoreleasePoolPage 的成员变量都是继承自 AutoreleasePoolPageDate,它们总共需要 56 个字节的空间,然后剩余 4040 字节空间,一个对象指针占 8 个字节,那么一个 AutoreleasePoolPage 能存放 505 个需要自动释放的对象。

image.png

runloop 与卡顿监测

通过监控 main run loop 从 kCFRunLoopBeforeSources(或者 kCFRunLoopBeforeTimers) 到 kCFRunLoopAfterWaiting 的活动变化所用时间是否超过了我们预定的阈值进而判断是否出现了卡顿,当出现卡顿时可以读出当前函数调用堆栈帮助我们来分析代码问题。

13.Dealloc流程解析 Dealloc 实现原理

inline void
objc_object::rootDealloc()
{
    //判断对象是否采用了Tagged Pointer技术
    if (isTaggedPointer()) return;  // fixme necessary?
    //判断是否能够进行快速释放
    //这里使用了isa指针里的属性来进行判断.
    if (fastpath(isa.nonpointer  &&  //对象是否采用了优化的isa计数方式
                 !isa.weakly_referenced  &&  //对象没有被弱引用
                 !isa.has_assoc  &&  //对象没有关联对象
                 !isa.has_cxx_dtor  &&  //对象没有自定义的C++析构函数
                 !isa.has_sidetable_rc  //对象没有用到sideTable来做引用计数
                 ))
    {
        //如果以上判断都符合条件,就会调用C函数 free 将对象释放
        assert(!sidetable_present());
        free(this);
    } 
    else {
        //如果以上判断没有通过,做下一步处理
        object_dispose((id)this);
    }
}

Dealloc整个方法释放流程如下图: image.png

objc_destructInstance

  • 执行了object_cxxDestruct 函数
  • 执行_object_remove_assocations,去除了关联对象.(这也是为什么category添加属性时,在释放时没有必要remove)
  • 清空引用计数表并清除弱引用表,将weak指针置为nil

ISA_BITFIELD 中的 64 位分别都代表什么

#   define ISA_BITFIELD                                                      \
      // 表示 isa 中只是存放的 Class cls 指针还是包含更多信息的 bits
      uintptr_t nonpointer        : 1;                                       \
      // 标记该对象是否有关联对象,如果没有的话对象能更快的销毁,
      // 如果有的话销毁前会调用 _object_remove_assocations 函数根据关联策略循环释放每个关联对象
      uintptr_t has_assoc         : 1;                                       \
      // 标记该对象所属类是否有自定义的 C++ 析构函数,如果没有的话对象能更快销毁,
      // 如果有的话对象销毁前会调用 object_cxxDestruct 函数去执行该类的析构函数
      uintptr_t has_cxx_dtor      : 1;                                       \
      // isa & ISA_MASK 得出该实例对象所属的的类的地址
      uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
      // 用于调试器判断当前对象是真的对象还是没有初始化的空间
      uintptr_t magic             : 6;                                       \
      // 标记该对象是否有弱引用,如果没有的话对象能更快销毁,
      // 如果有的话对象销毁前会调用 weak_clear_no_lock 函数把该对象的弱引用置为 nil,
      // 并调用 weak_entry_remove 把对象的 entry 从 weak_table 中移除
      uintptr_t weakly_referenced : 1;                                       \
      // 标记该对象是否正在执行销毁
      uintptr_t deallocating      : 1;                                       \
      // 标记 refcnts 中是否也有保存实例对象的引用计数,当 extra_rc 溢出时会把一部分引用计数保存到 refcnts 中去,
      uintptr_t has_sidetable_rc  : 1;                                       \
      // 保存该对象的引用计数 -1 的值(未溢出之前,溢出后存放 RC_HALF)
      uintptr_t extra_rc          : 19 // 最大保存 2^19 - 1,觉得这个值很大呀, mac 下是 2^8 - 1 = 255
#   define RC_ONE   (1ULL<<45)
#   define RC_HALF  (1ULL<<18)

14.Associated Object 原理 使用 objc_setAssociatedObject 和 objc_getAssociatedObject 来分别模拟属性的存取方法,而使用关联对象模拟实例变量


/** 
 * @param object 要进行关联行为的源对象
 * @param key 关联的 key
 * @param value 与源对象的键相关联的值。传递 nil 以清除现有的关联。
 * @param policy 关联策略
 * 
 * @see objc_setAssociatedObject
 * @see objc_removeAssociatedObjects
 */
OBJC_EXPORT void
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy);


/** 
 * @param object 关联的源对象
 * @param key The 关联的 key
 * @return The value associated with the key \e key for \e object.
 * 
 * @see objc_setAssociatedObject
 */
OBJC_EXPORT id _Nullable
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key);

Associated Object 所使用的数据结构可总结如下:

  • 通过 AssociationsManager 的 get 函数取得一个全局的 AssociationsHashMap。
  • 根据我们源对象的 DisguisedPtr<objc_object> 从 AssociationsHashMap 取得 ObjectAssociationMap。
  • 根据我们指定的关联 key(const void *key)从 ObjectAssociationMap 取得 ObjcAssociation。
  • ObjcAssociation 的两个成员变量分别保存了我们的关联策略 _policy 和关联值 _value。

15.Category

1、Category的实现原理

Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)

2、Category和Class Extension的区别是什么?

Class Extension在编译的时候,它的数据就已经包含在类信息中 Category是在运行时,才会将数据合并到类信息中

3、load、initialize方法的区别什么?

1.调用方式

1> load是根据函数地址直接调用 2> initialize是通过objc_msgSend调用

2.调用时刻

1> load是runtime加载类、分类的时候调用(只会调用1次 ) 2> initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)

4、load、initialize的调用顺序

1.load

1> 先调用类的load a) 先编译的类,优先调用load b) 调用子类的load之前,会先调用父类的load

2> 再调用分类的load a) 先编译的分类,优先调用load

2.initialize

1> 先初始化父类 2> 再初始化子类(可能最终调用的是父类的initialize方法)

5、如何实现给分类“添加成员变量”?

默认情况下,因为分类底层结构的限制,不能添加成员变量到分类中。但可以通过关联对象来间接实现

关联对象提供了以下API
添加关联对象
void objc_setAssociatedObject(id object, const void * key,
                                id value, objc_AssociationPolicy policy)

获得关联对象
id objc_getAssociatedObject(id object, const void * key)

移除所有的关联对象
void objc_removeAssociatedObjects(id object)

16.Block

1、block的原理是怎样的?本质是什么?

block本质上也是一个OC对象,它内部也有个isa指针 block是封装了函数调用以及函数调用环境的OC对象

2、block的(capture)

为了保证block内部能够正常访问外部的变量,block有个变量捕获机制

3、Block类型有哪几种

block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型

1、NSGlobalBlock ( _NSConcreteGlobalBlock 2、NSStackBlock ( _NSConcreteStackBlock ) 3、NSMallocBlock ( _NSConcreteMallocBlock )

4、block的copy

在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况

  • 1、block作为函数返回值时
  • 2、将block赋值给__strong指针时
  • 3、block作为Cocoa API中方法名含有usingBlock的方法参数时
  • 4、block作为GCD API的方法参数时
MRC下block属性的建议写法
@property (copy, nonatomic) void (^block)(void);

ARC下block属性的建议写法
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);

5、__block修饰符

__block可以用于解决block内部无法修改auto变量值的问题

__block不能修饰全局变量、静态变量(static)

编译器会将__block变量包装成一个对象

当__block变量在栈上时,不会对指向的对象产生强引用

当__block变量被copy到堆时

会调用__block变量内部的copy函数 copy函数内部会调用_Block_object_assign函数 _Block_object_assign函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain)

如果__block变量从堆上移除

会调用__block变量内部的dispose函数 dispose函数内部会调用_Block_object_dispose函数 _Block_object_dispose函数会自动释放指向的对象(release)

6、循环引用

用__weak、__unsafe_unretained解决

__unsafe_unretained typeof(self) weakSelf = self;
self.block = ^{
 print(@"%p", weakSelf);
}
复制代码__weak typeof(self) weakSelf = self;
self.block = ^{
 print(@"%p", weakSelf);
}
复制代码
用__block解决(必须要调用block)

__block id weakSelf = self;
self.block = ^{
weakSelf = nil;
}
self.block();

17.weak指针的实现原理

runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象指针的地址)数组

  • 1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址
  • 2、添加引用时:objc_initWeak函数会调用 storeWeak() 函数, storeWeak() 的作用是更新指针指向,创建对应的弱引用表
  • 3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录

18.与OC比较.Swift有什么优点

  • Swift更加安全,它是类型安全的语言。
  • Swift容易阅读,语法和文件结构简易化。
  • Swift更易于维护,文件分离后结构更清晰。
  • Swift代码更少,简洁的语法,可以省去大量冗余代码
  • Swift速度更快,运算性能更高。

为什么Swift编译很慢?

因为Swift在编译的时候做了很多事情,所以消耗时间比较多是正常的。如对类型的分析等。

为什么Swift相比较OC会更快?

编译器 Whole Module Optimizations 机制的全局优化、更多的栈内存分配、更少的引用计数、更多的静态、协议类型的使用等都是Swift比OC更快的原因。

19.delegate、notification、KVO

  • delegate. 一对一
  • notification 一对多,多对多
  • KVO 一对一

三者各有自己的特点:

  • delegate 语法简洁,方便阅读,易于调试
  • notification 灵活多变,可以跨越多个类之间进行使用
  • KVO 实现属性监听,实现model和view同步
  • 可以根据实际开发遇到的场景来使用不同的方式

Notification 和KVO区别

  • KVO提供一种机制,当指定的被观察的对像的属性被修改后,KVO会自动通知响应的观察者,KVC(键值编码)是KVO的基础
  • 通知:是一种广播机制,在实践发生的时候,通过通知中心对象,一个对象能够为所有关心这个时间发生的对象发送消息,两者都是观察者模式,不同在于KVO是被观察者直接发送消息给观察者,是对象间的直接交互,通知则是两者都和通知中心对象交互,对象之间不知道彼此
  • 本质区别,底层原理不一样.kvo 基于 runtime, 通知则是有个通知中心来进行通知

Notification和delegate区别

  • Delegate和Notification的本质区别是命令式和响应式
  • Delegate一对一,Notification一对多

20.NSCache & NSDictionary & NSURLCache

  • 哈希表(NSDictionary 是通过hash表来实现key和value之间的映射和存储的)
  • NSURLCache 为您的应用的 URL 请求提供了内存中以及磁盘上的综合缓存机制。 作为基础类库 URL 加载系统 的一部分,任何通过 NSURLConnection 加载的请求都将被 NSURLCache 处理。(NSCache和NSURLCache一点关系也没有)

相同点:

  • NSCache和NSMutableDictionary功能用法基本是相同的。

区别:

  • NSCache是线程安全的,NSMutableDictionary线程不安全
  • NSCache线程是安全的,Mutable开发的类一般都是线程不安全的
  • 当内存不足时NSCache会自动释放内存(所以从缓存中取数据的时候总要判断是否为空)
  • NSCache可以指定缓存的限额,当缓存超出限额自动释放内存
  • NSCache 并不会“拷贝”键,而是会“保留”它。因此,NSCache 不会自动拷贝键,所以说,在健不支持拷贝操作的情况下,该类用起来比字典更方便。
缓存限额: 
缓存数量 
@property NSUInteger countLimit; 
缓存成本 
@property NSUInteger totalCostLimit; 
苹果给NSCache封装了更多的方法和属性,比NSMutableDictionary的功能要强大很多

21.函数式编程 & 链式编程 & 响应式编程

  • 函数式编程是一种编程模型,他将计算机运算看做是数学中函数的计算,并且避免了状态以及变量的概念。
  • 链式编程是将需要执行的代码连续的书写,它使得代码简单易懂。
  • 响应式编程是一种面向数据流和变化传播的编程范式。

22.Block和Protocol的区别,Block是为了解决什么问题而使用的。

  • 代理和block的共同特性是回调机制,不同的是,代理的方法比较多,比较分散,公共接口,方法较多也选择用delegate进行解耦,使用block的代码比较集中统一,异步和简单的回调用block更好
  • block为了多线程之间调度产生的;
  • block 也是一个OC对象,可以当参数传递,使用方便简单,灵活,很少的代码就可以实现代码回调.比协议省很多代码

23.线程池的原理

image.png

  • 若线程池大小小于核心线程池大小时
    1. 创建线程执行任务
  • 若线程池大小大于等于核心线程池大小时
    1. 先判断线程池工作队列是否已满
    2. 若没满就将任务push进队列
    3. 若已满时,且maximumPoolSize>corePoolSize,将创建新的线程来执行任务
    4. 反之则交给饱和策略去处理
    参数名代表意义
    corePoolSize线程池的基本大小(核心线程池大小)
    maximumPool线程池的最大大小
    keepAliveTime线程池中超过corePoolSize树木的空闲线程的最大存活时间
    unitkeepAliveTime参数的时间单位
    workQueue任务阻塞队列
    threadFactory新建线程的工厂
    handler当提交的任务数超过maxmumPoolSize与workQueue之和时,任务会交给RejectedExecutionHandler来处理

饱和策略有如下四个:

  • AbortPolicy直接抛出RejectedExecutionExeception异常来阻止系统正常运行
  • CallerRunsPolicy将任务回退到调用者
  • DisOldestPolicy丢掉等待最久的任务
  • DisCardPolicy直接丢弃任务

24.atomic的实现机制;为什么不能保证绝对的线程安全

  • atomic是在setter和getter方法里会使用自旋锁spinlock_t来保证setter方法和getter方法的线程的安全。
@property (nonatomic, assign) NSInteger obj;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLock *m_lock = [NSLock new];
    
    //开启一个异步线程对obj的值+1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (int i = 0;i < 10000;i ++){
            [m_lock lock];
            self.obj = self.obj + 1;
            [m_lock unlock];
        }
        NSLog(@"obj : %ld  线程:%@",(long)self.obj , [NSThread currentThread]);
    });
    
    //开启一个异步线程对obj的值+1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (int i = 0;i < 10000;i ++){
            [m_lock lock];
            self.obj = self.obj + 1;
            [m_lock unlock];
        }
        NSLog(@"obj : %ld 线程: %@",(long)self.obj , [NSThread currentThread]);
    });
}

// 输出:
2020-04-15 16:19:54.566420+0800 Atomic2Nonatomic[31970:4554604] obj : 15712 线程: <NSThread: 0x600000f22880>{number = 6, name = (null)}
2020-04-15 16:19:54.566542+0800 Atomic2Nonatomic[31970:4554603] obj : 20000  线程:<NSThread: 0x600000f1e040>{number = 4, name = (null)}

atomic只是对set方法加锁,而我们程序里面的self.obj = self.obj + 1; 这一部分不是线程安全的,后面这个+1操作不是线程安全的,所以要想最终得到20000的结果,需要使用锁对self.obj = self.obj + 1加锁。代码就会得到我们想要的结果。 
  • atomic并不能保证线程绝对安全

25.PerformSelecter

当调用 NSObject 的performSelecter:afterDelay:后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。 当调用performSelector:onThread:时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

26.NSOperation 相比于 GCD 有哪些优势?

GCD是基于c的底层api,NSOperation属于object-c类。iOS 首先引入的是NSOperation,IOS4之后引入了GCD和NSOperationQueue并且其内部是用GCD实现的。 相对于GCD:

  • 1、NSOperation拥有更多的函数可用,具体查看api。
  • 2、在NSOperationQueue中,可以建立各个NSOperation之间的依赖关系。
  • 3、有kvo可以监测operation是否正在执行(isExecuted)、是否结束(isFinished),是否取消(isCanceld)。
  • 4、NSOperationQueue可以方便的管理并发、NSOperation之间的优先级。 GCD主要与block结合使用。代码简洁高效。 GCD也可以实现复杂的多线程应用,主要是建立个个线程时间的依赖关系这类的情况,但是需要自己实现相比NSOperation要复杂

27.TCP三次握手与四次挥手

image.png

TCP三次握手

第一次握手:客户端向服务器端发送SYN包,SYN标志位(Flag)置1,包的序号为客户端的ISN(Initial Sequence Number),包发出后,客户端进入SYN_SEND状态,客户端知晓客户端的发包能力正常;

第二次握手:服务端接收客户端发来的连接请求,以SYN+ACK包做出回应,包的SYN标志位置1,ACK标志位置1,序号为服务端的ISN(与客户端的ISN不同),确认码为客户端的ISN加1,包发出后,服务端进入SYN_RCVD状态,服务端知晓客户端的发包能力正常、服务端的发包能力正常,服务器的收包能力正常;

第三次握手:客户端接收服务端发来的响应,以ACK包做出回应,包的ACK置1,序号为客户端的ISN+1,确认码为服务端的ISN+1,此包发出后,客户端转为ESTABLISHED状态,客户端知晓服务端的发包能力正常,服务端的收包能力正常、客户端的收包能力正常。服务器接收到这个包后,也进入ESTABLISHED状态,服务端知晓客户端的收包能力正常,至此TCP连接被成功建立,可以进行数据传输。

“三次握手”的作用就是保证客户端、服务器端双方都能明确自己和对方的收、发能力是正常的。为了防止服务器端开启一些无用的连接增加服务器开销以及防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。

TCP四次挥手

第一次挥手:主动方 向 被动方 发出FIN报文,序号为u(等于前面已经传送过来的数据的最后一个字节的序号加1),确认码为被动方的ISN+1,此时主动方进入FIN_WAIT_1状态,等待被动方确认。

第二次挥手:被动方发送ACK报文给主动方,确认主动方可以关闭连接,这个报文中序号为v(由之前已发出的数据序列决定),确认码为u+1,被动方发出ACK报文后进入CLOSE_WAIT状态,检查自身是否还有数据发送给对方——这时候被动方处于半关闭状态,即主动方已经没有数据要发送了,但是被动方若发送数据,主动方依然要接收。这个状态还要持续一段时间,也就是整个CLOSE_WAIT状态持续的时间。主动方接收到ACK消息即转入FIN_WAIT_2状态,继续等待被动方发来的FIN报文。

第三次挥手:还是被动方,被动方检查自身没有数据要发给主动方了,而在这之前被动方很可能又发送了一些数据给主动方,假定此时的序号为w。被动方发送FIN+ACK报文给主动方,序号为w,确认码为u+1,发出后,被动方转入LAST_ACK状态,等待主动方关于断开连接的确认。

第四次挥手:主动方收到被动方发来的FIN报文,回应一个ACK报文,序号为u+1,确认码为w+1。发出后,主动方转入TIME_WAIT状态,将在2∗MSL( Maximum Segment Lifetime,最长报文段寿命)后断开连接,随后进入CLOSED可用状态;被动方接收到最后的ACK报文后,随即断开连接,进入CLOSED可用状态。

TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议,全双工模式。如果不经过四次挥手,是不能断开连接的,因为对方那里可能还有数据没发送。只有双方互相确认(ACK)了彼此的FIN报文,TCP连接才能断开,TCP才靠得住。

27.iOS之数据解析之XML解析

XML解析常见的两种方式:DOM解析和SAX解析

DOM解析

DOM:Document Object Model(文档对象类型).解析XML时,读入整个XML文档并构建一个驻留内存的树结构(节点树),通过遍历数结构可以检索任意XML节点,读取它的属性和值,而且通常情况下,可以借助XPath,直接查询XML节点. 进行DOM方式解析数据需要使用一个第三方的类GDataXMLNode

SAX解析

SAX:Simple API for XML,基于事件驱动的解析方式,逐行解析数据(采用协议回调机制). NSXMLParser

28.iOS中持久化方式有哪些?

  • 属性列表文件 -- NSUserDefaults 的存储,实际是本地生成一个 plist 文件,将所需属性存储在 plist 文件中
  • 对象归档 -- 本地创建文件并写入数据,文件类型不限
  • SQLite 数据库 -- 本地创建数据库文件,进行数据处理
  • CoreData -- 同数据库处理思想相同,但实现方式不同

29.深拷贝、浅拷贝

  • 深拷贝:内容相同的、新的 内存空间,新的指针
  • 浅拷贝:拷贝的是指针
  • 不可变对象的 copy 为浅拷贝,mutableCopy 为深拷贝
  • 可变对象的 copy 和 mutableCopy 均为深拷贝

未完待续...