1. 面试问题整理
24.8.5 更新 24.8.6 更新
1.1. 消息转发
消息发送的本质:
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
。
消息发送步骤:
- 首先检查这个
Selector
是不是要忽略。比如Mac OS X开发,有了垃圾回收就不会理会retain
,release
这些函数。 - 检测这个
Selector
的target是不是nil,OC允许我们对一个nil对象执行任何方法不会Crash,因为运行时会被忽略掉。 - 如果上面两步都通过了,就开始查找这个类的实现IMP,先从cache里查找,如果找到了就运行对应的函数去执行相应的代码。
- 如果cache中没有找到就找类的方法列表中是否有对应的方法。
- 如果类的方法列表中找不到就到父类的方法列表中查找,一直找到NSObject类为止。
- 如果没有找到,Runtime 会发送
+resolveInstanceMethod:
或者+resolveClassMethod:
尝试去 resolve 这个消息。 - 如果 resolve 方法返回 NO,
Runtime
就发送 -forwardingTargetForSelector: 允许你把这个消息转发给另一个对象。 - 如果没有新的目标对象返回,
Runtime
就会发送-methodSignatureForSelector:
和-forwardInvocation:
消息。你可以发送-invokeWithTarget:
消息来手动转发消息或者发送-doesNotRecognizeSelector:
抛出异常。
消息转发应用:
- 预防没有方法实现而会导致的崩溃。
- 预防苹果系统API迭代造成API不兼容的崩溃。
- 模拟多继承。
参考:
1.2. KVO的底层实现?
当一个对象使用了KVO
监听,iOS系统会修改这个对象的isa
指针,改为指向一个全新的通过Runtime
动态创建的子类,子类拥有自己的set
方法实现,set
方法实现内部会顺序调用willChangeValueForKey
方法、原来的setter
方法实现、didChangeValueForKey
方法,而didChangeValueForKey
方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:
监听方法。
手动触发:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
BOOL automatic = YES;
if ([key isEqualToString:@"name"]) {
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
- (void)setName:(NSString *)name
{
if (![_name isEqualToString:name]) {
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
}
参考:
1.3. isa是什么?
isa是结构体指针,只要是OC对象都有isa,作用如下:
- 实例对象
instance
的isa
指向类对象class
。 - 类对象
class
的isa
指向元类对象meta-class
。 class
的superclass
指向父类的class
,如果没有父类,superclass
指针为nil
。meta-class
的superclass
指向父类的meta-class
,基类的meta-class
的superclass
指向基类的class
。instance
调用对象方法的轨迹:isa
找到class
,方法不存在,就通过superclass
找父类。class
调用类方法的轨迹:isa
找meta-class
,方法不存在,就通过superclass
找父类。
参考:
1.4. 自动释放池
@autoreleasepool
在main
函数中的c++代码:
int main(int argc, const char * argv[]) {
{
void * atautoreleasepoolobj = objc_autoreleasePoolPush();
// do whatever you want
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}
AutoreleasePoolPage
的结构:
class AutoreleasePoolPage {
magic_t const magic;// 用于对当前 AutoreleasePoolPage 完整性的校验
id *next;// 指向了下一个为空的内存地址
pthread_t const thread;// 保存了当前页所在的线程
AutoreleasePoolPage * const parent;// 实现双向链表的parent节点
AutoreleasePoolPage *child;// 实现双向链表的child节点
uint32_t const depth;
uint32_t hiwat;
};
AutoreleasePoolPage 的大小都是 4096 字节,其中56bit用于存储成员变量,剩下的从哨兵的位置开始都是用来存对象地址的。
objc_autoreleasePoolPush
方法:
static inline void *push() {
return autoreleaseFast(POOL_SENTINEL);
}
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {// 有hotpage,且不满
return page->add(obj);
} else if (page) {// 有hotpage,且已满
return autoreleaseFullPage(obj, page);
} else {// 没有hotpage
return autoreleaseNoPage(obj);
}
}
Objective-C
objc_autoreleasePoolPop
方法:
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}
static inline void pop(void *token) {
AutoreleasePoolPage *page = pageForPointer(token);
id *stop = (id *)token;// 获取当前 token 所在的 AutoreleasePoolPage
page->releaseUntil(stop);// 方法释放栈中的对象,直到 stop
/**
* 当前page一半都没满,说明剩余的page空间已经暂时够了,把多余的child page就可以全kill掉,释放空间;
* 如果超过一半,就认为下一页page还有存在的必要,说不定添加的对象太多就能用的到,所以kill掉孙子page,有个儿子page就暂时够了。
*/
if (page->child) {
if (page->lessThanHalfFull()) {
page->child->kill();
} else if (page->child->child) {
page->child->child->kill();
}
}
}
autorelease
方法:调用上面提到的autoreleaseFast
方法,将当前对象加到AutoreleasePoolPage
中。
RunLoop
和autoreleasePool
:
- 第1个Observer监听了
kCFRunLoopEntry
事件,会调用objc_autoreleasePoolPush()
; - 第2个Observer:
- 监听了
kCFRunLoopBeforeWaiting
事件,会调用objc_autoreleasePoolPop()
、objc_autoreleasePoolPush()
; - 监听了
kCFRunLoopBeforeExit
事件,会调用objc_autoreleasePoolPop()
。
- 监听了
参考:
- 自动释放池的前世今生 ---- 深入解析 autoreleasepool
- iOS autoreleasepool详解
- iOS内存管理(5)-autorelease原理和autorelease和runloop的结合使用
- iOS——Autoreleasepool底层原理
1.5. Runloop
- 顾名思义,运行循环,线程与
Runloop
是一一对应,Runloop
运行可以是线程不退出。 Runloop
默认注册5个Mode
:kCFRunLoopDefaultMode
:App的默认 Mode,通常主线程是在这个 Mode 下运行的。UITrackingRunLoopMode
:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。UIInitializationRunLoopMode
:在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。GSEventReceiveRunLoopMode
:接受系统事件的内部 Mode,通常用不到。kCFRunLoopCommonModes
:这是一个占位的 Mode,没有实际作用。
Runloop
包含多个Mode
,每个Mode
包含Source
数组,Observer
数组,Timer
数组。Runloop
状态:即将进入Loop、即将处理 Timer、即将处理 Source、即将进入休眠、即将退出Loop。Runloop
的核心就是一个mach_msg()
,RunLoop
调用这个函数去接收消息,如果没有别人发送port
消息过来,内核会将线程置于等待状态。Runloop
与AutoreleasePool
Runloop
进入循环时刻,创建自动释放池;Runloop
进入休眠,释放旧的池,创建新的池;Runloop
退出,释放自动释放池。
Runloop
与事件响应:苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为__IOHIDEventSystemClientQueueCallback()
。Runloop
与界面更新:苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件。Runloop
应用:AFNetworking
单独创建了一个线程,并在这个线程中启动了一个RunLoop
,执行后台请求任务。AsyncDisplayKit
在主线程的RunLoop
中添加一个Observer
,监听了kCFRunLoopBeforeWaiting
和kCFRunLoopExit
事件,在收到回调时,处理队列里的UI渲染任务。
参考:
- 深入理解RunLoop
- iOS多线程——RunLoop与GCD、AutoreleasePool
- iOS之武功秘籍⑲: 内存管理与NSRunLoop
- 一份走心的runloop源码分析
- 老司机出品——源码解析之RunLoop详解
- Run Loop 记录与源码注释
- 从RunLoop来看iOS内核中消息的发送: mach_msg
- 理解 iOS 的内存管理
1.6. category实现方式
category
的作用:a)分散功能,b)声明私有方法;category
与extension
:category
是运行期决议的,不可以添加属性。extension
是编译期决议的,可以添加属性,一般为了私有化。
category
和原来类中存在相同方法,category
的方法更加靠前,运行期优先被查找和调用。category
可以使用runtime
关联对象来实现添加实例变量。
参考:
- 深入理解Objective-C:Category
- iOS Category 源码解析
- iOS:Category为什么不能直接添加成员变量却能添加方法
- ios动态添加属性的几种方法
- iOS-分类Category详解和关联对象
- ios Category
- Monkey-Patching iOS with Objective-C Categories Part II: Adding Instance Properties
1.7. 方法交换
Swizzling
需要在+(void)load
中添加。Swizzling
应该总是在dispatch_once
中执行。Swizzling
在+load
中执行时,不要调用[super load]
。如果多次调用了[super load]
,可能会出现“Swizzle无效”的假象。
场景:
- 统计VC加载次数。
- 防止UI控件短时间多次激活事件。
- 防奔溃处理:数组越界问题。
- 适配iOS13的模态的的样式问题。
参考:
- iOS开发·runtime原理与实践: 方法交换篇(Method Swizzling)(iOS“黑魔法”,埋点统计,禁止UI控件连续点击,防奔溃处理)
- iOS 小技能:Method Swizzling (交换方法的IMP)
1.8. weak实现
weak
是Runtime
维护了一个hash
(哈希)表,用于存储指向某个对象的所有weak
指针。weak表其实是一个hash
(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象指针的地址)数组。
参考:
- iOS底层原理:weak的实现原理
- 老生常谈的iOS- weak原理,你真的懂得还是为了应付面试
- D4-007-weak对象存储原理和销毁为什么会置nil(上)
- D4-008-weak对象存储原理和销毁为什么会置nil(下)
1.9. 监控卡顿
卡顿原因:
- 复杂UI、图文混排的绘制量过大;
- 在主线程上做网络同步请求;
- 在主线程做大量的
IO
操作; - 运算量过大,
CPU
持续高占用; - 死锁和主子线程抢锁。
监控卡顿:
- 创建一个
CFRunLoopObserverContext
观察者; - 监听
kCFRunLoopBeforeSources
到kCFRunLoopBeforeWaiting
再到kCFRunLoopAfterWaiting
的状态。 - 开启子线程,利用信号量计算这几个状态切换的时间,间隔时间如果超过50ms,卡顿计数+1。
- 如果卡顿计数超过大于等于5次,触发卡顿上报线程调用栈。
参考:
1.10. block实现
block
是个结构图,拥有isa
指针。
捕获机制:
- 全局变量,捕获指针,修改可以生效。
- 局部变量,捕获值,修改不生效。
block
类型:
- 全局
block
- 栈
block
- 堆
block
__block
:
依然是个结构体,拥有isa
指针,局部变量被包装成了一个对象,里面有个成员变量,指向了局部变量,所以block
修改这个变量是有效果的。
参考:
1.11. 五大区
- 栈区:运行期分配,例如:局部变量、函数参数等。
- 堆区:运行期分配,例如:alloc、new的对象。
- 全局区:编译期分配,例如:static修饰变量。
- 常量区:编译期分配,例如:string的引用,但是string的指针是堆区
- 代码区:编译期分配,存放程序的代码。
参考:
1.12. 性能优化(启动等)
- 启动优化
- 卡顿优化
- 耗电优化
- 包体积优化
1.13. TCP三次握手,四次挥手
三次握手:
- 客户端发送
[SYN] Seq=x
; - 服务端发送
[SYN, ACK] Seq=y Ack=x+1
; - 客户端发送
[ACK] Seq=y+1
;
四次挥手:
- 客户端发送
[FIN] Seq=x
; - 服务端发送
[FIN, ACK] Ack=x+1
; - 服务端发送
[FIN] Seq=y
; - 客户端发送
[ACK] Ack=y+1
; - 等待
2MSL
,服务端无响应,说明服务端已关闭,这时,客户端也关闭。
参考:
1.14. 通知
本地通知:
- 请求权限;
- 创建通知内容;
- 创建通知触发
- 创建请求;
- 将请求添加到通知中心。
远程通知:
- 请求权限;
- 上传token至业务服务器;
业务服务器使用
token
向苹果APNS
服务器提交请求;
1.15. 对象在什么时候释放?
Runloop
状态为休眠或退出。weak
引用的对象在dealloc
中释放。- 离开自动释放池作用域。
1.16. ipa文件结构
- Frameworks:App引入的动态库;
- Assets.car;
- 可执行文件:MachO文件;
- 资源文件:xx.bundle;
- 签名文件:_CodeSignature
参考:
1.17. 动态库和静态库
静态库:链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。利用静态函数库编译成的文件比较大,因为整个 函数库的所有数据都会被整合进目标代码中,他的优点就显而易见了,即编译后的执行程序不需要外部的函数库支持,因为所有使用的函数都已经被编译进去了。当然这也会成为他的缺点,因为如果静态函数库改变了,那么你的程序必须重新编译。
动态库:链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。由于函数库没有被整合进你的程序,而是程序运行时动态的申请并调用,所以程序的运行环境中必须提供相应的库。动态函数库的改变并不影响你的程序,所以动态函数库的升级比较方便。
参考:
1.18. 事件传递
- 点击一个UIView或产生一个触摸事件A,这个触摸事件A会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。
- UIApplication会从事件对列中取出最前面的事件(此处假设为触摸事件A),把事件A传递给应用程序的主窗口(keyWindow)。
- 窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。
参考:
1.19. 面向对象编程特征有哪些?
- “抽象”,把现实世界中的某一类东西,提取出来,用程序代码表示;
- “封装”,把过程和数据包围起来,对数据的访问只能通过已定义的界面;
- “继承”,一种联结类的层次模型;
- “多态”,允许不同类的对象对同一消息做出响应。
1.20. struct和class的区别?
https://juejin.cn/post/6844903799413276685
- 类属于引用类型,结构体属于值类型
- 类允许被继承,结构体不允许被继承
- 类中的每一个成员变量都必须被初始化,否则编译器会报错,而结构体不需要,编译器会自动帮我们生成init函数,给变量赋一个默认值
1.21. Swift设置访问权限如何设置?
https://juejin.cn/post/7012087397765054494
- open
- public
- internal
- fileprivate
- private
1.22. Swift中的逃逸闭包(@escaping )与非逃逸闭包(@noescaping)
https://juejin.cn/post/6844903951519727629
概念:一个接受闭包作为参数的函数,该闭包可能在函数返回后才被调用,也就是说这个闭包逃离了函数的作用域,这种闭包称为逃逸闭包。当你声明一个接受闭包作为形式参数的函数时,你可以在形式参数前写@escaping来明确闭包是允许逃逸。
概念:一个接受闭包作为参数的函数, 闭包是在这个函数结束前内被调用。
1.23. 常见设计模式有哪些?
https://www.cnblogs.com/newsouls/archive/2011/07/28/DesignTemplage.html
- 单例模式
- 工厂模式
1.24. 如何用GCD同步若干个异步调用?
https://cloud.tencent.com/developer/article/1521135
- 将几个线程加入到group中, 然后利用group_notify来执行最后要做的动作
- 利用GCD信号量dispatch_semaphore_t来实现
1.25. 创建线程有几种方式?
- pthread 实现多线程操作
- NSThread实现多线程
- GCD 实现多线程
- NSOperation
https://youle.zhipin.com/questions/4c09e5e18447d9dbtnV809W7FlQ~.html
引用计数小于1的时候释放的。在ARC环境下我们不能直接去操作引用计数的值,但是我们可以跟踪是否有strong指针指向、如果没有strong指针指向、则立即销毁。 这里有一个地方值得关注的事自动缓存池,他会延迟销毁时机,但是实际上也是延迟执行re lease而已。
1.26. 异步上传实现
- 分块上传;
Socket
监听;
参考:
1.27. https是什么?
- 客户端向服务器发送 HTTPS 请求。
- 服务器将公钥证书发送给客户端。
- 客户端验证服务器的证书。
- 如果验证通过,客户端生成一个用于会话的对称密钥。
- 客户端使用服务器的公钥对对称密钥进行加密,并将加密后的密钥发送给服务器。
- 服务器使用私钥对客户端发送的加密密钥进行解密,得到对称密钥。
- 服务器和客户端使用对称密钥进行加密和解密数据传输。
参考:
1.28. 对称加密和非对称加密的区别?
- 对称加密:加密解密都是用相同规则。
- 非对称加密:公钥加密的信息只有私钥解得开,那么只要私钥不泄漏,通信就是安全的。
苹果证书校验:
- Mac生成c公钥、c私钥;
- 苹果服务器使用s私钥对c公钥加密,生成证书文件给Mac。
- Mac使用c私钥加密可执行文件(带证书),通过可执行文件在手机安装。
- 手机内置苹果服务器s公钥,解密证书,等到c公钥。
- 手机通过c公钥匙验证可执行文件,通过,则可以安装。
参考:
1.29. https证书校验
- 创建NSURLSession对象。
- 创建NSURLSessionDataTask对象。
- 设置NSURLSessionDelegate代理。
- 实现NSURLSessionDelegate代理方法
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
// 验证证书
SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, 0);
NSData *remoteCertificateData = CFBridgingRelease(CFDataCreateCopy(NULL, SecCertificateCopyData(certificate)));
NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"server" ofType:@"cer"];
NSData *localCertData = [NSData dataWithContentsOfFile:cerPath];
if ([remoteCertificateData isEqualToData:localCertData]) {
// 验证通过
NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
} else {
// 验证失败
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
} else {
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}
}
参考:
1.30. CocoaPods
1.31. 组件化开发方式有哪些?
1.32. 轮播图实现原理
1.33. 如何存放敏感信息?
1.34. 如何使用Git?
1.35. OC和Swift如何互相调用?
1.36. RESTful
1.37. NoSql
1.38. JAVA面试题大全(200+道题目)
1.39. Java容器有哪些?
- Collection
- List
- ArrayList
- LinkedList
- Vector
- Set
- HashSet
- LinkedHashSet
- TreeSet
- HashSet
- List
- Map
- HashMap
- LinkedHashMap
- HashTable
- TreeMap
- HashMap
1.40. List、Set、Map 之间的区别是什么?
- List:有序集合、元素可重复。
- Set:元素不可重复,HashSet无序,LinkedHashSet按照插入排序,SortedSet可排序。
Map:键值对集合,存储键、值之间的映射。key无序,唯一,value可重复。
1.41. HashMap 和 Hashtable 有什么区别?
- HashMap不是线程安全的,HashTable是线程安全的
HashMap允许Null Key和Null Value,HashTable不允许
- HashMap和HashTable到底哪不同?
1.42. ArrayList 和 LinkedList 的区别是什么?
- ArrayList的数据结构是动态数组;LinkedList的数据结构是双向链表。
- ArrayList比LinkedList在随机访问的时候效率要高,因为LinkedList是线性的数据结构,需要依次往后查找。
- 在非首尾的增删操作,LinkedList要比ArrayList的效率要高,因为ArrayList在操作增删时要影响其他元素的下标。
总结:需要频繁读取集合中的元素时,推荐使用ArrayList;插入和删除操作较多时,推荐使用LinkedList。
1.43. ArrayList 和 Vector 的区别是什么?
- 相同点:都实现了List接口,都是有序集合;
- 区别:Vector是线程安全的,ArrayList不是线程安全的;
- 当Vector或ArrayList中的元素超过它的初始大小时,Vector会将容量翻倍,而ArrayList只会将容量扩大50%。
1.44. Array 和 ArrayList 有何区别?
- Array类型的变量在声明时必须实例化;ArrayList可以只是先声明;
- Array大小是固定的,而ArrayList的大小是动态变化的;
- Array可以包含基本类型和对象类型,ArrayList只能包含对象类型;
1.45. 说一下 spring 的事务隔离?
- 脏读:当前事务读取了其他事物没有提交成功的数据。
- 使用“读提交”
- 不可重复度:当前事务读了同一条记录,数据不一样,因为其他事务在两次读取期间修改了这条记录。
- 使用行级锁
- 幻读:当前事务两次统计表行数不一样,因为其他事务新增或者删除表记录。
- 使用表级锁
事务隔离级别:
- 读未提交
- 读提交
- 重复度
- 序列化
1.46. 说一下 spring mvc 运行流程?
- 请求被
DispatcherServlet
处理 DispatcherServlet
通过HandlerMapping
找到Controlller
DispatcherServlet
调用HandlerAdapter
执行Controller
方法- 返回
ModelAndView
对象 ViewResolver
渲染
1.47. 数据库的三范式是什么?
- 无重复的列
- 列属性完全依赖主键
列属性不能简介与主键关联
1.48. 说一下 jvm 的主要组成部分?及其作用?
- 类加载器(ClassLoader)
- 运行时数据区(Runtime Data Area)
- 执行引擎(Execution Engine)
- 本地库接口(Native Interface)
组件的作用: 首先通过类加载器(ClassLoader)会把 Java 代码转换成字节码,运行时数据区(Runtime Data Area)再把字节码加载到内存中,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
1.49. 说一下 jvm 运行时数据区域?
- 程序计数器
- 虚拟机栈
- 本地方法栈
- 堆(创建的对象)
- 方法区(类的属性、成员变量、构造函数等)
1.50. 说一下类加载的执行过程?
- 加载:根据查找路径找到相应的 class 文件然后导入
- 检查:检查加载的 class 文件的正确性
- 准备:给类中的静态变量分配内存空间
- 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标识,而在直接引用直接指向内存中的地址;
- 初始化:对静态变量和静态代码块执行初始化工作。