天天瞎忙活,QQ:705719110,欢迎来撩!

iOS面试之前准备的部分知识

iOS基础 随风 501℃ 0评论

OC对象的本质

– 一个NSObject对象占用多少内存?
系统分配了16个字节给NSObject对象(通过malloc_size函数获得)
但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得)

sizeof() 是个运算符,计算类型大小
本质是结构体
首先objc源码中规定的是16个字节 instanceSize()函数
其次由于结构体内存对齐,所分配的内存必须是结构体内最大的成员所占空间的倍数,最大的就是 isa 占8个字节,所以分配16个字节

– 对象的isa指针指向哪里?
instance对象的isa指向class对象
class对象的isa指向meta-class对象
meta-class对象的isa指向基类的meta-class对象

1. instance的isa指向class
2. class的isa指向meta-class
3. meta-class的isa指向基类的meta-class
4. class的superclass指向父类的class
如果没有父类,superclass指针为nil
5. meta-class的superclass指向父类的meta-class
基类的meta-class的superclass指向基类的class
6. instance调用对象方法的轨迹
isa找到class,方法不存在,就通过superclass找父类
7. class调用类方法的轨迹
isa找meta-class,方法不存在,就通过superclass找父类

类方法最终没有的话可能会调用对象方法😳

– OC的类信息存放在哪里?
对象方法、属性、成员变量、协议信息,存放在class对象中
类方法,存放在meta-class对象中
成员变量的具体值,存放在instance对象

一些讲解

子类实例对象调用父类方法过程:先通过实例对象的isa查找到类对象,通过类对象的superclass查找到父类的类对象(这里就能找到方法了),如果是类方法,要根据isa找到元类对象,再根据元类的对象的superclass查找到父类的元类对象

想窥探内存中类的结构只需要写一个新版object的结构体强转一下对象就可以看到了

KVO 键值监听 KVC 键值编码

– iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
willChangeValueForKey:
父类原来的setter
didChangeValueForKey:
内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)

– 如何手动触发KVO?
手动调用willChangeValueForKey:和didChangeValueForKey:

– 直接修改成员变量会触发KVO么?
不会触发KVO,因为KVO是重写了set方法,不经过set方法设值不会触发KVO

调用set方法才会走KVO,前提是设置监听,OC会在程序运行过程中动态创建一个子类,在set方法中插入监听
重要的两个方法

自动生成的中间子类还会重写 class、dealloc、__isKOA这几个方法
class重写是因为用 objc_getClass() 获取的 和 直接调用class方法获取的类名打印出来并不一样,objc_getClass()可以直接看到系统插入的子类名称

– 通过KVC修改属性会触发KVO么?
会触发KVO,走的其实也是set方法

– KVC的赋值和取值过程是怎样的?原理是什么?
这个过程东西多,网上容易搜。

KVC可以通过一个key来访问某个属性
常见的API有

setValue时候的步骤
getValue时候的步骤

Category 分类

– Category的使用场合是什么?
搜之

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

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

– Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
有load方法
load方法在runtime加载类、分类的时候调用
load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用

– load、initialize方法的区别什么?它们在category中的调用的顺序?以及出现继承时他们之间的调用过程?
1. 调用方式
load是根据函数地址直接调用
initialize是通过objc_msgSend调用

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

3. 调用顺序
load总是先调用类的load,其次再去调用分类的load,先编译的类优先调用,调用子类的load方法之前,会先调用父类的load
initialize是先初始化父类,再初始化子类(可能最终调用的是父类的initialize方法)

4. 继承
参考3

– Category能否添加成员变量?如果可以,如何给Category添加成员变量?
不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果

一些讲解
分类的方法会在运行时合并到类中去,而且是插入到合并之前类方法的位置,这就是为什么同样的方法优先调用分类的
类扩展就是 .m文件中的interface,是在编译时就会合并到类中,而分类要到运行时才会合并

