0%

对于MVVM的理解?

MVVM 是 Model-View-ViewModel 的缩写。
Model代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑。
View 代表UI 组件,它负责将数据模型转化成UI 展现出来。
ViewModel 监听模型数据的改变和控制视图行为、处理用户交互,简单理解就是一个同步View 和 Model的对象,连接Model和View。
在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。
ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。

Vue的生命周期

  1. beforeCreate(创建前) 在数据观测和初始化事件还未开始
  2. created(创建后) 完成数据观测,属性和方法的运算,初始化事件,$el属性还没有显示出来
  3. beforeMount(载入前) 在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。注意此时还没有挂载html到页面上。
  4. mounted(载入后) 在el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html页面中。此过程中进行ajax交互。
  5. beforeUpdate(更新前) 在数据更新之前调用,发生在虚拟DOM重新渲染和打补丁之前。可以在该钩子中进一步地更改状态,不会触发附加的重渲染过程。
  6. updated(更新后) 在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。调用时,组件DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
  7. beforeDestroy(销毁前) 在实例销毁之前调用。实例仍然完全可用。
    destroyed(销毁后) 在实例销毁之后调用。调用后,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。

1.什么是vue生命周期?
答: Vue 实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程,称之为 Vue 的生命周期。

2.vue生命周期的作用是什么?
答:它的生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程时更容易形成好的逻辑。

3.vue生命周期总共有几个阶段?
答:它可以总共分为8个阶段:创建前/后, 载入前/后,更新前/后,销毁前/销毁后。

4.第一次页面加载会触发哪几个钩子?
答:会触发 下面这几个beforeCreate, created, beforeMount, mounted 。

5.DOM 渲染在 哪个周期中就已经完成?
答:DOM 渲染在 mounted 中就已经完成了。

Vue实现数据双向绑定的原理:Object.defineProperty()

vue实现数据双向绑定主要是:采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应监听回调。当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用 Object.defineProperty 将它们转为 getter/setter。用户看不到 getter/setter,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。

vue的数据双向绑定 将MVVM作为数据绑定的入口,整合Observer,Compile和Watcher三者,通过Observer来监听自己的model的数据变化,通过Compile来解析编译模板指令(vue中是用来解析 {{}}),最终利用watcher搭起observer和Compile之间的通信桥梁,达到数据变化 —>视图更新;视图交互变化(input)—>数据model变更双向绑定效果。

js实现简单的双向绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<body>
<div id="app">
<input type="text" id="txt">
<p id="show"></p>
</div>
</body>
<script type="text/javascript">
var obj = {}
Object.defineProperty(obj, 'txt', {
get: function () {
return obj
},
set: function (newValue) {
document.getElementById('txt').value = newValue
document.getElementById('show').innerHTML = newValue
}
})
document.addEventListener('keyup', function (e) {
obj.txt = e.target.value
})
</script>

Vue组件间的参数传递

  1. 父组件与子组件传值
    父组件传给子组件:子组件通过props方法接受数据;
    子组件传给父组件:$emit方法传递参数
  2. 非父子组件间的数据传递,兄弟组件传值
    eventBus,就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。项目比较小时,用这个比较合适。(虽然也有不少人推荐直接用VUEX,具体来说看需求咯。技术只是手段,目的达到才是王道。)

Vue的路由实现:hash模式 和 history模式

hash模式:在浏览器中符号“#”,#以及#后面的字符称之为hash,用window.location.hash读取;
特点:hash虽然在URL中,但不被包括在HTTP请求中;用来指导浏览器动作,对服务端安全无用,hash不会重加载页面。
hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 http://www.xxx.com, 因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回 404 错误。

history模式:history采用HTML5的新特性;且提供了两个新方法:pushState(),replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更。
history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,如 http://www.xxx.com/items/id。后端如果缺少对 /items/id 的路由处理,将返回 404 错误。Vue-Router 官网里如此描述:“不过这种模式要玩好,还需要后台配置支持……所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。”

vue路由的钩子函数

首页可以控制导航跳转,beforeEach,afterEach等,一般用于页面title的修改。一些需要登录才能调整页面的重定向功能。

beforeEach主要有3个参数to,from,next:

to:route即将进入的目标路由对象,

from:route当前导航正要离开的路由

next:function一定要调用该方法resolve这个钩子。执行效果依赖next方法的调用参数。可以控制网页的跳转。

vuex是什么?怎么使用?哪种功能场景使用它?

只用来读取的状态集中放在store中; 改变状态的方式是提交mutations,这是个同步的事物; 异步逻辑应该封装在action中。
在main.js引入store,注入。新建了一个目录store,….. export 。
场景有:单页应用中,组件之间的状态、音乐播放、登录状态、加入购物车

state
Vuex 使用单一状态树,即每个应用将仅仅包含一个store 实例,但单一状态树和模块化并不冲突。存放的数据状态,不可以直接修改里面的数据。
mutations
mutations定义的方法动态修改Vuex 的 store 中的状态或数据。
getters
类似vue的计算属性,主要用来过滤一些数据。
action
actions可以理解为通过将mutations里面处里数据的方法变成可异步的处理数据的方法,简单的说就是异步操作数据。view 层通过 store.dispath 来分发 action。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const store = new Vuex.Store({ //store实例
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})

modules
项目特别复杂的时候,可以让每一个模块拥有自己的state、mutation、action、getters,使得结构非常清晰,方便管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... }
}

const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
})

vue-cli如何新增自定义指令?

  1. 创建局部指令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var app = new Vue({
el: '#app',
data: {
},
// 创建指令(可以多个)
directives: {
// 指令名称
dir1: {
inserted(el) {
// 指令中第一个参数是当前使用指令的DOM
console.log(el);
console.log(arguments);
// 对DOM进行操作
el.style.width = '200px';
el.style.height = '200px';
el.style.background = '#000';
}
}
}
})
  1. 全局指令
1
2
3
4
5
Vue.directive('dir2', {
inserted(el) {
console.log(el);
}
})
  1. 指令的使用
    1
    2
    3
    4
    <div id="app">
    <div v-dir1></div>
    <div v-dir2></div>
    </div>

vue如何自定义一个过滤器?

html代码:

1
2
3
4
<div id="app">
<input type="text" v-model="msg" />
{{msg| capitalize }}
</div>

JS代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
var vm=new Vue({
el:"#app",
data:{
msg:''
},
filters: {
capitalize: function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
}
}
})

全局定义过滤器:

1
2
3
4
5
Vue.filter('capitalize', function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
})

过滤器接收表达式的值 (msg) 作为第一个参数。capitalize 过滤器将会收到 msg的值作为第一个参数。

对keep-alive 的了解?

keep-alive是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染。
在vue 2.1.0 版本之后,keep-alive新加入了两个属性: include(包含的组件缓存) 与 exclude(排除的组件不缓存,优先级大于include) 。

使用方法

1
2
3
4
5
<keep-alive include='include_components' exclude='exclude_components'>
<component>
<!-- 该组件是否缓存取决于include和exclude属性 -->
</component>
</keep-alive>

参数解释
include - 字符串或正则表达式,只有名称匹配的组件会被缓存
exclude - 字符串或正则表达式,任何名称匹配的组件都不会被缓存
include 和 exclude 的属性允许组件有条件地缓存。二者都可以用“,”分隔字符串、正则表达式、数组。当使用正则或者是数组时,要记得使用v-bind 。

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 逗号分隔字符串,只有组件a与b被缓存。 -->
<keep-alive include="a,b">
<component></component>
</keep-alive>

<!-- 正则表达式 (需要使用 v-bind,符合匹配规则的都会被缓存) -->
<keep-alive :include="/a|b/">
<component></component>
</keep-alive>

<!-- Array (需要使用 v-bind,被包含的都会被缓存) -->
<keep-alive :include="['a', 'b']">
<component></component>
</keep-alive>

