当前位置: 首页 > news >正文

【iOS】—— autoreleasePool以及总结

【iOS】—— autoreleasePool以及总结

      • 1. 什么是autoreleasePool
      • 2. autoreleasePoolPage
        • objc_autoreleasePoolPush方法:
        • objc_autoreleasePoolPop方法:
          • token
          • kill()方法
      • 3. 总结
      • 3.1 autoreleasePool的原理
      • 3.2 autoreleasePool的问题
        • 3.2.1 autoreleasepool的嵌套操作
        • 3.2.2 autoreleasepool的释放时机
        • 3.2.3 那些对象可以加入到autoreleasePool中
        • 3.2.4 关于哨兵对象和next指针
        • 3.2.5 next和child:
        • 3.2.6 thread 和 AutoreleasePool的关系
        • 3.2.7 RunLoop 和 AutoreleasePool的关系

1. 什么是autoreleasePool

AutoreleasePool(自动释放池)是在Objective-CSwift中用于管理内存释放的机制。通过创建自动释放池,可以将需要延迟释放的对象放入其中,在自动释放池被销毁时,其中的对象会被释放,从而帮助避免内存泄漏并优化内存管理。

大概的意思就是:

  • 自动释放池是栈结构,存储的是指针。
  • 指针指向需要自动释放的对象或者 POOL_BOUNDARY 边界值。以 POOL_BOUNDARY 为边界,当释放池释放时,在边界之内的对象会被释放。
  • page 以双向链表的形式构成 pool,page 会自动创建或释放。
  • 线程本地存储指向当前最新的 page。

2. autoreleasePoolPage

每个autoreleasePool都是由一系列autoreleasePoolPage组成的,并且每个autoreleasePoolPage大小都为4096字节,相关源码如下:

#define I386_PGBYTES 4096
#define PAGE_SIZE I386_PGBYTESclass AutoreleasePoolPage {magic_t const magic;//AutoreleasePoolPage 完整性校验id *next;//下一个存放autorelease对象的地址pthread_t const thread; //AutoreleasePoolPage 所在的线程AutoreleasePoolPage * const parent;//父节点AutoreleasePoolPage *child;//子节点uint32_t const depth;//深度,也可以理解为当前page在链表中的位置uint32_t hiwat;
}

自动释放池其实就是一个由AutoreleasePoolPage构成的双向链表,其结构中的childparent分别指向其前趋和后继。
在这里插入图片描述
单个AutoreleasePoolPage结构如下:
在这里插入图片描述

其中有 56 bit 用于存储AutoreleasePoolPage的成员变量。

  • 该结构体的第一个成员变量是magic,我们在isa中也学习过,isa中是分判对象是否未完成初始化,在这里也一样,用来检查这个节点是否已经被初始化了。
  • begin()和end()这两个类的实例方法帮助我们快速获取 0x100816038 ~ 0x100817000 这一范围的边界地址。
  • next:指向下一个为空的内存地址,如果next指向的地址加入一个object,它就会如下图所示移动到下一个为空的内存地址中,就像栈顶指针一样。
  • thread:保存了当前页所在的线程。
  • depth:表示page的深度,首次为0,每个page的大小都是4096字节(16进制0x1000),每次初始化一个page,depth都加一。
  • POOL_BOUNDARY:就是哨兵对象,它只是nil的别名,用于分隔Autoreleasepool。POOL_BOUNDARY直译过来就是POOL的边界。它的作用是隔开page中的对象。因为并不是每次push与pop之间存进的对象都刚好占满一个page,可能会不满,可能会超过,因此这个POOL_BOUNDARY帮助我们分隔每个@autoreleasepool块之间的对象。也就是说这个page可能存储很多个@autoreleasepool块的对象,使用POOL_BOUNDARY来隔开每个@autoreleasepool块的对象。
 #define POOL_BOUNDARY nil
objc_autoreleasePoolPush方法:
 void *objc_autoreleasePoolPush(void) {return AutoreleasePoolPage::push();
}

这里调用了AutoreleasePoolPage::push()方法:

 static inline void *push() 
{id *dest;// POOL_BOUNDARY就是nil// 首先将一个哨兵对象插入到栈顶if (DebugPoolAllocation) {// 区别调试模式// 调试模式下将新建一个链表节点,并将一个哨兵对象添加到链表栈中// Each autorelease pool starts on a new pool page.dest = autoreleaseNewPage(POOL_BOUNDARY);} else {dest = autoreleaseFast(POOL_BOUNDARY);}assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);return dest;
}

其中调用了autoreleaseFast方法,hotPage指的是当前正在使用的AutoreleasePoolPage

static inline id *autoreleaseFast(id obj)
{AutoreleasePoolPage *page = hotPage();if (page && !page->full()) {//有 hotPage 并且当前 page 不满,将object加入当前栈中return page->add(obj);} else if (page) {//有hotPage 但当前page已满,找未满页或创建新页,将object添加到新页中return autoreleaseFullPage(obj, page);} else {//无hotPage,创建hotPage,加入其中return autoreleaseNoPage(obj);}
}

有hotPage但当前page未满,直接调用page->add(obj)方法将对象添加到自动释放池中。

// 这其实就是一个压栈操作,将对象加入AutoreleasePoolPage,然后移动栈顶指针
id *add(id obj) {id *ret = next;*next = obj;next++;return ret;
}

这个方法其实就是一个压栈的操作,将对象加入 AutoreleasePoolPage 然后移动栈顶的指针。

有hotPage但当前page已满,(找到未满页或者创建新页,将object添加到新页中autoreleaseFullPage当前page满的时候调用)

static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
//一直遍历,直到找到一个未满的 AutoreleasePoolPage,如果找到最后还没找到,就新建一个 AutoreleasePoolPagedo {if (page->child) page = page->child;else page = new AutoreleasePoolPage(page);} while (page->full());//将找到的,或者构建的page作为hotPage,然后将obj加入setHotPage(page);return page->add(obj);
}

从传入的 page 开始遍历整个双向链表,直到查找到一个未满的 AutoreleasePoolPage

如果找到最后还是没找到创建一个新的 AutoreleasePoolPage

将找到的或者构建的page标记成 hotPage,然后调动上面分析过的 page->add 方法添加对象。

无hotPage,创建hotPage,加入其中:

这个时候,由于内存中没AutoreleasePoolPage,就要从头开始构建这个自动释放池的双向链表,那么当前页表作为第一张页表,是没有parent指针的。并且我们在第一次创建page时其首位都是要加POOL_SENTINEL标识的,方便让page知道在哪就结束了。

 static id *autoreleaseNoPage(id obj) {AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); // 创建AutoreleasePoolPagesetHotPage(page); // 设置page为当前页if (obj != POOL_SENTINEL) { // 加POOL_SENTINEL哨兵page->add(POOL_SENTINEL);}return page->add(obj); // 将obj加入
}
objc_autoreleasePoolPop方法:
 void objc_autoreleasePoolPop(void *ctxt) {AutoreleasePoolPage::pop(ctxt);
}

其调用的pop方法如下:

 static inline void pop(void *token) {AutoreleasePoolPage *page = pageForPointer(token);//使用 pageForPointer 获取当前 token 所在的 AutoreleasePoolPageid *stop = (id *)token;page->releaseUntil(stop);//调用 releaseUntil 方法释放栈中的对象,直到 stop 位置,stop就是传递的参数,一般为哨兵对象//调用 child 的 kill 方法,系统根据当前页的不同状态kill掉不同child的页面//releaseUntil把page里的对象进行了释放,但是page本身也会占据很多空间,所以要通过kill()来处理,释放空间if (page->child) {if (page->lessThanHalfFull()) { // 当前page小于一半满page->child->kill(); // 把当前页的孩子杀掉} else if (page->child->child) { // 否则,留下一个孩子,从孙子开始杀page->child->child->kill();}}
}

假设当前page一半都没满,说明剩余的page空间已经暂时够了,把多余的儿子page就可以kill掉,如果超过一半页,就认为下一半page还有存在的必要,所以kill孙子page,保留一个儿子page。

token
  • token是指向该pool的POOL_BOUNDARY指针
  • token的本质就是指向哨兵对象的指针,存储着每次push时插入的POOL_BOUNDARY的地址
  • 只有第一次push的时候会在page中插入一个POOL_BOUNDARY【或者page满了,或者没有hotPage需要使用新的page了】,并不是page的开头都一定是POOL_BOUNDARY
kill()方法
void kill() {AutoreleasePoolPage *page = this; // 获取当前页while (page->child) page = page->child; // child存在就一直往下找,直到找到一个不存在的AutoreleasePoolPage *deathptr;do {deathptr = page;page = page->parent;if (page) {page->unprotect();page->child = nil; // 将其child指向置nil,防止出现悬垂指针page->protect();}delete deathptr; // 删除} while (deathptr != this); // 直到this处停止
}

3. 总结

3.1 autoreleasePool的原理

自动释放池本质是autoreleasePoolPage结构体对象,是一个以栈结构存储的页,每一个autoreleasePoolPage都是以双向链表的形式来连接起来。

大小可根据宏定义查得数值为:4096字节

自动释放池的出栈和入栈主要通过objc_autoreleasePoolPushobjc_autoreleasePoolPop,实际上调用的是autoreleasePoolPagepushpop方法。

**push操作:**每次调用push操作都会创建一个新的autoreleasePoolPage,而autoreleasePoolPagePush的具体操作就是插入一个POOL BOUNDARY,并且返回插入POOL BOUNDARY的内存地址。在push中的操作,需要调用autoreleaseFast方法处理,具体情况分下面三个:

  • page存在并且未满,直接调用add方法将对象添加到page的next指针处,next指针递增。
  • page存在并且已满,需要调用autoreleaseFullPage,初始化一个新的page,然后通过add方法添加。
  • page不存在,需要先调用autoreleaseNoPage,创建一个hotPage,然后调用add方法添加对象到栈中。

**pop操作:**当执行pop操作的时候,会传入一个参数,这个参数是push操作的返回值,也就是POOLBOUNDARY的内存地址token,因此pop操作,就是根据token找到哨兵对象的位置,然后objc_release释放token之前的对象,把next指针指到正确的位置。

POOL_BOUNDARY是一种特殊标记,用于标记池的边界,就是说这个page可能存储很多个@autoreleasepool块的对象,在早期可能是一个特殊的指针或者整数值;在现代时,POOL_BOUNDARY可能是一个特殊的结构体或者无效指针。

整体思路:

通过push创建一个autoreleasePoolPage对象,会在开始的位置存放POOL BOUNDARY哨兵对象,然后将注册了autorelease的对象添加到存储到里面,调用pop的时候,会根据push返回的token的位置,释放到token之前的位置。

3.2 autoreleasePool的问题

3.2.1 autoreleasepool的嵌套操作

可以使用嵌套操作,其目的是控制应用程序的内存峰值,是其不要太高。

嵌套原因:自动释放池是以栈为节点,通过双向链表的形式链接,且与线程一一对应。

3.2.2 autoreleasepool的释放时机

在没有手动添加autoreleasePool的情况下,autorelease对象是在当前的runloop迭代结束的时候释放。

  1. 在准备进入runloop的时候,创建一个autoreleasePool(优先级最高)。
  2. runloop准备休眠的时候,会先释放掉autoreleasePool(优先级最低),然后创建一个新的autoreleasePool
  3. 在退出runloop的时候,就释放掉autoreleasePool
3.2.3 那些对象可以加入到autoreleasePool中
  1. 在MRC情况下,使用new,copy,alloc关键字修饰的对象或者retain持有的对象,不能加入到自动释放池中。
  2. MRC中设置为autorelease的对象,不需要手动,自动加入到autoreleasePool中。
  3. 所有autorelease的对象,在出了作用域之后,会被自动添加到最近创建的自动释放池中。
3.2.4 关于哨兵对象和next指针

next指针只有一个,永远指向下一个能存放autoreleasepool的地址,而哨兵对象有很多个,每个autoreleasepool都对应一个哨兵对象,标示这个autoreleasepool对象从哪里开始存。

3.2.5 next和child:

next指向下一个能存放autoreleasepool对象的地址,child是autoreleasePoolPage的参数,指向下一个page。

3.2.6 thread 和 AutoreleasePool的关系

每个线程都有与之关联的自动释放池堆栈结构,新的pool在创建时会被压栈到栈顶,pool销毁时,会被出栈,对于当前线程来说,释放对象会被压栈到栈顶,线程停止时,会自动释放与之关联的自动释放池

3.2.7 RunLoop 和 AutoreleasePool的关系

主程序的RunLoop在每次事件循环之前之前,会自动创建一个 autoreleasePool 并且会在事件循环结束时,执行drain操作,释放其中的对象

相关文章:

【iOS】—— autoreleasePool以及总结

【iOS】—— autoreleasePool以及总结 1. 什么是autoreleasePool2. autoreleasePoolPageobjc_autoreleasePoolPush方法:objc_autoreleasePoolPop方法:tokenkill()方法 3. 总结3.1 autoreleasePool的原理3.2 autoreleasePool的问题3.2.1 autoreleasepool的…...

培训第二十五天(python中执行mysql操作并将python脚本共享)

mysql下载路径&#xff1a; MySQL :: MySQL Community Downloads [root2 ~]# vim py001.pya3b4print(ab)print(a**2b**2)[root2 ~]# python py001.py 725[root2 ~]# python3>>> import random>>> random<module random from /usr/lib64/python3.6/random…...

LVS实战项目

LVS简介 LVS:Linux Virtual Server&#xff0c;负载调度器&#xff0c;内核集成&#xff0c;章文嵩&#xff0c;阿里的四层SLB(Server LoadBalance)是基于LVSkeepalived实现。 LVS集群的类型 lvs-nat &#xff1a; 修改请求报文的目标IP, 多目标 IP 的 DNAT lvs-dr &#xff…...

笔记小结:《利用python进行数据分析》之层次化索引

层次化索引 导入样例 层次化索引&#xff08;hierarchical indexing&#xff09;是pandas的一项重要功能&#xff0c;它使你能在一个轴上拥有多个&#xff08;两个以上&#xff09;索引级别。抽象点说&#xff0c;它使你能以低维度形式处理高维度数据。我们先来看一个简单的例…...

Linux的线程篇章 - 线程池、进程池等知识】

Linux学习笔记---018 Linux之线程池、进程池知识储备1、线程池1.1、池化技术1.1.1、定义与原理1.1.2、优点1.1.3、应用场景 1.2、线程池的特点与优势1.3、线程池的适用场景1.4、线程池的实现 2、进程池2.1、定义和基本概念2.2、进程池的特点与优势2.3、进程池的适用场景&#x…...

汇昌联信做拼多多运营正规吗?

汇昌联信在拼多多平台上的运营是否正规&#xff0c;是许多商家和消费者都关心的问题。随着电商行业的快速发展&#xff0c;平台运营的正规性直接关系到消费者的购物体验和商家的信誉。 一、公司背景与资质审核 明确回答问题&#xff1a;汇昌联信作为一家专业的电商运营公司&…...

240810-Gradio自定义Button按钮+事件函数+按钮图标样式设定

A. 最终效果 B. 参考代码 要通过自定义HTML按钮来触发Gradio自带按钮的 click 函数&#xff0c;你可以使用JavaScript来模拟点击Gradio的按钮。这里是一个示例代码&#xff0c;展示了如何实现这一点&#xff1a; import gradio as gr# 自定义的 JavaScript&#xff0c;用于捕…...

排序算法--快速排序

一、三色旗问题 问题描述 有一个数组是只由0&#xff0c;1&#xff0c;2三种元素构成的整数数组&#xff0c;请使用交换、原地排序而不是计数进行排序&#xff1a; 解题思路 1.定义两个变量&#xff0c;i和j&#xff08;下标&#xff09;&#xff0c;从index0开始遍历 2.如…...

springMVC -- 学习笔记

前言简介演示 ⇒ &#xff08;最简单的原理&#xff0c;开发并不这样&#xff0c;理解一下就好&#xff09;演示 ⇒ 接近真实注解开发&#xff08;好好理解一下&#xff09;重要的源码献上 Controller 讲解RequestMapper ⇒ 没啥记的&#xff0c;第一个案例看看就行了RestFul 风…...

修复本地终端(windows)连接服务器使用zsh出现乱跳的问题

目前市面上还没有发现解决方案&#xff0c;记录一下&#xff01; 1.起因&#xff1a; 在服务器配置了zsh后&#xff0c;用本地的windows去连接的时候&#xff0c;终端内容会出现乱跳&#xff0c;比如输入了一个“l”&#xff0c;后面出现多个“lll”&#xff0c;如下: ⚡ roo…...

【扒代码】regression_head.py

import torch from torch import nnclass UpsamplingLayer(nn.Module):# 初始化 UpsamplingLayer 类def __init__(self, in_channels, out_channels, leakyTrue):super(UpsamplingLayer, self).__init__() # 调用基类的初始化方法# 初始化一个序列模型&#xff0c;包含卷积层、…...

vue2 使用axios 请求后台返回文件流导出为excel

目录 步骤 1: 安装 Axios 步骤 2: 创建 Axios 实例 步骤 3: 发起请求并处理文件流 说明 步骤 1: 安装 Axios 首先&#xff0c;确保项目中已经安装了 Axios。如果没有&#xff0c;可以通过以下命令进行安装&#xff1a; npm install axios 步骤 2: 创建 Axios 实例 为了更…...

MATLAB数据可视化:在地图上画京沪线的城市连线

matlab自带的geoplot(lat,lon) 可以在地理坐标中绘制线条。使用 lat和lon分别指定以度为单位的经度和纬度坐标。 绘制京沪线所经城市线条&#xff1a; citys [116.350009,39.853928; 116.683546,39.538304; 117.201509,39.085318; 116.838715,38.304676;...116.359244,37.436…...

【AI】CV基础1

定期更新&#xff0c;建议关注更新收藏。 本站友情链接&#xff1a; OCR篇1 可变形卷积Deformable Conv opencv-python形态学操作合集 目录 仿射变换图像二阶导数本质探讨PIL通道、模式、尺寸、坐标系统、调色板、信息滤波器实现图像格式转换 OpenCV轮廓提取 仿射变换 仿射变换…...

数据结构《栈》

数据结构《栈》 1、栈的概念及结构2、栈的实现3、练习: 1、栈的概念及结构 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端 称为栈顶&#xff0c;另一端称为栈底。栈中的数据元素遵守后进先出LIFO&…...

说一说mysql的having?和where有什么区别?

在 MySQL 中&#xff0c;HAVING 子句和 WHERE 子句都是用于过滤查询结果的&#xff0c;但它们之间有一些重要的区别。下面我将详细介绍这两个子句的区别以及它们的使用场景。 1. HAVING 子句 作用: HAVING 子句用于过滤聚合后的结果集。它通常与 GROUP BY 子句一起使用&#x…...

LeetCode45. 跳跃游戏 II

题目链接&#xff1a; 45. 跳跃游戏 II - 力扣&#xff08;LeetCode&#xff09; 思路分析&#xff1a;这属于上一题的变种&#xff0c;思路有所不同&#xff0c;要用到贪心的思想。从第一步开始&#xff0c;在可以跳跃的范围内&#xff0c;选择能够到达最远位置的点将其作为…...

算法打卡 Day19(二叉树)-平衡二叉树 + 二叉树的所有路径 + 左叶子之和 + 完全二叉树的节点个数

Leetcode 101-平衡二叉树 文章目录 Leetcode 101-平衡二叉树题目描述解题思路 Leetcode 257-二叉树的所有路径题目描述解题思路 Leetcode 404-左叶子之和题目描述解题思路 Leetcode 222-完全二叉树的节点个数题目描述解题思路 题目描述 https://leetcode.cn/problems/balanced…...

国际以太网专线 (IEPL)/国际专线(IPLC)-全球覆盖,无界沟通

中国联通国际公司产品&#xff1a;国际以太网专线 (IEPL)/国际专线&#xff08;IPLC&#xff09;—— 跨境数据传输的坚实桥梁 在全球化日益加深的今天&#xff0c;跨境、跨地域的数据传输需求激增&#xff0c;企业对数据传输的速度、安全性和稳定性提出了前所未有的高要求。中…...

信息安全管理知识体系攻略(至简)

信息安全管理知识体系主要包括信息安全管理体系、信息安全策略、信息安全系统、信息安全技术体系等。 一、信息安全管理 1、信息安全管理体系&#xff08;ISMS&#xff09;。ISO27001是国际标准化组织&#xff08;ISO&#xff09;和国际电工委员会&#xff08;ICE&#xff09…...

