feature: APM
@@ -162,13 +162,13 @@ typedef NS_ENUM(NSUInteger, OrderSubmitReminderType) {
|
|||||||
|
|
||||||
### 责任链设计模式
|
### 责任链设计模式
|
||||||
|
|
||||||
责任链模式即 Chain Of Responsibility,属于行为型模式。行为型模式不仅秒死对象或类的模式,还描述他们之间的通信模式,比如对操作的处理该如何传递等等。
|
责任链模式即 Chain Of Responsibility,属于行为型模式。行为型模式不仅描述对象或类的模式,还描述他们之间的通信模式,比如对操作的处理该如何传递等等。
|
||||||
|
|
||||||
为什么会有这个思路?
|
为什么会有这个思路?
|
||||||
|
|
||||||
主要来源于2个方向:Node 的洋葱模式、移动端的点击事件传递。
|
主要来源于2个方向:Node 的洋葱模式、移动端的点击事件传递。
|
||||||
|
|
||||||
移动端的事件响应模型:点击 view 看看能不能响应,不能响应则继续向上抛,知道抛到 window 为止;
|
移动端的事件响应模型:点击 view 看看能不能响应,不能响应则继续向上抛,直到抛到 window 为止;
|
||||||
|
|
||||||
前端 JS 事件冒泡机制:点击事件假设是动态绑定到 DOM 节点上的,浏览器本身不知道哪些地方会处理点击事件,但又要让每层 DOM 拥有对该点击事件的平等处理权,所以就诞生了事件冒泡和组织冒泡的能力 `event.stopPropagation()`
|
前端 JS 事件冒泡机制:点击事件假设是动态绑定到 DOM 节点上的,浏览器本身不知道哪些地方会处理点击事件,但又要让每层 DOM 拥有对该点击事件的平等处理权,所以就诞生了事件冒泡和组织冒泡的能力 `event.stopPropagation()`
|
||||||
|
|
||||||
@@ -425,7 +425,7 @@ App 动态请求,然后执行业务逻辑。需思考一些问题:
|
|||||||
|
|
||||||
- 网络请求慢怎么处理?
|
- 网络请求慢怎么处理?
|
||||||
- 需不需要缓存?
|
- 需不需要缓存?
|
||||||
- 有缓存的花,更新策略是什么?
|
- 有缓存的话,更新策略是什么?
|
||||||
- 需不需要内置的产品逻辑?
|
- 需不需要内置的产品逻辑?
|
||||||
|
|
||||||
当然,这不在本篇文章范畴内,不做展开。
|
当然,这不在本篇文章范畴内,不做展开。
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
> 端上内存寸土寸金,对于内存知识你掌握了吗?掌握内存分配、释放的细节有助于我们写出内存使用有好的代码?同时为做 APM 内存监控打下坚实基础。接下来通过下面几个问题,研究下 iOS 侧内存分配、释放的相关知识:
|
> 端上内存寸土寸金,对于内存知识你掌握了吗?掌握内存分配、释放的细节有助于我们写出内存使用有好的代码?同时为做 APM 内存监控打下坚实基础。接下来通过下面几个问题,研究下 iOS 侧内存分配、释放的相关知识:
|
||||||
>
|
>
|
||||||
|
> - 虚拟内存是什么、为什么需要分页?
|
||||||
> - NSTimer 存在什么问题、CADisplayLink 存在什么问题?
|
> - NSTimer 存在什么问题、CADisplayLink 存在什么问题?
|
||||||
> - weak 指针的实现原理是什么?
|
> - weak 指针的实现原理是什么?
|
||||||
> - ARC 帮我们做了什么处理?
|
> - ARC 帮我们做了什么处理?
|
||||||
@@ -12,6 +13,83 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 虚拟内存是什么、为什么需要分页
|
||||||
|
|
||||||
|
在早期的计算机中,程序是直接运行在物理内存上的,也就是说:程序在运行时访问的地址就是物理地址。这样也就是单运行的时候没有什么问题!可是,计算机会有多到程序、分时系统和多任务,当我们能够同时运行多个程序时,`CPU`的利用率将会比较高。那么有一个非常严重的问题:**如何将计算机的有限的物理内存分配给多个程序使用**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
假设我们计算有`128MB`内存,程序A需要`10MB`,程序B需要`100MB`,程序C需要`20MB`。如果我们需要同时运行程序A和B,那么比较直接的做法是将内存的`前10MB`分配给程序A,`10MB~110MB`分配给B。
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/IOSMemoryWithPhysisMemory.png" style="zoom:20%" />
|
||||||
|
|
||||||
|
但存在以下问题:
|
||||||
|
|
||||||
|
- 当多个程序需要运行时,必须保证这些程序用到的内存总量要小于计算机实际的物理内存的大小。
|
||||||
|
- 安全性低。进程地址空间不隔离,由于程序是直接访问物理内存的,所以每一个进程都可以修改其他进程的内存数据,设置修改内核地址空间中的数据,所以有些恶意程序可以随意修改别的进程,就会造成一些破坏
|
||||||
|
- 内存使用效率低。内存空间不足,就需要将其他程序展示拷贝到硬盘当中,然后将新的程序装入内存。然而由于大量的数据装入装出,内存的使用效率会非常低
|
||||||
|
- 程序运行的地址不确定。因为内存地址是随机分配的,所以程序运行的地址也是不正确的
|
||||||
|
|
||||||
|
计算机世界中的问题,大多可以用增加中间层的方式解决。即使用一种间接的地址访问方式。
|
||||||
|
|
||||||
|
把程序给出的地址看做是一种**虚拟地址**,然后通过某种映射,将这个虚拟地址转化到实际的物理地址。这样,只需要控制好映射过程,就能保证程序所能访问的物理内存区域跟别的程序不重叠,达到空间隔离的效果。
|
||||||
|
|
||||||
|
### 隔离
|
||||||
|
|
||||||
|
普通的程序它只需要一个简单的执行环境,一个单一的地址空间,有自己的CPU。
|
||||||
|
地址空间比较抽象,如果把它想象成一个数组,每一个数组是一字节,数组大小就是地址空间的长度,那么32位的地址空间大小就是`2^32=4294967296`字节,即`4G`,地址空间有效位是 `0x00000000~0xFFFFFFFF`。
|
||||||
|
地址空间分为两种:
|
||||||
|
|
||||||
|
- 物理空间:就是物理内存。32 位的机器,地址线就有 32条,物理空间 4G,但如果只装有 512M 的内存,那么实际有效的空间地址就是 `0x00000000~0x1FFFFFFF`,其他部分都是无效的。
|
||||||
|
- 虚拟空间:每个进程都有自己独立的虚拟空间,而且每个进程只能访问自己的空间地址,这样就有效的做到了进程隔离。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 分段
|
||||||
|
|
||||||
|
**基本思路:** 把一段与程序所需要的内存空间大小的虚拟空间映射到某个地址空间。虚拟空间的每个字节对应物理空间的每个字节。这个映射过程由软件来完成。
|
||||||
|
|
||||||
|
比如A需要`10M`,就假设有`0x00000000` 到`0x00A00000`大小的虚拟空间,然后从物理内存分配一个相同大小的空间,比如是`0x00100000`到`0x00B00000`。操作系统来设置这个映射函数,实际的地址转换由硬件完成。如果越界,硬件就会判断这是一个非法访问,拒绝这个地址请求,并上报操作系统或监控程序。
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/iOSMemoryParagraph.png" style="zoom:20%" />
|
||||||
|
|
||||||
|
这样一来利用**分段**的方式可以解决之前的**地址空间不隔离**和**程序运行地址不确定**
|
||||||
|
|
||||||
|
首先做到了地址隔离,因为A和B被映射到了两块不同的物理空间,它们之间没有任何重叠,如果A访问虚拟空间的地址超过了`0x00A00000`这个范围,硬件就会判断这是一个非法的访问,并将这个请求报告给操作系统或者监控程序,由它决定如何处理。
|
||||||
|
|
||||||
|
再者,对于每个程序来说,无论它们被分配到地址空间的哪一个区域,对于程序来说都是透明的,它们不需要关心物理地址的变化,它们只要按照从地址`0x00000000`到`0x00A00000`来编写程序、放置变量,所以程序不需要重定位。
|
||||||
|
|
||||||
|
第二问题内存使用效率问题依旧没有解决。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
但是分段的方法没有解决内存使用效率的问题。**分段对于内存区域的映射还是按照程序为单位,如果内存不足,被换入换出的磁盘的都是整个程序,这样势必会造成大量的磁盘访问操作,从而严重影响速度,这种方法还是显得粗糙,粒度比较大。**事实上根据程序的局部性原理,当一个程序正在运行时,在某个时间段内,它只是频繁用到了一小部分数据,也就是说,程序的很多数据其实在一个时间段内是不会被用到的。人们很自然地想到了更小粒度的内存分割和映射方法,使得程序的局部性原理得到充分利用,大大提高了内存的使用率。这种方法就是**分页。**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 分页
|
||||||
|
|
||||||
|
**分页的基本方法是把地址空间人为得等分成固定大小的页,每一个页的大小由硬件决定,或硬件支持多种页的大小,由操作系统选择决定页的大小。** 目前几乎所有PC的操作系统都是用`4KB`大小的页。我们使用的PC机是32位虚拟地址空间,也就是`4GB`,按`4KB`分页,总共有`1048576`个页。
|
||||||
|
|
||||||
|
那么,当我们把进程的虚拟地址空间按页分割,把常用的数据和代码装载到内存中,把不常用的代码和数据保存在磁盘里,当需要用到的时候再把它们从磁盘里取出即可。图中的线表示映射关系,我们可以看到虚拟空间有些页被映射到同一个**物理页**,这样就可以实现内存共享。
|
||||||
|
**虚拟页,物理页,磁盘页**根据内存空间不一样而区分
|
||||||
|
|
||||||
|
我们可以看到Process 1 的VP2和VP3不在内存中,但是当进程需要用到这两个页的时候,硬件就会捕获到这个消息,就是所谓的**页错误(Page Fault)**,然后操作系统接管进程,负责将VP2和VP3从磁盘读取出来装入内存,然都将内存中的这两个页和VP2和VP3建立映射关系。以页为单位存取和交换数据非常方便,硬件本身就支持这种以页为单位的操作方式。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/iOSMemoryPage.png" style="zoom:20%" />
|
||||||
|
|
||||||
|
保护页也是页映射的目的之一,简单地说就是每个页可以设置权限属性,谁可以修改,谁可以访问,而且只有操作系统有权修改这些属性,那么操作系统就可以做到保护自己和保护进程。
|
||||||
|
|
||||||
|
虚拟存储的实现需要硬件支持,几乎所有CPU都采用称为**MMU的部件来进行页的映射**
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/iOSVirtualMemoryVisitPhysisMemoryViaMMU.png" style="zoom:40%" />
|
||||||
|
|
||||||
|
在页映射模式下,`CPU`发出的是`Virtual Address`,即我们程序看到的是`虚拟地址`。经过`MMU`转换以后就变成了`Physical Address`。一般`MMU`集成在`CPU`内部,不会以独立的部件存在。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 定时器内存泄漏
|
## 定时器内存泄漏
|
||||||
|
|
||||||
NSTimer、CADisplayLink 的 基础 API `[NSTimer scheduledTimersWithTimeInterval:1 repeat:YES block:nil]` 对其 target 会产生强引用,如果 target 再对其产生强引用,则互相持有,会造成环,产生内存泄漏
|
NSTimer、CADisplayLink 的 基础 API `[NSTimer scheduledTimersWithTimeInterval:1 repeat:YES block:nil]` 对其 target 会产生强引用,如果 target 再对其产生强引用,则互相持有,会造成环,产生内存泄漏
|
||||||
|
|||||||
@@ -7778,7 +7778,149 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
|
|||||||
|
|
||||||
// todo? 参考前端资源模块化,如何实现资源预加载(全局 LRU或者基于用户行为路径的预热功能,比如某个收银员角色固定的情况下,他日常的操作行为是固定的,商品扫码、开单)
|
// todo? 参考前端资源模块化,如何实现资源预加载(全局 LRU或者基于用户行为路径的预热功能,比如某个收银员角色固定的情况下,他日常的操作行为是固定的,商品扫码、开单)
|
||||||
|
|
||||||
#### 2. Flutter 异常监控
|
|
||||||
|
|
||||||
|
### Flutter 异常监控
|
||||||
|
|
||||||
|
Flutter 异常指的是,Flutter 程序中 Dart 代码运行时意外发生的错误事件。我们可以通过与 Java 类似的 try-catch 机制来捕获它。但与 Java 不同的是,Dart 程序不强制要求我们必须处理异常。
|
||||||
|
|
||||||
|
这是由于 Flutter 本身是 Dart 事件循环机制来运行任务的。各个任务的运行状态是相互独立的。即使某个任务出现了异常,Dart 也不会退出,只会导致当前任务的后续代码不会执行,用户还可以继续使用其他功能。
|
||||||
|
|
||||||
|
Dart 异常可以分为 App 异常和 Framework 异常,分别处理。
|
||||||
|
|
||||||
|
#### App 异常监控
|
||||||
|
|
||||||
|
App 异常,就是应用代码的异常,通常由未处理应用层其他模块所抛出的异常引起。根据异常代码的执行时序,App 异常可以分为两类:
|
||||||
|
|
||||||
|
- 同步异常:可以通过 try-catch 机制捕获
|
||||||
|
- 异步异常:需要采用 Future 提供的 catchError 语句捕获
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// 使用 try-catch 捕获同步异常
|
||||||
|
try {
|
||||||
|
throw StateError('This is a Dart exception.');
|
||||||
|
} catch(e) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 catchError 捕获异步异常
|
||||||
|
Future.delayed(Duration(seconds: 1))
|
||||||
|
.then((e) => throw StateError('This is a Dart exception in Future.'))
|
||||||
|
.catchError((e)=>print(e));
|
||||||
|
```
|
||||||
|
|
||||||
|
需要注意的是,这两种方式是不能混用的。可以看到,在上面的代码中,我们是无法使用 try-catch 去捕获一个异步调用所抛出的异常的
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// try...catch...无法捕获异步异常
|
||||||
|
try {
|
||||||
|
Future.delayed(Duration(seconds: 1))
|
||||||
|
.then((e) => throw StateError('This is a Dart exception in Future.'))
|
||||||
|
} catch(e) {
|
||||||
|
print("This line will never be executed. ");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
但这适合写业务代码的时候分散处理,如果做 APM,需要一种统一收口的方式监控异常,该怎么办呢?
|
||||||
|
|
||||||
|
Flutter 提供了 `Zone.runZoned` 方法,给代码执行对象指定一个 Zone,在 Dart 中,Zone 表示一个代码执行的环境范围,其概念类似沙盒,不同沙盒之间是互相隔离的。如果我们想要观察沙盒中代码执行出现的异常,沙盒提供了 onError 回调函数,拦截那些在代码执行对象中的未捕获异常。
|
||||||
|
|
||||||
|
可以将代码都写到 Zone 里,即使没有使用 `try...catch...` 和 `catchError` ,任何同步、异步异常也都被 Zone 捕获到
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// 同步异常
|
||||||
|
runZoned(() {
|
||||||
|
throw StateError('This is a Dart exception.');
|
||||||
|
}, onError: (dynamic e, StackTrace stack) {
|
||||||
|
print('Sync error caught by zone');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 异步异常
|
||||||
|
runZoned(() {
|
||||||
|
Future.delayed(Duration(seconds: 1))
|
||||||
|
.then((e) => throw StateError('This is a Dart exception in Future.'));
|
||||||
|
}, onError: (dynamic e, StackTrace stack) {
|
||||||
|
print('Async error aught by zone');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
如何收口?
|
||||||
|
|
||||||
|
要集中捕获 Flutter 应用中的未处理异常,可以把 main 函数中的 runApp 语句也放置在 Zone 中。这样在检测到代码中运行异常时,我们就能根据获取到的异常上下文信息,进行统一处理。
|
||||||
|
|
||||||
|
```dart
|
||||||
|
runZoned<Future<Null>>(() async {
|
||||||
|
runApp(MyApp());
|
||||||
|
}, onError: (error, stackTrace) async {
|
||||||
|
// 异常数据收集,上报 APM
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Framework 异常监控
|
||||||
|
|
||||||
|
Framework 异常,就是 Flutter 框架引发的异常,通常是由应用代码触发了 Flutter 框架底层的异常判断引起的。比如,当布局不合规范时,Flutter 就会自动弹出一个红色错误界面。
|
||||||
|
|
||||||
|
这是由于 Flutter 框架在调用 build 方法构建页面时进行了 `try...catch...` 处理,并提供了一个 `ErrorWidget`,用于组建渲染错误的时候进行信息展示
|
||||||
|
|
||||||
|
```dart
|
||||||
|
@override
|
||||||
|
void performRebuild() {
|
||||||
|
Widget built;
|
||||||
|
try {
|
||||||
|
// 创建页面
|
||||||
|
built = build();
|
||||||
|
} catch (e, stack) {
|
||||||
|
// 使用 ErrorWidget 创建页面,展示错误信息
|
||||||
|
built = ErrorWidget.builder(_debugReportException(ErrorDescription("building $this"), e, stack));
|
||||||
|
...
|
||||||
|
}
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
该方案适合在开发阶段定位问题。但如果让用户看到这样一个页面,就很糟糕。因此,通常会重写 `ErrorWidget.builder` 方法,将这样的错误提示页面替换成一个更加友好的页面。
|
||||||
|
|
||||||
|
如何重写?
|
||||||
|
|
||||||
|
```dart
|
||||||
|
ErrorWidget.builder = (FlutterErrorDetails flutterErrorDetails){
|
||||||
|
return Scaffold(
|
||||||
|
body: Center(
|
||||||
|
// ... UI 描述
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`ErrorWidget.builder`方法提供了一个参数 `FlutterErrorDetails` 用于表示当前的错误上下文,可以将异常信息上报 APM,用于后续分析异常原因。
|
||||||
|
|
||||||
|
为了集中处理框架异常,Flutter 提供了 `FlutterError` 类,这个类的 `onError` 属性会在接收到框架异常时执行相应的回调。因此,要实现自定义捕获逻辑,我们只要为它提供一个自定义的错误处理回调即可。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 如何收口
|
||||||
|
|
||||||
|
App 异常和 Framework 异常都存在,写2个口子收集也可以。但 Flutter 提供了更加友好的口子。使用 Zone 提供的 `handleUncaughtError` 语句,将 Flutter 框架的异常统一转发到当前的 Zone 中,这样我们就可以统一使用 Zone 去处理应用内的所有异常了
|
||||||
|
|
||||||
|
```dart
|
||||||
|
FlutterError.onError = (FlutterErrorDetails details) async {
|
||||||
|
// Framework 异常转发至 Zone 中
|
||||||
|
Zone.current.handleUncaughtError(details.exception, details.stack);
|
||||||
|
};
|
||||||
|
|
||||||
|
runZoned<Future<Null>>(() async {
|
||||||
|
// App 异常本身就在 Zone 的 onError 中收集
|
||||||
|
runApp(MyApp());
|
||||||
|
}, onError: (error, stackTrace) async {
|
||||||
|
//Do sth for error
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
异常信息收集了,走 Native 的数据聚合上报策略。用于后续的异常问题自动定位和告警机制。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 十、子线程 UI 监控
|
## 十、子线程 UI 监控
|
||||||
|
|
||||||
|
|||||||
BIN
assets/DispatchGroupCreateSourceCode.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
assets/DispatchGroupEnterSourceCode.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
assets/DispatchGroupTAllocObject.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
assets/DispatchGroupTSourceLocation.png
Normal file
|
After Width: | Height: | Size: 535 KiB |
BIN
assets/DispatchableThreadPool.png
Normal file
|
After Width: | Height: | Size: 513 KiB |
BIN
assets/IOSMemoryWithPhysisMemory.png
Normal file
|
After Width: | Height: | Size: 629 KiB |
BIN
assets/ThreadLifeCycle.png
Normal file
|
After Width: | Height: | Size: 574 KiB |
BIN
assets/iOSMemoryPage.png
Normal file
|
After Width: | Height: | Size: 878 KiB |
BIN
assets/iOSMemoryParagraph.png
Normal file
|
After Width: | Height: | Size: 646 KiB |
BIN
assets/iOSVirtualMemoryVisitPhysisMemoryViaMMU.png
Normal file
|
After Width: | Height: | Size: 70 KiB |