Objective-C 部分
OC对象的本质
一个OC对象占用多少内存?
系统分配了16个字节给NSObject对象(通过malloc_size函数获得)。
但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得)。
- OC对象 最少占用 16 个字节内存 .
- 当对象中包含属性, 会按属性占用内存开辟空间. 在结构体内存分配原则下自动偏移和补齐 .
- 对象最终满足 16 字节对齐标准 .
- 属性最终满足 8 字节对齐标准 .
- 可以通过 #pragma pack() 自定义对齐方式 .
对象的isa指针指向哪里?
instance对象的isa指向class对象
class对象的isa指向meta-class对象
meta-class对象的isa指向基类的meta-class对象
OC的类信息存放在哪里?
对象方法、属性、成员变量、协议信息,存放在class对象中
类方法,存放在meta-class对象中
成员变量的具体值,存放在instance对象
KVO本质
iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
表述方案一:
- 利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
- 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
父类原来的setterwillChangeValueForKey:
didChangeValueForKey:
- 内部会触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:)
表述方案二:
当一个对象使用了KVO监听,iOS系统会修改这个对象的isa指针,改为指向一个全新的通过Runtime动态创建的子类,子类拥有自己的set方法实现,set方法实现内部会顺序调用willChangeValueForKey方法、原来的setter方法实现、didChangeValueForKey方法,而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:监听方法。
如何手动触发KVO?
手动调用willChangeValueForKey:和didChangeValueForKey:
1 | // 实现 |
被监听的属性的值被修改时,就会自动触发KVO。如果想要手动触发KVO,则需要我们自己调用willChangeValueForKey和didChangeValueForKey方法即可在不改变属性值的情况下手动触发KVO,并且这两个方法缺一不可。
直接 修改成员变量 会触发KVO么?
不会触发KVO
- KVC 对属性赋值时候 是会在这个类里边 去查找 _age isAge setAge setIsAge 等方法的 ,最终会调用属性的 setter 方法 ,那么如果添加了 KVO 还是会被触发的 .
- 相反 设置成员变量 _age 由于不会触发 setter 方法 ,因此不会去触发 KVO 相关的代码 .
通过KVC修改属性会触发KVO么?
会触发KVO
KVC在赋值时候,内部会触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:) 发送通知.
setValue:forKey:的原理
valueForKey:的原理
KVC 原理
KVC的赋值和取值过程是怎样的?原理是什么?
KVC的全称是Key-Value Coding,俗称“键值编码”,可以通过一个key来访问某个属性
调用 setValue:forKey:
setKey,_setKey ->找到了则进行赋值,未找到调用 accessInstanceVarlableDirctly 是否运行 修改值,返回YES
调用_key, _isKey, key, isKey 进行赋值
Category原理探究
Category的使用场合是什么?
- 在不修改原有类代码的情况下,为类添对象方法或者类方法
- 或者为类关联新的属性
- 分解庞大的类文件
使用场合:
- 添加实例方法
- 添加类方法
- 添加协议
- 添加属性
- 关联成员变量
Category的实现原理
Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)
Category和Class Extension的区别是什么?
Class Extension在编译的时候,它的数据就已经包含在类信息中
Category是在运行时,才会将数据合并到类信息中
Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
有load方法
load方法在runtime加载类、分类的时候调用
load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用
initialize方法如何调用,以及调用时机
当类第一次收到消息的时候会调用类的initialize方法
是通过 runtime 的消息机制 objc_msgSend(obj,@selector()) 进行调用的
优先调用分类的 initialize, 如果没有分类会调用 子类的,如果子类未实现则调用 父类的
load、initialize方法的区别什么?它们在category中的调用的顺序?以及出现继承时他们之间的调用过程?
load 是类加载到内存时候调用, 优先父类->子类->分类
initialize 是类第一次收到消息时候调用,优先分类->子类->父类
同级别和编译顺序有关系
load 方法是在 main 函数之前调用的
Category能否添加成员变量?如果可以,如何给Category添加成员变量?
不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果
Category是发生在运行时,编译完毕,类的内存布局已经确定,无法添加成员变量(Category的底层数据结构也没有成员变量的结构) 可以通过 runtime 动态的关联属性
Block 原理
block的原理是怎样的?本质是什么?
block 本质其实是OC对象
block 内部封装了函数调用以及调用环境
__block的作用是什么?有什么使用注意点?
如果需要在 block 内部修改外部的 局部变量的值,就需要使用__block 修饰(全局变量和静态变量不需要加__block 可以修改)
__block 修饰以后,局部变量的数据结构就会发生改变,底层会变成一个结构体的对象,结构内部会声明 一个 __block修饰变量的成员, 并且将 __block修饰变量的地址保存到堆内存中. 后面如果修改 这个变量的值,可以通过 isa 指针找到这个结构体,进来修改 这个变量的值;
可以在 block 内部修改 变量的值
block的属性修饰词为什么是copy?使用block有哪些使用注意?
block 一旦没有进行copy操作,就不会在堆上
使用注意:循环引用问题 (外部使用__weak 解决)
block在修改NSMutableArray,需不需要添加__block?
如果是操作 NSMutableArray 对象不需要,因为 block 内部拷贝了 NSMutableArray对象的内存地址,实际是通过内存地址操作的
如果 NSMutableArray 对象要重新赋值,就需要加__block
Block 内部为什么不能修改局部变量,需要加__block
通过查看Block 源码,可以发现, block 内部如果单纯使用 外部变量, 会在 block 内部创建同样的一个变量,并且将 外部变量的值引用过来..(只是将外部变量值拷贝到 block 内部), 内部这个变量和外部 实际已经没关系了
从另一方面分析,block 本质也是一个 函数指针, 外部的变量也是一个局部变量,很有可能 block 在使用这个变量时候,外部变量已经释放了,会造成错误
加了__block 以后, 会将外部变量的内存拷贝到堆中, 内存由 block 去管理.
OC 的消息机制
讲一下 OC 的消息机制
OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)
objc_msgSend底层有3大阶段: 消息发送(当前类、父类中查找)、动态方法解析、消息转发
消息发送流程
当我们的一个 receiver(实例对象)收到消息的时候, 会通过 isa 指针找到 他的类对象, 然后在类对象方法列表中查找 对应的方法实现,如果 未找到,则会通过 superClass 指针找到其父类的类对象, 找到则返回,未找打则会一级一级往上查到,最终到NSObject 对象, 如果还是未找到就会进行动态方法解析
类方法调用同上,只不过 isa 指针找到元类对象;
动态方法解析机制
当我们发送消息未找到方法实现,就会进入第二步,动态方法解析: 代码实现如下
1 | // 动态方法绑定- 实例法法调用 |
消息转发机制流程
未找到动态方法绑定,就会进行消息转发阶段
1 | // 快速消息转发- 指定消息处理对象 |
Runtime
- Objective-C runtime是一个
运行时
库,它为Objective-C语言的动态特性提供支持,我们所写的OC代码在运行时都转成了runtime相关的代码,类转换成C语言对应的结构体,方法转化为C语言对应的函数,发消息转成了C语言对应的函数调用。通过了解runtime以及源码,可以更加深入的了解OC其特性和原理
什么是Runtime?平时项目中有用过么?
OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行
OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数
平时编写的OC代码,底层都是转换成了Runtime API进行调用
runtime具体应用
利用关联对象(AssociatedObject)给分类添加属性
遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
交换方法实现(交换系统的方法)
利用消息转发机制解决方法找不到的异常问题
[self class] 和 [super class] 的区别
[self class] 和 [super class] 都是给当前类返送消息,spuer 表示在父类中查找
[self superClass] 和 [super superclass] 也是也当前类发消息,返回父类
第一个打印:
MJStudent / MJStudent/ MJerson / MJPerson
1 | ```isMemberOfClass``` 表示是否为当前类的的类型 |
runloop 是怎么响应用户操作的, 具体流程是什么样的?
- source1 捕捉用户触摸事件
- source0 去处理触摸时间
说说runLoop的几种状态
添加Observer监听RunLoop的所有状态
- kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
- kCFRunLoopBeforeTimers = (1UL << 1), //即将处理Timer
- kCFRunLoopBeforeSources = (1UL << 2), //即将处理Source
- kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
- kCFRunLoopAfterWaiting = (1UL << 6), //刚从休眠中唤醒
- kCFRunLoopExit = (1UL << 7), //即将退出Loop
- kCFRunLoopAllActivities = 0x0FFFFFFFU //所有状态改变
runloop的mode作用是什么?
runloop 只能在一种 mode 下运行, 做不同的事情,runloop 会切换到对应的 model 下来执行,默认是 kCFRunLoopDefaultMode 如果视图滑动再回切换到 UITrackingRunLoopMode,如果需要在多种 mode 下运行则需要手动设置 kCFRunLoopCommonModes;
kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode
GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode
如何实现一个常驻线程
- 创建RunLoop
- 向RunLoop中添加port、source等来保证RunLoop不退出
- 启动RunLoop
多线程
同一时间,CPU 只能处理理一条线程, 只有一条线程在⼯工作 多线程并发执行,其实是 CPU 快速的在多条线程之间调度(切换) 如果 CPU 调度线程的时间⾜够快, 就造成了多线程并发执⾏的假象
优势
充分发挥多核处理器的优势,将不同线程任务分配给不同的处理器,真正进入“⾏行 计算”状态
弊端
新线程会消耗内存控件和cpu时间,线程太多会降低系统行性能。
iOS的多线程方案有哪几种?你更倾向于哪一种?
倾向于GCD ,简单灵活,使用方便
你在项目中用过 GCD 吗?
使用过
GCD中有2个用来执行任务的函数
用同步的方式执行任务
1
2
3dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
queue:队列
block:任务用异步的方式执行任务
1 | dispatch_async(dispatch_queue_t queue, dispatch_block_t block); |
GCD 的队列类型
GCD的队列可以分为2大类型
并发队列(Concurrent Dispatch Queue)
可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
并发功能只有在异步(dispatch_async)函数下才有效
串行队列(Serial Dispatch Queue)
让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
说一下 OperationQueue 和 GCD 的区别,以及各自的优势
GCD是纯C语⾔言的API,NSOperationQueue是基于GCD的OC版本封装
GCD只⽀支持FIFO的队列列,NSOperationQueue可以很⽅方便便地调整执⾏行行顺序、设置最⼤大并发数量量
NSOperationQueue可以在轻松在Operation间设置依赖关系,⽽而GCD 需要写很 多的代码才能实现
NSOperationQueue⽀支持KVO,可以监测operation是否正在执⾏行行 (isExecuted)、 是否结束(isFinished),是否取消(isCanceld)
GCD的执⾏行行速度⽐NSOperationQueue快 任务之间不不太互相依赖:GCD 任务之间 有依赖\或者要监听任务的执⾏行行情况:NSOperationQueue
线程安全的处理手段有哪些?
- 加锁
- 同步执行
OC你了解的锁有哪些?在你回答基础上进行二次提问?
os_unfair_lock
ios10 开始OSSpinLock
ios10 废弃dispatch_semaphore
建议使用,性能也比较好dispatch_mutex
dispatch_queue
串行NSLock
对 mutex 封装@synchronized
性能最差
追问一:自旋和互斥对比?
什么情况使用自旋锁比较划算?
- 预计线程等待锁的时间很短
- 加锁的代码(临界区)经常被调用,但竞争情况很少发生
- CPU资源不紧张
- 多核处理器
什么情况使用互斥锁比较划算?
- 预计线程等待锁的时间较长
- 单核处理器
- 临界区有IO操作
- 临界区代码复杂或者循环量大
- 临界区竞争非常激烈
追问二:使用以上锁需要注意哪些?
- 注意死锁
- 在串行队列使用同步,容易造成死锁
追问三:用C/OC/C++,任选其一,实现自旋或互斥?口述即可!
两种锁的加锁原理:
互斥锁:线程会从sleep(加锁)——>running(解锁),过程中有上下文的切换,cpu的抢占,信号的发送等开销。
自旋锁:线程一直是running(加锁——>解锁),死循环检测锁的标志位
请问下面代码的打印结果是什么?
打印 1,3
performSelector after 是基于 timer 定制器,定时器又是基于 runloop 实现的
任务2在子线程中,子线程默认 runloop 是不开启的,所以不执行2
请问下面代码的打印结果是什么?
打印1
start 执行完,线程就销毁了.任务 test 没法执行了
使用CADisplayLink、NSTimer有什么注意点?
CADisplayLink 保证调用频率和刷帧频率一直,60FPS, 不用设置时间间隔,每秒钟60次
- 可以使用 proxy 代理解决循环引用
CADisplayLink、NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用
内存管理
介绍下内存的几大区域
低地址-> 高地址
保留->代码段->数据段(字符串常量,已初始化全局数据,未初始化数据)>堆->栈内存-> 内核区域
代码段: 编译之后的代码
数据段: 字符串常量,已经初始化的全局变量,或者静态变量,未初始化的全局变量,静态变量
堆 (低>高) 通过 alloc malloc calloc 动态分配的内存
栈 (高地址 从 低地址) 函数调用开销()
讲一下你对 iOS 内存管理的理解
在iOS中,使用引用计数来管理OC对象的内存
一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
内存管理的经验总结
- 当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
- 想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1
可以通过以下私有函数来查看自动释放池的情况
`extern void _objc_autoreleasePoolPrint(void);`
ARC 都帮我们做了什么?
LLVM + Runtime 会为我们代码自动插入 retain 和 release 以及 autorelease等代码,不需要我们手动管理
weak指针的实现原理
Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。
runtime对注册的类, 会进行布局,对于weak对象会放入一个hash表中。 用weak指向的对象内存地址作为key,当此对象的引用计数为0的时候会dealloc,假如weak指向的对象内存地址是a,那么就会以a为键, 在这个weak表中搜索,找到所有以a为键的weak对象,从而设置为nil。
autorelease对象在什么时机会被调用release
iOS在主线程的Runloop中注册了2个Observer
第1个
Observer
监听了kCFRunLoopEntry
事件,会调用objc_autoreleasePoolPush()
第2个
Observer
监听了
kCFRunLoopBeforeWaiting
事件,会调用objc_autoreleasePoolPop()
、objc_autoreleasePoolPush()
监听了
kCFRunLoopBeforeExit
事件,会调用objc_autoreleasePoolPop()
objc_autoreleasePoolPop()
调用时候回给pool
中的对象发送一次release
消息
方法里有局部对象, 出了方法后会立即释放吗
- 如果是普通的 局部对象 会立即释放
- 如果是放在了 autoreleasePool 自动释放池,则会等runloop 循环,进入休眠前释放
思考以下2段代码能发生什么事?有什么区别?
- 第一个内存会暴涨,self.name 会不停的创建
- 第二个内存固定,会使用 Tagged Pointer 将值存在地址中
你在项目中是怎么优化内存的?
内存优化可以从 内存泄漏 和 内存开销 2方面入口
减少内存泄露
- 可以使用静态分析以及instruments的leaks 分析
- 注意 NStimer 以及 block ,delegate 等的使用,避免循环引用
降低内存使用峰值
关于图片加载占用内存问题:imageNamed: 方法会在内存中缓存图片,用于常用的图片。
imageWithContentsOfFile: 方法在视图销毁的时候会释放图片占用的内存,适合不常用的大图等。tableView cell 尽量使用重用机制,减少额外的开销
tableView 列表图片展示尽量使用缩略图
延迟加载对象,节约内存开销
避免短时间大量创建对象,配合 autoreleasePool 减少内存峰值
重用大开销对象,比如: NSDateFormatter和NSCalendar
加载 html 尽量使用 wkwebView
单例使用不易过多
线程最大并发数
APP优化
优化你是从哪几方面着手?
- 卡顿优化
- 启动优化
- 耗电量优化
- app 瘦身
CPU 占用率、 内存使用情况、网络状况监控、启动时闪退、卡顿、FPS、使用时崩溃、耗电量监控、流量监控….
列表卡顿的原因可能有哪些?你平时是怎么优化的?
最常用的就是cell的重用, 注册重用标识符
- 如果不重用cell时,每当一个cell显示到屏幕上时,就会重新创建一个新的cell;
— 如果有很多数据的时候,就会堆积很多cell。
— 如果重用cell,为cell创建一个ID,每当需要显示cell 的时候,都会先去缓冲池中寻找可循环利用的cell,如果没有再重新创建cell
- 如果不重用cell时,每当一个cell显示到屏幕上时,就会重新创建一个新的cell;
避免cell的重新布局
— cell的布局填充等操作 比较耗时,一般创建时就布局好
— 如可以将cell单独放到一个自定义类,初始化时就布局好提前计算并缓存cell的属性及内容
— 当我们创建cell的数据源方法时,编译器并不是先创建cell 再定cell的高度
— 而是先根据内容一次确定每一个cell的高度,高度确定后,再创建要显示的cell,滚动时,每当cell进入凭虚都会计算高度,提前估算高度告诉编译器,编译器知道高度后,紧接着就会创建cell,这时再调用高度的具体计算方法,这样可以方式浪费时间去计算显示以外的cell减少cell中控件的数量
— 尽量使cell得布局大致相同,不同风格的cell可以使用不用的重用标识符,初始化时添加控件,
— 不适用的可以先隐藏不要使用ClearColor,无背景色,透明度也不要设置为0
— 渲染耗时比较长使用局部更新
— 如果只是更新某组的话,使用reloadSection进行局部更新加载网络数据,下载图片,使用异步加载,并缓存
少使用addView 给cell动态添加view
按需加载cell,cell滚动很快时,只加载范围内的cell
不要实现无用的代理方法,tableView只遵守两个协议
缓存行高:estimatedHeightForRow不能和HeightForRow里面的layoutIfNeed同时存在,这两者同时存在才会出现“窜动”的bug。所以我的建议是:只要是固定行高就写预估行高来减少行高调用次数提升性能。如果是动态行高就不要写预估方法了,用一个行高的缓存字典来减少代码的调用次数即可
不要做多余的绘制工作。在实现drawRect:的时候,它的rect参数就是需要绘制的区域,这个区域之外的不需要进行绘制。例如上例中,就可以用CGRectIntersectsRect、CGRectIntersection或CGRectContainsRect判断是否需要绘制image和text,然后再调用绘制方法。
预渲染图像。当新的图像出现时,仍然会有短暂的停顿现象。解决的办法就是在bitmap context里先将其画一遍,导出成UIImage对象,然后再绘制到屏幕;
使用正确的数据结构来存储数据。
app 启动优化
pre-main 之前
- 排查无用的动态库(定期清理)
- 减少ObjC类(项目中不适用的的库,废弃的代码等)、方法(selector)、分类(category)的数量、无用的库
- 少在类的+load方法里做事情,尽量把这些事情推迟到+initiailize1.
main 函数之后的 didFinishLaunchingWithOptions 加载完之前
- 不影响用户体验的操作,做延迟加载,不要全部放在 didFinishLaunchingWithOptions中去做
- 版本更新,一些三方初始化,不需要在 didFinishLaunchingWithOptions 初始化的放到,界面展示完以后再初始化
- 一些网络请求延迟 请求..
- 一些业务逻辑延迟 加载
- 初始化第三方 SDK
- 配置 APP 运行需要的环境
- 自己的一些工具类的初始化
app 耗电量优化
不要频繁的刷新页面,能刷新1行cell最好只刷新一行,尽量不要使用reloadData.
选择正确的集合
- NSArray,使用index来查找很快(插入和删除很慢)
- 字典,使用键来查找很快
- NSSets,是无序的,用键查找很快,插入/删除很快
少用运算获得圆角,必须要用圆角的话,不如把图片本身就做成圆角
懒加载,不要一次性创建所有的subview,而是需要时才创建.
重用机制
图片处理
- 图片与imageView相同大小,避免多余运算
- 可以使用整副的图片,增加应用体积,但是节省CPU
- 可调大小的图片,可以省去一些不必要的空间
- CALayer,CoreGraphics,甚至OpenGL来绘制,消耗CPU
cache,cache,cache(缓存所有需要的)
- 服务器相应结果的缓存(图片)
- 复杂计算结果的缓存(UITableView的行高)
尽量少用透明或半透明,会产生额外的运算.
使用ARC减少内存失误,dealloc需要重写并对属性置为nil
避免庞大的xib,storyBoard,尽量使用纯代码开发
CPU层面
- Timer的时间间隔不宜太短,满足需求即可
- 线程适量,不宜过多,不要阻塞主线程
- 优化算法,减少循环次数
- 定位和蓝牙按需取用,定位之后要关闭或降低定位频率
- 一些硬件的使用,不使用就关掉
app 的包瘦身
APP 架构
讲讲 MVC、MVVM、MVP,以及你在项目里具体是怎么写的?
MVC Model-view-controller 数据-视图-控制器
- 一般控制器用于管理数据和视图, 数据和视图交互都是通过控制器来进行的.视图和数据进行了解耦, 但是我们日常使用经常会将模型绑定给视图.模型封装在视图内部,外部不用管理视图内部业务逻辑,这数据 mvc 的变种, 控制器只给视图模型数据就好了. 缺点是视图和 模型有耦合;
MVVM Model-view-viewModel 模型-视图-视图模型
- view 和 model 的交互通过viewmodel 来进行交互,实现数据的双向绑定
MVP Model-view - Presenter 模型-视图-主持人
- view 和 model 的交互通过Presenter,controller通过Presenter来管理 model 和 View
你自己用过哪些设计模式?
结合自己项目来讲吧
一般开始做一个项目,你的架构是如何思考的?
- 根据模块,使用 mvc 功能划分..结合自己项目讲比较容易
- 涉及到东西也比较多,比较杂,大到整个项目架构,小到一个 view 的架构;没具体的答案