类
分类的作用?
- 声明私有方法,分解体积。
分类的特点? - 运行时决议,可以为系统类添加分类
分类可以添加哪些内容?
分类可以添加实例方法和类方法
分类添加属性,根据数据结构默认不能添加实例变量,需要通过关联对象来实现 。
分类的加载调用栈
分类中关联对象存储?
-
由AssociationManger管理并在AssociationHashMap(key-被关联对象的指针值,Value-ObjectAssociationMap)存储,一个对象可以添加多个关联值,因此采用ObjectAssociationMap存储。所有关联对象存储在同一容器中。
image.png
通知实现的机制?
- 通知全局维护的一个字典:key为通知名称:value为ObserverList(通过一个名称添加多个监听),ObserverList存储这通知回调的方法以及参数等等。
KVO的概念
- 观察者设计模式的一种实现,使用isa混写(isa-swizzling)来实现KVO。
- 原实例对象的指针isa指向 - NSKVONotifying_A通过重写Setter来实现监听。
手动KVO如何实现的?
- 对称成员变量直接赋值无法触发KVO,在赋值前后分别设置willChangeValue,didChangeValue。
属性关键字?
三种关键字:读写权限,原子性,引用计数。
- atomic:保证赋值,获取是线程安全的,不代表操。对数组进行操作是无法保证线程安全
assign和weak的区别? - assign:修饰基本类型,修饰对象不改变引用计数,悬垂对象指针(对象销毁后,指针依然指向原地址)。
- weak:不改变修饰对象的引用计数,对象释放后会自动设置为nil
深浅拷贝 - 可变对象copy和mutableCopy都是深拷贝
- 不可变对象的copy是浅拷贝,mutableCopy是深拷贝
- copy对象返回的都是不可变对象。
分类和扩展的区别?
- 扩展:编译时决议,生命在.m文件中,不能为系统类增加扩展
- 分类:运行时决议,可以为系统类增加扩展。
了解过类簇么?类簇的优势在哪里?
- 管理了一组隐藏在公共接口下的私有类,NSNumber,UIbutton,对外部暴露隐藏在通用接口之下的与实现相关的类,方便维护和拓展,具体的实现代码放在对应的子类里
如果分类和元类有相同方法,会先执行哪个?如果有多个分类呢?
- 分类方法合并到方法列表中,根据源码所知,原类方法和分类方法会优先调用分类方法,如果多个分类有相同方法,后编译分类方法会优先调用(源码attachCategories中,先编译的文件分类文件会放到数组的末端)
查询方法时,快速查找有序方法列表是按照什么规则排序的?
- 按照方法的地址大小来排序的(源码fixupMethodList方法)
RunLoop(你想要的基本都有)
https://www.jianshu.com/p/719aa97078b5
Autorelease和AutoreleasePool
autorelease对象在什么时候释放
- autorelease对象的释放在不同场景下释放的时机不一样(显式和隐式)
autorelease对象直接是由@autorelasepool{}包裹,那么autorelease对象会在@autorelasepool{}的大括号之后释放,ARC下自动补全release。
没有显式使用@autorelasepool{},会根据某次Runloop循环中调用_objc_autoreleasePoolPop()释放(可能是在runloop休眠之前或者退出runloop时)
使用autorelease和@autoreleasePool的区别
第一个是加到当前池里面 等待run loop 释放,第二个是新开辟一个自动释放池。
使用@autoreleasePool的工作原理?
- 1.首先会将POOL_BOUNDARY(一个nil对象)入栈,并返回当前压入栈中的地址值,用作 pop 时的终点
- 2.调用到 AutoreleasePoolPage 的 autoreleaseFast 函数,添加到 page 中。
- 3.在objc_autoreleasePoolPop函数调用时会将POOL_BOUNDARY的地址值传入,用作自动释放池pop的停止边界
page边界处理 - 如果存在page、且未满会直接通过next指针将对象地址存放在next中,存在page。
- 如果page 存在且满了,会创建一个新的page再压入POOL_BOUNDARY,再添加autorelease对象,然后通过parent 和 child将上下两个page关联
- 如果page都不存在那么会创建一个新的page、压入POOL_BOUNDARY后再添加autorelease对象。
Runtime
如果A和B交换了方法,C在不知情的情况下又和A交换了方法,会有什么问题,如何避免此问题?
- 可以交换成功,A和C交换后, A方法执行的C方法,B执行的A方法,C执行的B方法,在原来的基础上再调用C的方法,因为C现在持有的是B的方法,还要再调用一下本身
objc_object,objc_class
objc_object:
id类型,isa_t(共用体),isa相关信息,引用计数啊相关
objc_class:
Class类型,继承自objc_object,包含(Class-SuperClass,cache_t-cache,class_data_bits_t-bits)
isa指针
共用体结构,分为指针型isa(代表Class地址)和非指针型isa(isa的值的部分代表Class地址)。
isa指向:
实例对象isa指向Class(调用对象方法是从类对象查找),
类对象isa指向Meta-Class(调用类方法是从元类对象查找)。
cache_t
快速查找方法执行函数,可扩容的哈希表结构,局部性原理(增加命中)。
cache_t底层结构:bucket_t类型:key,IMP.
class_data_bits_t
- class_data_bits_t对class_rw_t的封装。
- class_rw_t代表了类的相关的读写信息,对class_ro_t的封装
- class_ro_t代表类相关的只读信息。
class_rw_t
- class_rw_t结构:class_ro_t,protocols,properties,methods
- protocols,properties,methods都是二维数组来实现的(比如N个分类,分类中有N个方法因此采用二维数组)。
class_ro_t
- class_ro_t结构:name,ivars,properties,protocols,methodList
- ivars,properties,protocols,methodList都是一位数组。
method_t
- method_t结构:SEL-name(名称),const char*-types(参数和返回值),IMP-imp(无类型的函数指针)。
-
types解析:返回值,参数1,参数2。。。。。。
image.png
完整的数据结构关系
类对象和元类对象的关系
- 类对象存储实例方法列表等信息
- 元类对象存储类方法列表等信息
一个NSObject分类文件,.h声明+(void)test,.m实现-(void)test,调用+(void)test方法。是否崩溃,为什么?
- 调用的类方法在元类对象找不到的时候,会通过superClass逐级想父类查找,最终找到根元类,根元类superClass又会指向根类,调用根类的方法,即实例方法,因此不会崩溃。
消息传递
objc_msgSend(接受者,选择器)。
struct objc_super: receiver指向当前对象(self,而不是super)。
super class : objc_msSendSuper(super,@selector(class))
方法查找
*缓存查找: 给定SEL找对应bucket_t中IMP。通过哈希查找:通过哈希算法和mask进行位与运算。
- 有序查找:对已经排序好的列表进行二分查找。、
- 无序查找:对于没有排序的列表,采用一般遍历查找。
- 父类逐级查找:通过superClass的指向找到父类,父类继续通过缓存,有序,无序查找。
消息转发流程
- 动态方法解析:resolveInstanceMethod,返回Yes处理完成
- 目标重定向:进行fowaringTargetForSeletor,通过别的类执行。
- 最后补救:methodSignatureForSelctor,
- 通过签名调用:forwardInvocation。
内存管理
内存布局分区
stack:方法调用
heap:通过alloc分配
bss:未初始化全局变量
data:已初始化的全局标量
text:程序代码
iOS如何进行内存管理的?
TaggedPointer(小对象)
NONPOINTER_ISA(64位比特位,存储isa用不到64,多余的部分存储了内存管理的内容(非指针类型的ISA))
散列表:(弱引用表,引用计数表)
SideTables了解么?
- SideTables:自旋锁,引用计数标(RefcountMap),弱引用表(weak_table_t)。
为什么会有多个SideTable组成
- 如果一张表会存在效率问题,在不同线程中,进行操作,需要进行加锁处理,影响效率。
- 分离锁计数方案,吧引用计数表拆分,分别加锁,不同线程同时操作,可以执行并发操作。
如何实现快速分流?
- sideTables本质是一张Hash表,Key为对象指针,通过hash运行找到对应的sideTable。
- 通过内存地址通过和数组的取余运算得到下标。
sideTable数据结构
- 自旋锁:忙等锁,适合轻量访问。
- 引用计数表:RefcountMap,通过对象的指针找到对象的引用计数。避免循环遍历,提高效率。
- weak_table_:本质也是hash表,通过对象指针,经过hash算法找到,弱引用存储的位置weak_entry_t,存储这弱引用指针。
ARC的特点:
- ARC是LLVM和Runtime协作的结果
- ARC禁止手动调用
- ARC增加了weak,strong。
retain实现:
1.在Sidetables对象指针找到对应的SideTable。
2.再通过引用计数变量和指针,找到对应的引用计数值。
3.进行+1
release实现:
1.在Sidetables对象指针找到对应的SideTable。
2.再通过对象指针,找到对应的引用计数表
3.进行-1
image.png
retainCount实现:
1.在Sidetables对象指针找到对应的SideTable。
2.声明result
3.找到对应的引用计数
4.进行位移运算赋值
image.png
-
dealloc实现:
image.png -
objc_dipose实现:
image.png -
object_destructInstance实现:
image.png -
clearDeallocating实现:
image.png
弱引用管理
-
__weak修饰后会转化为objc_initWeak.
image.png
一个修饰__weak通过编译会转换成objc_initWeak,执行storeWeak,最后通过weak_register_no_lock进行弱引用变量的添加,添加的位置通过hash算法进行查找,如果有弱引用数组,直接插入,如果没有创建。
循环引用的种类?
- 自循环引用,相互循环引用,多循环引用。
如何破除循环引用和方案?
- 避免产生循环引用
- 在合适时机手动断环
方案:__weak,__block
NSTimer解决循环引用的方式?
创建一个中间对象弱引用原对象和NSTimer,NSTimer的实现是在中间对象实现的。
多线程
NSOperation和NSOperationQueue有什么优势和特点
- 添加任务依赖( [bp1 addDependency:bp2]),前者依赖后者,bp2先执行。
*任务执行状态的控制:
四种状态:isReady,isExecuting,isFinished,isCancel
重写main方法和start方法,main方法底层控制变更任务完成状态以及退出,重写start方法自行控制任务状态。
底层变更状态通过KVO监听。 - 控制最大并发量
NSThread的启动流程
start() - 创建pthread - main() - [performSelector] - exit()
iOS常用锁有哪些?
- synchronize,atomic,OSSplinkLock(自旋锁),NSLock,NSRecursiveLock(递归锁),信号量
自旋锁和互斥锁的特点?
自旋锁-循环等待,不释放当前资源,轻量级的访问。
NSLock重复加锁的问题?如何解决?
会产生死锁,使用递归锁(NSRecursiveLock)
串行队列,并行队列,同步,异步
- pragma mark --主队列
主线程
执行同步任务
会死锁:同步任务在添加进线程后就要当即执行,主线程正在执行当前函数,相互等待。
主线程
执行异步任务
:不会开新的线程,会在主线程依次执行任务。主队列添加完成block以后直接返回,主队列是顺序添加的block,block按照fifo执行。
- pragma mark --并发队列
并发队列
执行同步任务
:不创建线程,有序的执行所有的任务。这些任务都是创建一个就立马执行,执行完才创建下一个。同步函数是不会添加线程的,并发队列与否,并不影响同步函数的创建,因为本身就不能多创建线程,也就不存在并发。
并发队列
执行异步任务
:创建新的线程,各个线程也是并发执行的。
- pragma mark --串行队列
串行队列
执行同步任务
:不会创建线程,顺序执行任务。block任务执行完成以后继续向下执行任务。
串行队列
执行异步任务
(与主队列一样):不会开新的线程,block任务会在主线程依次执行任务。主队列添加完成block以后直接返回继续执行下面的任务,主队列是顺序添加的block,block按照fifo执行。
信号量打印
- (void)semaphoreTest {
dispatch_semaphore_t signal = dispatch_semaphore_create(1);
__block long x = 0;
NSLog(@"0x:%ld",x);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
NSLog(@"w1");
x = dispatch_semaphore_signal(signal);
NSLog(@"1x:%ld",x);
sleep(2);//睡两秒
NSLog(@"w2");
x = dispatch_semaphore_signal(signal);
NSLog(@"2x:%ld",x);
});
x = dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);
NSLog(@"3x:%ld",x);
x = dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);
NSLog(@"w3");
NSLog(@"4x:%ld",x);
x = dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);
NSLog(@"w4");
NSLog(@"5x:%ld",x);
}
- 打印顺序:0x,3x,w1,1x,w3,4x,w2,2x,w4,5x,w的打印会存在异步的浮动 信号量的执行顺序是确定的 0x,3x,1x,4x,2x,5x
如果一个线程组中3个线程为异步操作, 线程组能否汇总任务,如果不能有什么解决方案?
- 如果加了异步操作就无法汇总,会先调用Notify中的方法
- 使用组中的
dispatch_group_enter
和dispatch_group_leave
即可处理,或者使用信号量dispatch_semaphore_t
。 - 正确的方法应该下边两个函数一起来使用,这样才能实现我们想要的最终效果:
dispatch_group_enter(dispatch_group_t group)
dispatch_group_leave(dispatch_group_t group)
- 参数group不能为空,在异步任务成功返回后调用
- 它明确的表明了一个 block 被加入到了队列组group中,此时group中的任务的引用计数会加1(类似于OC的内存管理),leave 是 -1
- 当队列组里的任务的引用计数等于0时,会调用dispatch_group_notify函数。
- (void)viewDidLoad {
[super viewDidLoad];
//创建一个group
__block dispatch_group_t group = dispatch_group_create();
//创建一个队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//group任务计数加3
dispatch_group_enter(group);
dispatch_group_enter(group);
dispatch_group_enter(group);
//创建一个GCD线程1
dispatch_group_async(group, queue, ^{
//模拟异步耗时操作
dispatch_async(dispatch_get_main_queue(), ^{
for (int i = 0; i < 10000; i ++) {}
NSLog(@"线程1");
//group任务计数减1
dispatch_group_leave(group);
});
});
//创建一个GCD线程2
dispatch_group_async(group, queue, ^{
//模拟异步耗时操作
dispatch_async(dispatch_get_main_queue(), ^{
for (int i = 0; i < 10000; i ++) {}
NSLog(@"线程2");
//group任务计数减1
dispatch_group_leave(group);
});
});
//创建一个GCD线程3
dispatch_group_async(group, queue, ^{
//模拟异步耗时操作
dispatch_async(dispatch_get_main_queue(), ^{
for (int i = 0; i < 10000; i ++) {}
NSLog(@"线程3");
//group任务计数减1
dispatch_group_leave(group);
});
});
//创建一个group通知, 任务计数为0时自动调用
dispatch_group_notify(group, queue, ^{
NSLog(@"结束");
});
}
- 使用信号量来解决的方案?
- (void)viewDidLoad {
[super viewDidLoad];
//创建一个group
__block dispatch_group_t group = dispatch_group_create();
//创建一个队列
__block dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//创建一个信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
//创建一个GCD线程1
dispatch_group_async(group, queue, ^{
//模拟异步耗时操作
dispatch_async(dispatch_get_main_queue(), ^{
for (int i = 0; i < 10000; i ++) { }
NSLog(@"线程1");
//完成迭代后, 增加信号量
dispatch_semaphore_signal(semaphore);
});
//在迭代完成之前, 信号量等待
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
});
//创建一个GCD线程2
dispatch_group_async(group, queue, ^{
//模拟异步耗时操作
dispatch_async(dispatch_get_main_queue(), ^{
for (int i = 0; i < 10000; i ++) {}
NSLog(@"线程2");
//完成迭代后, 增加信号量
dispatch_semaphore_signal(semaphore);
});
//在迭代完成之前, 信号量等待
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
});
//创建一个GCD线程3
dispatch_group_async(group, queue, ^{
//模拟异步耗时操作
dispatch_async(dispatch_get_main_queue(), ^{
for (int i = 0; i < 10000; i ++) { }
NSLog(@"线程3");
//完成迭代后, 增加信号量
dispatch_semaphore_signal(semaphore);
});
//在迭代完成之前, 信号量等待
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
});
//创建一个group通知, 任务计数为0时自动调用
dispatch_group_notify(group, queue, ^{
NSLog(@"结束");
});
}
使用信号量,不同线程相互干扰,如何处理?
- 每个线程分别创建自己的信号量即可。
GCD实现异步任务同步除了线程组还能使用方式? - dispatch_barrier_async,以及上面的两种方法。
三方框架(AF,SD)
AFNetworking2.X为什么需要常驻线程?
- 首先需要在子线程去start connection,请求发送后,所在的
子线程需要保活以保证正常接收到 NSURLConnectionDelegate 回调方法
。如果每来一个请求就开一条线程,并且保活线程,这样开销太大了。所以只需要保活一条固定的线程,在这个线程里发起请求、接收回调。
AFNetworking3.0后为什么不再需要常驻线程?
NSURLSession发起的请求,不再需要在当前线程进行代理方法的回调!可以指定回调的delegateQueue,这样我们就不用为了等待代理回调方法而苦苦保活线程了。
operationQueue.maxConcurrentOperationCount =1在3.0中需要 ?
AF3.0的operationQueue是用来接收NSURLSessionDelegate回调的,鉴于一些多线程数据访问的安全性考虑,设置了maxConcurrentOperationCount = 1来
达到串行回调的效果`。
而AF2.0的operationQueue是用来添加operation并进行并发请求的,所以不要设置为1。
Swift
swift中的class和struct区别?
-
属性初始化的不同
,struct可直接在构造函数中初始化属性
, class不可直接在构造函数中初始化
,要用带参数的构造方法 -
变量赋值方式不同
,值拷贝和引用拷贝 - 可变与不可变内容:struct使用let和var甄别,class不具备这一特性。
- struct 的 function 要去改变 property 的值的时候要加上
mutating
,而 class 不用 - struct不能继承,class可以继承。
- struct分配在栈中,class分配在堆中。
swift中struct的优势和劣势在哪里?
优势:
- 数据安全:因为 Struct 是用值类型传递的,它们没有引用计数。
- 内存安全:由于他们没有引用数,他们不会因为循环引用导致内存泄漏。
- 速度优势:值类型通常来说是以栈的形式分配的,而不是用堆。因此他们比 Class 要快。
- 拷贝斟酌: OC 里拷贝一个对象,你必须选用正确的拷贝类型(深拷贝、浅拷贝),而struct不需要担心。
- 线程安全:值类型是自动线程安全的。无论你从哪个线程去访问你的 Struct ,都非常简单。
劣势: - OC与swift混编无法使用
- Struct 不能被序列化成 NSData 对象。
- Struct 不能相互继承。
如果模型较小,并且无需继承、无需储存到 NSUserDefault 或者无需 Objective-C 使用时,建议使用 Struct。
UI相关:
tableView数据源同步的问题?
- 并发访问:主线程进行了数据源的操作,将操作同步给子线程,进行刷新
- 串行访问:子线程请求一组数据,主线程进行了数据增删进行等待,子线程请求完成后,同步数据,刷新UI(耗时体验)
UIView和CALayer的区别?为什么要这样设计?
- UIView提供内容,以及负责交互事件的处理,参与响应链的访问
- CALayer提供展示内容
- 保证单一职责原则,展示和交互各自负责
事件传递
- 事件传递主要是和两个方法有关hitTest和pointInside(视图响应和是否在当前视图范围)
-
事件流程图
image.png
视图响应链的流程
- 继承于UIResponder,nextResponder是下一个响应者
UI卡顿丢帧原因以及方案
- VSyn垂直信号 一秒60帧
- CPU做视图的布局工作,GPU进行图层渲染
- 在规定的时间内,下一帧VSyn信号到来前未完成CPU和GPU的图片合成工作,因此产生丢帧。
image.png
优化方案
CPU: - 对象创建,调整,销毁放在子线程操作
- 预排版(布局计算,文本计算)放在子线程
- 预渲染(文本异步绘制,图片解码)
GPU: - 避免离屏渲染
- 视图混合
UIView绘制原理 -
setNeedsDisplay - 调用layer.setNeedsDisplay(脏标记) - Layer.display - 响应方法
绘制流程图:
image.png
image.png
异步绘制
-
通过实现 layer.delegate displayLayer 负责生成对应的bitmap,操作layer.contents
在子线程中异步绘制好后 ,回到主线程layer设置Contents
image.png
离屏渲染
-
在屏渲染
在当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示屏幕缓冲区中进行 -
离屏渲染
指的是GPU在当前屏幕缓冲区以外新开辟
的一个缓冲区进行渲染
设置UI视图某些属性(圆角),在未预合成之前不能直接显示的就会触发离屏渲染。
离屏渲染触发的场景 - 圆角和maskToBounds一起使用
- 图层蒙版
- 阴影
离屏渲染触发的场景
离屏渲染会增加GPU的工作量,会导致CPU和GPU总耗时超过16.7ms,即丢帧或者卡顿
iOS拓展问题:
动态库和静态库的区别,以及链接过程?
- 静态库:以.a 和 .framework为文件后缀名。
- 动态库:以.tbd(之前叫.dylib) 和 .framework 为文件后缀名。
- 区别
静态库:链接时会被完整的复制到可执行文件中,被多次使用就有多份拷贝。
动态库:链接时不复制,程序运行时由系统动态加载到内存,系统只加载一次,多个程序共用(如系统的UIKit.framework等),节省内存。 - 库的链接过程
链接一个库的3个必要条件:
-I(大写i):在指令目录寻找头文件 (xc setting:header search path)
-L:指定链接的库文件路径 (.a/.dylib)(xc setting: Libary search path)
-l(小写L):指定链接库文件的名称(xc setting:other linker flags)
Macho文件如何生成的?
- LLVM预处理代码 -> LLVM 进行词法语法分析 -> LLVM生成AST -> AST 会生成 IR -> IR 生成的可执行文件就是 Mach-O
Macho的结构?
Header (头部)
用于快速确认该文件的CPU类型、文件类型等信息
LoadCommands (加载命令)
用于告诉loader如何设置并加载二进制数据
Data (数据段 segment)
存放数据:代码、字符常量、类、方法等
可以拥有多个segment,每个segment可以有零到多个section。每个段都有一段虚拟地址映射到进程的地址空间
Loader Info (链接信息)
一个完整的用户级MachO文件
的末端是一系列链接信息。其中包含了动态加载器用来链接可执行文件或者依赖所需使用的符号表、字符串表等
网络
HTTPS的加载流程?
1.客户端
发送一些随机数,和加密算法,通过tcp发送给服务端。
2.服务端
得到信息后通过私钥解密返回给客户端(返回SSL证书的版本号,公钥进行非对称加密,随机数)
3.需要验证服务端
返回的证书是否合法
4.客户端会生成预主秘钥
5.预主秘钥
加上两个随机数 进行公钥加密 ,再回传给服务端
。
6.服务端会进行私钥解密
,获取预主密钥
7.最后进行对称加密
进行信息交互 。
非对称加密流程,(通过公钥算不出私钥,发送后进行私钥解密),耗时只验证身份使用非对称加密,验证通过后进行对称加密通信。
算法,数据结构
链表的数组的使用场景?(字节-懂车)
-
数组应用场景:数据比较少;经常做的运算是按序号访问数据元素;数组更容易实现,任何高级语言都支持;构建的线性表较稳定。
-
链表应用场景:对线性表的长度或者规模难以估计;频繁做插入删除操作;构建动态性比较强的线性表。
二叉树 按层次遍历 (字节-懂车)
用底层设计内存 缓存 ,最大为40MB,以及超过40MB处理机制 (字节-懂车)
网友评论