草庐IT

iOS底层面试题(下篇)

wx5fb7717101de4 2023-03-28 原文

7月,iOS求职跳槽的相对较少,能在这个时间段求职的,不是被迫,就是对自己的技术很自信; 针对7月,特别总结了第三份iOS常见大厂面试题(下);

iOS面试题分为 上、中、下三部分,方便大家观看;

请先自己答一答

话不多说;直接上题本文收录:公众号【iOS进阶宝典《iOS底层面试题(下篇)》】

13. 如何用Charles抓HTTPS的包?其中原理和流程是什么?

流程:

  • 首先在手机上安装Charles证书

  • 在代理设置中开启Enable SSL Proxying

  • 之后添加需要抓取服务端的地址

原理:

Charles作为中间人,对客户端伪装成服务端,对服务端伪装成客户端。简单来说:

  • 截获客户端的HTTPS请求,伪装成中间人客户端去向服务端发送HTTPS请求
  • 接受服务端返回,用自己的证书伪装成中间人服务端向客户端发送数据内容。
具体流程如下图:扯一扯HTTPS单向认证、双向认证、抓包原理、反抓包策略

14. 什么是中间人?如何避免?

中间人就是截获到客户端的请求以及服务器的响应,比如Charles抓取HTTPS的包就属于中间人。

避免的方式:客户端可以预埋证书在本地,然后进行证书的比较是否是匹配的

15. 了解编译的过程么?分为哪几个步骤?

1:预编译:主要处理以“#”开始的预编译指令。

2:编译:

  • 词法分析:将字符序列分割成一系列的记号。

  • 语法分析:根据产生的记号进行语法分析生成语法树。

  • 语义分析:分析语法树的语义,进行类型的匹配、转换、标识等。

  • 中间代码生成:源码级优化器将语法树转换成中间代码,然后进行源码级优化,比如把 1+2 优化为 3。中间代码使得编译器被分为前端和后端,不同的平台可以利用不同的编译器后端将中间代码转换为机器代码,实现跨平台。

  • 目标代码生成:此后的过程属于编译器后端,代码生成器将中间代码转换成目标代码(汇编代码),其后目标代码优化器对目标代码进行优化,比如调整寻址方式、使用位移代替乘法、删除多余指令、调整指令顺序等。

3:汇编:汇编器将汇编代码转变成机器指令。

  • 静态链接:链接器将各个已经编译成机器指令的目标文件链接起来,经过重定位过后输出一个可执行文件。

  • 装载:装载可执行文件、装载其依赖的共享对象。

  • 动态链接:动态链接器将可执行文件和共享对象中需要重定位的位置进行修正。

最后,进程的控制权转交给程序入口,程序终于运行起来了。

16. 静态链接了解么?静态库和动态库的区别?

静态链接是指将多个目标文件合并为一个可执行文件,直观感觉就是将所有目标文件的段合并。需要注意的是可执行文件与目标文件的结构基本一致,不同的是是否“可执行”。

  • 静态库:链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。

  • 动态库:链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。

17. App网络层有哪些优化策略?

  • 优化DNS解析和缓存

  • 对传输的数据进行压缩,减少传输的数据

  • 使用缓存手段减少请求的发起次数

  • 使用策略来减少请求的发起次数,比如在上一个请求未着地之前,不进行新的请求

  • 避免网络抖动,提供重发机制

18:[self class] 与 [super class]

@implementation Son : Father (id)init { self = [super init]; if (self) { NSLog(@"%@", NSStringFromClass([self class])); NSLog(@"%@", NSStringFromClass([super class])); } return self; } @end self和super的区别:

  • self 是类的一个隐藏参数,每个方法的实现的第一个参数即为self
  • super并不是隐藏参数,它实际上只是一个**”编译器标示符”**,它负责告诉编译器,当调用方法时,去调用父类的方法,而不是本类中的方法。
在调用[super class]的时候,runtime会去调用objc_msgSendSuper方法,而不是objc_msgSend

OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ ) /// Specifies the superclass of an instance. struct objc_super { /// Specifies an instance of a class. __unsafe_unretained id receiver; /// Specifies the particular superclass of the instance to message. # if !defined(__cplusplus) && !__OBJC2__ /* For compatibility with old objc-runtime.h header */ __unsafe_unretained Class class; # else __unsafe_unretained Class super_class; # endif /* super_class is the first class to search */ } objc_msgSendSuper方法中,第一个参数是一个objc_super的结构体,这个结构体里面有两个变量,一个是接收消息的receiver,一个是当前类的父类super_class

objc_super结构体指向的superClass父类的方法列表开始查找selector,父类找到了,父类就执行这个方法。

class 方法的内部实现:

- (Class)class { return object_getClass(self); } 在class 方法内,默认传入的是self, 无论调用者是谁。

所以这个道题的答案就出来了: 两个打印的都是当前的类。

18.isKindOfClass 与 isMemberOfClass

下面代码输出什么?

@interface Sark : NSObject @end @implementation Sark @end int main(int argc, const char * argv[]) { @autoreleasepool { BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]]; BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]]; BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]]; BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]]; NSLog(@"%d %d %d %d", res1, res2, res3, res4); } return 0; } 先来分析一下源码这两个函数的对象实现

+ (Class)class { return self; } (Class)class { return object_getClass(self); } Class object_getClass(id obj) { if (obj) return obj->getIsa(); else return Nil; } inline Class objc_object::getIsa() { if (isTaggedPointer()) { uintptr_t slot = ((uintptr_t)this >> TAG_SLOT_SHIFT) & TAG_SLOT_MASK; return objc_tag_classes[slot]; } return ISA(); } inline Class objc_object::ISA() { assert(!isTaggedPointer()); return (Class)(isa.bits & ISA_MASK); } (BOOL)isKindOfClass:(Class)cls { for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) { if (tcls == cls) return YES; } return NO; } (BOOL)isKindOfClass:(Class)cls { for (Class tcls = [self class]; tcls; tcls = tcls->superclass) { if (tcls == cls) return YES; } return NO; } (BOOL)isMemberOfClass:(Class)cls { return object_getClass((id)self) == cls; } (BOOL)isMemberOfClass:(Class)cls { return [self class] == cls; } 首先题目中NSObject 和 Sark分别调用了class方法。

  • + (BOOL)isKindOfClass:(Class)cls方法内部,会先去获得object_getClass的类,而object_getClass的源码实现是去调用当前类的obj->getIsa(),最后在ISA()方法中获得meta class的指针。

  • 接着在isKindOfClass中有一个循环,先判断class是否等于meta class,不等就继续循环判断是否等于super class,不等再继续取super class,如此循环下去。

  • [NSObject class]执行完之后调用isKindOfClass,第一次判断先判断NSObject 和 NSObjectmeta class是否相等,之前讲到meta class的时候放了一张很详细的图,从图上我们也可以看出,NSObjectmeta class与本身不等。

  • 接着第二次循环判断NSObjectmeta classsuperclass是否相等。还是从那张图上面我们可以看到:Root class(meta) 的superclass 就是 Root class(class),也就是NSObject本身。所以第二次循环相等,于是第一行res1输出应该为YES

  • 同理,[Sark class]执行完之后调用isKindOfClass,第一次for循环,SarkMeta Class[Sark class]不等,第二次for循环Sark Meta Class的super class 指向的是 NSObject Meta Class, 和 Sark Class不相等。

  • 第三次for循环,NSObject Meta Classsuper class指向的是NSObject Class,和 Sark Class 不相等。第四次循环,NSObject Class的 super class 指向 nil, 和 Sark Class不相等。第四次循环之后,退出循环,所以第三行的res3输出为NO

  • 如果把这里的Sark改成它的实例对象,[sark isKindOfClass:[Sark class],那么此时就应该输出YES了。因为在isKindOfClass函数中,判断sark的meta class是自己的元类Sark,第一次for循环就能输出YES了。

  • isMemberOfClass的源码实现是拿到自己的isa指针和自己比较,是否相等。

  • 第二行isa 指向 NSObject 的 Meta Class,所以和 NSObject Class不相等。第四行,isa指向SarkMeta Class,和Sark Class也不等,所以第二行res2和第四行res4都输出NO。

19.Class与内存地址

下面的代码会?**Compile Error / Runtime Crash / NSLog…?**

@interface Sark : NSObject @property (nonatomic, copy) NSString *name; (void)speak; @end @implementation Sark (void)speak { NSLog(@"my name's %@", [self.name](http://self.name/)); } @end @implementation ViewController (void)viewDidLoad { [super viewDidLoad]; id cls = [Sark class]; void *obj = &cls; [(__bridge id)obj speak]; } @end 这道题有两个难点。

  • 难点一:obj调用speak方法,到底会不会崩溃。
  • 难点二:如果speak方法不崩溃,应该输出什么?
首先需要谈谈隐藏参数self和_cmd的问题。 当[receiver message]调用方法时,系统会在运行时偷偷地动态传入两个隐藏参数self_cmd,之所以称它们为隐藏参数,是因为在源代码中没有声明和定义这两个参数。self在已经明白了,接下来就来说说_cmd_cmd表示当前调用方法,其实它就是一个方法选择器SEL

难点一,能不能调用speak方法?

id cls = [Sark class]; void *obj = &cls; 答案是可以的。obj被转换成了一个指向Sark Class的指针,然后使用id转换成了objc_object类型。obj现在已经是一个Sark类型的实例对象了。当然接下来可以调用speak的方法。

难点二,如果能调用speak,会输出什么呢?

很多人可能会认为会输出sark相关的信息。这样答案就错误了。

正确的答案会输出

my name is <ViewController: 0x7ff6d9f31c50>

内存地址每次运行都不同,但是前面一定是ViewController。why?

我们把代码改变一下,打印更多的信息出来。

- (void)viewDidLoad { [super viewDidLoad]; NSLog(@"ViewController = %@ , 地址 = %p", self, &self); id cls = [Sark class]; NSLog(@"Sark class = %@ 地址 = %p", cls, &cls); void *obj = &cls; NSLog(@"Void *obj = %@ 地址 = %p", obj,&obj); [(__bridge id)obj speak]; Sark *sark = [[Sark alloc]init]; NSLog(@"Sark instance = %@ 地址 = %p",sark,&sark); [sark speak]; } 我们把对象的指针地址都打印出来。输出结果:

ViewController = <ViewController: 0x7fb570e2ad00> , 地址 = 0x7fff543f5aa8 Sark class = Sark 地址 = 0x7fff543f5a88 Void *obj = <Sark: 0x7fff543f5a88> 地址 = 0x7fff543f5a80 my name is <ViewController: 0x7fb570e2ad00> Sark instance = <Sark: 0x7fb570d20b10> 地址 = 0x7fff543f5a78 my name is (null) objc_msgSendSuper2 解读

// objc_msgSendSuper2() takes the current search class, not its superclass. OBJC_EXPORT id objc_msgSendSuper2(struct objc_super *super, SEL op, ...) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_2_0); objc_msgSendSuper2方法入参是一个objc_super *super。

/// Specifies the superclass of an instance. struct objc_super { /// Specifies an instance of a class. __unsafe_unretained id receiver; /// Specifies the particular superclass of the instance to message. # if !defined(__cplusplus) && !**OBJC2** /* For compatibility with old objc-runtime.h header */ __unsafe_unretained Class class; # else __unsafe_unretained Class super_class; # endi /* super_class is the first class to search */ }; # endif 所以按viewDidLoad执行时各个变量入栈顺序从高到底为self_cmdself.classselfobj

  • 第一个self和第二个_cmd是隐藏参数。

  • 第三个self.class和第四个self[super viewDidLoad]方法执行时候的参数。

  • 在调用self.name的时候,本质上是self指针在内存向高位地址偏移一个指针。在32位下面,一个指针是4字节=4*8bit=32bit(64位不一样但是思路是一样的)

  • 从打印结果我们可以看到,obj就是cls的地址。在obj向上偏移32bit就到了0x7fff543f5aa8,这正好是ViewController的地址。

所以输出为my name is **<ViewController: 0x7fb570e2ad00>**

至此,Objc中的对象到底是什么呢?

实质:Objc中的对象是一个指向ClassObject地址的变量,即 id obj = &ClassObject , 而对象的实例变量 void *ivar = &obj + offset(N)

加深一下对上面这句话的理解,下面这段代码会输出什么?

- (void)viewDidLoad { [super viewDidLoad]; NSLog(@"ViewController = %@ , 地址 = %p", self, &self); NSString *myName = @"halfrost"; id cls = [Sark class]; NSLog(@"Sark class = %@ 地址 = %p", cls, &cls); void *obj = &cls; NSLog(@"Void *obj = %@ 地址 = %p", obj,&obj); [(__bridge id)obj speak]; Sark *sark = [[Sark alloc]init]; NSLog(@"Sark instance = %@ 地址 = %p",sark,&sark); [sark speak]; } ViewController = <ViewController: 0x7fff44404ab0> , 地址 = 0x7fff56a48a78 Sark class = Sark 地址 = 0x7fff56a48a50 Void *obj = <Sark: 0x7fff56a48a50> 地址 = 0x7fff56a48a48 my name is halfrost Sark instance = <Sark: 0x6080000233e0> 地址 = 0x7fff56a48a40 my name is (null) 由于加了一个字符串,结果输出就完全变了,[(__bridge id)obj speak];这句话会输出“my name is halfrost”

原因还是和上面的类似。按viewDidLoad执行时各个变量入栈顺序从高到底为self_cmdself.classselfmyNameobjobj往上偏移32位,就是myName字符串,所以输出变成了输出myName了。

20. 排序题:冒泡排序,选择排序,插入排序,快速排序(二路,三路)能写出那些?

这里简单的说下几种快速排序的不同之处,随机快排,是为了解决在近似有序的情况下,时间复杂度会退化为O(n^2),双路快排是为了解决快速排序在大量数据重复的情况下,时间复杂度会退化为O(n^2),三路快排是在大量数据重复的情况下,对双路快排的一种优化。

  • 冒泡排序
extension Array where Element : Comparable{ public mutating func bubbleSort() { let count = self.count for i in 0..<count { for j in 0..<(count - 1 - i) { if self[j] > self[j + 1] { (self[j], self[j + 1]) = (self[j + 1], self[j]) } } } } }
  • 选择排序
extension Array where Element : Comparable{ public mutating func selectionSort() { let count = self.count for i in 0..<count { var minIndex = i for j in (i+1)..<count { if self[j] < self[minIndex] { minIndex = j } } (self[i], self[minIndex]) = (self[minIndex], self[i]) } } }
  • 插入排序
extension Array where Element : Comparable{ public mutating func insertionSort() { let count = self.count guard count > 1 else { return } for i in 1..<count { var preIndex = i - 1 let currentValue = self[i] while preIndex >= 0 && currentValue < self[preIndex] { self[preIndex + 1] = self[preIndex] preIndex -= 1 } self[preIndex + 1] = currentValue } } }
  • 快速排序
extension Array where Element : Comparable{ public mutating func quickSort() { func quickSort(left:Int, right:Int) { guard left < right else { return } var i = left + 1,j = left let key = self[left] while i <= right { if self[i] < key { j += 1 (self[i], self[j]) = (self[j], self[i]) } i += 1 } (self[left], self[j]) = (self[j], self[left]) quickSort(left: j + 1, right: right) quickSort(left: left, right: j - 1) } quickSort(left: 0, right: self.count - 1) } }
  • 随机快排
extension Array where Element : Comparable{ public mutating func quickSort1() { func quickSort(left:Int, right:Int) { guard left < right else { return } let randomIndex = Int.random(in: left...right) (self[left], self[randomIndex]) = (self[randomIndex], self[left]) var i = left + 1,j = left let key = self[left] while i <= right { if self[i] < key { j += 1 (self[i], self[j]) = (self[j], self[i]) } i += 1 } (self[left], self[j]) = (self[j], self[left]) quickSort(left: j + 1, right: right) quickSort(left: left, right: j - 1) } quickSort(left: 0, right: self.count - 1) } }
  • 双路快排
extension Array where Element : Comparable{ public mutating func quickSort2() { func quickSort(left:Int, right:Int) { guard left < right else { return } let randomIndex = Int.random(in: left...right) (self[left], self[randomIndex]) = (self[randomIndex], self[left]) var l = left + 1, r = right let key = self[left] while true { while l <= r && self[l] < key { l += 1 } while l < r && key < self[r]{ r -= 1 } if l > r { break } (self[l], self[r]) = (self[r], self[l]) l += 1 r -= 1 } (self[r], self[left]) = (self[left], self[r]) quickSort(left: r + 1, right: right) quickSort(left: left, right: r - 1) } quickSort(left: 0, right: self.count - 1) } }
  • 三路快排
// 三路快排 extension Array where Element : Comparable{ public mutating func quickSort3() { func quickSort(left:Int, right:Int) { guard left < right else { return } let randomIndex = Int.random(in: left...right) (self[left], self[randomIndex]) = (self[randomIndex], self[left]) var lt = left, gt = right var i = left + 1 let key = self[left] while i <= gt { if self[i] == key { i += 1 }else if self[i] < key{ (self[i], self[lt + 1]) = (self[lt + 1], self[i]) lt += 1 i += 1 }else { (self[i], self[gt]) = (self[gt], self[i]) gt -= 1 } } (self[left], self[lt]) = (self[lt], self[left]) quickSort(left: gt + 1, right: right) quickSort(left: left, right: lt - 1) } quickSort(left: 0, right: self.count - 1) } }

文末推荐:iOS热门文集&视频解析

① Swift

② iOS底层技术

③ iOS逆向防护

④ iOS面试合集

⑤ 大厂面试题+底层技术+逆向安防+Swift

喜欢的小伙伴记得点赞喔~

收藏等于白嫖,点赞才是真情ღ( ´・ᴗ・` )ღ

有关iOS底层面试题(下篇)的更多相关文章

  1. ruby - 如何验证 IO.copy_stream 是否成功 - 2

    这里有一个很好的答案解释了如何在Ruby中下载文件而不将其加载到内存中:https://stackoverflow.com/a/29743394/4852737require'open-uri'download=open('http://example.com/image.png')IO.copy_stream(download,'~/image.png')我如何验证下载文件的IO.copy_stream调用是否真的成功——这意味着下载的文件与我打算下载的文件完全相同,而不是下载一半的损坏文件?documentation说IO.copy_stream返回它复制的字节数,但是当我还没有下

  2. Ruby 文件 IO 定界符? - 2

    我正在尝试解析一个文本文件,该文件每行包含可变数量的单词和数字,如下所示:foo4.500bar3.001.33foobar如何读取由空格而不是换行符分隔的文件?有什么方法可以设置File("file.txt").foreach方法以使用空格而不是换行符作为分隔符? 最佳答案 接受的答案将slurp文件,这可能是大文本文件的问题。更好的解决方案是IO.foreach.它是惯用的,将按字符流式传输文件:File.foreach(filename,""){|string|putsstring}包含“thisisanexample”结果的

  3. Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting - 2

    1.错误信息:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:requestcanceledwhilewaitingforconnection(Client.Timeoutexceededwhileawaitingheaders)或者:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:TLShandshaketimeout2.报错原因:docker使用的镜像网址默认为国外,下载容易超时,需要修改成国内镜像地址(首先阿里

  4. Hive SQL 五大经典面试题 - 2

    目录第1题连续问题分析:解法:第2题分组问题分析:解法:第3题间隔连续问题分析:解法:第4题打折日期交叉问题分析:解法:第5题同时在线问题分析:解法:第1题连续问题如下数据为蚂蚁森林中用户领取的减少碳排放量iddtlowcarbon10012021-12-1212310022021-12-124510012021-12-134310012021-12-134510012021-12-132310022021-12-144510012021-12-1423010022021-12-154510012021-12-1523.......找出连续3天及以上减少碳排放量在100以上的用户分析:遇到这类

  5. ruby - 为什么不能使用类IO的实例方法noecho? - 2

    print"Enteryourpassword:"pass=STDIN.noecho(&:gets)puts"Yourpasswordis#{pass}!"输出:Enteryourpassword:input.rb:2:in`':undefinedmethod`noecho'for#>(NoMethodError) 最佳答案 一开始require'io/console'后来的Ruby1.9.3 关于ruby-为什么不能使用类IO的实例方法noecho?,我们在StackOverflow上

  6. ruby - 为 IO::popen 拯救 "command not found" - 2

    当我将IO::popen与不存在的命令一起使用时,我在屏幕上打印了一条错误消息:irb>IO.popen"fakefake"#=>#irb>(irb):1:commandnotfound:fakefake有什么方法可以捕获此错误,以便我可以在脚本中进行检查? 最佳答案 是:升级到ruby​​1.9。如果您在1.9中运行它,则会引发Errno::ENOENT,您将能够拯救它。(编辑)这是在1.8中的一种hackish方式:error=IO.pipe$stderr.reopenerror[1]pipe=IO.popen'qwe'#

  7. ruby - IO::EAGAINWaitReadable:资源暂时不可用 - 读取会阻塞 - 2

    当我尝试使用“套接字”库中的方法“read_nonblock”时出现以下错误IO::EAGAINWaitReadable:Resourcetemporarilyunavailable-readwouldblock但是当我通过终端上的IRB尝试时它工作正常如何让它读取缓冲区? 最佳答案 IgetthefollowingerrorwhenItrytousethemethod"read_nonblock"fromthe"socket"library当缓冲区中的数据未准备好时,这是预期的行为。由于异常IO::EAGAINWaitReadab

  8. ruby - 如何使用 ruby​​ fibers 避免阻塞 IO - 2

    我需要将目录中的一堆文件上传到S3。由于上传所需的90%以上的时间都花在了等待http请求完成上,所以我想以某种方式同时执行其中的几个。Fibers能帮我解决这个问题吗?它们被描述为解决此类问题的一种方法,但我想不出在http调用阻塞时我可以做任何工作的任何方法。有什么方法可以在没有线程的情况下解决这个问题? 最佳答案 我没有使用1.9中的纤程,但是1.8.6中的常规线程可以解决这个问题。尝试使用队列http://ruby-doc.org/stdlib/libdoc/thread/rdoc/classes/Queue.html查看文

  9. ruby - 如何从 ruby​​ 中的 IO 对象获取文件名 - 2

    在ruby中...我有一个由外部进程创建的IO对象,我需要从中获取文件名。然而我似乎只能得到文件描述符(3),这对我来说不是很有用。有没有办法从此对象获取文件名甚至获取文件对象?我正在从通知程序中获取IO对象。所以这也可能是获取文件路径的一种方式? 最佳答案 关于howtogetathefilenameinC也有类似的问题,我将在这里以ruby​​的方式给出这个问题的答案。在Linux中获取文件名假设io是您的IO对象。以下代码为您提供了文件名。File.readlink("/proc/self/fd/#{io.fileno}")例

  10. 蓝桥杯C/C++VIP试题每日一练之报时助手 - 2

    ?作者主页:静Yu?简介:CSDN全栈优质创作者、华为云享专家、阿里云社区博客专家,前端知识交流社区创建者?社区地址:前端知识交流社区?博主的个人博客:静Yu的个人博客?博主的个人笔记本:前端面试题个人笔记本只记录前端领域的面试题目,项目总结,面试技巧等等。接下来会更新蓝桥杯官方系统基础练习的VIP试题,依然包括解题思路,源代码等等。问题描述:给定当前的时间,请用英文的读法将它读出来。时间用时h和分m表示,在英文的读法中,读一个时间的方法是:  如果m为0,则将时读出来,然后加上“o’clock”,如3:00读作“threeo’clock”。  如果m不为0,则将时读出来,然后将分读出来,如5

随机推荐