一句话就能回答的面试题

  1. css只在当前组件起作用
    答:在style标签中写入scoped即可 例如:<style scoped></style>

  2. v-if 和 v-show 区别
    答:v-if按照条件是否渲染,v-show是display的block或none;

  3. $route和$router的区别
    答:$route是“路由信息对象”,包括path,params,hash,query,fullPath,matched,name等路由信息参数。而$router是“路由实例”对象包括了路由的跳转方法,钩子函数等。

  4. vue.js的两个核心是什么?
    答:数据驱动、组件系统

  5. vue几种常用的指令
    答:v-for 、 v-if 、v-bind、v-on、v-show、v-else

  6. vue常用的修饰符?
    答:.prevent: 提交事件不再重载页面;.stop: 阻止单击事件冒泡;.self: 当事件发生在该元素本身而不是子元素的时候会触发;.capture: 事件侦听,事件发生的时候会调用

  7. v-on 可以绑定多个方法吗?
    答:可以

  8. vue中 key 值的作用?
    答:当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。key的作用主要是为了高效的更新虚拟DOM。

  9. 什么是vue的计算属性?
    答:在模板中放入太多的逻辑会让模板过重且难以维护,在需要对数据进行复杂处理,且可能多次使用的情况下,尽量采取计算属性的方式。好处:①使得数据处理结构清晰;②依赖于数据,数据更新,处理结果自动更新;③计算属性内部this指向vm实例;④在template调用时,直接写计算属性名即可;⑤常用的是getter方法,获取数据,也可以使用set方法改变数据;⑥相较于methods,不管依赖的数据变不变,methods都会重新计算,但是依赖数据不变的时候computed从缓存中获取,不会重新计算。

  10. vue等单页面应用及其优缺点
    答:优点:Vue 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件,核心是一个响应的数据绑定系统。MVVM、数据驱动、组件化、轻量、简洁、高效、快速、模块友好。
    缺点:不支持低版本的浏览器,最低只支持到IE9;不利于SEO的优化(如果要支持SEO,建议通过服务端来进行渲染组件);第一次加载首页耗时相对长一些;不可以使用浏览器的导航按钮需要自行实现前进、后退。

  11. 怎么定义 vue-router 的动态路由? 怎么获取传过来的值
    答:在 router 目录下的 index.js 文件中,对 path 属性加上 /:id,使用 router 对象的 params.id 获取。

我不是推广,我也是个学习者,推荐一个慕课网学习,我还专门写了一个笔记博客

iOS面试题大全—全方面剖析面试

  • UIView与CALayer
  • 事件传递与视图响应链
  • 图像显示原理
  • UI卡顿掉帧原因
  • 滑动优化方案
  • UI绘制原理
  • 离屏渲染

UIView与CALayer

<单一职责原则>
UIView为CALayer提供内容,以及负责处理触摸等事件,参与响应链
CALayer负责显示内容contents

事件传递与视图响应链

1
2
3
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

如果事件一直传递到UIAppliction还是没处理,那就会忽略掉

图像显示原理

  1. CPU:输出位图
  2. GPU :图层渲染,纹理合成
  3. 把结果放到帧缓冲区(frame buffer)中
  4. 再由视频控制器根据vsync信号在指定时间之前去提取帧缓冲区的屏幕显示内容
  5. 显示到屏幕上

CPU工作

  1. Layout: UI布局,文本计算
  2. Display: 绘制
  3. Prepare: 图片解码
  4. Commit:提交位图

GPU渲染管线(OpenGL)

  • 顶点着色,图元装配,光栅化,片段着色,片段处理

UI卡顿掉帧原因

iOS设备的硬件时钟会发出Vsync(垂直同步信号),然后App的CPU会去计算屏幕要显示的内容,之后将计算好的内容提交到GPU去渲染。随后,GPU将渲染结果提交到帧缓冲区,等到下一个VSync到来时将缓冲区的帧显示到屏幕上。也就是说,一帧的显示是由CPU和GPU共同决定的。
一般来说,页面滑动流畅是60fps,也就是1s有60帧更新,即每隔16.7ms就要产生一帧画面,而如果CPU和GPU加起来的处理时间超过了16.7ms,就会造成掉帧甚至卡顿。

滑动优化方案

CPU:把以下操作放在子线程中

  1. 对象创建、调整、销毁
  2. 预排版(布局计算、文本计算、缓存高度等等)
  3. 预渲染(文本等异步绘制,图片解码等)
    GPU:
  • 纹理渲染,视图混合
  • 一般遇到性能问题时,考虑以下问题:
    • 是否受到CPU或者GPU的限制?
    • 是否有不必要的CPU渲染?
    • 是否有太多的离屏渲染操作?
    • 是否有太多的图层混合操作?
    • 是否有奇怪的图片格式或者尺寸?
    • 是否涉及到昂贵的view或者效果?
    • view的层次结构是否合理?

UI绘制原理

异步绘制:

  • [self.layer.delegate displayLayer: ]
  • 代理负责生成对应的bitmap
  • 设置该bitmap作为该layer.contents属性的值

离屏渲染

On-Screen Rendering:当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行
Off-Screen Rendering:离屏渲染,分为CPU离屏渲染和GPU离屏渲染两种形式。GPU离屏渲染指的是GPU在当前屏幕缓冲区外新开辟一个缓冲区进行渲染操作
应当尽量避免的则是GPU离屏渲染
GPU离屏渲染何时会触发呢?
圆角(当和maskToBounds一起使用时)、图层蒙版、阴影,设置

layer.shouldRasterize = YES

为什么要避免GPU离屏渲染?

  • GPU需要做额外的渲染操作。通常GPU在做渲染的时候是很快的,但是涉及到offscreen-render的时候情况就可能有些不同,因为需要额外开辟一个新的缓冲区进行渲染,然后绘制到当前屏幕的过程需要做onscreen跟offscreen上下文之间的切换,这个过程的消耗会比较昂贵,涉及到OpenGL的pipeline跟barrier,而且offscreen-render在每一帧都会涉及到,因此处理不当肯定会对性能产生一定的影响。另外由于离屏渲染会增加GPU的工作量,可能会导致CPU+GPU的处理时间超出16.7ms,导致掉帧卡顿。所以可以的话应尽量减少offscreen-render的图层

Runtime/RunLoop 面试请看我相关具体博客内容,不再累赘

暂停,不定期更新,看心情。。。。。

关于断点 objc_exception_throw 问题分析

通过bt打印出堆栈信息
例如:

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
thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.3
frame #0: 0x00000002232679c0 libobjc.A.dylib`objc_exception_throw
frame #1: 0x0000000223fabe10 CoreFoundation`-[NSException raise] + 12
frame #2: 0x0000000224a778fc Foundation`-[NSObject(NSKeyValueCoding) valueForUndefinedKey:] + 248
frame #3: 0x00000002249ebe1c Foundation`-[NSObject(NSKeyValueCoding) valueForKey:] + 260
frame #4: 0x0000000224a091a4 Foundation`-[NSObject(NSKeyValueCoding) setValue:forKeyPath:] + 256
frame #5: 0x00000002507d45ac UIKitCore`-[UINibKeyValuePair apply] + 72
frame #6: 0x0000000223f7be88 CoreFoundation`-[NSArray makeObjectsPerformSelector:] + 280
frame #7: 0x00000002507d3fec UIKitCore`-[UINib instantiateWithOwner:options:] + 1628
frame #8: 0x0000000250d1ef84 UIKitCore`-[UITableView _dequeueReusableViewOfType:withIdentifier:] + 592
frame #9: 0x000000010351f838 Gundam`-[UITableView(TestinUITableView) testin_DequeueReusableCellWithIdentifier:] + 112
frame #10: 0x0000000250d1f42c UIKitCore`-[UITableView _dequeueReusableCellWithIdentifier:forIndexPath:usingPresentationValues:] + 164
frame #11: 0x0000000250d1f358 UIKitCore`-[UITableView dequeueReusableCellWithIdentifier:forIndexPath:] + 96
frame #12: 0x000000010351f9b8 Gundam`-[UITableView(TestinUITableView) testin_DequeueReusableCellWithIdentifier:forIndexPath:] + 292
* frame #13: 0x000000010267cfe8 Gundam`-[DKLandlordEntrustViewController tableView:cellForRowAtIndexPath:](self=0x0000000155b0aa00, _cmd="tableView:cellForRowAtIndexPath:", tableView=0x000000015495fa00, indexPath=0xb59300120e671f5c) at DKLandlordEntrustViewController.m:220:33
frame #14: 0x0000000250d38618 UIKitCore`-[UITableView _createPreparedCellForGlobalRow:withIndexPath:willDisplay:] + 680
frame #15: 0x0000000250d38b18 UIKitCore`-[UITableView _createPreparedCellForGlobalRow:willDisplay:] + 80
frame #16: 0x0000000250d05320 UIKitCore`-[UITableView _updateVisibleCellsNow:isRecursive:] + 2260
frame #17: 0x0000000250d22640 UIKitCore`-[UITableView layoutSubviews] + 140
frame #18: 0x0000000250fb1170 UIKitCore`-[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1292
frame #19: 0x00000002285cbc60 QuartzCore`-[CALayer layoutSublayers] + 184
frame #20: 0x00000002285d0c08 QuartzCore`CA::Layer::layout_if_needed(CA::Transaction*) + 332
frame #21: 0x00000002285333e4 QuartzCore`CA::Context::commit_transaction(CA::Transaction*) + 348
frame #22: 0x0000000228561620 QuartzCore`CA::Transaction::commit() + 640
frame #23: 0x000000022856215c QuartzCore`CA::Transaction::observer_callback(__CFRunLoopObserver*, unsigned long, void*) + 92
frame #24: 0x000000022401fd08 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 32
frame #25: 0x000000022401aa30 CoreFoundation`__CFRunLoopDoObservers + 412
frame #26: 0x000000022401afac CoreFoundation`__CFRunLoopRun + 1228
frame #27: 0x000000022401a7c0 CoreFoundation`CFRunLoopRunSpecific + 436
frame #28: 0x000000022621b79c GraphicsServices`GSEventRunModal + 104
frame #29: 0x0000000250b19c38 UIKitCore`UIApplicationMain + 212
frame #30: 0x00000001024cd140 Gundam`main(argc=1, argv=0x000000016dbc37f0) at main.m:15:16
frame #31: 0x0000000223ade8e0 libdyld.dylib`start + 4

