update: image source

This commit is contained in:
杭城小刘
2024-02-23 15:58:55 +08:00
parent fb51939f76
commit 6e47061735
22 changed files with 783 additions and 61 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -1,7 +1,7 @@
## 写给 iOSer 的鸿蒙开发 tips
## 下载问题
![](./../assets/DevEco-Studio-DownloadNetworkErrror.png)
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/DevEco-Studio-DownloadNetworkErrror.png)
The other possible cause is that the system language of the PC is English and the region code is US. You could try to perform the following operations to change the region code to CN. Before changing the region code, close DevEco Studio.

4
Chapter1 - iOS/1.112.md Normal file
View File

@@ -0,0 +1,4 @@
# 虚拟内存
- 进程隔离的必要性
- 虚拟内存是如何实现进程隔离的
- 线性地址和物理地址是如何转换的

View File

@@ -10,7 +10,7 @@ NSTimer、CADisplayLink 的 基础 API `[NSTimer scheduledTimersWithTimeInterval
栈、堆、BSS、数据段、代码段
![](./../assets/iOS-MemoryLayout.png)
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/iOS-MemoryLayout.png)
stack又称作堆栈用来存储程序的局部变量但不包括static声明的变量static修饰的数据存放于数据段中。除此之外在函数被调用时栈用来传递参数和返回值。栈内存地址越来越少
@@ -37,7 +37,7 @@ BSS段bss segment通常用来存储程序中未被初始化的全局变
代码段code segment编译之后的代码。通常是指用来存储程序可执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定并且内存区域通常属于只读某些架构也允许代码段为可写即允许修改程序。
![内存](./../assets/ram.png)
![内存](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ram.png)
上 Demo 验证
@@ -147,7 +147,7 @@ Demo1
运行该代码会 Crash报错信息如下
![](./../assets/TaggedPointerCrash.png)
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/TaggedPointerCrash.png)
说明:一开始的报错信息只说坏内存访问,但是并没有显示具体的方法调用堆。想知道具体 Crash 原因还是需要看看堆栈比较方便。输入 bt 查看最后是由于 `objc_release` 方法造成 crash。
@@ -257,7 +257,7 @@ static inline bool _objc_isTaggedPointer(const void * _Nullable ptr)
tips某些对象虽然是 TaggedPointer 类型,但是打印 class 发现不是,猜测可能是系统用类簇隐藏了某些实现细节。比如下面
![](./../assets/NSTaggedPointerOfNSNumber.png)
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/NSTaggedPointerOfNSNumber.png)
针对 NSNumber 的 TaggedPoniter 的 case查看 class 打印出 `__NSCFNumber`。但根据源码和内存高地址位分析确实是 TaggedPoniter。
@@ -826,7 +826,7 @@ void sel_init(size_t selrefCount){
在 gone 处加断点,利用 runtime 查看类中的方法信息
![](./../assets/cxx_destructDemo1.png)
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/cxx_destructDemo1.png)
发现存在 `.cxx_destruct` 方法。
@@ -861,7 +861,7 @@ void sel_init(size_t selrefCount){
@end
```
![](./../assets/cxx_destructdemo3.png)
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/cxx_destructdemo3.png)
Tips@property 会自动生成成员变量,另外类后面加 `{}` 在内部也可以加成员变量,假如成员变量是对象类型,比如 NSString则叫实例变量。
@@ -875,7 +875,7 @@ Tips@property 会自动生成成员变量,另外类后面加 `{}` 在内部
在 gone 的地方加断点,输入 `watchpoint set variable p->_name`,则会将 `_name` 实例变量加入 watchpoint当变量被修改时会触发断点可以看出从某个值变为 0x0也就是 nil。此时边上调用堆栈显示在 `objc_storestrong` 方法中,被设置为 nil.
![](./../assets/cxx_destructDemo2.png)
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/cxx_destructDemo2.png)
### 深入 .cxx_destruct
@@ -1123,7 +1123,7 @@ class AutoreleasePoolPage {
- 每个 AutoreleasePoolPage 对象占用 4096 字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放 autorelease 对象的地址
- 所有的 AutoreleasePoolPage 对象通过**双向链表**的形式连接在一起。child 指向下一个对象parent 指向上一个对象
![](./../assets/autoreleasepool.png)
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/autoreleasepool.png)
```objectivec
id * begin() {
@@ -1181,7 +1181,7 @@ int main(int argc, const char * argv[]) {
main 方法内部3个 autoreleasepool 底层怎么样工作的?
![](./../assets/AutoreleasePoolMoreItem.png)
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/AutoreleasePoolMoreItem.png)
3个@auto releasepool 系统遇到第一个的时候底层就是初始化一个结构体 `__AtAutoreleasePool`,结构体构造方法内部调用 `AutoreleasePoolPage::push` 方法,系统给 AutoreleasePoolPage 真正保存 autorelease 对象的地方存储进一个 `POOL_BOUNDARY` 对象,然后储存 P1、P2 对象地址,遇到第二个则继续初始化结构体,调用 push 方法,存储一个` POOL_BOUNDARY` 对象,继续保存 P3遇到第三个则继续初始化结构体调用 push 方法,存储一个 `POOL_BOUNDARY` 对象,继续保存 P4。
@@ -1963,7 +1963,7 @@ static inline id *autoreleaseFast(id obj) {
每当进行一次`objc_autoreleasePoolPush`调用时runtime 向当前的 AutoreleasePoolPage 中 add 进一个`哨兵对象`值为0也就是个nil那么这一个page就变成了下面的样子
![](./../assets/autoreleasepool-push.png)
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/autoreleasepool-push.png)
`objc_autoreleasePoolPush`的返回值正是这个哨兵对象的地址,被`objc_autoreleasePoolPop(哨兵对象)`作为入参,于是:
@@ -1981,7 +1981,7 @@ iOS 在主线程的 Runloop 中注册了2个 Observer
结合 RunLoop 运行图
![](./../assets/RunLoop-SourceCode.png)
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/RunLoop-SourceCode.png)
- 01 通知 Observer 进入 Loop 会调用 `objc_autoreleasePoolPush`
@@ -2070,7 +2070,7 @@ NSHashMap、NSMapTable 都可以描述 key、value 的内存修饰。
这段代码运行会 crash信息如下
![](./../assets/NSErrorZombieCrash.png)
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/NSErrorZombieCrash.png)
原因是 NSError 构造方法内部会加 autorelease。源码如下
@@ -2133,7 +2133,7 @@ MRC 下的 `[(id)(object) autorelease]` 等价于 ARC 下的 `id __autoreleasing
我写了个僵尸对象检测工具,效果如下
![](./../assets/ZombieSniffer.png)
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ZombieSniffer.png)
可以定位僵尸对象,并且打印出具体堆栈,并模拟系统行为调用 `abort` 。对监控原理和工具实现感兴趣的可以查看这里[带你打造一套 APM 监控系统-内存监控之野指针/内存泄漏监控](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.74.md#zombieSniffer)

View File

@@ -1,5 +1,604 @@
# React Native 总结
## 一、为什么React/React Native 采用组件式的设计?
除了组件式之外,常见的构建方式还有类似 HTML + CSS + JS 分层的架构、基于 MVC 的架构,那为什么会采用组件的架构设计?
- 随着前端/客户端等展示层 UI 交互承载的业务逻辑越来越复杂,单纯的 MVC 会面临「**胖 Model**」、「**巨大 Controller**」等情况
- 业务千变万化,昨日的设计无法支撑下个版本的产品调性,但写过的代码还是有用的,直接 copy 过去?
- 设计部门也在千变万化的版本迭代中沉淀公司自己的设计规范、交互规范、Android、iOS 各个平台的人机交互指南,基于此也沉淀产出了 UI 组件库。那端上同学必然基于组件式去写一套 UI 组件代码。
- 组件化的风慢慢的吹到了 RN 诞生之初2010年开源的 MVC 模式的 AngularJS 也被最新的 Angular>= V2这种基于组件的架构模式所取代。组件式影响到新诞生的一批框架
React/React Native 选择基于组件的架构模式好处有3:
- 第一,组件是内聚的,组件内既有逻辑,也有状态,又有视图。一个组件可以独立完成一个事情,这也使得 UI 模块复用变得简单;
- 第二,组件之间是可以组合的,多个组件可以组合成一个更大的页面或者一个更大的组件。当一个组件很大很臃肿、难以维护的时候也可以拆分优化成粒度更合理的组件
- 第三,组件和组件之间的数据流动永远是确定的,从上到下单向流动
组件可组合、可复用的特性,和组件之间单向数据流的模式,是现代应用重交互重展示的情况下,更方便,这也是 React/React Native 采用组件式的核心原因。
## 二、热更新平台
热更新能力是大家选择 RN 的一个重要原因之一,有了热更新能力,就相当于在用户手机和公司业务之间铺设了一道直达的高速公路,公司新业务开发后不再受限于 App 发版审核下载更新这个流程了。那么如何设计一个热更新平台呢?
业界主流的有:
- Code Push是微软 App Center 的服务之一。底层是微软自家的 Azure 云服务。由于国内网络环境的原因,访问国外云服务较慢,不推荐使用
- Expo是亚马逊的 AWS 和 Google Cloud 云服务。由于国内网络环境的原因,访问国外云服务较慢,不推荐使用
- Pushy是 React Native 中文网提供的热更新方案使用的是国内的阿里云服务且比前2者有更省流量的增量更新方案。也是国内可直接使用的开源热更新方案之一了。
- 自研:灵活自由度高,可控
热更新方案主要包括2部分打包服务 + 静态资源服务。
打包服务核心就是将 React Native 项目中的 JS 代码打包成一个 Bundle 文件。静态资源服务就是将 Bundle 文件分发给客户端的服务。客户端拿到 Bundle 文件后,就可以渲染展示了。
1. 通过 react-native bundle 命令,提前把 JS 代码打包成一个 Bundle 文件,本质上是一个可执行的 JavaScript 文件。
```shell
npx react-native bundle --entry-file index.js --dev false --minify false --bundle-output ./build/index.bundle --assets-dest ./build
```
2. 如果使用的是 Hermes还需要把 Javascript 文件转成相应的字节码文件。Hermes 提供了方案
```
hermes -emit-binary -out ./build/index.hbc ./build/index.bundle
```
转换后会得到一个 `.hbc` 字节码包hbc 也就是 hermes bytecode。
得到 Bundle 文件后就需要上传到 CDN 上了。但 CDN 存在一个问题,就是 CDN 资源地址是固定不变的,所以不够灵活,**存在几分钟的更新延迟问题**,假设线上出现一个重大故障,需要等几分钟才可以完全回滚,这对于公司形象、用户损失都会产生很大影响。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ReactNativeBundleCDNIssue.png" style="zoom:40%">
上图中旧版本的 JS Bundle 包是绿色的,新版本 JS Bundle 是蓝色的。在旧版本覆盖新版本的过程中:删除 CDN 缓存的旧版本资源,当 CDN 没有缓存了,这时候用户新的请求才不会命中缓存,而是到 OSS 拉取最新资源。
然而 CDN 不是单点计算机,而是分布在不同位置的网络节点,当使用 CDN 刷新能力时,实际上就是删除上千个节点的缓存。搞过 CDN 的人说要把这成千个节点的缓存删除干净最长需要5分钟同时无法保证这5分钟的时效。
也就是说这5分钟内请求 JS Bundle 存在3种情况
- 命中老版缓存
- 未命中缓存,从 OSS 拉取新的资源
- 命中新版缓存
也就是说在享受 CDN 的同时也要接受这5分钟渐进式更新的延迟也就是说要有5分钟内用户可能全部访问有问题的业务 JS Bundle 的预见性。
有没有改进措施?
在端上设备和 CDN 之间再架设一层版本服务。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ReactNativeBundleServer.png" style="zoom:40%">
具体步骤:
1. 本地打包好的 JS Bundle 根据文件内容生成 MD5然后命名对应的 JS Bundle 文件,格式为 `{MD5}.bundle` ,保证唯一性,然后上传 Bundle 到 OSS。
2. 发布上线,比如一个可视化平台点击上线按钮,要做的事情就是告诉版本服务,当前业务 JS Bundle 版本为 0.01JS Bundle 名为 `s2d8...j07.bundle`
3. 端上发起请求,请求版本服务,版本服务根据信息返回对应的 Bundle 名。`{uri: s2d8...j07.bundle}`
4. 端上再次发起真正的 CDN 资源请求。资源请求会先询问某个 CDN 边缘节点,如果边缘节点没有缓存,则去源站拉取资源;如果边缘节点有缓存,则直接返回
## 三、RN 启动速度优化
### 3.1 Hermes
Hermes 是 FaceBook 2019 年中旬开源的一款 JS 引擎,从 **release[1]** 记录可以看出,这个是专为 React Native 打造的 JS 引擎,可以说从设计之初就是为 Hybrid UI 系统打造。
Hermes **支持直接加载字节码**,也就是说,`Babel`、`Minify`、`Parse` 和 `Compile` 这些流程全部都在开发者电脑上完成,直接下发字节码让 Hermes 运行就行,这样做可以**省去 JSEngine 解析编译 JavaScript 的流程**JS 代码的加载速度将会大大加快,启动速度也会有非常大的提升。
### 3.2 减小 JS Bundle 体积
前面的优化其实都是 Native 层的优化,从这里开始就进入 Web 前端最熟悉的领域了。
其实谈到 JS Bundle 的优化,来来回回就是那么几条路:
- **缩**:缩小 Bundle 的总体积,减少 JS 加载和解析的时间
- **延**动态导入dynamic import懒加载按需加载延迟执行
- **拆**:拆分公共模块和业务模块,避免公共模块重复引入
如果有 webpack 打包优化经验的小伙伴,看到上面的优化方式,是不是脑海中已经浮现出 webpack 的一些配置项了?不过 React Native 的打包工具不是 webpack 而是 Facebook 自研的 Metro虽然配置细节不一样但道理是相通的下面我就这几个点讲讲 React Native 如何优化 JS Bundle。
Metro 打包 JS 时,会把 ESM 模块转为 CommonJS 模块,这就导致现在比较火的依赖于 ESM 的 Tree Shaking 完全不起作用而且根据官方回复Metro 未来也不会支持 Tree Shaking
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/RNWillNotSupportTreeShaking.jpeg" style="zoom:40%">
说人话就是 Tree-Shaking 现在不搞了,现在在搞更有前途的方式来保证 bundle 体积的减少:
#### 1. 使用 react-native-bundle-visualizer 查看包体积
使用 react-native-bundle-visualizer 查看包体积。优化 bundle 文件前,一定要知道 bundle 里有些什么最好的方式就是用可视化的方式把所有的依赖包列出来。web 开发中,可以借助 Webpack 的 `webpack-bundle-analyzer` 插件查看 bundle 的依赖大小分布React Native 也有类似的工具,可以借助 RN`react-native-bundle-visualizer` 查看依赖关系:
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/RNDemoMoudleDependcencyVisualizer.jpeg" style="zoom:40%">
#### 2. 对于同样的功能,优先选择体积更小的第三方库
这是一个非常经典的例子。同样是时间格式化的第三方库, moment.js 体积 200 KBday.js 体积只有 2KB而且 API 与 moment.js 保持一致。如果项目里用了 moment.js替换为 day.js 后可以立马减少 JSBundle 的体积。
**利用 babel 插件,避免全量引用**
lodash 基本上属于 Web 前端的工程标配了,但是对于大多数人来说,对于 lodash 封装的近 300 个函数,只会用常用的几个,例如 `get`、 `chunk`,为了这几个函数全量引用还是有些浪费的。
社区上面对这种场景,当然也有优化方案,比如说 `lodash-es`,以 ESM 的形式导出函数,再借助 Webpack 等工具的 Tree Sharking 优化就可以只保留引用的文件。但是就如前面所说React Native 的打包工具 Metro 不支持 Tree Shaking所以对于 `lodash-es` 文件,其实还会全量引入,而且 `lodash-es` 的全量文件比 `lodash` 要大得多。
做了个简单的测试,对于一个刚刚初始化的 React Native 应用,全量引入 lodash 后,包体积增大了 71.23KB,全量引入 `lodash-es` 后,包体积会扩大 173.85KB。
`lodash-es ` 太大了,能不能在 lodash 上做文章?
```js
// 全量
import { join } from 'lodash'
// 局部
import join from 'lodash/join'
```
这样打包就只会打 `lodash/join` 这一个文件,但这严格依赖于团队小伙伴的共识,不能严格保证。且使用 lodash 的七八个方法,就需要 import 七八次,这很低效。有个 `babel-plugin-lodash` 插件,可以在 JS 编译时操作 AST 做如下转换:
```js
import { join, chunk } from 'lodash'
// 转换为
import join from 'lodash/join'
import chunk from 'lodash/chunk'
```
终极大杀器:**`babel-plugin-import`** 基本可以解决所有按需引用的问题
如何使用?
有个 ahooks 开源库,封装了很多常用的 React hooks但问题是这个库是针对 Web 平台封装的,比如说 `useTitle` 这个 hook是用来设置网页标题的但是 React Native 平台是没有相关的 BOM API 的,所以这个 hooks 完全没有必要引入RN 也永远用不到这个 API。
这时候我们就可以用 `babel-plugin-import` 实现按需引用了,假设我们只要用到 `useInterval` 这个 Hooks我们现在业务代码中引入
```javascript
import { useInterval } from 'ahooks'
```
然后运行 `yarn add babel-plugin-import -D` 安装插件,在 `babel.config.js` 文件里启用插件:
```
// babel.config.js
module.exports = {
plugins: [
[
'import',
{
libraryName: 'ahooks',
camel2DashComponentName: false, // 是否需要驼峰转短线
camel2UnderlineComponentName: false, // 是否需要驼峰转下划线
},
],
],
presets: ['module:metro-react-native-babel-preset'],
};
```
启用后就可以实现 ahooks 的按需引入
```js
import { useInterval } from 'ahooks'
// 等价于
import useInterval from 'ahooks/lib/useInterval'
```
| 全量 ahooks | ahooks/lib/useInterval 单文件引用 | ahooks + babel-plugin-import |
| :---------- | :-------------------------------- | :--------------------------- |
| 111.41 KiB | 443 Bytes | 443 Bytes |
#### 3. 制定编码规范,减少重复代码
##### 移除 console
`babel-plugin-transform-remove-console` 插件,我们可以配置它在打包发布的时候移除 `console` 语句,减小包体积的同时还会加快 JS 运行速度,我们只要安装后再简单的配置一下就好了:
```js
// babel.config.js
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
env: {
production: {
plugins: ['transform-remove-console'],
},
},
};
```
##### 制定良好的编码规范
- 代码的抽象和复用:代码中重复的逻辑根据可复用程度,尽量抽象为一个方法,不要用一次复制一次
- 删除无效的逻辑:这个也很常见,随着业务的迭代,很多代码都不会用了,如果某个功能下线了,就直接删掉,哪天要用到再从 git 记录里找
- 删除冗余的样式:例如引入 ESLint plugin for React Native开启 `"react-native/no-unused-styles"` 选项,借助 ESLint 提示无效的样式文件
### 3.3 Inline Requires
懒执行。一般情况下 RN 容器初始化之后就会加载全量的 JS Bundle 文件,而 Inline Requires 延迟运行,只有需要使用的时候才会执行 JS 代码而不是启动的时候就执行RN 0.64 版本,默认开启。需要在 `metro.config.js` 中进行修改
```js
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
const fs = require('fs');
const path = require('path');
const config = {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
}),
},
};
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
```
比如说我们写了个工具函数 `join` 放在 `utils.js` 文件里:
```js
// utils.js
export function join(list, j) {
return list.join(j);
}
```
```js
// App.js
import { join } from 'my-module';
const App = (props) => {
const result = join(['a', 'b', 'c'], '~');
return <Text>{result}</Text>;
};
```
被 Metro 编译后
```js
const App = (props) => {
const result = require('./utils').join(['a', 'b', 'c'], '~');
return <Text>{result}</Text>;
};
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/RNInlineRequireDemo1.png" style="zoom:40%">
`r()` 代表 `require()` 函数,可以看到在顶部的 import 实际上被替换成在实际使用的位置 import 了。
然而 Metro 的 import 是不支持 export default 的。需要注意,可以具体查看 [RN 官方文档](https://reactnative.dev/docs/ram-bundles-inline-requires)
### 3.4 RN 拆包
假设业务 A 和 B 的业务代码是通过 JS Bundle 动态下发的:
- Business A JS Bundle共300KB其中 200KB 基础包React、RN、100KB 业务代码
- Business B JS Bundle共400KB其中 200KB 基础包React、RN、200KB 业务代码
访问完业务 A 之后再去访问业务 B下载好300KB 代码后需要继续下载400KB 代码。存在冗余,完全没有必要多次下载和加载,这时候一个想法自然而然就出来了:
> 能否把一些共有库打包到一个 `common.bundle` 文件里,我们每次只要动态下发业务包 `businessA.bundle` 和 `businessB.bundle`,然后在客户端实现先加载 `common.bundle` 文件,再加载 `business.bundle` 文件就可以了
这样做的好处有几个:
- `common.bundle` 可以直接放在本地,省去多业务线的多次下载,**节省流量和带宽**
- 可以在 RN 容器预初始化的时候就加载 `common.bundle` **二次加载的业务包体积更小,初始化速度更快**
顺着上面的思路,上面问题就会转换为两个小问题:
- 如何实现 JSBundle 的拆包?
- iOS/Android 的 RN 容器如何实现多 bundle 加载?
RN 本地调试还是构建,底层都是用到了 Metro 打包工具的能力。然而 Facebook 的 Metro 本身没有拆包能力,只能将 JS 代码打包成一个 Bundle 文件,且 Metro 不支持三方插件。在查看 Metro 源代码的时候发现了一个令人眼前一亮的方法 `customSerializer` ,可以实现不侵入修改 Metro 源码,通过配置的方式给 Metro 写第三方插件的能力
#### 3.4.1 拆包原理
为什么选择基于模块拆包而不是基于文本?因为基于模块拆包,加载速度会更快。
为什么基于模块的拆包方式能够独立运行,而基于文本的拆包方式不能独立运行?
来做一些说明,架设采用的是多 Bundle 基于文本的拆包方式,多个 Bundle 之间的公共代码是 React、React Native 库,用 `console.log('react')`、`console.log('react native')` 代替。多个 Bundle 不同的部分是业务代码,用 `console.log('foo')` 代表业务代码。
基于文本拆包一般采用 Google 开源的 [diff-match-patch](https://github.com/google/diff-match-patch?tab=readme-ov-file) 算法。repo 主页也提供了 Demo 入口,可以在线体验效果
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ReactNativeBundlePatchDemo.png" style="zoom:40%">
实际上,在基于文本计算热更新包的场景下,我们会内置 Old version这部分代码除了升级 RN 版本外不会改动,而 New Version 的字符串是本次热更新的目标代码,但为了传输效率不需要下载完成的 Bundle 文件,因为 Old Version 已经内置了,基于 diff-match-patch 计算出需要热更新的部分即可。
客户端拉取到需要 Patch 热更新包后,会和 Old Version 代表的内置包进行合并,合并的结果就是 New Version 所代表的完整的 Bundle
很显然Patch 热更新是一段记录修改位置、修改内容的文本,而不是一段可单独运行的代码。会导致内置包没法提前执行,只能等下在完成再合并,生成完整的 Bundle 文件后,作为整体才执行。这就是为什么基于文本的拆包方式不能独立运行的原因。
引入正题,基于模块的拆包方式,内置包和热更新包就可以分别独立运行了。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ReactNativeBundlePatch.png" style="zoom:40%">
可以看到基于模块的拆包方案,拆出来热更新包是一个业务代码,单独可运行。所以可以在客户端先运行内置包,然后下载热更新包,等热更新包下载完成,再运行热更新包。
#### 3.4.2 Bundle 文件结构及内容说明
React Native打包形成的Bundle文件的内容从上到下依次是
- Polyfills定义基本的JS环境`__d()`函数、`__r()`函数、`__DEV__` 变量等)
- Module定义使用`__d()`函数定义所有用到的模块该函数为每个模块赋予了一个模块ID模块之间的依赖关系都是通过这个ID进行关联的。
- Require调用使用`__r()`函数引用根模块。
业务不同的2个 Bundle 但是会在 Polyfills 部分和 Module 定义的部分有大量重复,因为都包含 React、React Native 2个模块代码重复部分大约500K 左右
`-d` 函数实际上就是 `define()` 函数3个参数分别为factory 方法、moudleID、dependencyMap
```js
function define(factory, moduleId, dependencyMap) {
if (moduleId in modules) {
// that are already loaded
return;
}
modules[moduleId] = { dependencyMap};
// other code ....
};
```
`_r` 函数实际上就是 `require()` 函数,这个方法首先判断所有要加载的模块是否已经存在并完成了初始化。如果是,则直接返回模块的 exports如果不是则调用 `guardedLoadModule` 方法来完成模块的初始化
```js
function require(moduleId) {
const module = modules[moduleId];
return module && module.isInitialized
? module.exports
: guardedLoadModule(moduleIdReallyIsNumber, module);
}
function guardedLoadModule(moduleId, module) {
return loadModuleImplementation(moduleId, module);
}
function loadModuleImplementation(moduleId, module) {
module.isInitialized = true;
const exports = (module.exports = {});
var _module = module;
const factory = _module.factory,
dependencyMap = _module.dependencyMap;
const moduleObject = { exports };
factory(global, require, moduleObject, exports, dependencyMap);
return (module.exports = moduleObject.exports);
}
```
#### 3.4.3 公共资源包
随着 RN 版本迭代官方已经逐步将bundle文件生成流程规范化并为此设计了独立的打包模块 Metro。Metro 通过输入一个需要打包的JS文件及几个配置参数返回一个包含了所有依赖内容的JS文件。
Metro将打包的过程分为了3个依次执行的阶段
1. **解析Resolution**:计算得到所有的依赖模块,形成依赖树,该过程是多线程并行执行。
2. **转义Transformation**:代码的编译转换,该过程是多线程并行执行。
3. **序列化Serialization**:所有代码转换完毕后,打印转换后的代码,生成一个或者多个 bundle 文件
Metro工具提供了配置功能开发人员可以通过配置RN项目中的**metro.config.js**文件修改bundle文件的生成流程。
可以看到,我们需要关注 Metro Serialization 阶段,只要借助 `Serialization` 暴露的各个方法就可以实现 bundle 分包了。主要是 `createModuleIdFactory(path)` 方法和 `processModuleFilter(module)` 。
`createModuleIdFactory(path)`是传入的模块绝对路径`path`,并为该模块返回一个唯一的`Id`。`processModuleFilter(module)`则可以实现对模块进行过滤使其不被写入到最后的bundle文件中。
官方的 `createModuleIdFactory` 内部实现是返回一个数字,该数字在 require 方法中被调用,以此来实现模块的导入和初始化
```js
"use strict";
function createModuleIdFactory() {
const fileToIdMap = new Map();
let nextId = 0;
return path => {
let id = fileToIdMap.get(path);
if (typeof id !== "number") {
id = nextId++;
fileToIdMap.set(path, id);
}
return id;
};
}
```
官方默认的实现存在一个问题,就是业务代码改动后,重新打包,由于 moduleID 是从0开始自增分配可能会存在前后2次构建中 moduleID 发生改变。
针对官方实现,可以重新自定义 `createModuleIdFactory(path)` 方法,该方法根据当前模块文件的路径哈希值作为分配 moduleID 的依据,并建立哈希值和模块 ID 的对应关系保存到本地文件缓存中,每次编译 Bundle 先读取本地缓存文件来初始化内存缓存,当需要分配 ID 的时候,先从缓存内部查找,找不到则重新分配 ID 并存储变化。
```js
// metro.common.config.js
/**
* Metro configuration
* https://facebook.github.io/metro/docs/configuration
*
* @type {import('metro-config').MetroConfig}
*/
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
const fs = require('fs');
const path = require('path');
const config = {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
}),
},
serializer: {
createModuleIdFactory: function () {
//获取命令行执行的目录__dirname是nodejs提供的变量
const projectRootPath = __dirname;
return (path) => {
let name = '';
// 如果需要去除react-native/Libraries路径去除可以放开下面代码
// if (path.indexOf('node_modules' + pathSep + 'react-native' + pathSep + 'Libraries' + pathSep) > 0) {
// //这里是react native 自带的库,因其一般不会改变路径,所以可直接截取最后的文件名称
// name = path.substr(path.lastIndexOf(pathSep) + 1);
// }
if (path.indexOf(projectRootPath) == 0) {
/*
这里是react native 自带库以外的其他库,因是绝对路径,带有设备信息,
为了避免重复名称,可以保留node_modules直至结尾
如/{User}/{username}/{userdir}/node_modules/xxx.js 需要将设备信息截掉
*/
name = path.substr(projectRootPath.length + 1);
}
//js png字符串 文件的后缀名可以去掉
// name = name.replace('.js', '');
// name = name.replace('.png', '');
//最后在将斜杠替换为下划线
let regExp = pathSep == '\\' ? new RegExp('\\\\', "gm") : new RegExp(pathSep, "gm");
name = name.replace(regExp, '_');
//名称加密
if (isEncrypt) {
name = md5(name);
}
fs.appendFileSync('./idList.txt', `${name}\n`);
return name;
};
},
},
};
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
```
同时,为了能够在`processModuleFilter(module)`方法中对模块进行过滤需要在构建Common文件时标记某个模块是否已包含在Common文件中。
打 common 包:`npx react-native bundle --platform ios --config metro.common.config.js --dev false --entry-file common.js --bundle-output='./ios/common.ios.bundle'`
#### 3.4.4 业务资源包
```js
// metro.business.config.js
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
/**
* Metro configuration
* https://facebook.github.io/metro/docs/configuration
*
* @type {import('metro-config').MetroConfig}
*/
const fs = require('fs');
const path = require('path');
const idList = fs.readFileSync('./idList.txt', 'utf8').toString().split('\n');
function createModuleId(path) {
// 和上面生成 moduleID 方法一样
return moduleId;
}
const config = {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
}),
},
serializer: {
createModuleIdFactory: function () {
return function (path) {
return createModuleId(path);
};
},
processModuleFilter: function (modules) {
const mouduleId = createModuleId(modules.path);
// 通过 mouduleId 过滤在 common.bundle 里的数据
if (idList.indexOf(mouduleId) < 0) {
console.log('createModuleIdFactory path', mouduleId);
return true;
}
return false;
},
},
};
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
```
此时打业务包 `npx react-native bundle --platform ios --config metro.business.config.js --dev false --entry-file index.js --bundle-output='./ios/businessA.ios.bundle'`
其中几个业务,就需要几个业务入口,也就需要打几次包。当然可以再次基础上包装一层,比如读取配置文件。
#### 3.4.5 客户端加载
以 iOS 为例,加载基础包
```objective-c
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"rnCodeSplitDemo"
initialProperties:nil];
if (@available(iOS 13.0, *)) {
rootView.backgroundColor = [UIColor systemBackgroundColor];
} else {
rootView.backgroundColor = [UIColor whiteColor];
}
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [UIViewController new];
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
return YES;
}
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
return [[NSBundle mainBundle] URLForResource:@"common.ios"" withExtension:@"bundle"];
}
```
加载业务包
为 `RCTCxxBridge` 添加分类 `RCTCxxBridge+RunBundleJS`
```objective-c
#import <React/RCTBridge+Private.h>
NS_ASSUME_NONNULL_BEGIN
@interface RCTCxxBridge (RunBundleJS)
- (void)executeSourceCode:(NSData *)sourceCode withSourceURL:(NSURL *)url sync:(BOOL)sync;
@end
NS_ASSUME_NONNULL_END
```
```objective-c
NSString *businessBundle = [[NSBundle mainBundle] pathForResource:@"business.ios.jsbundle" ofType:nil];
NSData *businessData = [NSData dataWithContentsOfFile:businessBundle options:NSDataReadingMappedIfSafe error:nil];
[(RCTCxxBridge *)bridge.batchedBridge executeSourceCode:businessData sync:YES];
```
## 学习方面
学过 React.js 之后你再去学习 React Native 会很简单,一些核心的东西理解之后会很简单。比如 React 中的单向数据流、虚拟 Dom、diff 算法、数据变动的批量更新机制、diff 之后的 UI 渲染。

View File

@@ -747,6 +747,13 @@ didFinishLaunching通过 UIApplicationDidFinishLaunchingNotification 拿到
}
```
### 5. RN 启动时间监控
跨端类的统计口径一般不会像 Native 那么严格,不会从 main 函数冷启动开始计算,启动还区分那么多 t1进程创建到 main函数执行、t2main 函数执行到 didFinishLaunching 完成、t3didFinishLaunching 完成到首屏渲染完成)
开始时间点:一般需要 Native 配合,容器页面创建好就是开始时间点。比如 iOS 的 VC `viewDidLoad`、Android 的 `onActivityCreated`
结束时间点:结束时间一般在跨端侧,比如 RN 中的组件挂载完成 componentDidMount 回调的时刻。
## 三、 CPU 使用率监控
### 1. CPU 架构
@@ -4497,6 +4504,29 @@ typedef CFHTTPMessageRef (*APMURLResponseFetchHTTPResponse)(CFURLRef response);
}
```
### 3. RN 网络监控
RN 中的 fetch 或者 axios 请求都是基于 XMLHttpRequest 包装的,所以要统计请求耗时,就可以统一收口在 XMLHttpRequest 侧,监听 open 事件、onreadystatechange 事件。open 事件记录请求开始的时间点,在 onreadystatechange 事件且 xht.readyState == 4 的时候记录结束点
```
let startTime = 0
const originalOpen = XMLHttpRequest.prototype.open
XMLHttpRequest.prototype.open(function(...args) {
startTime = Date.now();
const xhr = this;
const originalOnReady = xhr.prototype.onreadystatechange;
xhr.prototype.onreadystatechange = function(...readyStateArgs) {
if (xhr.readyState === 4) {
const totalTimeSpan = Date.now() - startTime;
EventUtil.save(totalTimeSpan, EventType.Request);
}
originalOnReady(...readyStateArgs);
}
originalOpen.apply(xhr, args);
})
```
## 七、 电量消耗
移动设备上电量一直是比较敏感的问题,如果用户在某款 App 的时候发现耗电量严重、手机发热严重,那么用户很大可能会马上卸载这款 App。所以需要在开发阶段关心耗电量问题。
@@ -6324,7 +6354,43 @@ if (global?.HermesInternal?.hasPromise?.()) {
}
```
定义配置项 `defualtRejectionTrackingOptions`,其中的 onHnhandled 回调函数,该回调函数主要来处理未被 catch 的 Promise 错误。然后通过 `global?.HermesInternal?.hasPromise` 来判断 RN 是否采用 Hermes 引擎。如果采用 Hermes 引擎,则使用 `enablePromiseRejectionTracker` 方法来捕获未被 catch 的 Promise 错误。如果不是 Hermes 引擎则使用第三方 Promise 库中的 `rejection-tracking` 文件中的 enable 方法来捕获未被 catch 的 Promise 错误。
首先,定义配置项 `defualtRejectionTrackingOptions`,其中的 onUnhandled 回调函数,该回调函数主要来处理未被 catch 的 Promise 错误。
然后通过 `global?.HermesInternal?.hasPromise` 来判断 RN 是否采用 Hermes 引擎:
- 如果采用 Hermes 引擎,则使用 `enablePromiseRejectionTracker` 方法来捕获未被 catch 的 Promise 错误
- 如果不是 Hermes 引擎则使用第三方 Promise 库中的 `rejection-tracking` 文件中的 enable 方法来捕获未被 catch 的 Promise 错误
那如何捕获 RN 侧产生的 Promise 错误?
做法和上面呼应。先判断是 Hermes 引擎还是非 Hermes 引擎,然后执行对应的异常处理函数,我们只需要注册好对应的异常发生后,数据收集方法即可
```
const customPromiseRejectionTrackingOptions = {
allRejections: true,
onUnhandled:(id:string, error: Error) => {
let errorInfo = {
errorMessage: error.message,
errorStack: error.stack,
type: ErrorType.Promise
}
ErrorUtils.save(errorInfo)
},
onHandled: (id:string) => {}
}
if (global?.HermesInternal?.hasPromise?.()) {
if (__DEV__) {
global.HermesInternal?.enablePromiseRejectionTracker?.(
defualtRejectionTrackingOptions,
);
}
} else {
if (__DEV__) {
require('promise/setimmediate/rejection-tracking').enable(
defualtRejectionTrackingOptions,
);
}
}
```
###### 2.7.3.3 组件 render 错误
@@ -6351,6 +6417,44 @@ render 错误在本地就是红屏,线上可能没有反应或者白屏。如
所以可以通过异常边界组件捕获组件生命周期内的所有异常然后渲染兜底组件 ,防止 App crash提高用户体验。也可引导用户反馈问题方便问题的排查和修复
不过React/React Native 只提供了类组件捕获 render 错误的方法,如果是函数组件,必须将其嵌套在类组件中才可以捕获其 render 错误。业界常见做法是将其封装成一个通用方法来给其他组件使用。比如 Sentry 就提供了 ErrorBoundary 组件和 withErrorBoundary 方法来帮助其他类组件和函数组件捕获 render 错误。
为了 cover 大多数组件渲染错误导致的 bug代码如下
```
class ErrorBoundary extends React.component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDrivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示出降级后的 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 将异常信息收集起来,然后按照数据上报策略做异常上报
ErrorUtils.save(error, errorInfo);
}
render () {
if (this.state.hasError) {
// 可以是安抚用户情绪的文案或者是统一的 feedback 页
return <View>错误引导或者降级页</View>
}
return this.props.children;
}
}
// 然后在 App 入口处做统一收口
<ErrorBoundary>
<App />
</ErrorBoundary>
```
如果 App 组件 render 没有报错,则会走 else 分支,也就是 this.props.children 正常渲染;如果 App 组件 render 出错了,则会触发 getDrivedStateFromError 回调,内部将 hasError 设置为 true再次 render 的时候则展示降级后的页面。同时会触发 componentDidCatch 回调,并记录异常信息。
至此 RN 的 crash 分为 2 种,分别是 js 逻辑错误、组件 js 错误,都已经被监控处理了。接下来就看看如何从工程化层面解决这些问题
##### 2.7.4 RN Crash 还原

