0%

在早期的计算机中,程序是直接运行在物理内存上的,程序在运行时所访问的地址都是物理地址。如果一个计算机同时只运行一个程序,这么操作不会有什么问题,只需要保证程序的内存空间需求不超过计算机物理内存的大小就行。然而现实是,为了更有效的利用硬件资源,计算机会同时运行多个程序。此时会出现一个问题,如何将计算机有限的物理内存分配给多个程序使用?

物理内存分配

我们来看一种简单的分配方式,假设我们的计算机有128MB的内存,程序A运行需要10MB,程序B需要100MB,程序C需要30MB。同时运行A和B,可以比较简单的将内存的前10MB分配给程序A,将10-110MB分配给程序B。这样可以实现程序A和B的同时运行,但是这样的内存分配策略存在明显的问题:

  • 地址空间不隔离:程序直接访问物理地址,程序所使用的内存空间不相互隔离。那么简直就是恶意程序的天堂,恶意程序可以很随意的改写其他程序的内存数据,为所欲为。除了恶意程序的侵入,还会有程序不小心修改了其他程序的数据。所以,地址空间不隔离会带来很严重的安全风险。
  • 内存使用效率低:由于没有有效的内存管理机制,通常需要执行一个程序时,系统会将整个程序载入内存中然后执行程序。例如,我们此时要执行程序C,但是程序A和B已经占用了110MB的内存,没有足够的空间给C,这时候会将其他程序的数据暂时写到磁盘里,等到要用的时候再读出来。因为程序内存是连续的,所以这里需要把程序B占用的空间数据写入到磁盘,然后在将程序C读入到内存中开始执行。下次程序B要运行时,再把程序C写入磁盘,程序B读入内存。可以看到整个过程中会有大量的数据读写,导致效率十分低下。
  • 程序运行地址不确定:程序每次载入运行时,需要为其分配一块足够大小的空闲内存空间,显然这个空闲区域每次是不固定的。这会给程序编写造成一定的麻烦,涉及到程序的重定位问题。

在计算机界有一句名言:

计算机领域的任何问题都可以通过增加一个中间层来解决。(Any problem in compute science can be solved by another layer of indirection.)

可以借助这一思想来解决上面的几个问题:增加中间层(用一种间接的地址访问方法)。把程序给出的地址看做是一种虚拟地址(Virtual Address),通过某种映射方法,将这个虚拟地址转换成实际的物理地址。这样,只要我们妥善掌控映射过程,就可以保证任意一个程序所能访问的物理内存区域跟另外一个程序互不重叠干扰,内存地址不能相互操作,已达到空间地址隔离的效果。
关于隔离,每个程序都有自己的进程,每个进程有自己的虚拟内存空间,在进程内只能访问自己的虚拟空间(程序间通信的部分除外,因为那是受程序允许的范围,这里不考虑),这样就有效的做到了进程间的隔离。
有了思路,那么具体如何设计呢?答案是分页。

分页(Paging)

把地址空间等分成固定大小的页,每一页大小有硬件或操作系统来决定。Intel公司设计了4KB和4MB页大小的芯片,目前市场上所有的PC几乎都使用的是4KB的页大小(我们使用的MAC是64位的系统,理论上64位最大支持的内存高达亿位数GB,实际上能支持到的是128GB,那么按每页4KB来计算,总共有33554432页)。来看一张图:

假设有8页虚拟内存,而物理内存只够支持6页,那么就会有一部分数据需要被保存在磁盘(Disk)中,当需要时再把它读出来即可。图中我们可以看到:

  • 有部分虚拟页面被存放在磁盘中,比如Process1的VP3、VP2位于磁盘的DP1、DP20中
  • 有部分虚拟页面没有被用或访问到,他们暂时处于未使用状态,比如VP4、VP5、VP6
  • 有部分虚拟页面被映射到了同一个物理页,实现了内存共享,比如PP3、PP0

Process1的VP2和VP3不在内存中,当进程需要使用这两个页的时候,硬件会捕获到这个信息,发生页错误(Page Fault)。然后由操作系统接管进程,负责将VP2和VP3从磁盘中读出来装入物理内存,再把这两个物理页与VP2、VP3建立映射关系。页映射还可以起到保护的作用,只需要给每个页设置相应的读写权限。
所有的硬件都采用一个叫MMU(Memory Management Unit)的部件来进行页映射:

页映射模式下,CPU发出的是Virtual Address,经过MMU(集成在CPU中)转换后变成Physical Address。

Page Fault

指当软件试图访问已映射在虚拟内存空间中,但是目前并未被加载在物理内存中的一个分页时,由CPU的内存管理单元所发出的中断。
通常情况下,用于处理此中断的程序是 操作系统的一部分。如果操作系统判断此次访问是有效的,那么操作系统会尝试将相关的分页从硬盘上的虚拟内存文件中调入内存。而如果访问是不被允许的,那么操作系统通常会结束相关的进程。
在iOS开发中,应用程序启动阶段就会有Page Fault,利用一系列手段(如二进制重排)优化Page Fault,可以减少应用的启动时间。