load方法
加载类的时候就会调用
分类中的load没有覆盖类中的load?分类中的load其实也合并了,但是调用的时候直接取的类中的load的地址,并不是像一般方法一样用消息机制去调用(isa查找),所以先调用的是本类的load方法(在此之前先调用父类的load方法),而分类的load方法按照编译顺序调用

initialize方法
在类第一次接收到消息的时候调用
是通过消息机制调用的,所以分类的initialize方法会覆盖类的,调用自己的之前先调用父类的initialize方法(内部自动调用的)
如果子类没有实现initialize方法则会调用父类的

分类中可以写属性,但是只会生成set/get方法的声明,不会生成成员变量和set/get的实现,而且手动添加成员变量会报错
关联对象
关联对象给分类添加的属性并不是给类添加成员变量,没有添加到成员变量列表里,而是runtime维护了一个全局的hashmap,以键值的方式给对象添加
,并不占用对象的内存

Block

– block的原理是怎样的?本质是什么?
封装了函数调用以及调用环境的OC对象

– __block的作用是什么?有什么使用注意点?
把变量包装成对象,解决block内部无法修改auto变量的问题,不能修饰静态变量和全局变量,要注意内存管理的问题

– block的属性修饰词为什么是copy?使用block有哪些使用注意?
block一旦没有进行copy操作,就不会在堆上
使用注意:循环引用问题(ARC环境下 strong 、copy没有区别)

– block在修改NSMutableArray,需不需要添加__block?
不需要,改的不是array

一些讲解
block是个代码块,其实就是OC对象,封装了isa、函数地址、函数大小、外部的局部变量等

block本质结构体,并且内部有一个指向自己的指针

局部变量默认的修饰词是 auto,在函数执行完会释放,所以block中会直接捕获值,block后面再改变这个变量在block中保存的也不会变了,而使用static修饰的变量在block中捕获的是地址值,所以后面改变能影响到block中的打印

全局变量并没有捕获到block内部

self会被捕获所以self是个局部变量,因为每个方法其实默认会传递self和_cmd两个参数,

成员变量访问的时候其实是先拿到self再去访问成员变量😳

block有三种类型
block是oc对象,所以block也可以直接调用class方法,就可以查看类型

栈段的无法控制释放时间,所以会加一个copy放到堆上去,这就是为啥block成员变量要用copy

以下情况ARC下将block会对Stack自动copy
block作为函数返回值时
将block赋值给__strong指针(强指针)时
block作为Cocoa API中方法名含有usingBlock的方法参数时
block作为GCD API的方法参数时

对对象的引用(默认是auto修饰的)
如果是在栈上的,不管MRC还是ARC都是弱引用
如果是在堆上,就要看强引用还是弱引用了

static 修饰的变量可以直接在block中修改,那是因为前面说过这样的变量block中存的是地址指针

__block 修饰的变量是生成了一个新的结构体,结构体内部有一个指针forwarding指向自己,改值访问值都是通过这个指针,而block中捕获的变量就是转成这样一个成员变量

循环引用

ARC解决循环引用
__weak:
__unsafe_unretained:不常用,不安全,使用完成之后不会吧对象置为nil,导致野指针
__block解决(必须要调用block,而且必须置空)

MRC解决循环引用(不支持__weak)
__unsafe_unretained:
__block:因为内部结构体对外部对象的引用总是弱引用

Runtime

– 讲一下 OC 的消息机制
OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)
objc_msgSend底层有3大阶段
消息发送(当前类、父类中查找)、动态方法解析、消息转发

– 消息转发机制流程
往下面看

– 什么是Runtime?平时项目中有用过么?
OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行
OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数
平时编写的OC代码,底层都是转换成了Runtime API进行调用

– 具体应用
利用关联对象(AssociatedObject)给分类添加属性
遍历类的所有成员变量(修改textfield的占位文字颜色(私有属性)、字典转模型、自动归档解档)
交换方法实现(交换系统的方法)
利用消息转发机制解决方法找不到的异常问题
……