BIN
Chapter2 - Web FrontEnd/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -5,7 +5,7 @@
从安全性、高性能等原因出发,目前浏览器已经是多进程架构模式,至于演进历史,本文不再展开,感兴趣的可以查看这篇[“Electron” 一个可圈可点的 PC 多端融合方案]()文章。
现在的架构设计如下:
![最新 Chrome 进程架构图](./../assets/2020-05-07-ChromeMordernArch.png)
![最新 Chrome 进程架构图](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-07-ChromeMordernArch.png)
一个页面最少包括1个网络进程、1个浏览器进程、1个 GPU 进程、多个渲染进程、多个插件进程
@@ -47,7 +47,7 @@
熟悉其他系统设计的同学可能会立马想到用**队列**来解决问题。浏览器对这个 case 也采用队列,叫做事件队列。
<img src="./../assets/JSMainThreadEventLoop.png" style="zoom:30%;">
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/JSMainThreadEventLoop.png" style="zoom:30%;">
@@ -88,7 +88,7 @@
应对这种情况,浏览器采用异步的设计来解决,如下图
<img src="./../assets/JSEventloop.png" style="zoom:40%;">
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/JSEventloop.png" style="zoom:40%;">
使用异步的方案,可以使得主线程不等待、不阻塞,高效有序的执行逻辑。
@@ -148,15 +148,15 @@
T0 时刻:最开始的时候,主线程没有任务任务需要执行。但是主线程告诉交互线程,你需要监听用户的点击事件,点击后需要执行 callback
<img src="./../assets/JS-UIClickLag1.png" style="zoom:60%;">
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/JS-UIClickLag1.png" style="zoom:60%;">
T1时刻当用户点击后交互线程会把 callback 封装成一个任务添加到事件循环队列的尾部。此时主线程依旧没有任务所以主线程事件循环将被唤醒从事件队列的头部取出一个任务去执行。大的一个任务里包含2个子事件修改 DOM 和 延迟3秒。修改 DOM 这句指令执行后浏览器要想看的见需要内部会产生一个绘制任务硬件设备显示图形的画家算法。绘制任务被添加到事件队列尾部后立马执行延迟3秒的事件。
<img src="./../assets/JS-UIClickLag2.png" style="zoom:60%;">
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/JS-UIClickLag2.png" style="zoom:60%;">
T2时刻到达T2时刻后主线程又空了此时从事件队列中读取绘制任务。进而去显示出 DOM 文本修改后的结果但此时前面已经等待了3秒钟所以体感上会有一种卡顿的现象
<img src="./../assets/JS-UIClickLag3.png" style="zoom:60%;">
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/JS-UIClickLag3.png" style="zoom:60%;">
@@ -198,7 +198,7 @@ T2时刻到达T2时刻后主线程又空了此时从事件队列中读
QAJS 计时器准吗?
## JS 计时器准吗?
不准。存在以下几个原因:
@@ -208,14 +208,15 @@ QAJS 计时器准吗?
- 受事件循环的影响,计时器的回调函数只能在主线程空闲时运行,因此又带来了偏差
## 计算机的时间
没有时间,我们的生活将无序,无法度量。计算机更是如此,比如纳秒级的运算,那么计算机到底是如何感知时间的呢?
计算机依靠**晶振**(全称是石英晶体振荡器),是一种高精度、高稳定度的振荡器。位置一般在主板上,晶振为系统提供很稳定的脉冲信号,一秒内产生的脉冲次数也被称为系统时钟频率。一个标准的脉冲信号如下:
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/time-pulse.png)
几个概念:
- 时钟周期:这里的时钟就是晶振,也就是晶振周期/振荡周期。是计算机的最小时间单元,其他周期只能是它的倍数
- 机器周期性:既生瑜何生亮?时钟周期太小了,小到表述很多东西不够方便。到了 CPU 操作层面来说时钟周期不好用类似人民币虽然早期有1分钱但某个商品价格比较大需要1238902分咋感觉这个单位不那么好用了所以设计了100百元这个度量单位。一个机器周期等于多个时钟周期它是 CPU 的一个基本操作时间单元,比如:取指、译码、存储器读/写、运算等等操作
- 指令周期:是 CPU 完成一条指令的时间,又称为提取-执行周期fetch-and-execute cycle是指 CPU 要执行一条指令经过的步骤,由若干机器周期组成。不同的机器分解指令周期的方式不同。
- 总线周期:它是 CPU 操作指令设备的时间,由于存储器和 I/O 端口是挂接在总线上的对它们的访问是通过总线实现的。通常将进行一次访问所需时间称为一个总线周期。这种访问速度较慢因硬件设备发展的原因各个设备的工作频率无法同步周期不一致甚至相差好几个数量级CPU 很快,硬盘很慢,大家又需要协同工作,所以出现了**分频**的概念。将高频信号变成低频信号。