一个沙盒浏览小工具

在平常开发调试的过程中,经常会需要查看日志文件,我们一般会将日志文件放在 Documents 文件夹下,那么有哪些方式可以查看呢:

  1. xcode 工具,Windows -> Devices 进入设备管理页面,可以查看 iPhone 上所有 Installed Apps,再通过 Download Container 将 APP 信息下载到 Mac 上,就可以查看你想要的文件。
  2. 通过浏览器打开 APP 的沙盒文件,内置一个 HTTP 服务器,浏览器直接访问 sandbox 目录,这种方式还算方便。
  3. 在 iOS 端集成一段代码,通过摇一摇或者其他方式呼出一个文件浏览工具页面,选中需要的文件,点击直接查看或者通过 Airdrop 分享至 Mac 端查看。

相比于前两种方式,第三种方案实现简单,体验更顺畅,所以花时间实现做了一个小工具。

核心方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/// About FileManager
extension WESandboxVC {
/// 加载指定路径下文件
fileprivate func loadPath(filePath: String) {
DispatchQueue.global().async {
var files: [WEFileItem] = []

var targetPath = filePath
if filePath.count == 0 || filePath == self.rootPath {
/// 进入主文件夹路径
targetPath = self.rootPath
}else {
/// 已经进入主文件夹内部,添加一个返回上一层的item
let item = WEFileItem(fileName: "🔙..", fileType: .backup, filePath: filePath)
files.append(item)
}

var paths: [String] = []
/// 取出路径下所有子路径
do {
paths = try FileManager.default.contentsOfDirectory(atPath: targetPath)
} catch {
print("no files exist!!!")
}

for inlinePath in paths {
/// 拼出完整路径
let fullPath = NSString(string: targetPath).appendingPathComponent(inlinePath)
var isDir: ObjCBool = false
/// 检查路径是否存在,且是否是文件夹
if !FileManager.default.fileExists(atPath: fullPath, isDirectory: &isDir) {
continue
}

var item = WEFileItem(fileName: "", fileType: .backup, filePath: fullPath)
if isDir.boolValue {
/// 文件夹
item.fileName = "📁 " + inlinePath
item.fileType = .dir
}else{
/// 文件
item.fileName = "📃 " + inlinePath
item.fileType = .file
}

files.append(item)
}
self.fileDatas = files
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}

/// 调用原生分享
fileprivate func sharePath(path: String) {
let url = URL(fileURLWithPath: path)
let objectNeedShare = [url]

let controller = UIActivityViewController(activityItems: objectNeedShare, applicationActivities: nil)
if UIDevice.current.model.contains("iPad") {
controller.popoverPresentationController?.sourceView = self.view
controller.popoverPresentationController?.sourceRect = CGRect(x: screenWidth * 0.5, y: screenHeight, width: 20, height: 20)
}
self.present(controller, animated: true, completion: nil)
}
}

只需要添加以下代码,可以通过摇一摇的方式呼出文件浏览器:

1
2
3
4
5
6
override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
super.motionEnded(motion, with: event)
#if DEBUG
WESandboxTool.switchSandbox()
#endif
}

Github地址

在日常开发测试过程中经常会需要获取设备的 UUID,正常我们会把设备连上电脑,在 iTunes 里去招设备序列号然后复制出来。这样操作虽然也不繁琐,但有没有办法更高效呢?让我们写一个小脚本工具来实现这一功能~

  1. 第一步,借助 system_profiler,在 terminal 中输入 指令system_profiler SPUSBDataType,可以获取到当前 mac 上所有 USB 信息,找到你的设备,可以看到对应的 Serial Number

  1. 第二步, 筛选出我们需要的 Serial Number 信息,修改上面的指令
    system_profiler SPUSBDataType | sed -n -E 's/Serial Number: (.+)/\\1/1p',打印结果如下,可以很轻松的拿到我们需要的设备序列号.

  2. 第三步,快捷启动。我们不可能每次都敲这么一长串命令,关键也不好记,为了操作方便,我们可以命令加个 alias

  • 编辑 .bash_profile:
    vim ~/.bash_profile
  • 加入命令别名:
    alias lsusb="system_profiler SPUSBDataType | sed -n -E 's/Serial Number: (.+)/\\1/1p'
  • 启用配置:
    source ~/.bash_profile
  • 完成,使用命令 lsusb,打印出想要的结果
    lsusb

总结:以上的脚本只是一个小工具,在开发过程中可以编写更多功能的脚本来提升开发效率。比如 system_profiler 并不仅仅只能获取 usb 信息,还有很多其他功能可以去探索。

涂鸦智能(西法)

  • jsbridge的实现原理
  • mvvm如何拆分vm,拆分的依据是什么
  • 多线程如何实现同时读读,读写互斥
  • 项目中运用到的觉得有技术深度的点
  • 在图片展示的过程中CPU与GPU各自负责什么工作
  • 怎样设计一套缓存
  • rxswift里的map等函数用法,题目忘记了

爱奇艺(德叔)

  • 函数调用时,栈怎么运作的;线程 与 线程 之间,栈是 共享的,还是每个线程有自己独立的栈,堆呢?

  • gcd 与 线程 的关系

  • block里面 把__weak 转成 __strong 那个对象就不会释放吗

  • 程序耗电时,怎么设计 监控程序怎么耗电的

  • 程序崩溃的时候,怎么设计 获取去获取 崩溃信息,发生崩溃之后,app退出前

  • iOS里,引用计数 存在哪里的,几种情况?

  • 图文混排哪些方案

  • 有1000桶水,其中一桶有毒;一只猪来试喝水,每只猪只能喝一次,喝到有毒的水会立刻挂掉,可以将每桶水混起来,至少要几只猪,才能找到有毒的那桶水

  • 接上提,如果一只猪不是只能喝一次,而是每隔15分钟能喝一次,喝到有毒的也会立刻挂掉,要在1小时内找出那桶有毒的水,至少要多少只猪

  • 冒泡,快速排序

  • 一个数比如657,将每个位置上的数重新排列,找出,大于当前数的,最小的那个:675,重新排列:675,567,576,765,756,大于 657 的最小的数是 675

  • 有一个矩阵,0表示水,1表示陆地,1的上下左右全是水的时候,表示一个岛屿,伪代码找出矩阵中岛屿个数,几个1相邻时看作一个岛屿:

    • 示例
  • 给定两个正整数row, col ,生成一个包含 1 到 row*col 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵

  • javascriptCore 与 原生交互原理

海康一面

  1. runtime应用
  2. runloop使用过程中要注意什么
  3. 怎样保证读写安全
  4. mvvm在项目中的应用
  5. iOS响应链
  6. UIView动画、masonry为什么不会有循环应用的问题

海康二面

  1. masonry为什么不会有循环引用的问题
  2. runloop和autoreleasePool的关系
  3. GCD group、semaphore
  4. 优化相关的工作做了哪些
  5. SDWebImage实现原理
  6. js与原生交互实现原理
  7. 事件响应链
  8. 怎么扩大button的点击范围?hitTest做了什么工作?

e签宝

  1. crash防护
  2. 通知为什么会引起崩溃
  3. Native和H5交互的原理
  4. KVO原理(OC和swift各自怎么实现)
  5. 父类和子类的析构函数调用顺序(init方法?)
  6. APP之间的通信方式

考拉海购

  • 直播里面的业务怎么解藕的,不同的模块之间如何通信的
  • 各种锁性能有没有做过对比
  • 点击事件的原理讲一下(讲了一波响应链)
  • block有几种类型,对基本数据类型和对象类型分别是如何引用的。如果要修改基本数据类型的变量呢?
  • BaseRequest里面
  • HFBasePageRequest
  • 引用计数器原理
  • Category如何加载的,用过这个吗?有没有遇到什么问题
  • Mach-O文件里有哪些东西,为什么要区分Segment,Section?
  • HTTPS为什么是安全的
  • Runloop有哪些Mode,源码对CommonModes中是如何存储的(没说出来)
  • 如何自己设计一个类似NSNotification的通知?
  • 不用dispatch_once,如何自己设计
  • 直播间点赞,主态-客态,客态-客态间有哪些需要注意的点
  • 工作中你是如何推进项目的

微拍堂

  • 如何Hook App中所有请求的地址
  • 如何Hook三方SDK提供的block方法
  • Extension和Category的区别
  • Runloop平时用到吗?
  • GCD平时用到哪些
  • 如何实现按顺序执行多个请求
  • AutoreleasePool,在pop的时候是如何找到哨兵对象的。
  • MVP,MVVM说一下
  • iOS异步渲染

手淘特价版

  • 让A,B两个线程交错打印
  • 举一个死锁的例子
  • SideTable格式再看一下
  • 两个VC:a,b。说一下从a跳转到b时,生命周期的过程。
  • 接上题:如果是a addChildVC b呢?
  • 一面笔试

其他

  • 在子线程中使用performSelector:withObject:afterDelay方法,使用完毕后,需要手动销毁线程吗?
  • 对象A被多个weak指针所指向。若持有其中一个weak指针的对象B被释放,A在执行dealloc方法时,让对应weak释放,会造成野指针崩溃吗?
  • barrier不能对dispath_global_queue生效
  • 静态库 与 动态库 区别
  • fishhook探索iOS应用编译及启动的原理
  • 子线程 runloop 是默认不开启的,子线程时使用 autorelease 局部变量,是怎么释放的?
  • 西法1
  • 方法替换,会替换掉父类的方法么?会影响其他类么?

边锋

天猫好房

  • 你做过最有难度的事?
  • 你做过最有挑战的事?
  • 工作中做过那些事?带来了哪些结果?
  • RXSwift的优势在哪?业务中哪些场景用到了?
  • RTMP,FLV,h264,hls这些协议有啥区别
  • 说一下isa指针
  • Swift和OC相比,有什么优势和劣势。

哈啰单车

  • 互斥锁,自旋锁的区别
  • SDWebImage了解过么
  • GCD在项目中用了哪些使用场景