– 打印结果分别是什么?

为什么会这样???
isKindOfClass
判断是否是当前类型或者是子类

isMemberOfClass
判断类型是否相同 [self class] == cls

如果是用+号方法进行比较,那右边用的其实是元类
如果是用-号方法进行比较,那右边用的其实是类

– 以下代码能不能执行成功?如果可以,打印结果是什么?

打印出来的是viewcontroller😳
1. print为什么能调用成功?
因为cls是类对象,obj取的cls的地址,查找的时候其实是取对象的前八个字节,就是isa,所以能调用成功

2. 为什么self.name有值?
因为内存存储结构的问题,访问到了isa后面的地址,就变成这了
其实这个打印就看cls这个对象创建之前一句代码是啥,是谁就打打印谁,如果都没有那就是走
objc_msgSendSuper({self,[UIViewController class]}, @selector(viewDidLoad)) === [super viewDidLoad]; 从父类查找方法,如果没有这个super调用,直接就崩了,坏内存地址访问,不信写到main中试试
objc_msgSendSuper其实最终用的objc_msgSendSuper2这个方法

一些讲解
isa指针
arm64之后 该指针变成了 isa_t 共用体
(了解一下结构)

位运算 一个字节可以存储很多信息
! 取反
| 按位或
& 按位与
~ 按位取反

位域 其实就是结构体的形式来表示,只是只申请需要位数的控件

共用体 共用一块内存

Class的结构(meta-class与class的结构是差不多哩)

开始是不存在rw_t的,类信息都存在ro_t中,在运行时会创建rw_t,将ro_t中的信息复制到rw_t中,因为rw_t是可读可写的,这样才能在运行时合并分类的信息

method_list_t里面存的method_t

SEL 不管哪个类,不管写多少次,同样名称的 SEL 其实就是代表同样的东西,地址是一样的
比如 不同类的test方法 @selector(test) 地址完全一样,但是这只是代表名称

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

散列表原理(空间换时间)
@selector(方法名) & _mask 来查找方法的索引

查找顺序
先在cache中查找方法,查不到去bits中查,查不到去父类中去找,找到了会把父类中的方法缓存在自己的cache中

OC的方法调用:消息机制,给方法调用者发送消息 objc_msgSend

消息机制三大阶段
消息发送—->动态方法解析—->消息转发
1. 消息发送
底层是汇编实现的 看arm64那个

2. 动态方法解析
找不到方法走下面的动态解析

会先判断是否动态解析过,解析过就不再解析了,直接走消息发送

3. 消息转发
还是找不到就走消息转发

LLVM的中间代码

能够生成.ll文件,这就是中间代码文件

Runtime会用到的API

交换类簇(NSString、NSArray、NSDictonary这些类,他们真是类型是别的类)的时候,记得不能直接用self来换,要用类似下面的方法

Runloop

– 讲讲 RunLoop,项目中有用到吗?
控制线程生命周期(线程保活):比如AFN就是这样做的(不要每次都创建线程,减少性能消耗)

解决NSTimer在滑动时停止工作的问题:设置模式标识
监控应用卡顿
性能优化

– runloop内部实现逻辑?
Source0
触摸事件处理
performSelector:onThread:

Source1
基于Port的线程间通信
系统事件捕捉

Timers
NSTimer
performSelector:withObject:afterDelay:

Observers
用于监听RunLoop的状态
UI刷新(BeforeWaiting)
Autorelease pool(BeforeWaiting)

01、通知Observers:进入Loop
02、通知Observers:即将处理Timers
03、通知Observers:即将处理Sources
04、处理Blocks
05、处理Source0(可能会再次处理Blocks)
06、如果存在Source1,就跳转到第8步
07、通知Observers:开始休眠(等待消息唤醒)
08、通知Observers:结束休眠(被某个消息唤醒)
01> 处理Timer
02> 处理GCD Async To Main Queue
03> 处理Source1
09、处理Blocks
10、根据前面的执行结果,决定如何操作
01> 回到第02步
02> 退出Loop
11、通知Observers:退出Loop

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