View File

@@ -29,11 +29,11 @@ GUI 架构过程化绘制drawLine、drawRect-> 面向对象抽象时代
3天时间写了个 PC 端应用程序。先看看结果吧
![Todo1](./../assets/2020-05-04-TodoApp1.png)
![Todo1](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-04-TodoApp1.png)
![Todo1](./../assets/2020-05-04-TodoApp2.png)
![Todo1](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-04-TodoApp2.png)
![Todo1](./../assets/2020-05-04-TodoApp3.png)
![Todo1](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-04-TodoApp3.png)
@@ -58,7 +58,7 @@ npm install && npm start
```
简单介绍下 Demo 工程,工程目录如下所示
![工程目录](./../assets/2020-04-21-electron-packagejson.png)
![工程目录](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-04-21-electron-packagejson.png)
在终端执行 `npm start` 执行的是 package.json 中的 `scripts` 节点下的 start 命令,也就是 `Electron .``.` 代表执行 main.js 中的逻辑。
@@ -159,7 +159,7 @@ Electron 分为**渲染进程和主进程**。和 Native 中的概念不一样
单进程浏览器指的是浏览器的所有功能模块都是运行在同一个进程里的这些模块包括网络、插件、Javascript 运行环境、渲染引擎和页面等。如此复杂的功能都在一个进程内运行,所以导致浏览器出现不稳定、不安全、不流畅等问题。
![单进程浏览器架构示意图](./../assets/2020-05-07-BrowserSingleThread.png)
![单进程浏览器架构示意图](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-07-BrowserSingleThread.png)
早在2007年之前市面上的浏览器都是单进程架构。
@@ -194,7 +194,7 @@ Electron 分为**渲染进程和主进程**。和 Native 中的概念不一样
#### 1.2 早期多进程架构浏览器
![早期 Chrome 进程架构图](./../assets/2020-05-07-ChromEarlyArch.png)
![早期 Chrome 进程架构图](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-07-ChromEarlyArch.png)
上图2008年 Chrome 发布时的进程架构图。可以看出 Chrome 的页面是运行在单独的渲染进程中,同时页面的插件也是运行在单独的插件进行中的,进程之间通过 IPC 进行通信。
@@ -218,7 +218,7 @@ Electron 分为**渲染进程和主进程**。和 Native 中的概念不一样
Chrome 团队不断发展,目前架构有了较新变化,最新 Chrome 架构图如下所示
![最新 Chrome 进程架构图](./../assets/2020-05-07-ChromeMordernArch.png)
![最新 Chrome 进程架构图](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-07-ChromeMordernArch.png)
最新 Chrome 浏览器包括1个网络进程、1个浏览器进程、1个 GPU 进程、多个渲染进程、多个插件进程。
@@ -247,7 +247,7 @@ Chrome 团队一直在寻求新的弹性方案,既可以解决资源占用较
Chrome 最终把 UI、数据库、文件、设备、网络等模块重构为基础服务。下图是 “Chrome 面向服务的架构”的进程模型图
![Chrome ”面向服务架构“的进程模型图](./../assets/2020-05-08-ChromeMltipleProcessArch.png)
![Chrome ”面向服务架构“的进程模型图](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-08-ChromeMltipleProcessArch.png)
@@ -257,7 +257,7 @@ Chrome 最终把 UI、数据库、文件、设备、网络等模块重构为基
Chrome 提供灵活的弹性架构在强大性能设备上会以多进程的方式运行基础服务但是在设备资源受限的情况下Chrome 会将很多服务整合到一个进程中,从而节省内存占用。
![Chrome弹性架构](./../assets/2020-05-08-FlexiableChromeMltipleProcessArch.png)
![Chrome弹性架构](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-08-FlexiableChromeMltipleProcessArch.png)
#### 1.5 小实验
@@ -273,7 +273,7 @@ Chrome 提供灵活的弹性架构,在强大性能设备上会以多进程的
实验现象:
![Chrome 进程仪表](./../assets/2020-05-08-ChromeProcessInspector.png)
![Chrome 进程仪表](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-08-ChromeProcessInspector.png)
实验结论:
@@ -310,7 +310,7 @@ Chrome 的默认策略是,每个标签对应一个渲染进程。但是如果
![Chromium 架构](./../assets/2020-05-03-ChromiumArch.png)
![Chromium 架构](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-03-ChromiumArch.png)
@@ -331,7 +331,7 @@ Chrome 的默认策略是,每个标签对应一个渲染进程。但是如果
### 2. Electron 架构
![](./../assets/2020-05-03-ElectronArch.png)
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-03-ElectronArch.png)
@@ -353,7 +353,7 @@ Electron 架构和 Chromium 架构类似也是具有1个主进程和多个渲
![Node.js与Chromium通信](./../assets/2020-05-03-ChromiumCommunicateWithNode.png)
![Node.js与Chromium通信](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-03-ChromiumCommunicateWithNode.png)
上图描述了 Node.js 如何融入到 Chromium 中。描述下原理
@@ -380,7 +380,7 @@ Electron 架构和 Chromium 架构类似也是具有1个主进程和多个渲
工程采用 Electron + Vue 技术,下面截图 Vue-devtools 很方便查看 Vue 组件层级等 Vue 相关的调试
![渲染进程调试](./../assets/2020-05-04-ElectronVue.png)
![渲染进程调试](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-04-ElectronVue.png)
### 2. 主进程调试方式
@@ -395,10 +395,10 @@ Electron 架构和 Chromium 架构类似也是具有1个主进程和多个渲
```
- 然后打开浏览器,在地址栏输入 `chrome://inspect`
- 点击 `configure`,在弹出的面板中填写需要调试的端口信息
- ![chrome inspect](./../assets/2020-04-21-electronChromeInspect.png)
- ![chrome inspect](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-04-21-electronChromeInspect.png)
- 重新开启服务 `npm start`,在 chrome inspect 面板的 `Target` 节点中选择需要调试的页面
- 在面板中可以看到主进程执行的 `main.js`。可以加断点进行调试
![chrome inspect](./../assets/2020-04-21-Electron-MainProcessInspect.png)
![chrome inspect](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-04-21-Electron-MainProcessInspect.png)
方法二:利用 VS Code 调试 Electron 主进程。
@@ -428,7 +428,7 @@ Electron 架构和 Chromium 架构类似也是具有1个主进程和多个渲
- 在调试模点击绿色小三角,会运行程序,可以添加断点信息。整体界面如下所示。可以单步调试、可以暂停、鼠标移上去可以看到对象的各种信息。
![VS Code 调试功能](./../assets/2020-04-21-electromDebugInVSCode.png)
![VS Code 调试功能](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-04-21-electromDebugInVSCode.png)
@@ -438,7 +438,7 @@ Electron 的渲染进程中的代码改变了,使用 Command + R 可以刷新
Webpack 有一个 api: `watch-run`,可以针对代码文件检测,有变化则 Restart
![main Process reload](./../assets/2020-05-04-ElectronMainProcessHotReload.png)
![main Process reload](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-04-ElectronMainProcessHotReload.png)
@@ -527,7 +527,7 @@ Electron 官方给出了解决方案 Squirrel基于 Squirrel 框架完成的
9. Electron 多窗口与单窗口应用区别
![用途](./../assets/2020-05-03-ElectronUsage.png)
![用途](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-03-ElectronUsage.png)
10. 知道 Electron 开发原理,所以大部分时间是在写前端代码。所以根据团队技术沉淀、选择对应的前端框架,比如 Vue、React、Angular。
@@ -535,7 +535,7 @@ Electron 官方给出了解决方案 Squirrel基于 Squirrel 框架完成的
12. Electron 和 Web 开发相比,各自有侧重点
![ElectronAndWeb](./../assets/2020-05-04-ElectronAndWeb.png)
![ElectronAndWeb](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-04-ElectronAndWeb.png)
13. 有些人开发 Electron 应用可能不喜欢 [electron-vue](https://github.com/SimulatedGREG/electron-vue) 这样的工具,喜欢自己自定义。假如自己利用 Vue 或者 React 开发的,开发过网页的同学都会习惯使用 Vue-devtools、React-devtools。所以在选用 Vue 或 React 后,习惯使用强大的 Vue-devtools、React-devtools 来查看 State、Action、Redux、Vuex、组件层级树等。
@@ -591,13 +591,13 @@ node --cpu-prof --heap-prof -e "require('request)”“
### 1. 构建
![构建](./../assets/2020-05-04-ElectronBuild.png)
![构建](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-04-ElectronBuild.png)
### 2. 工程解耦
![工程解耦](./../assets/2020-05-04-ElectronCode.png)
![工程解耦](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-04-ElectronCode.png)
@@ -605,7 +605,7 @@ node --cpu-prof --heap-prof -e "require('request)”“
Electron 提供的 crash 信息进行包装。
![crash 分析](./../assets/2020-05-04-ElectronCrash.png)
![crash 分析](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-04-ElectronCrash.png)
```js
import { BrowserWindow, app, dialog} from 'Electron';

View File

@@ -43,7 +43,7 @@ QA
为了提高解析效率,浏览器会启动一个预解析器率先下载和解析 CSS
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/HTMLParse-CSSPreload.png" style="zoom:20%;">
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/HTMLParse-CSSPreLoad.png" style="zoom:20%;">
@@ -261,7 +261,7 @@ p {
都处于页面作者样式表中,选择器的权重也相同,但根据所处位置的不同,下面的样式声明会覆盖上面的值,最终采用 #0000ff
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CssStylePositionPriority.png" style="zoom:25%;">
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CSSStylePositionPriority.png" style="zoom:25%;">
样式声明冲突的情况解决了
@@ -284,7 +284,7 @@ p {
只对类名为 container color 进行了设置,针对 p 标签没有任何的设置,但由于 color 可以继承,所以 p 就从最近的 div 继承了颜色。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CssStyleInherited.png" style="zoom:25%;">
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CSSStyleInherited.png" style="zoom:25%;">
看另一个现象
@@ -302,7 +302,7 @@ p {
</div>
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CssStyleDoubleInherited.png" style="zoom:25%;">
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CSSStyleDoubleInherited.png" style="zoom:25%;">
这里继承了 container、innerContainer 2个的属性值说了继承会选择更近的一个innerContainer 胜出。
@@ -318,7 +318,7 @@ p {
</div>
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CssStyleDefaultValue.png" style="zoom:25%;">
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CSSStyleDefaultValue.png" style="zoom:25%;">
任何一个元素要在浏览器上渲染出来,必须具备所有的 css 属性值,但很多属性我们没有去设置,用户代理样式表中也没有设置,也无法从继承中拿到,因此最终都是使用默认值的。

View File

@@ -1,5 +1,5 @@
## 晋升答辩的逻辑是什么?
<img src="./../assets/upgrageReviewMeeting.png" style="zoom:30%; align:left;">
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/upgrageReviewMeeting.png" style="zoom:30%; align:left;">
> 来源是脉脉 App 的一张图,这里做简单的扯淡。这个老前端的具体背景是什么不清楚,评委还问了什么也不清楚。

View File

@@ -0,0 +1,8 @@
# 对于”文件“的新认识
```
int open(const char* pathnameint flags);
ssize_t read(int fd void *buf size_t count);
ssize_t write(int fd const void *buf size_t count);
int close(int fd);
```

View File

@@ -23,4 +23,7 @@
* [19、规范化团队 git 提交信息](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.20.md)
* [20、如何画架构图](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.21.md)
* [21、项目管理案例分析](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.22.md)
* [22、2022年度总结](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.23.md)
* [22、2022年度总结](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.23.md)
* [23、晋升答辩的逻辑是什么](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.24.md)
* [24、短视频刷多了会变笨吗怎么样提升我们的表达和思辨能力](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.25.md)
* [25、对于”文件“的新认识](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.26.md)

View File

@@ -237,4 +237,7 @@
* [19、规范化团队 git 提交信息](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.20.md)
* [20、如何画架构图](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.21.md)
* [21、项目管理案例分析](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.22.md)
* [22、2022年度总结](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.23.md)
* [22、2022年度总结](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.23.md)
* [23、晋升答辩的逻辑是什么](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.24.md)
* [24、短视频刷多了会变笨吗怎么样提升我们的表达和思辨能力](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.25.md)
* [25、对于”文件“的新认识](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.26.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

BIN
assets/time-pulse.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB