iOS 面试题(2)
1.编译过程做了哪些事情?
C++,Objective C 都是编译语言,编译语言在执行的时候,必须先通过编译器生成机器码,机器码可以直接在CPU上执行,所以执行效率很高。OC 编译依赖于Clang + LLVM
iOS 编译
不管是OC 还是swift,都是采用Clang 作为编译器的前端,LLVM 作为编译器的后端。所以简单的编译过程 CLang->LLVM Optimizer -> LLVM Code Generator
编译器前端
编译器前端的任务是进行:语法分析,语义分析,生成中间代码。在这个过程会进行类型检查,如果发现错误或者警告会标注出来在哪一行。
Lexical Analysis Driver Parser Indexing Semantic Analysis Code Completion Abstract Syntax Trees Rewriter Static Analyzer Tooling
编译器后端
编译器后端会进行机器无关的代码化,生成机器语言,并进行及其相关的代码优化。
LVVM 优化器会进行BitCode 的生成,链接期优化等等
Bitcode Passes Analysis Link-Time Optimization Transforms
LVVM 会针对不同的架构生成不同的机器码
执行一次Xcode build 的流程
当你在Xcode中,选择build 的时候,会执行如下过程
编译信息写入辅助文件,创建编译后的文件架构
处理文件打包信息,例如在debug 环境下
Entitilements: {
"application-identifier" = "";
"aps-enviroment" = development;
}
执行cocoaPods 编译前的脚本
例如对于使用CocoaPod的工程会执行CheckPods Manifest.lock
编译各个.m文件,使用CompileC和clang命令1.CompileC ClassName.o ClassName.m normal x86_64 objective-c com.apple.compilers.llvm.clang.1_0.compiler
2.export LANG=en_US.US-ASCII
3.export PATH="..."
4.clang-x objective-c -arch x86_64 -fmessage-length=0 -fobjc-arc...
-Wno-missing-field-initializers ... -DDEBUG=1 ... -isysroot
iPhoneSimulator10.1.sdk -fasm-blocks ... -I 上文提到的文件 -F 所需要的Framework-iquote 所需要的Framework ... -c ClassName.c -o ClassName.o
2、字典的大致实现原理
NSDictionary(字典)是使用hash 表来实现key 和value 之间的映射和存储的
oc NSDictionary 的底层其实是一个hash 表
哈希原理
散列表,是根据关键码值(key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中的一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为hash表,函数为hash 函数。
哈希概念:哈希表的本质是一个数组,数组中每一个元素成为一个箱子,箱子中存放的是键值对。
哈希存储的过程
1、根据key 计算出它的哈希值h
2、假设箱子的个数为n,那么这个键值对应该放在(h%n)个箱子中
3、如果该箱子中已经有了键值对,就使用开放寻址法或者拉链法解决冲突
在使用拉链法解决哈希冲突时,每个箱子其实是一个链表,属于同一个箱子的所有键值对都会排列在链表中
哈希表还有一个重要的属性:负载因子,它用来衡量哈希表的空/满程度,一定程度上也可以提现查询的效率,计算公式为:
负载因子 = 总键值对数 / 箱子个数
负载因子越大,意味着哈希表越满,越容易导致冲突,性能也就越低。因此,一般来说,当负载因子大于某个常数(可能是1,或者0.75等)时,哈希表将自动扩容。
哈希表在自动扩容时,一般会创建两倍于原来个数的箱子,因此即使 key 的哈希值不变,对箱子个数取余的结果也会发生改变,因此所有键值对的存放位置都有可能发生改变,这个过程也称为重哈希(rehash)。
哈希表的扩容并不总是能够有效解决负载因子过大的问题。假设所有 key 的哈希值都一样,那么即使扩容以后他们的位置也不会变化。虽然负载因子会降低,但实际存储在每个箱子中的链表长度并不发生改变,因此也就不能提高哈希表的查询性能。
基于以上总结,细心的朋友可能会发现哈希表的两个问题:
1.如果哈希表中本来箱子就比较多,扩容时需要重新哈希并移动数据,性能影响较大。
2.如果哈希函数设计不合理,哈希表在极端情况下会变成线性表,性能极低。
3、block 和函数指针的理解
相似点:
函数指针和block 都可以实现回调的操作,声明上也很相似,实现上都可以看成一个代码片段。
函数指针类型和block 类型都可以作为变量和函数参数的类型。(typedef 定义别名后,这个别名就是一个类型)
不同点:
函数指针只能指向预先定义好的函数代码块(可以是其他文件里面定义,通过函数参数动态传入的),函数地址是在编译连接时就已经确定好的。
block 本质是Objective-C 对象,是NSObject 的子类,可以接收消息。
函数里面只能访问全局变量,而Block 代码块不光能访问全局变量,还拥有当前栈内存和堆内存变量的可读性(当然通过__block 访问指示符修饰的局部变量还可以在block代码块里面进行修改)。
从内存的角度看,函数指针只不过是指向代码区的一段可执行代码,而block 实际上是程序运行过程中在栈内存动态创建的对象。可以向其发送copy 消息将block 对象拷贝到堆内存,以延长其生命周期。
4、一般开始做一个项目,你的架构是如何思考的
https://casatwy.com/iosying-yong-jia-gou-tan-wang-luo-ceng-she-ji-fang-an.html
https://casatwy.com/iosying-yong-jia-gou-tan-viewceng-de-zu-zhi-he-diao-yong-fang-an.html
6、KVO 的实现原理
KVO 是基于运行时实现的
isa Class NSKVONotifying_Person 0x000001a12de2f545
|
上述例子中,当 p1.name
的值改变时,p1对象的 isa 指针会指向 NSKVONotifying_Person
,意味着,在程序运行时,会动态生成一个 NSKVONotifying_Person
类,该类继承于 Person,而且该类中也有个 -setName:
方法,方法中在设置 name 的同时实现了:
- (void)setName:(NSString *)name { [super setName:name]; // 这两个方法底层会调用observer的- (void)observeValueForKeyPath: ofObject: change: context:这个方法 [self willChangeValueForKey:@"age"]; [self didChangeValueForKey:@"age"]; } |