– timer 与 runloop 的关系?
runloop结构里面有一堆模式,模式里面放的有timers
timer运行在runloop中,runloop控制timer的执行

– 程序中添加每3秒响应一次的NSTimer,当拖动tableview时timer可能无法响应要怎么解决?
设置标记 NSRunLoopCommonModes,这个只是标记,不是模式,这个标记表示两种模式下都能运行

– runloop 是怎么响应用户操作的, 具体流程是什么样的?
source1捕捉触摸事件,source0处理触摸事件

– 说说runLoop的几种状态

– runloop的mode作用是什么?
常见的2种Mode
kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行
UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响

CFRunLoopModeRef代表RunLoop的运行模式
一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer
RunLoop启动时只能选择其中一个Mode,作为currentMode
如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入
不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响
如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出

Foundation

Core Foundation

Runloop的休眠
runloop 使用mach_msg函数(内核层面的api,不开放)休眠线程,实现等待消息,有消息过来的时候再唤醒,节省资源

为啥要线程放在runloop中
不要每次都创建线程,减少性能消耗

多线程
– 你理解的多线程?
巴拉巴拉巴拉~

– iOS的多线程方案有哪几种?你更倾向于哪一种?
pthread:
NSThread:
GCD:
NSOperation:

– 你在项目中用过 GCD 吗?
必须的

– GCD 的队列类型
main queue: 主队列.
global queue: 全局队列.
custom queue: 自定义队列.

– 说一下 OperationQueue 和 GCD 的区别,以及各自的优势
GCD,是苹果基于 C 语言开发的一套低层API,提供了一种新的方法来进行并发程序编写。从基本功能上讲,GCD有点像NSOperationQueue,他们都允许程序将任务切分为多个单一任务然后提交至工作队列来并发地或者串行地执行。GCD比之NSOpertionQueue更底层更高效,并且它不是Cocoa框架的一部分。
NSOprationQueue是一个建立在 GCD 的基础之上的,面向对象的解决方案。它使用起来比 GCD 更加灵活,功能也更加强大。

GCD以 FIFO 的顺序执行并发任务的方式,使用 GCD 时我们并不关心任务的调度情况,而让系统帮我们自动处理。但是 GCD 的短板也是非常明显的,比如我们想要给任务之间添加依赖关系、取消或者暂停一个正在执行的任务时就会变得非常棘手。
Operation Queues :相对 GCD 来说,使用 Operation Queues 会增加一点点额外的开销,但是我们却换来了非常强大的灵活性和功能,我们可以给 operation 之间添加依赖关系、取消一个正在执行的 operation 、暂停和恢复 operation queue 等;

– 线程安全的处理手段有哪些?
使用线程同步技术,常用的方案:加锁

– OC你了解的锁有哪些?在你回答基础上进行二次提问;
追问一:自旋和互斥对比?
追问二:使用以上锁需要注意哪些?
追问三:用C/OC/C++,任选其一,实现自旋或互斥?口述即可!

– 请问下面代码的打印结果是什么?

打印结果是:111、333
原因
performSelector:withObject:afterDelay:的本质是往Runloop中添加定时器
子线程默认没有启动Runloop

– 请问下面代码的打印结果是什么?

打印结果:111

– 思考:如何用gcd实现以下功能
异步并发执行任务1、任务2
等任务1、任务2都执行完毕后,再回到主线程执行任务3

使用队列组

– 什么情况使用自旋锁比较划算?
预计线程等待锁的时间很短
加锁的代码(临界区)经常被调用,但竞争情况很少发生
CPU资源不紧张
多核处理器