HCIE学习笔记:IPV6 地址、ICMP V6、NDP 、DAD (更新补充中)

系列文章目录 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 系列文章目录前言一、IPV4、IPv6包头对比1. IPV4包头2.IPv6包头3.IPV6扩展包头 二、IPV6基础知识地址结构、地址分类三、ICMPV4、ICMPV61、 lnternet控…...

人工智能】Transformers之Pipeline(九):物体检测(object-detection)

目录​​​​​​​ 一、引言 二、物体检测&#xff08;object-detection&#xff09; 2.1 概述 2.2 技术原理 2.3 应用场景 2.4 pipeline参数 2.4.1 pipeline对象实例化参数 2.4.2 pipeline对象使用参数 2.4 pipeline实战 2.5 模型排名 三、总结 一、引言 pipel…...

[SWPUCTF 2021 新生赛]easy_md5

分析代码&#xff1a;1.包含flag2.php 2.GET传name&#xff0c;POST传password $name ! $password && md5($name) md5($password) 属于MD5绕过中的php 弱类型绕过 解题方法: 方法一 import requests# 网站的URL url "http://node7.anna.nssctf.cn:28026&q…...

Redis面试题大全

文章目录 Redis有哪几种基本类型Redis为什么快&#xff1f;为什么Redis6.0后改用多线程?什么是热key吗&#xff1f;热key问题怎么解决&#xff1f;什么是热Key&#xff1f;解决热Key问题的方法 什么是缓存击穿、缓存穿透、缓存雪崩&#xff1f;缓存击穿缓存穿透缓存雪崩 Redis…...

【langchain学习】BM25Retriever和FaissRetriever组合 实现EnsembleRetriever混合检索器的实践

展示如何使用 LangChain 的 EnsembleRetriever 组合 BM25 和 FAISS 两种检索方法&#xff0c;从而在检索过程中结合关键词匹配和语义相似性搜索的优势。通过这种组合&#xff0c;我们能够在查询时获得更全面的结果。 1. 导入必要的库和模块 首先&#xff0c;我们需要导入所需…...

【C语言】预处理详解(上)

文章目录 前言1. 预定义符号2. #define 定义常量3. #define定义宏4. 带有副作用的宏参数5. 宏替换的规则 前言 在讲解编译和链接的知识点中&#xff0c;我提到过翻译环境中主要由编译和链接两大部分所组成。 其中&#xff0c;编译又包括了预处理、编译和汇编。当时&#xff0c…...

uni-app内置组件(基本内容,表单组件)()二

文章目录 一、 基础内容1.icon 图标2.text3.rich-text4.progress 二、表单组件1.button2.checkbox-group和checkbox3.editor 组件4.form5.input6.label7.picker8.picker-view 和 picker-view-column9.radio-group 和 radio10.slider11.switch12.textarea 一、 基础内容 1.icon…...

linux搭建redis超详细

1、下载redis包 链接: https://download.redis.io/releases/ 我以7.0.11为例 2、上传解压 mkdir /usr/local/redis tar -zxvf redis-7.0.11.tar.gz3、进入redis-7.0.11&#xff0c;依次执行 makemake install4、修改配置文件redis.conf vim redis.conf为了能够远程连接redis…...

Flink-DataWorks第二部分:数据集成(第58天)

系列文章目录 数据集成 2.1 概述 2.1.1 离线&#xff08;批量&#xff09;同步简介 2.1.2 实时同步简介 2.1.3 全增量同步任务简介 2.2 支持的数据源及同步方案 2.3 创建和管理数据源 文章目录 系列文章目录前言2. 数据集成2.1 概述2.1.1 离线&#xff08;批量&#xff09;同步…...

4个从阿里毕业的P7打工人,当起了包子铺的老板

吉祥知识星球http://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247483727&idx1&sndb05d8c1115a4539716eddd9fde4e5c9&chksmc0e47813f793f105017fb8551c9b996dc7782987e19efb166ab665f44ca6d900210e6c4c0281&scene21#wechat_redirect 《网安面试指南》h…...