然后通过单步调试,在汇编里面看到生成exception (creat)等等,最后定位到位置。

HttpHttps 的区别?为什么更加安全?

区别

  • 1.HTTPS 需要向机构申请 CA 证书,极少免费。
  • 2.HTTP 属于明文传输,HTTPS基于 SSL 进行加密传输。
  • 3.HTTP 端口号为 80HTTPS 端口号为 443
  • 4.HTTPS 是加密传输,有身份验证的环节,更加安全。

安全

SSL(安全套接层)
TLS(传输层安全)

以上两者在传输层之上,对网络连接进行加密处理,保障数据的完整性,更加的安全。

TCP/IP 五层模型的协议?

  • 应用层
  • 传输层
  • 网路层
  • 数据链路层
  • 物理层

OSI 七层模型的协议?

  • 应用层 文件传输,电子邮件,文件服务,虚拟终端 TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet
  • 表示层 数据格式化,代码转换,数据加密 没有协议
  • 会话层 解除或建立与别的接点的联系 没有协议
  • 传输层 提供端对端的接口 TCP,UDP
  • 网络层 为数据包选择路由 IP,ICMP,RIP,OSPF,BGP,IGMP
  • 数据链路层 传输有地址的帧以及错误检测功能 SLIP,CSLIP,PPP,ARP,RARP,MTU
  • 物理层 以二进制数据形式在物理媒体上传输数据 ISO2110,IEEE802,IEEE802.2

断点续传 功能该怎么实现?

  • 1.利用 http 的头部字段 ,设置 httpRange 属性
  • 2.检测下载进度,并且判断是否需要重新创建下载文件

DNS 的解析过程?网络的 DNS 优化。

DNS 是域名到 IP 地址的映射,DNS 解析使用 UDP 数据报,端口号53,并且采用明文传输的方式

客户端在向服务端发送请求时,会先将 域名DNS 服务器映射出 IP 地址,然后再访问。

DNS 解析的两种方式

递归查询

不断地自下而上遍历解析,“我去给你问一下”的方式

迭代查询

迭代查询 是 “我告诉你谁可能知道”的方式

DNS 解析存在的问题

DNS 劫持

被钓鱼网站劫持,有可能返回错误的 IP,浏览的不是目标浏览器

DNS 解析转发

晓得运营商可能将 DNS 解析请求转发,解析的比较慢,效率低

DNS 劫持解决办法

httpDNS

使用 http 协议向 DNS 服务器 80 端口进行请求

长连接

找一个中间的长连 server ,在内网专线进行 Http 请求。客户端和这个长连 server通信即可。

Post请求体有哪些格式?

application/x-www-form-urlencoded

这应该是最常见的 POST 提交数据的方式了。浏览器的原生 form 表单,如果不设置 enctype 属性,那么最终就会以 application/x-www-form-urlencoded 方式提交数据

1
2
3
4
5
6
7
POST  HTTP/1.1
Host: www.demo.com
Cache-Control: no-cache
Postman-Token: 81d7b315-d4be-8ee8-1237-04f3976de032
Content-Type: application/x-www-form-urlencoded

key=value&testKey=testValue

multipart/form-data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST  HTTP/1.1
Host: www.demo.com
Cache-Control: no-cache
Postman-Token: 679d816d-8757-14fd-57f2-fbc2518dddd9
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="key"

value
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="testKey"

testValue
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="imgFile"; filename="no-file"
Content-Type: application/octet-stream


<data in here>
------WebKitFormBoundary7MA4YWxkTrZu0gW--

application/json

text/xml

什么是 Mimetype ?

在浏览器中显示的内容有 HTML、有 XML、有 GIF、还有 Flash ……那么,浏览器是如何区分它们,决定什么内容用什么形式来显示呢?答案是 MIME Type,也就是该资源的媒体类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//向该文件发送请求,根据请求头拿到该文件的MIMEType
-(NSString *)getMIMETypeURLRequestAtPath:(NSString*)path
{
//1.确定请求路径
NSURL *url = [NSURL fileURLWithPath:path];

//2.创建可变的请求对象
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];

//3.发送请求
NSHTTPURLResponse *response = nil;
[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:nil];

NSString *mimeType = response.MIMEType;
return mimeType;
}

网络请求的状态码都大致代表什么意思?

1.1xx

1xx 代表临时响应,需要请求者继续执行操作的状态代码。

2.2xx

2xx 代表的多是操作成功。

3.3xx

3xx 代表重定向,表示要完成请求,需要进一步操作

4.4xx

4xx 代表请求错误,表示请求可能出错,妨碍了服务器的处理。

5.5xx

5xx 代表服务器错误,表示服务器在尝试处理请求时发生内部错误。 这些错误可能是服务器本身的错误,而不是请求出错

如何判断一个请求是否结束?

  • 1.查看 Content-Length 是否达到 1024 字节。
  • 2.通过 Post 发送请求时,消息一般都是一段一段返回的,看最后的一个 chunked 是否为空?

SSL 传输协议?说一下 SSL 验证过程?

  • 1.配备了 身份证书,防止被冒充。
  • 2.有 校验机制,一旦被篡改,双方都会知道。
  • 3.通过 加密 进行传播。

连接过程如下:

  • ①:客户端向服务端发消息,包括客户端支持的 加密算法 以及 随机数1TLS 版本号。
  • ②:服务端向客户端返回信息,包括双方已经匹配好的 加密算法 以及 随机数2Server证书
  • ③:服务端向客户端返回 验证证书
  • ④:客户端进行证书验证,首先要评估信任证书
    • 1.服务端向客户端返回证书的 数字摘要 与服务端证书返回客户端,解密后的是否一致,防止被篡改。
    • 2.验证证书链:逐级验证服务端证书是否在可信任证书列表。
    • 3.验证通过后:
      • ①:将 随机数1随机数2、服务器端返回 证书公钥 加密过后的 预主秘钥 三者组合成 会话密钥
      • ②:服务端拿到 会话秘钥 后,会用服务端证书 私钥进行解密,得到 预主秘钥
      • ③:服务端将 随机数1随机数2、服务器端证书私钥解密得到的 预主秘钥 三者组合成 会话密钥。此时客户端的 会话秘钥 和 服务端的 会话秘钥 应该是一致的。
      • ④:接下来会经行验证:
        - 1.客户端通过 `会话秘钥`,加密信息发给服务端,看服务端能否成功接收。
        - 2.服务端通过 `会话秘钥`,加密信息返给客户端,看客户端能否成功解析。
        
    • 4.以上步骤都通过了,此时 SSL 验证连接。

解释一下 三次握手四次挥手

三次握手

  • 1.由客户端向服务端发送 SYN 同步报文。
  • 2.当服务端收到 SYN 同步报文之后,会返回给客户端 SYN 同步报文和 ACK 确认报文。
  • 3.客户端会向服务端发送 ACK 确认报文,此时客户端和服务端的连接正式建立。

建立连接

  • 1.这个时候客户端就可以通过 Http 请求报文,向服务端发送请求
  • 2.服务端接收到客户端的请求之后,向客户端回复 Http 响应报文。

四次挥手

当客户端和服务端的连接想要断开的时候,要经历四次挥手的过程,步骤如下:

  • 1.先由客户端向服务端发送 FIN 结束报文。
  • 2.服务端会返回给客户端 ACK 确认报文 。此时,由客户端发起的断开连接已经完成。
  • 3.服务端会发送给客户端 FIN 结束报文 和 ACK 确认报文。
  • 4.客户端会返回 ACK 确认报文到服务端,至此,由服务端方向的断开连接已经完成。

解释一下为什么是”三次握手”

客户端在与服务端建立连接的过程中,客户端向服务端发送连接请求,如果因为网络的原因,导致它超时触发了重传机制,那么客户端会重新向服务端发送一次连接请求,那有可能第二次发送的连接请求,服务端收到之后,直接返回给客户端确认连接的报文。

如果只需要两次就可以建立连接,假如说第一次建立连接的请求终于被服务端接收到了,那服务端收到之后,会以为客户端想新建另外一个链接,这样的话就会建立两个连接。

而采取三次握手,在客户端收到服务端的确认报文之后,客户端再向服务端发送一个确认报文,这时才正式建立连接。那么第一次因为超时而迟迟没有收到的建立连接的请求,这时在收到之后,服务端依然会向客户端返回一个确认报文,但是服务端现在已经与客户端建立的连接,所以客户端就不会再向服务端发送确认报文,服务端迟迟没有收到确认报文,那么也就不会真正的建立再一次连接。

又为什么是”四次挥手”?

建立的时候是双方面的建立,而释放的时候,也必然是客户端和服务端双方面的断开连接。

客户端与服务端建立的连接是双通的,这也就意味着客户端可以向服务端发送请求,服务端向客户端返回响应,同样的也可以从服务端向客户端发送,然后由客户端向服务端返回,如果你只关闭了客户端,向服务端通道,这只叫半关闭状态,并没有完全的切断客户端与服务端的关联。

因为 Http 无状态的特性,如果需要判断是哪个用户,这时就需要 CookieSession

Cookie 存储在客户端,用来记录用户状态,区分用户。一般都是服务端把生成的 Cookie 通过响应返回给客户端,客户端保存。

Session 存储在网络端,需要依赖 Cookie 机制。服务端生成了 Session 后,返回给客户端,客户端 setCookie:sessionID,所以下次请求的时候,客户端把 Cookie 发送给服务端,服务端解析出 SessionID,在服务端根据 SessionID 判断当前的用户。

  • 相同 Key 值得新的 Cookie 会覆盖旧的 Cookie.

  • 覆盖规则是 name path 和 domain 等需要与原有的一致

  • 相同 Key 值得新的 Cookie 会覆盖旧的 Cookie.

  • 覆盖规则是 name path 和 domain 等需要与原有的一致

  • 设置 Cookieexpires 为过去的一个时间点,或者 maxAge = 0

  • Cookie 进行加密处理
  • 只在 Https 上携带 Cookie
  • 设置 CookiehttpOnly,防止跨站脚本攻击。

Hybrid

为什么需要这种设计,或者技术方案,我们就不多说了,但是我还是要强调几点个人的见解

  • 最好不要大面积使用此方案,以免影响用户体验,最好别超过项目的30%。
  • 在某些页面变化比较大,而又不太需要性能的页面,采取此设计非常合适
  • 还有一个非常重要的,一定要注意,此交互方案在app内是异步线程,注意要回到主线程处理UI。安卓和iOS都是如此!!!

在安卓上面

我们有三种方案,分别是

  1. Native提供方法,js调用Native方法
  2. Native在本地注入js方法并且调用
  3. Webview拦截Ajax请求,并做业务处理

Native提供方法,js调用Native方法

  • native注入对象(有一系列供js使用的方法)以及提供给js调用的名称。
    1
    2
    webView.addJavascriptInterface(new HybridJsInterface(),"HybridJSInterface");

  • native提供可以供js调用的方法。
    1
    2
    3
    4
    5
    public class HybridJsInterface {
    @JavascriptInterface
    final public void hello(String text)
    Log.i("method hello","text="+text);
    }
  • js调用native的方法。
    1
    2
    3
    function hybrid(){
   
    window.HybridJSInterface.hello("hello hybrid");

    }

Native在本地注入js方法并且调用

1
2
3
4
5
6
7
8
9
10
11
//    function helloJs(){
// alert("hello js");
// }
String script = "function helloJs(){ alert('hello js');}";
//script是js方法对应的字符串
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
mWebContent.evaluateJavascript(script, null);
} else {
mWebContent.loadUrl("javascript:" + script);
}
}
1
2
3
4
5
6
String handle = "helloJs()";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
mWebContent.evaluateJavascript(handle, null);
} else {
mWebContent.loadUrl("javascript:" + handle);
}

Webview拦截Ajax请求,并做业务处理

  1. native拦截h5发过来的请求协议(自定义协议)。
    webview位置拦截的WebViewClient对象。

    1
    webView.setWebViewClient(new HybridWebViewClient());

    HybridWebViewClient重写shouldOverrideUrlLoading方法

    1
    2
    3
    4
    5
    public boolean shouldOverrideUrlLoading(WebView view, String url) {Uri parse = Uri.parse(url);
    String path = parse.getPath();witch (path) {
    //TODO
    }
    }
  2. js那边发起请求协议。

方案分析

这三种方案单独使用都比较有局限性。

  • 方案一的思路是native提前给h5实现好h5需要使用的方法,方法调用完全由h5控制,缺陷是h5调用的功能实现都由native实现,当h5页面中需要新功能的时候需要修改native才能支持。

  • 方案二的思路是js提前给native提供方法,方法调用完全由native控制,缺陷是当js提供了新的方法那么需要修改native主动调用才能使用新功能。

  • 方案三的思路是native拦截h5发过了的请求进行分发控制,js需要使用的功能是native已经提前准备好的,使用不同的url进行分发来使用,缺陷是h5使用新的功能那么需要修改native提供新功能。

单独使用这三种方案的共同缺点就是js和native都是一个单向的调用,相互太过依赖。要是三种方案都用上是不是可以解决问题呢,其实根本问题不在这里,而在于这些方案都没有办法动态扩展,也就是说native一旦完成之后那么单纯靠js是很难完成功能扩展的。我们需要一种可以适应一定程度动态扩展的方案,那就让h5作为项目主导,native提供服务。

hybrid设计

总体设计思路是h5控制整个业务流程以及交互流程。h5那边负责发命令并且回调,native负责响应命令。我是采用方案三来实现,客户端需要做的就是预先处理好这些命令(url)。既然是响应命令那么首先就需要把和业务无关的命令给整理出来,比较通用的包括下面内容(不一定全):

  • header控制。

heade的样式可以参考新闻类app的详情页(这里不截图),包括内容:左边按钮(多个),右边按钮(多个),主标题,副标题。

需要做的控制是左、右按钮是否显示、显示的文本及图标以及点击按钮的回调,主、副标题是否显示及显示内容。

  • 页面刷新。

页面刷新用于内容改变之后h5主动通知native进行刷新。

  • 页面跳转。

页面跳转分成两种一种是页面跳转到一个新的native页面,另一种是在webview内部做跳转。native的跳转包括内容:跳转动画、跳转目标页、目标页需要的参数。

  • loadingview/progressbar。

通常情况建议直接使用h5的进度显示。loadingview的控制包括:loadingview是否显示,loadingview的显示样式(通常只有一种样式)。

  • 传感器数据。

传感器这部分不一定每个应用都需要。比如某些h5页面需要做活动,那么里面可能会用的摇一摇这样的功能。传感器的控制包括:地理位置、方向、震动、运动量。

  • h5离线包更新。

离线包的更新对于h5来说是一条更新命令。不过在native实现上面需要包括:离线包更新检查(版本比较)、离线包下载、离线包解压保存。

  • 离线包开关。

是否使用离线数据。native需要做的是开启离线包命令之后需要把请求的url映射到本地文件缓存。

  • 数据请求。

数据请求是指h5需要请求数据不通过直接网络访问,而是通过native自己的网络服务获取数据,尤其是在跨域的情况下很方便。

iOS上面

同样的,iOS也具备上述三个方案,分别是

  • JavaScriptCore
  • WebviewLoad
  • Url Schema

JavaScriptCore

在ios7后,Apple新增了一个JavaScriptCore让Native可以与H5更好的交互(Android早就有了),我们这里主要讨论js如何与Native通信,这里举一个简单的例子:
① 首先定义一个js方法,这里注意其中调用了一个没有声明的方法:

1
2
3
4
function printHello() {
//未声明方法
print("Hello, World!");
}

然后,上述未声明方法事实上是Native注入给window对象的:

1
2
3
4
5
6
7
8
9
10
11
12
NSString * scriptPath = [[NSBundle mainBundle] pathForResource:@"hello" ofType:@"js"];
NSString * scriptString = [NSString stringWithContentsOfFile:scriptPath encoding:NSUTF8StringEncoding error:nil];

JSContext *context = [[JSContext alloc] init];
[context evaluateScript:scriptString];

self.context[@"print"] = ^(NSString *text) {
NSLog(@"%@", text");
};

JSValue *function = self.context[@"printHello"];
[function callWithArguments:@[]];

这个样子,JavaScript就可以调用Native的方法了,这里Native需要注意方法注入的时机,一般是一旦载入页面便需要载入变量

  • 此方案需要特别注意的就是注入时机,要保证JS能调用到注入的方法

WebviewLoad

此方案和安卓一样

1
[self.wkWebView loadHTMLString:@"javascriptStr" baseURL:@"urlstr"];

Url Schema

H5与Native交互的桥梁为Webview,而“联系”的方式是以url schema的方式做的,在用户安装app后,app可以自定义url schema,并且把自定义的url注册在调度中心, 例如

  • dongfangdi://open 打开懂房帝App
  • weixin:// 打开微信

事实上Native能捕捉webview发出的一切请求,所以就算这里不是这种协议,Native也能捕捉,这个协议的意义在于可以在浏览器中直接打开APP,
我们在H5获取Native方法时一般是会构造一个这样的请求,使用iframe发出(设置location会有多次请求覆盖的问题):

1
2
3
4
5
6
7
8
9
10
11
requestHybrid({
//创建一个新的webview对话框窗口
tagname: 'dongfangdiApi',
//请求参数,会被Native使用
param: {},
//Native处理成功后回调前端的方法
callback: function (data) {
}
});
//=====>
dongfangdi://dongfangdi?callback=dongfangdiAction&param=%7B%22data1%22%3A1%2C%22data2%22%3A2%7D

总结

由于一开始我就强调过,不要完全依赖Hybrid来做app主要框架,它只是为我们提供更多的页面热更新,页面获取Native信息的一种技术。

  • 在iOS8.0以后,WebKit框架又给我们提供了一套方案
  • 在WKUserContentController类中,我们可以看到这个方法
    1
    2
    3
    4
    5
    6
    7
    8
    /*! @abstract Adds a script message handler.
    @param scriptMessageHandler The message handler to add.
    @param name The name of the message handler.
    @discussion Adding a scriptMessageHandler adds a function
    window.webkit.messageHandlers.<name>.postMessage(<messageBody>) for all
    frames.
    */
    - (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
    然后我们可以在下面这个方法中处理对应的name
    1
    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{}
    强烈推荐此方法!!!

相关demo代码已放在github上

  • https://github.com/Jack-Ving/Hybrid_Interaction 这个是用Vue写的对应的js代码
  • https://github.com/Jack-Ving/Hybrid 这个是采用拦截的方式写的OC代码

由于仓促,如果遇到什么问题,尽管联系我.

QQ:727881945 微信:Q727881945

简介

Linux内核官网www.kernel.org

  • 内核版本2.6.18 3.16等等
  • 发行版本 redhat==conterOS ubuntu debian等等
  • 使用的自由
    • 绝大多数开源软件免费
  • 研究的自由
    • 可以获得软件源代码
  • 散步及改良的自由
    • 可以自由传播、改良甚至销售

应用

  • 基于Linux服务器
  • 嵌入式应用 手机,接顶盒等等

Linux和Windows的不同

  • Linux严格区分大小写
  • Linux中所有内容以文件形式保存,包括硬件
  • 以前内容皆文件
  • Linux不靠扩展名区分文件类型 (->权限)
    • 压缩包 .gz .bz2 .tar.bz2 .tgz登
    • 二进制软件包:.rpm
    • 网页文件: *.html .php
    • 脚本文件: *.sh
    • 配置文件: *.conf

字符界面

  • 占用系统资源更少
  • 减少出错、被攻击的可能性越少

挂载 给每个分区分配挂载点

安装

  • 服务器最小化安装

安装日志

  • /root/install.log: 储存了安装在系统中的软件包及其版本信息
  • /root/install.log.syslog: 储存了安装过程中留下的事件记录
  • /root/anaconda-ks.cfg: 以Kickstart配置文件的格式记录安装过程中设置的选项信息

常用的目录的作用

  • /根目录
  • /bin 命令保存目录(普通用户就可以读取的命令)
  • /sbin 超级权限才能读取的命令
  • /boot 启动目录,启动相关文件
  • /dev 设备文件保存目录
  • /etc 配置文件
  • /home 普通用户家目录
  • /lib 系统库保存目录
  • /mnt 系统挂载目录
  • /media 挂载目录
  • /proc和/sys 目录不能直接操作,保存的是内存的过载点
  • /usr 系统软件资源目录
    • /usr/bin/ 系统命令(普通用户)
    • /usr/sbin/ 系统命令(超级用户)
  • /var 系统相关文档内容

常用命令(文件处理命令)

命令的基本格式

  • ~代表当前所在位置 家目录,初始目录
  • #代表超级用户
  • $代表普通用户
  • 命令 [选项] [参数] -a -all简化

文件格式

  • rw- r– r– 分为三组,三组的权限
  • -文件类型(- 文件, d 目录, | 软连接文件)
  • rw- r– r–
  • 所有者 g所属着 o其他人
  • r读 w写 x执行
  • .文件为隐藏文件

pwd 显示当前所在路径 print working dictionary

mkdir -p [目录名]

  • -p 递归创建 /s/p 这样的

cd 进入目录

  • ~进入当前用户目录,家目录
  • ..上一级目录
  • .当前目录

rmdir [目录] remove empty dir

  • 只能删除空白目录

rm -rf 不询问是否删除

  • -r 删除目录
  • -f 强制

cp [选项] [源文件] [目标目录]

  • -r 复制目录
  • -p 连带文件属性复制
  • -d 若源文件是链接文件,则复制链接属性
  • -a 相当于 -pdr

mv 剪切或者改名命令

  • 源文件或者目录和目标文件目录 在一起,就是改名

链接命令

  • ln -s [源文件] [目标文件]
  • 命令英文愿意: link
  • 功能描述:生成链接文件
    • 选项: -s 创建软链接
  • 硬链接特征 (i 文件索引点)
    1. 拥有相同的i节点和储存block块,可以看作是同一个文件
    2. 可通过i节点识别
    3. 不能跨分区
    4. 不能准对目录使用
    5. 引用计数加1,同一个文件
  • 软链接特征
    • 类似于windows快捷方式
    • 拥有自己的i节点和block块,但是数据块中只保存源文件的文件名和i节点号,并没有实际的文件数据
    • Irwxrwxrwx I 软连接
      • 软连接文件的权限都为rwxrwxrwx
    • 修改任意文件,另一个都改变
    • 删除源文件,软连接不能使用

文件搜索命令

  • 文件搜索命令locate
  • 命令搜索命令whereis与which
  • 文件搜索命令find
  • 字符串搜索命令grep
  • find命令与grep命令的区别

locate (速度相对较快)

  • locate 文件名。 在后台数据库中按照文件名搜索,搜索速度更快
  • /var/lib/mlocate #locate命令所搜索的后台数据库
  • updatedb 更新数据库, 可能是一天更新一次,开机更新一次

whereis与which 搜索命令的命令

  • where 只能搜索系统的命令
    • -b 只查看命令所在位置
    • -m 只差看帮助文档
  • which 只能搜索系统的命令
    • 看命令所在位置,还能查到看别名

find命令

  • find [搜索范围] [搜索条件]

  • find / -name install.log

    • 避免大范围搜索,会非常耗费系统资源
    • find是在系统当中搜索符合条件的文件名。如果需要匹配,通常使用通配符匹配,通配符是完全匹配
  • * 匹配任意字符

  • ? 匹配任意一个字符

  • [] 匹配任意一个中括号内的字符 find -name ab[cd]

  • find /root -inname install.log 就不区分大小写了

  • find /root -user root 按照所有者搜索

  • find /root -nouser 查找没有所有者的文件

  • find /var/log/ -mtime +10 查找10天前修改的文件

    • -10 10天内修改的文件
    • 10 10天当天修改的文件
    • +10 10天前修改的文件
    • atime 文件访问时间
    • ctime 改变文件属性
    • mtime 修改文件内容
  • find . -size 25k 查找文件大小是25k的文件

    • -25k 小于25KB的文件 注意是小写的k,兆字节用大写的M
    • 25k 等于25KB的文件
    • +25k 大于25KB的文件
    • find . -size 25 不加单位就是扇区搜索
  • find /root -inum 262422 查找i节点是262422的文件

  • find /etc -size +20k -a -size -50k

    • 查找/etc/目录下,大于20kb小于50kb的文件
    • -a and 逻辑与,两个条件都满足
    • -0 or 逻辑或,两个条件满足一个即可
  • find /etc -size +20k -a -size -50k -exec ls -lh {} ;

    • 查找/etc/目录下,大于20KB并且小于50KB的文件,并显示详细信息
    • -exec/-ok 命令{} \; 对搜索结果执行操作,"{} \;"为标准格式,只要有-exec就要写它

grep命令

  • grep [选项] 字符串 文件名
    • 在文件档中匹配符合条件的字符串
    • -i 忽略大小写
    • -v 排除指定字符串

find 与 grep区别

  • find命令,在当前系统中搜索符合条件的文件名,如果需要匹配。使用通配符匹配,通配符是完全匹配
  • grep命令:在文件当中搜索符合条件的字符串,如果需要匹配,使用正则表达式进行匹配,正则表达式是包含匹配

帮助命令

  • man帮助等级

    1. 查看命令的帮助
    2. 查看可被内核调用的函数的帮助
    3. 查看函数和函数库的帮助
    4. 查看特殊文件的帮助(主要是/dev目录下的文件)
    5. 查看配置文件的帮助
    6. 查看游戏的帮助
    7. 查看其他杂项的帮助
    8. 查看系统管理员可用命令的帮助
    9. 查看和内核相关文件的帮助
  • man 命令 -> 获取指定命令的帮助 man ls

  • man -f 相当于 whatis 命令

  • man -k 相当于 apropos命令 eg: apropos passwd

  • shell内部命令帮助

    • help 只能获取shell内部命令的帮助 eg: whereis cd. help cd
    • 是安装的软件,就有路径,比如 /bin/ls ,如果 没有比如cd 就是shell内部命令。『whereis cd』
  • info 命令

    • 回车 进入子帮助页面 (带有*号标记)
    • u 上层
    • n 下一帮助小节
    • p 上一帮助小节
    • q 退出

压缩与解压缩命令

.zip .7z .rar .gz .bz2 .tar.gz .tar.bz2等等

.zip格式

  • zip 压缩文件名 源文件名 eg: zip xxx.zip xxx
  • zip -r 压缩目录 源文件
  • unzip 解压缩文件

.gz格式

  • gzip 源文件 压缩为.gz格式的压缩文件,源文件会消失
  • gzip -c 源文件 > 压缩文件
    • 压缩为.gz 格式,源文件保留
    • eg: gzip -c cangls > cangls.gz
  • gzip -r 目录
    • 压缩目录下所有的子文件,但是不能压缩目录
  • gzip -d 解压缩
  • gunzip 解压缩

.bz2格式

  • bzip2 源文件 不保留源文件
  • bzip2 -k 源文件
    • 压缩之后保留源文件

      注意:bzip2命令不能压缩目录

  • bzip2 -d 解压缩 -k保留源文件
  • bunzip2 -k保留源文件

.tar.gz .tar.bz2

先压缩成.tar 再压缩成.gz / .bz2

tar -cvf 打包文件名 源文件

  • -c 打包

  • -v 显示过程

  • -f 指定打包后的文件名

  • -x 解压缩 eg -xvf

  • -t 只查看

  • .tar.gz (z)

    • -z 压缩为.tar.gz格式 tar -zcvf xx.tar.gz 源文件
    • -x 解压缩为.tar.gz格式 tar -zxvf xx.tar.gz
  • .tar.bz2 (j)

    • -j 压缩为.tar.bz2格式 tar -jcvf xx.tar.bz2 源文件
    • -x 解压缩为.tar.bz2格式 tar -jxvf xx.tar.bz2
  • -C 位置

关机和重启命令

shutdown [选项] 时间

  • -c 取消前一个关机命令
  • -h 关机
  • -r 重启

其他关机命令 (非安全,建议shutdown)

  • halt (非安全,建议shutdown)
  • poweroff (非安全,建议shutdown)
  • init 0 (非安全,建议shutdown)

其他重启命令

  • reboot 相对安全
  • init 6 非安全

系统运行级别

查看系统运行级别
cat /etc/inittab 默认级别
runlevel

  • 0 关机
  • 1 单用户
  • 2 不完全多用户,不包含NFS服务
  • 3 完全多用户
  • 4 未分配
  • 5 图形界面
  • 6 重启

其他命令

挂载命令

用户登录查看和用户交互命令

  • mount 查询系统中已经挂载的设备
  • mount -a 依据配置文件/etc/fstab的内容,自动挂载

挂载命令格式

mount [-t 文件系统] [-o 特殊选项] 设备文件名 挂载点

  • -t 文件系统:加入文件系统类型来指定挂载的类型,可以ext3、ext4、iso9660等文件系统
  • -0 特殊选项:可以指定挂载的额外选项
  • #!/bin/bash 最好必须写
  • 默认是可执行 exec.
  • mount -0 remount, noexec /home 重新挂载/boot分区, 并使用noexec权限
  • remount 重新挂载已经挂载的文件系统,一般用于指定修改特殊权限

挂载光盘

  • mkdir /mnt/cdrom/ 建立挂载点
  • mount -t iso9660 /dev/cdrom /mnt/cdrom 挂载光盘
  • mount /dev/sr0 /mnt/cdrom
  • umount /mnt/cdrom 必须卸载

挂载U盘

  • fdisk -i 查看U盘设备文件
  • mount -t vfat /dev/sdb1 /mnt/usb/
  • 注意:Linux默认不支持NTFS文件系统

用户登录查看命令

  • w 用户名 命令输出如下
    • USER: 登录的用户名
    • TTY: 登录终端
    • FROM: 从哪个IP地址登录
    • LOGIN@: 登录时间
    • IDLE: 用户闲置时间
    • JCPU: 指的是和该终端廉洁的所有进程占用的时间。这个时间里并不包括过去的后台作业时间,但却包括当前正在运行的后台作业所占用的时间
    • PCUP:是指当前进程所占用的时间
    • WHAT: 当前正在运行的命令
  • who 用户名
  • last 默认是读取 /var/log/wtmp文件数据,输出如下
    • 用户名
    • 登录终端
    • 登录IP
    • 登录时间
    • 退出时间(在线时间)
  • lastlog 默认是读取/var/log/lastlog文件内容
    • 用户名
    • 登录终端
    • 登录ip
    • 最后一次登录时间

Shell部分

Shell概况

  • 命令行解释器,计算机可不认识命令,只认识0101. 为用户提供了一个向Linux内核发送请求以便运行程序的界面系统级程序,用户可以用Shell来启东、挂起、挺直甚至是编写一个程序
  • SHell还是一个功能相当强大的编译语言,易变写,易调试,灵活性较强。Shell是解释执行的脚本语言,在Sjell中可以直接调用Linux系统命令

Shell的分类

  • Bourne Shell: 从1979起Unix就开始使用Bourne Shell, 主文件名为sh。
  • C Shell: 主要在BSD版的Unix系统中使用,其语言和C语言像相似而得名
  • Shell的两种主要语法类型有Bourne和C,这两种语法彼此不兼容。Bourne家族主要有sh、ksh、Bash、psh、zsh; C家族主要包括csh、tcsh

脚本执行方式

  • echo
  • vi hello.sh (#!/bin/bash) 可以调用系统命令
  • 赋予执行权限,直接运行
    • chmod 755 hello.sh
    • ./hello.sh
  • 通过Bash调用执行脚本
    • bash hello.sh

别名与快捷键

  • alias cp = ‘cp -i’, mv = ‘mv -i’, rm = ‘rm -i’等等,提示,是否xxx?
  • unalias 删除别名
  • vi ~/.bashrc 写入环境变量配置文件
  • source ~/.bashrc 可以让配置重新生效

命令生效顺序

  1. 用绝对路径或相对路径执行的命令
  2. 执行别名
  3. 执行Bash的内部命令
  4. 执行按照$PATH环境变量定义的目录查找顺序找到的第一个命令

快捷键

  • ctrl+c 强制终止当前命令
  • ctrl+l 清屏
  • ctrl+a 光标移动到命令行首
  • ctrl+e 光标移动到命令行尾
  • ctrl+u 从光标所在位置删除到行首
  • ctrl+z 把命令放入后台
  • ctrl+r 在历史命令中搜索

历史命令

  • history [选项] [历史命令保存文件]
  • -c 清空历史命令
  • -w 把缓存中的历史命令写入历史命令保存文件 ~/.bash_history
  • vim /etc/profile
  • 上下剪头调用以前的历史命令
  • !n 重复执行第n条历史命令
  • !! 重复执行上一条命令
  • !字符串 重复执行最后一条以该字符串开头的命令

Bash命令补全

  • tab键

输出重定向

把命令输出保存到文件, > 代表覆盖的形式写入, >>代表追加的方式写入

  • ‘>’覆盖,>> 追加
  • 命令 > 文件 2>&1
  • 命令 >> 文件 2>&1
  • 命令 &> 文件
  • 命令 &>> 文件
  • 命令>>文件1 2>>文件2 把正确的输出追加到文件1,把错误的输出追加到文件2中
  • ls &>/dev/null 相当于既不显示到屏幕上,也不保存

输入重定向

wc [选项] [文件名]

  • -c 统计字节数
  • -w 统计单词数
  • -l 统计行数

管道符

多命令顺序执行

  • ; 命令1:命令2 多个命令顺序执行,命令之间没有任何联系
  • && cmd1 && cmd2 只有当cmd1正确执行 命令cmd2才会执行
  • || cmd1 || cmd2 当命令cmd1不正确的时候,命令2才会执行,正确则不执行

管道符

命令1 | 命令2,命令1的正确输出作为命令2的操作对象

  • ll -a /etc/ | more
  • netstat -an | grep xxx

通配符

  • ? 匹配一个任意字符
  • * 匹配0个或任意多个任意字符,也就是可以匹配任何内容
  • [] 匹配中括号中任意一个字符,例如:[abc]代表一定匹配一个字符串,或者是a,或者b,或者c。
  • [-] 匹配中括号中任意一个字符, -代表一个范围。例如[a-z]代表匹配一个小写字母
  • [^] 逻辑非,代表匹配不是中括号内的一个字符。例如:[^0-9]代表匹配不是一个数字的字符

Bash中其他特殊字符

符号 作用
‘’ 单引号。在单引号中所有的特殊符号,如”$”和”`“(反引号)都 没有特殊含义
“” 双引号。在双引号中特殊字符都没有特殊含义,但是”$”、”`”、和”"是例外,拥有”调用变量的值”、”引用命令”、”转义符”的特殊含义。
`` 反引号。反引号括起来的内容是系统命令,在Bash中会先执行它。和$()作用一样,补货推荐使用$(),因为反引号非常容易看错
$() 和反引号作用一样,用来引用系统命令
# 在Shell脚本中,#开头的行代表注释。
$ 用于调用变量的值,如需要调用变量name的值得时候,需要用$name的方式得到变量的值
\ 转义符,跟在\之后的特殊字符将失去特殊含义,变为普通字符。如$ 将输出”$”符号

动画

AirBnbLottie

Layar的认识

layer 层是涂层绘制、渲染、以及动画的完成者,它无法直接的处理触摸事件(也可以捕捉事件)
layer 包含的方面非常多,常见的属性有 FrameBoundsPositionAnchorPointContents 等等。
想详细了解 CALayer 以及动画的 ,可以看看这本书 - Core-Animation

回溯算法 问题

类型 题目链接
子集、组合 子集、子集 II、组合、组合总和、组合总和 II
全排列 全排列、全排列 II、字符串的全排列、字母大小写全排列
搜索 解数独、单词搜索、N皇后、分割回文串、二进制手表

参考力扣解法
C++
java

解题思路

①递归树
②找结束条件
③找准选择列表
④判断是否需要剪枝
⑤做出选择
⑥撤销选择

总结:可以发现“排列”类型问题和“子集、组合”问题不同在于:“排列”问题使用used数组来标识选择列表,而“子集、组合”问题则使用start参数

1、不用中间变量,用两种方法交换A和B的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1.中间变量
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
}

// 2.加法
void swap(int a, int b) {
a = a + b;
b = a - b;
a = a - b;
}

// 3.异或(相同为0,不同为1. 可以理解为不进位加法)
void swap(int a, int b) {
a = a ^ b;
b = a ^ b;
a = a ^ b;
}

2、求最大公约数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** 1.直接遍历法 */
int maxCommonDivisor(int a, int b) {
int max = 0;
for (int i = 1; i <=b; i++) {
if (a % i == 0 && b % i == 0) {
max = i;
}
}
return max;
}
/** 2.辗转相除法 */
int maxCommonDivisor(int a, int b) {
int r;
while(a % b > 0) {
r = a % b;
a = b;
b = r;
}
return b;
}

// 扩展:最小公倍数 = (a * b)/最大公约数

3、模拟栈操作

  • 栈是一种数据结构,特点:先进后出 -
  • 练习:使用全局变量模拟栈的操作

#include <stdio.h>
#include <stdbool.h>
#include <assert.h>

//保护全局变量:在全局变量前加static后,这个全局变量就只能在本文件中使用
static int data[1024];//栈最多能保存1024个数据
static int count = 0;//目前已经放了多少个数(相当于栈顶位置)

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
//数据入栈 push
void push(int x){
assert(!full());//防止数组越界
data[count++] = x;
}
//数据出栈 pop
int pop(){
assert(!empty());
return data[--count];
}
//查看栈顶元素 top
int top(){
assert(!empty());
return data[count-1];
}

//查询栈满 full
bool full() {
if(count >= 1024) {
return 1;
}
return 0;
}

//查询栈空 empty
bool empty() {
if(count <= 0) {
return 1;
}
return 0;
}

int main(){
//入栈
for (int i = 1; i <= 10; i++) {
push(i);
}

//出栈
while(!empty()){
printf("%d ", top()); //栈顶元素
pop(); //出栈
}
printf("\n");

return 0;
}

4、排序算法

选择排序、冒泡排序、插入排序三种排序算法可以总结为如下:
都将数组分为已排序部分和未排序部分。

1.选择排序将已排序部分定义在左端,然后选择未排序部分的最小元素和未排序部分的第一个元素交换。

2.冒泡排序将已排序部分定义在右端,在遍历未排序部分的过程执行交换,将最大元素交换到最右端。

3.插入排序将已排序部分定义在左端,将未排序部分元的第一个元素插入到已排序部分合适的位置。

4.1、选择排序

  • 【选择排序】:最值出现在起始端
  • 第1趟:在n个数中找到最小(大)数与第一个数交换位置
  • 第2趟:在剩下n-1个数中找到最小(大)数与第二个数交换位置
  • 重复这样的操作…依次与第三个、第四个…数交换位置
  • 第n-1趟,最终可实现数据的升序(降序)排列。
1
2
3
4
5
6
7
8
9
10
11
void selectSort(int *arr, int length) {
for (int i = 0; i < length - 1; i++) { //趟数
for (int j = i + 1; j < length; j++) { //比较次数
if (arr[i] > arr[j]) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
}

4.2、冒泡排序

  • 【冒泡排序】:相邻元素两两比较,比较完一趟,最值出现在末尾
  • 第1趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个推进,最值最后出现在第n个元素位置
  • 第2趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个推进,最值最后出现在第n-1个元素位置
  • …… ……
  • 第n-1趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个推进,最值最后出现在第2个元素位置
1
2
3
4
5
6
7
8
9
10
11
void bublleSort(int *arr, int length) {
for(int i = 0; i < length - 1; i++) { //趟数
for(int j = 0; j < length - i - 1; j++) { //比较次数
if(arr[j] > arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}

#### 5、折半查找(二分查找)

折半查找:优化查找时间(不用遍历全部数据)
折半查找的原理:

  • 1> 数组必须是有序的
  • 2> 必须已知min和max(知道范围)
  • 3> 动态计算mid的值,取出mid对应的值进行比较
  • 4> 如果mid对应的值大于要查找的值,那么max要变小为mid-1
  • 5> 如果mid对应的值小于要查找的值,那么min要变大为mid+1

// 已知一个有序数组, 和一个key, 要求从数组中找到key对应的索引位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int findKey(int *arr, int length, int key) {
int min = 0, max = length - 1, mid;
while (min <= max) {
mid = (min + max) / 2; //计算中间值
if (key > arr[mid]) {
min = mid + 1;
} else if (key < arr[mid]) {
max = mid - 1;
} else {
return mid;
}
}
return -1;
}

6、集合结构 线性结构 树形结构 图形结构

  • 1.1、集合结构 说白了就是一个集合,就是一个圆圈中有很多个元素,元素与元素之间没有任何关系 这个很简单
  • 1.2、线性结构 说白了就是一个条线上站着很多个人。 这条线不一定是直的。也可以是弯的。也可以是值的 相当于一条线被分成了好几段的样子 (发挥你的想象力)。 线性结构是一对一的关系
  • 1.3、树形结构 说白了 做开发的肯定或多或少的知道xml 解析 树形结构跟他非常类似。也可以想象成一个金字塔。树形结构是一对多的关系
  • 1.4、图形结构 这个就比较复杂了。他呢 无穷。无边 无向(没有方向)图形机构 你可以理解为多对多 类似于我们人的交集关系

7、数据结构的存储

数据结构的存储一般常用的有两种 顺序存储结构 和 链式存储结构

  • 2.1 顺序存储结构

发挥想象力啊。 举个列子。数组。1-2-3-4-5-6-7-8-9-10。这个就是一个顺序存储结构 ,存储是按顺序的 举例说明啊。 栈。做开发的都熟悉。栈是先进后出 ,后进先出的形式 对不对 ?!他的你可以这样理解

hello world 在栈里面从栈底到栈顶的逻辑依次为 h-e-l-l-o-w-o-r-l-d 这就是顺序存储 再比如 队列 ,队列是先进先出的对吧,从头到尾 h-e-l-l-o-w-o-r-l-d 就是这样排对的

  • 2.2 链式存储结构

再次发挥想象力 这个稍微复杂一点 这个图片我一直弄好 ,回头找美工问问,再贴上 例如 还是一个数组

1-2-3-4-5-6-7-8-9-10 链式存储就不一样了 1(地址)-2(地址)-7(地址)-4(地址)-5(地址)-9(地址)-8(地址)-3(地址)-6(地址)-10(地址)。每个数字后面跟着一个地址 而且存储形式不再是顺序 ,也就说顺序乱了,1(地址) 1后面跟着的这个地址指向的是2,2后面的地址指向的是3,3后面的地址指向是谁你应该清楚了吧。他执行的时候是 1(地址)-2(地址)-3(地址)-4(地址)-5(地址)-6(地址)-7(地址)-8(地址)-9(地址)-10(地址),但是存储的时候就是完全随机的。明白了?!

8、单向链表\双向链表\循环链表

还是举例子。理解最重要。不要去死记硬背 哪些什么。定义啊。逻辑啊。理解才是最重要滴

  • 3.1 单向链表

A->B->C->D->E->F->G->H. 这就是单向链表 H 是头 A 是尾 像一个只有一个头的火车一样 只能一个头拉着跑

  • 3.2 双向链表

数组和链表区别:
数组:数组元素在内存上连续存放,可以通过下标查找元素;插入、删除需要移动大量元素,比较适用于元素很少变化的情况
链表:链表中的元素在内存中不是顺序存储的,查找慢,插入、删除只需要对元素指针重新赋值,效率高

  • 3.3 循环链表

循环链表是与单向链表一样,是一种链式的存储结构,所不同的是,循环链表的最后一个结点的指针是指向该循环链表的第一个结点或者表头结点,从而构成一个环形的链。发挥想象力 A->B->C->D->E->F->G->H->A. 绕成一个圈。就像蛇吃自己的这就是循环 不需要去死记硬背哪些理论知识。

9、二叉树/平衡二叉树

  • 4.1 什么是二叉树

树形结构下,两个节点以内 都称之为二叉树 不存在大于2 的节点 分为左子树 右子树 有顺序 不能颠倒 ,懵逼了吧,你肯定会想这是什么玩意,什么左子树右子树 ,都什么跟什么鬼? 现在我以普通话再讲一遍,你把二叉树看成一个人 ,人的头呢就是树的根 ,左子树就是左手,右子树就是右手,左右手可以都没有(残疾嘛,声明一下,绝非歧视残疾朋友,勿怪,勿怪就是举个例子,i am very sorry) , 左右手呢可以有一个,就是不能颠倒。这样讲应该明白了吧

二叉树有五种表现形式

1.空的树(没有节点)可以理解为什么都没 像空气一样
2.只有根节点。 (理解一个人只有一个头 其他的什么都没,说的有点恐怖)
3.只有左子树 (一个头 一个左手 感觉越来越写不下去了)
4.只有右子树
5.左右子树都有

二叉树可以转换成森林 树也可以转换成二叉树。这里就不介绍了 你做项目绝对用不到
数据结构大致介绍这么多吧。理解为主, 别死记,死记没什么用

Block的几种形式

分为全局Block(_NSConcreteGlobalBlock)、栈Block(_NSConcreteStackBlock)、堆Block
(_NSConcreteMallocBlock)三种形式
其中栈Block存储在栈(stack)区,堆Block存储在堆(heap)区,全局Block存储在已初始化数据(.data)区

1、不使用外部变量的block是全局block

比如:

1
2
3
NSLog(@"%@",[^{
NSLog(@"globalBlock");
} class]);

输出:

1
__NSGlobalBlock__

2、使用外部变量并且未进行copy操作的block是栈block

比如:

1
2
3
4
NSInteger num = 10;
NSLog(@"%@",[^{
NSLog(@"stackBlock:%zd",num);
} class]);

输出:

1
__NSStackBlock__

日常开发常用于这种情况:

1
2
3
4
5
6
7
8
9
[self testWithBlock:^{
NSLog(@"%@",self);
}];

- (void)testWithBlock:(dispatch_block_t)block {
block();

NSLog(@"%@",[block class]);
}

3、对栈block进行copy操作,就是堆block,而对全局block进行copy,仍是全局block

  • 比如堆1中的全局进行copy操作,即赋值:
1
2
3
4
5
void (^globalBlock)(void) = ^{
NSLog(@"globalBlock");
};

NSLog(@"%@",[globalBlock class]);

输出:

1
__NSGlobalBlock__

仍是全局block

  • 而对2中的栈block进行赋值操作:
1
2
3
4
5
6
7
8
NSInteger num = 10;

void (^mallocBlock)(void) = ^{

NSLog(@"stackBlock:%zd",num);
};

NSLog(@"%@",[mallocBlock class]);

输出:

1
__NSMallocBlock__

对栈blockcopy之后,并不代表着栈block就消失了,左边的mallock是堆block,右边被copy的仍是栈block
比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
[self testWithBlock:^{

NSLog(@"%@",self);
}];

- (void)testWithBlock:(dispatch_block_t)block
{
block();

dispatch_block_t tempBlock = block;

NSLog(@"%@,%@",[block class],[tempBlock class]);
}

输出:

1
__NSStackBlock__,__NSMallocBlock__
  • 即如果对栈Block进行copy,将会copy到堆区,对堆Block进行copy,将会增加引用计数,对全局Block进行copy,因为是已经初始化的,所以什么也不做。

另外,__block变量在copy时,由于__forwarding的存在,栈上的__forwarding指针会指向堆上的__forwarding变量,而堆上的__forwarding指针指向其自身,所以,如果对__block的修改,实际上是在修改堆上的__block变量。

即__forwarding指针存在的意义就是,无论在任何内存位置,都可以顺利地访问同一个__block变量

  • 另外由于block捕获的__block修饰的变量会去持有变量,那么如果用__block修饰self,且self持有block,并且block内部使用到__block修饰的self时,就会造成多循环引用,即self持有block,block 持有__block变量,而__block变量持有self,造成内存泄漏。
    比如:
1
2
3
4
5
6
7
8
__block typeof(self) blockSelf = self;

_testBlock = ^{

NSLog(@"%@",blockSelf);
};

_testBlock();

如果要解决这种循环引用,可以主动断开__block变量对self的持有,即在block内部使用完blockSelf后,将其置为nil,但这种方式有个问题,如果block一直不被调用,那么循环引用将一直存在。
所以,我们最好还是用__weak来修饰self

Clang ARC的文档

autoReleasePool 什么时候释放?

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()

第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observerorder2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

Obj-C 中,如何检测内存泄漏?你知道哪些方式?

目前我知道的方式有以下几种

  • Memory Leaks
  • Alloctions
  • Analyse
  • Debug Memory Graph
  • MLeaksFinder

泄露的内存主要有以下两种:

  • Laek Memory 这种是忘记 Release 操作所泄露的内存。
  • Abandon Memory 这种是循环引用,无法释放掉的内存。

上面所说的五种方式,其实前四种都比较麻烦,需要不断地调试运行,第五种是腾讯阅读团队出品,效果好一些,感兴趣的可以看一下这两篇文章:

  • MLeaksFinder:精准 iOS 内存泄露检测工具
  • MLeaksFinder 新特性