– 什么情况使用互斥锁比较划算?
预计线程等待锁的时间较长
单核处理器
临界区有IO操作
临界区代码复杂或者循环量大
临界区竞争非常激烈

一些讲解
同步、异步、串行、并发
同步的线程不论用的是串行队列还是并发队列,或者是在主队列中,都是串行执行,并且没有开启新线程
异步的线程在主队列中也没有开启新线程,并且是串行执行,在并发队列中创建新线程并并发执行,在串行队列中创建新线程了但是是串行执行

死锁:使用sync往当前串行队列添加任务,会卡主当前的串行队列(产生死锁)
1. 死锁问题一

为什么上面的会造成死锁?
首先任务二添加到主队列中,队列先进先出,排队执行,所以任务二要等任务三执行完才执行
其次任务二是个同步操作,任务三要等任务二执行完才执行
互相等就是死锁
如果任务二是dispatch_async异步操作就不会死锁,因为不是同步操作,不需要一定要执行完才往下执行
如果任务二放在新创建的队列也不会造成死锁

2. 死锁问题二

上面的也会死锁,两个block互相等待,原理同上个问题

多线程的安全隐患
1. 资源共享
1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源
比如多个线程访问同一个对象、同一个变量、同一个文件

买票问题、银行存取钱问题

2. 当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题

3. 解决方案:使用线程同步技术(同步,就是协同步调,按预定的先后次序进行)
常见的线程同步技术是:加锁
OSSpinLock:
叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源
目前已经不再安全,可能会出现优先级反转问题
如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁

dispatch_semaphore
semaphore叫做”信号量”
信号量的初始值,可以用来控制线程并发访问的最大数量
信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步

dispatch_queue(DISPATCH_QUEUE_SERIAL)
直接使用GCD的串行队列,也是可以实现线程同步的

NSLock
对pthread_mutex普通锁的封装

NSRecursiveLock
对pthread_mutex递归锁的封装

NSCondition
对pthread_mutex与事件cond的封装

NSConditionLock
对NSCondition的进一步封装

@synchronized
@synchronized是对mutex递归锁的封装
源码查看:objc4中的objc-sync.mm文件
@synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作
利用传进去的对象作为key,存储结构为hashtable

性能从高到低排序
os_unfair_lock
OSSpinLock
dispatch_semaphore
pthread_mutex
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSCondition
pthread_mutex(recursive)
NSRecursiveLock
NSConditionLock
@synchronized

atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁
可以参考源码objc4的objc-accessors.mm
它并不能保证使用属性的过程是线程安全的
太耗性能,所以iOS一般不用,mac上用的多点

iOS中的读写安全方案
1. 思考如何实现以下场景
同一时间,只能有1个线程进行写的操作
同一时间,允许有多个线程进行读的操作
同一时间,不允许既有写的操作,又有读的操作

2. 上面的场景就是典型的“多读单写”,经常用于文件等数据的读写操作,iOS中的实现方案有
pthread_rwlock:读写锁
dispatch_barrier_async:异步栅栏调用(写的时候用这个,保证同一时间只有一个写操作)

内存管理

– 使用CADisplayLink、NSTimer有什么注意点?
循环引用
不准时

– 介绍下内存的几大区域
1. 代码段:编译之后的代码
2. 数据段
字符串常量:比如NSString *str = @”123″
已初始化数据:已初始化的全局变量、静态变量等
未初始化数据:未初始化的全局变量、静态变量等
3. 栈:函数调用开销,比如局部变量。分配的内存空间地址越来越小
4. 堆:通过alloc、malloc、calloc等动态分配的空间,分配的内存空间地址越来越大

– 讲一下你对 iOS 内存管理的理解
巴拉巴拉巴拉

– ARC 都帮我们做了什么?
LLVM + Runtime

LLVM编译器为我们生成释放的操作
弱引用需要Runtime的支持
所以说ARC是LLVM编译器与Runtime相互协作的结果

– weak指针的实现原理
weak指针会存储在SideTable结构体中的weak_table散列表中

– autorelease对象在什么时机会被调用release

– 方法里有局部对象, 出了方法后会立即释放吗

一些讲解
定时器
CADisplayLink 与 NSTimer
都是基于runloop的,时间会不准确

CADisplayLink 与 NSTimer 的循环引用解决方法
NSProxy 专门用来消息转发的,所以大部分的方法都会转发,比如 isKindOfClass:

GCD 定时器
与系统内核挂钩,所以时间准确,并且与runloop无关

内存布局
1. 代码段:编译之后的代码
2. 数据段
字符串常量:比如NSString *str = @”123″
已初始化数据:已初始化的全局变量、静态变量等
未初始化数据:未初始化的全局变量、静态变量等
3. 栈:函数调用开销,比如局部变量。分配的内存空间地址越来越小
4. 堆:通过alloc、malloc、calloc等动态分配的空间,分配的内存空间地址越来越大

Tagged Pointer
从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储

在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值

使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中

当指针不够存储数据时,才会使用动态分配内存的方式来存储数据

objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销

如何判断一个指针是否为Tagged Pointer?
iOS平台,最高有效位是1(第64bit)
Mac平台,最低有效位是1

思考以下2段代码能发生什么事?有什么区别?

OC对象的内存管理
在iOS中,使用引用计数来管理OC对象的内存

一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间

调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1

内存管理的经验总结
当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1,而类方法创建的比如array等类方法不用释放,因为内部做了autorelease

copy总结
产生一个副本对象,跟源对象互不影响

如果字符串太短就会使用Tagged Pointer技术,那就不是真正意义上的对象,就没有引用计数这一说

属性关键词:不可变属性可以用copy,可变属性不能用copy要用strong,因为set方法中赋值会使用copy而不是retain变成了不可变属性,改变的时候会报错,而字符串可不可变都用copy因为字符串一般只是赋值操作,并不需要append

自定义类想使用copy需要实现NSCopying协议,实现copyWithZone方法

引用计数是存储在isa中的,但是只有19位,如果存不下会存在SideTable结构体中的refcnts散列表中

weak指针的原理
当一个对象要释放时,会自动调用dealloc,接下的调用轨迹是

如果是weak指针会存储在SideTable结构体中的weak_table散列表中

autorelease

编译之后如下

每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址
所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起

调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址
调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY
id *next指向了下一个能存放autorelease对象地址的区域

可以通过以下私有函数来查看自动释放池的情况
extern void _objc_autoreleasePoolPrint(void);

iOS在主线程的Runloop中注册了2个Observer
第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()
第2个Observer
监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()

性能优化

– 你在项目中是怎么优化内存的?

– 优化你是从哪几方面着手?

– 列表卡顿的原因可能有哪些?你平时是怎么优化的?

– 遇到tableView卡顿嘛?会造成卡顿的原因大致有哪些?

一些讲解
CPU优化
尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用CALayer取代UIView

不要频繁地调用UIView的相关属性,比如frame、bounds、transform等属性,尽量减少不必要的修改

尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性

Autolayout会比直接设置frame消耗更多的CPU资源

图片的size最好刚好跟UIImageView的size保持一致

控制一下线程的最大并发数量

尽量把耗时的操作放到子线程
文本处理(尺寸计算、绘制)
图片处理(解码、绘制)

GPU优化
尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示

GPU能处理的最大纹理尺寸是4096×4096,一旦超过这个尺寸,就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸

尽量减少视图数量和层次

减少透明的视图(alpha<1),不透明的就设置opaque为YES

尽量避免出现离屏渲染

离屏渲染
在OpenGL中,GPU有2种渲染方式
On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作
Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作

离屏渲染消耗性能的原因
需要创建新的缓冲区
离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕

哪些操作会触发离屏渲染?
光栅化,layer.shouldRasterize = YES
遮罩,layer.mask
圆角,同时设置layer.masksToBounds = YES、layer.cornerRadius大于0
考虑通过CoreGraphics绘制裁剪圆角,或者叫美工提供圆角图片
阴影,layer.shadowXXX
如果设置了layer.shadowPath就不会产生离屏渲染

卡顿检测
平时所说的“卡顿”主要是因为在主线程执行了比较耗时的操作
可以添加Observer到主线程RunLoop中,通过监听RunLoop状态切换的耗时,以达到监控卡顿的目的

耗电的主要来源
CPU处理,Processing
网络,Networking
定位,Location
图像,Graphics

耗电优化
尽可能降低CPU、GPU功耗
少用定时器

优化I/O操作
尽量不要频繁写入小数据,最好批量一次性写入
读写大量重要数据时,考虑用dispatch_io,其提供了基于GCD的异步操作文件I/O的API。用dispatch_io系统会优化磁盘访问
数据量比较大的,建议使用数据库(比如SQLite、CoreData)

网络优化
减少、压缩网络数据
如果多次请求的结果是相同的,尽量使用缓存
使用断点续传,否则网络不稳定时可能多次传输相同的内容
网络不可用时,不要尝试执行网络请求
让用户可以取消长时间运行或者速度很慢的网络操作,设置合适的超时时间
批量传输,比如,下载视频流时,不要传输很小的数据包,直接下载整个文件或者一大块一大块地下载。如果下载广告,一次性多下载一些,然后再慢慢展示。如果下载电子邮件,一次下载多封,不要一封一封地下载

定位优化
如果只是需要快速确定用户位置,最好用CLLocationManager的requestLocation方法。定位完成后,会自动让定位硬件断电
如果不是导航应用,尽量不要实时更新位置,定位完毕就关掉定位服务
尽量降低定位精度,比如尽量不要使用精度最高的kCLLocationAccuracyBest
需要后台定位时,尽量设置pausesLocationUpdatesAutomatically为YES,如果用户不太可能移动的时候系统会自动暂停位置更新
尽量不要使用startMonitoringSignificantLocationChanges,优先考虑startMonitoringForRegion:

硬件检测优化
用户移动、摇晃、倾斜设备时,会产生动作(motion)事件,这些事件由加速度计、陀螺仪、磁力计等硬件检测。在不需要检测的场合,应该及时关闭这些硬件

APP的启动
APP的启动可以分为2种
冷启动(Cold Launch):从零开始启动APP
热启动(Warm Launch):APP已经在内存中,在后台存活着,再次点击图标启动APP

APP启动时间的优化,主要是针对冷启动进行优化

通过添加环境变量可以打印出APP的启动时间分析(Edit scheme -> Run -> Arguments)
DYLD_PRINT_STATISTICS设置为1
如果需要更详细的信息,那就将DYLD_PRINT_STATISTICS_DETAILS设置为1

APP的启动 – dyld
dyld(dynamic link editor),Apple的动态链接器,可以用来装载Mach-O文件(可执行文件、动态库等)

启动APP时,dyld所做的事情有
装载APP的可执行文件,同时会递归加载所有依赖的动态库
当dyld把可执行文件、动态库都装载完毕后,会通知Runtime进行下一步的处理

APP的启动 – runtime
启动APP时,runtime所做的事情有
调用map_images进行可执行文件内容的解析和处理
在load_images中调用call_load_methods,调用所有Class和Category的+load方法
进行各种objc结构的初始化(注册Objc类 、初始化类对象等等)
调用C++静态初始化器和__attribute__((constructor))修饰的函数

到此为止,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被runtime 所管理

APP的启动 – main
总结一下
APP的启动由dyld主导,将可执行文件加载到内存,顺便加载所有依赖的动态库
并由runtime负责加载成objc定义的结构
所有初始化工作结束后,dyld就会调用main函数
接下来就是UIApplicationMain函数,AppDelegate的application:didFinishLaunchingWithOptions:方法

APP的启动优化
按照不同的阶段
dyld
减少动态库、合并一些动态库(定期清理不必要的动态库)
减少Objc类、分类的数量、减少Selector数量(定期清理不必要的类、分类)
减少C++虚函数数量
Swift尽量使用struct

runtime
用+initialize方法和dispatch_once取代所有的__attribute__((constructor))、C++静态构造器、ObjC的+load

main
在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在finishLaunching方法中
按需加载

安装包瘦身
安装包(IPA)主要由可执行文件、资源组成
资源(图片、音频、视频等)
采取无损压缩
去除没有用到的资源: https://github.com/tinymind/LSUnusedResources

可执行文件瘦身
编译器优化
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插件检测出重复代码、未被调用的代码(不熟🙅‍♀️)

LinkMap
生成LinkMap文件,可以查看可执行文件的具体组成
可借助第三方工具解析LinkMap文件: https://github.com/huanxsd/LinkMap

架构设计

– 讲讲 MVC、MVVM、MVP,以及你在项目里具体是怎么写的?
1. MVC
Model-Controller-View
优点:Model/View不知道对方的存在,耦合度低,Model/View可重用
缺点:Controller过于臃肿

2. MVC-变种
Model-Controller-View 不同点在于View与Model之间添加一层联系
优点:Controller瘦身,View内部封装
缺点:View依赖于Model

3. MVP
Model-Presenter-View 业务逻辑放在Presenter中
优点:
缺点:

4. MVVM
Model-ViewModel-View 与MVP不同的点是属性绑定监听,ViewModel管理数据与视图,View监听ViewModel中数据的变化并跟着变化
优点:
缺点:

5. 三层架构、四层架构
界面层、业务层、数据层
界面层、业务层、网络层、数据层
MVC/MVP/MVVM都是界面层的东西

– 你自己用过哪些设计模式?
1. 是一套被反复使用、代码设计经验的总结
使用设计模式的好处是:可重用代码、让代码更容易被他人理解、保证代码可靠性
一般与编程语言无关,是一套比较成熟的编程思想

2. 设计模式可以分为三大类
创建型模式:对象实例化的模式,用于解耦对象的实例化过程
单例模式、工厂方法模式,等等

结构型模式:把类或对象结合在一起形成一个更大的结构
代理模式、适配器模式、组合模式、装饰模式,等等

行为型模式:类或对象之间如何交互,及划分责任和算法
观察者模式、命令模式、责任链模式,等等

– 一般开始做一个项目,你的架构是如何思考的?
具体看项目

加密与签名

base64编码

对称密码算法
加密解密用的同一个密钥
DES,3DES,AES(高级密码标准,钥匙串用的就是这个)
3DES:进行3次DES加密

加密方式
ECB:点子代码本,每一个数据块进行独立加密
CBC:密码块链,使用一个密码和一个初始化向量对数据进行加密,数据块相互依赖,丢失一个数据块就不能解密,防范窃听

非对称密码算法
RSA算法
公钥加密,私钥解密
私钥加密,公钥解密

Hash散列算法
MD4、MD5、SHA-1、SHA-2、SHA-3
方案HMAC(不是算法)
特点:算法公开,同一个数据加密算出的结果是相同的,不同数据加密之后是定长的,不能反算(信息的摘要,信息指纹,做数据的识别)

这些因为算出的结果相同,所以不太安全,以前的解决办法是加盐(MD5+salt这个盐就是一个固定的字符串,但是也有泄漏的风险)

方案HMAC可以解决加盐也不安全的问题
使用key(服务器随机生成,保存到数据库的,一个账号一个key)进行明文加密,然后进行两次Hash算法

之前是用Markdown做的笔记,搞到这里就改了改html标签,布局乱啦,懒得改了~
如有错误,敬请指正~

转载请注明:怼码人生 » iOS面试之前准备的部分知识

喜欢 (0)
头像
发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址