释放内存流程

news2024/10/2 16:24:01

在这里插入图片描述

你好,我是安然无虞。

thread cache回收内存

当从 thread cache 中申请的内存对象使用完毕需要还回来的时候, 只需要计算出该内存对象对应 thread cache 中的哪一个自由链表桶, 然后将该内存对象插入进去即可.

不过需要注意的是, 如果不断有内存对象释放回来, 那么可能就会导致 thread cache 当中的某一个或一些自由链表桶变得越来越长, 但是当前的 thread cache 暂时不会使用这些内存对象, 从而造成浪费.

所以我们规定, 当释放内存对象回 thread cache, 如果出现自由链表桶的长度大于一次批量申请内存对象的个数时, 我们需要取出这个自由链表桶中的一段内存对象还给 central cache, 这样一来, 如果其他线程有内存需求的时候, 这些内存对象是可以被它们申请到的.

// 释放内存对象
void ThreadCache::Deallocate(void* ptr, size_t size)
{
	assert(ptr);
	assert(size <= MAX_BYTES);

	// 将内存对象插入到对应的哈希桶中
	size_t index = SizeClass::Index(size);
	_freeLists[index].Push(ptr);

	// 释放对象链表过长时, 归还一段list给central cache的span
	if (_freeLists[index].Size() >= _freeLists[index].MaxSize())
	{
		ListTooLong(_freeLists[index], size);
	}
}

当自由链表的长度大于一次批量申请内存对象的个数时, 我们需要从该自由链表中取出一段长度为一次批量申请个数的内存对象, 然后将其还给 central cache 当中对应的span.

// 释放对象链表过长时, 归还一段list给central cache的span
void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
	void* start = nullptr;
	void* end = nullptr;
	// 从自由链表中取出一次批量申请个数的内存对象
	list.PopRange(start, end, list.MaxSize());

	// 将取出的一段内存对象还给central cache相应的span
	CentralCache::GetInstance()->ReleaseListToSpans(start, size);
}

观察上面的代码我们会发现需要对自由链表FreeList类的定义进行补充, 需要补充头删一段范围的成员函数PopRange, 以及一个成员变量_size用于统计自由链表的长度, 还有与它相关的成员函数Size, 当然了, 在对自由链表执行增删操作的时候, 需要对成员变量_size进行加减运算.

//管理切分好的小对象的自由链表
class FreeList
{
public:
	// 头插
	void Push(void* obj)
	{
		assert(obj);

		NextObj(obj) = _freeList;
		_freeList = obj;

		_size++;
	}

	// 头删
	void* Pop()
	{
		assert(_freeList);

		void* obj = _freeList;
		_freeList = NextObj(_freeList);

		_size--;

		return obj;
	}

	// 插入一段范围的对象到自由链表
	void PushRange(void* start, void* end, size_t n)
	{
		assert(start);
		assert(end);

		NextObj(end) = _freeList;
		_freeList = start;

		_size += n;
	}

	// 从自由链表中取出一段范围的对象
	void PopRange(void*& start, void*& end, size_t n)
	{
		assert(n <= _size);

		start = _freeList;
		end = start;

		for (size_t i = 0; i < n - 1; i++)
		{
			end = NextObj(end);
		}

		_freeList = NextObj(end); // 自由链表指向end的下一个对象
		NextObj(end) = nullptr; // 将取出的一段链表的尾置空

		_size -= n;
	}

	bool Empty()
	{
		return _freeList == nullptr;
	}

	size_t& MaxSize()
	{
		return _maxSize;
	}

	size_t Size()
	{
		return _size;
	}

private:
	void* _freeList = nullptr;
	size_t _maxSize = 1;
	size_t _size = 0;
};

补充说明:

实际上 TCMalloc 在判断 thread cache 是否应该还一部分对象给 central cache 时, 还有一个判断依据, 就是还会考虑 thread cache 整体的大小, 当其整体的大小超过某一个值时, 我们就会将 thread cache 中的一部分对象还给 central cache, 这也就有效避免了某个线程的 thread cache 占用了太多内存的情况.


central cache回收内存

前面我们说了当 thread cache 某个自由链表过长时, 会还一段内存对象给 central cache 对应的span, 但到底是 central cache 中哪一个span, 这是需要计算的, 因为 central cache 中有很多个SpanList双向链表结构, 每个双向链表里面又有很多个span, 所以不同的内存对象可能对应不同的span.

那么我们如何找到一个内存对象对应的span呢?

我们知道, 我们可以通过内存对象的地址计算出其所在的页号(内存对象的地址直接除以页的大小即可得出其所在的页号), 但是一个span可能有多个页, 所以我们只需要根据页号计算出其对应的span即可得出正确结果.

有了上面的推论, 我们可以建立页号和span的映射, 因为在 page cache 尝试做前后页的span的合并的时候也会用到这个映射关系, 所以将这个映射关系定义在 page cache 中.

// page cache本质是一个按页数映射的哈希桶
class PageCache
{
public:
	//获取从对象到span的映射
	Span* MapObjectToSpan(void* obj);
private:
	std::unordered_map<PAGE_ID, Span*> _idSpanMap; // 建立页号到span的映射
};

建立页号到span映射关系之后, 我们需要注意, 每当 page cache 分配大块span给 central cache 时, 都需要建立页号到span的映射, 这样一来, 之后 thread cache 再归还内存对象给 central cache 时, 就可以直接找到其所对应的span了.

所以我们需要对之前 page cache 中NewSpan函数中的代码进行修改:

// 获取一个K页的span
Span* PageCache::NewSpan(size_t k)
{
	assert(k > 0 && k < NPAGES);

	// 先检查第K个桶里面有没有span
	if (!_spanLists[k].Empty())
	{
		Span* kSpan = _spanLists[k].PopFront();

		//建立页号与span的映射,方便central cache回收小块内存时查找对应的span
		for (PAGE_ID i = 0; i < kSpan->_n; i++)
		{
			_idSpanMap[kSpan->_pageId + i] = kSpan;
		}

		return kSpan;
	}

	// 如果第k个桶里没有, 再找第K+1到最后一个桶
	for (size_t i = k + 1; i < NPAGES; i++)
	{
		if (!_spanLists[i].Empty())
		{
			Span* nSpan = _spanLists[i].PopFront();
			Span* kSpan = new Span;

			// 在nSpan的头部切一个k页下来
			// k页被返回
			// nSpan被挂回去
			kSpan->_pageId = nSpan->_pageId;
			kSpan->_n = k;

			nSpan->_pageId += k;
			nSpan->_n -= k;

			_spanLists[nSpan->_n].PushFront(nSpan);

			//建立页号与span的映射,方便central cache回收小块内存时查找对应的span
			for (size_t i = 0; i < kSpan->_n; i++)
			{
				_idSpanMap[kSpan->_pageId + i] = kSpan;
			}

			return kSpan;
		}
	}

	// 走到这个位置就说明后面没有大页的span了
	// 此时就需要向系统堆申请128页的span
	Span* bigSpan = new Span;
	void* ptr = SystemAlloc(NPAGES - 1);
	bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
	bigSpan->_n = NPAGES - 1;

	_spanLists[bigSpan->_n].PushFront(bigSpan);

	return NewSpan(k);
}

通过对象的地址找到其对应的span:

// 获取从内存对象到span的映射
Span* PageCache::MapObjectToSpan(void* obj)
{
	assert(obj);

	PAGE_ID id = (PAGE_ID)obj >> PAGE_SHIFT; // 计算出内存对象对应的页号
	auto ret = _idSpanMap.find(id);
	if (ret != _idSpanMap.end())
	{
		return ret->second;
	}
	else
	{
		assert(false);
		return nullptr;
	}
}

将 thread cache 中的一段内存对象还给 central cache, 是一个一个内存对象归还给 central cache 对应的span, 首先要计算出该内存对象对应的span, 然后将其头插到对应span的自由链表中, 然后_useCount–.

需要注意的是, 如果_useCount减到0了, 那么这个span切给 thread cache 用的内存对象全部还回来了, 此时这个span属于空闲的span, 会被 page cache 回收, 在此之前需要将该span从对应的双向链表中剥离下来, 然后将其对应的自由链表置空(因为page cache中的span不需要切成一个个内存对象), 以及双向链表的前后指针置空(因为之后要将其插入到page cache对应的双向链表中). 但是哦, span的大块内存的起始页号和页数是不能变的, 因为它们要用于表征这个span的位置及大小.

// 将一定数量的内存对象释放回span
void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
	assert(start);

	size_t index = SizeClass::Index(size);

	_spanLists[index]._mtx.lock();

	while (start)
	{
		void* next = NextObj(start);

		// 先计算出内存对象到span的映射
		Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
		NextObj(start) = span->_freeList;
		span->_freeList = start;
		span->_usecount--;

		// _useCount减到0说明span分配给thread cache的内存对象都回来了
		// 释放空闲的span, 并合并前后页span
		if (span->_usecount == 0)
		{
			_spanLists[index].Erase(span);
			span->_freeList = nullptr;
			span->_prev = nullptr;
			span->_next = nullptr;

			// 先解除桶锁, 使用page cache这把大锁就可以了
			// 方便其他执行流向该桶申请和释放内存
			_spanLists[index]._mtx.unlock();
			PageCache::GetInstance()->_pageMtx.lock();
			PageCache::GetInstance()->ReleaseSpanToPageCache(span);
			PageCache::GetInstance()->_pageMtx.unlock();
			_spanLists[index]._mtx.lock();

		}
		start = next;
	}

	_spanLists[index]._mtx.unlock();
}

最后就是关于加锁解锁的问题了, 在 central cache 还span给 page cache 时也是存在加锁解锁的, 首先要把 central cache 中对应的桶锁给解掉, 只使用 page cache 的大锁就可以了, 这样一来方便其他执行流向该桶中申请和释放内存不被阻塞, 当归还span给 page cache 之后, 记得要重新加上桶锁, 然后再执行上述过程将还未还完的内存对象继续还给 central cache.


page cache回收内存

前面我们说到, 当_useCount减到0的时候, 也就是span分配给 thread cache 使用的内存对象全部还回来了, 这个span就可以还给 page cache 了.

但是为了缓解内存碎片, 在回收span的基础上, 还需要尝试合并前后页的span, 以达到缓解内存碎片的目的.

page cache 如何进行前后页的合并?

前后页的合并也就是向前合并和向后合并, 向前合并首先需要计算出该span前一页的页号, 如果归还回来的span的起始页号是begin, 页数是n页, 那么它的前一页的页号就是begin-1, 然后再判断前一页对应的span是否适合合并, 如果适合合并, 就合并前一页的span, 然后继续向前合并; 向后合并跟前面的操作基本一致, 但需要注意的是, 后一页的起始页号是begin+n.

还有一点需要注意, 当我们通过页号计算出其所在的span时, 这个span可能挂在 page cache 中, 也可能挂在 central cache 中正在被使用, 正在 central cache 中被使用的span是不能合并的, 我们要合并的是 page cache 空闲的span, 所以仅根据_useCount减为0就将这个span合并是存在问题的, 因为当 page cache 刚分配span到 central cache 中_useCount就是0, 它还没有分配内存对象给 thread cache 使用, 所以我们应该在span的定义中增加一个成员变量_isUse用于表征当前的span是否正在被使用.

// Span 管理一个以页为单位的大块内存
struct Span
{
	PAGE_ID _pageId = 0; // 大块内存起始页的页号
	size_t _n = 0; // 页的数量

	Span* _prev = nullptr; // 双向链表的结构
	Span* _next = nullptr;

	void* _freeList = nullptr; // 切好的小块内存的自由链表
	size_t _usecount = 0; // 切好的小块内存被分配给thread cache的计数

	bool _isUse = false; // span是否正在被使用
};

前面我们提到 page cache 在做前后页的合并时, 需要根据首尾页号计算出对应的span, 所以我们在挂span到 page cache中时, 只需要建立该span的首尾页号到span的映射即可, 因为向前合并只需要根据当前span前一页的页号(也是该span的尾页)即可得到对应的span, 向后合并只需要根据当前span后一页的页号(也是该span的首页)即可计算出对应的span.

所以在NewSpan成员函数中, 我们在获取k页的span后, 不但要将k页span中的页号与span建立映射关系, 还需要将n-k页span的首尾页号与其建立映射关系, 方便合并前后页的span.

// 获取一个K页的span
Span* PageCache::NewSpan(size_t k)
{
	assert(k > 0 && k < NPAGES);

	// 先检查第K个桶里面有没有span
	if (!_spanLists[k].Empty())
	{
		Span* kSpan = _spanLists[k].PopFront();

		//建立页号与span的映射,方便central cache回收小块内存时查找对应的span
		// 分配给central cache使用的span, 每一页都要建立页号到span的映射
		for (PAGE_ID i = 0; i < kSpan->_n; i++)
		{
			_idSpanMap[kSpan->_pageId + i] = kSpan;
		}

		return kSpan;
	}

	// 如果第k个桶里没有, 再找第K+1到最后一个桶
	for (size_t i = k + 1; i < NPAGES; i++)
	{
		if (!_spanLists[i].Empty())
		{
			Span* nSpan = _spanLists[i].PopFront();
			Span* kSpan = new Span;

			// 在nSpan的头部切一个k页下来
			// k页被返回
			// nSpan被挂回去
			kSpan->_pageId = nSpan->_pageId;
			kSpan->_n = k;

			nSpan->_pageId += k;
			nSpan->_n -= k;

			_spanLists[nSpan->_n].PushFront(nSpan);

			// 挂到page cache中的span, 只需要建立其前后页到span的映射即可, 用于前后页的合并
			_idSpanMap[nSpan->_pageId] = nSpan;
			_idSpanMap[nSpan->_pageId + nSpan->_n - 1] = nSpan;

			for (size_t i = 0; i < kSpan->_n; i++)
			{
				_idSpanMap[kSpan->_pageId + i] = kSpan;
			}

			return kSpan;
		}
	}

	// 走到这个位置就说明后面没有大页的span了
	// 此时就需要向系统堆申请128页的span
	Span* bigSpan = new Span;
	void* ptr = SystemAlloc(NPAGES - 1);
	bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
	bigSpan->_n = NPAGES - 1;

	_spanLists[bigSpan->_n].PushFront(bigSpan);

	return NewSpan(k);
}

理解了上面的内容, 我们就可以对前后页的span进行合并了.

// 释放空闲的span到page cache, 并合并相邻的span
void PageCache::ReleaseSpanToPageCache(Span* span)
{
	assert(span);

	// 尝试合并前后页span, 缓解内存碎片的问题

	// 向前合并
	while (1)
	{
		PAGE_ID prevId = span->_pageId - 1; // 前一页
		// 1. 前一页对应的span不存在, 不合并了
		auto ret = _idSpanMap.find(prevId);
		if (ret == _idSpanMap.end())
		{
			break;
		}

		// 2. 前一页对应的span被使用了, 不合并了
		Span* prevSpan = ret->second;
		if (prevSpan->_isUse == true)
		{
			break;
		}

		// 3. 合并出比128页大的span没办法管理, 不合并了
		if (span->_n + prevSpan->_n > NPAGES - 1)
		{
			break;
		}

		// 更新span的起始页号和页数
		span->_pageId = prevSpan->_pageId;
		span->_n += prevSpan->_n;
	
		// 将前一页的span从对应的双向链表中剥离
		_spanLists[prevSpan->_n].Erase(prevSpan);
		delete prevSpan;
	}

	// 向后合并
	while (1)
	{
		PAGE_ID nextId = span->_pageId + span->_n; // 后一页

		// 1. 后一页的span不存在, 不合并了
		auto ret = _idSpanMap.find(nextId);
		if (ret == _idSpanMap.end())
		{
			break;
		}

		// 2. 后一页的span被使用了, 不合并了
		Span* nextSpan = ret->second;
		if (nextSpan->_isUse == true)
		{
			break;
		}

		// 3. 合并出了超过128页的span不能管理, 不合并了
		if (span->_n + nextSpan->_n > NPAGES - 1)
		{
			break;
		}

		span->_n += nextSpan->_n;

		_spanLists[nextSpan->_n].Erase(nextSpan);
		delete nextSpan;
	}
	
	// 将合并后的span挂到page cache对应的双向链表中
	_spanLists[span->_n].PushFront(span);
	span->_isUse = false; // 将状态设置为未被使用

	// 建立span与其首尾页的映射
	_idSpanMap[span->_pageId] = span;
	_idSpanMap[span->_pageId + span->_n - 1] = span;
}

上面的代码需要注意三点:

  • 通过页号获取对应的span, 如果该span暂时不存在, 停止合并;
  • 如果获取到的span正在被使用, 停止合并;
  • 如果合并出了大于128页的span导致没办法管理, 停止合并.

合并结束后, 需要将span挂到对应的双向链表中, 并且需要建立前后页到该span的映射, 方便后面合并出更大的span.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/369769.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java实现根据拼音首字母的排序

1.项目 手机APP端要对企业列表按企业名称首字母(如果企业名是英文的就按)进行分类排序&#xff0c;效果如下&#xff1a; 2.实现过程 2.1 首先引入项目的pinyin4j-2.5.0.jar包。 这个jar的下载地址如下&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1hkP_gGAYcgzyK_D…

跨链桥:Web3黑客必争之地

跨链桥&#xff0c;区块链的基础设施之一&#xff0c;所实现的功能是允许用户将自己的资产从一条链转移至另外一条链上&#xff0c;是连接不同的区块链的关键桥梁&#xff0c;常使用中心化的方式进行实现。由于跨链桥自身往往存储有用户所质押的巨额资产&#xff0c;是Web3黑客…

KUKA KR C4机器人与S7-1200PLC进行PROFINET通信的具体方法和步骤

KUKA KR C4机器人与S7-1200PLC进行PROFINET通信的具体方法和步骤 首先,从KUKA机器人控制柜中将KOP备选软件包拷贝出来,然后在“WorkVisual Development Environment”安装KUKA备选软件包(版本非常重要,尽量从控制柜中拷贝), 也可以从以下链接中获取: KUKA机器人PROFINET…

php 任务调度

在日常开发中&#xff0c;我们总会遇到一些在某个指定的时刻去执行&#xff0c;或是每隔xx时间执行&#xff0c;或是需要一直在后台监听的任务执行。基于这个需求&#xff0c;对于php我找了一些办法来实现这些功能 1、依赖于laravel的任务调度。 每隔xx时间执行一次命令&#…

七、虚拟机栈

虚拟机栈出现的背景 1.由于跨平台性的设计&#xff0c;Java的指令都是根据栈来设计的&#xff0c;不同平台CPU架构不同&#xff0c;所以不能设计为基于寄存器的。 2.优点是跨平台&#xff0c;指令集小&#xff0c;编译器容易实现&#xff0c;缺点是性能下降&#xff0c;实现同…

XC7K70T-1FBG676I【FPGA】XC3S200-4FTG256C参数XC7K70T-2FBG676C

Kintex-7 FPGA为快速增长应用和无线通信提供最优性价比和低功耗。Kintex-7 FPGA允许设计人员构建卓越带宽和12位数字可编程模拟&#xff0c;同时满足成本和功耗要求。Kintex-7内置支持8通道PCI Express (Gen1/Gen2)&#xff0c;用于连接主机系统。7系列器件利用Xilinx统一架构保…

如何实现外网跨网远程控制内网计算机?快解析来解决

远程控制&#xff0c;是指管理人员在异地通过计算机网络异地拨号或双方都接入Internet等手段&#xff0c;连通需被控制的计算机&#xff0c;将被控计算机的桌面环境显示到自己的计算机上&#xff0c;通过本地计算机对远方计算机进行配置、软件安装程序、修改等工作。通俗来讲&a…

超图软件许可过期后的处理

超图软件许可过期&#xff0c;博主犯了一个很低级的错误&#xff0c;特此发帖记录一下。 按照官网的介绍&#xff0c;直接申请新的许可&#xff0c;配置本地许可很简单的。 1.打开桌面软件会弹出以下的框。 2.配置本地许可对话框 3.切换到激活更新页面 4.选择激活文件即可 以…

内容团队如何快速出稿

对于内容团队而言&#xff0c;每个内容选题就相当于一个小项目&#xff0c;它们并非简单的线性工作流&#xff0c;相反其复杂程度不亚于一个小型工厂。一个内容选题会涉及内容形式&#xff0c;选题类型等多个变量&#xff0c;这些变量因素组合起来就是十几种不同类型的工作流。…

我花6个月从0开始面上大厂自动化测试岗,拿个20K不过分吧?

我是着急忙慌的准备简历——3年软件测试经验&#xff0c;可独立测试大型产品项目&#xff0c;熟悉项目测试流程...薪资要求&#xff1f; 3年测试经验起码能要个20K吧&#xff1f;” 我一个朋友跟我说&#xff1a; “我加班肝了一页半简历&#xff0c;投出去一周&#xff0c;…

03--框架基础

1、目录结构1&#xff09;创建项目使用测试号或小程序申请成功之后的appid即可&#xff0c;注意这里选择“不使用云服务”&#xff0c;选择javascript模板工具结构如图目录结构小程序包含一个描述整体程序的 app 和多个描述各自页面的 page。一个小程序主体部分由三个文件组成&…

python自学之《21天学通Python》(13)——第16章 数据库编程

数据库指的是以一定方式存储在一起、能为多个用户共享、具有尽可能小的冗余度、与应用程序彼此独立的数据集合。而我们平时所说的数据库实际上是包含了数据库管理系统&#xff08;DBMS&#xff09;的&#xff0c;数据库管理系统是为管理数据库而设计的软件系统&#xff0c;它一…

jvm调优参数配置

在JVM启动参数中&#xff0c;可以设置跟内存、垃圾回收相关的一些参数设置&#xff0c;默认情况不做任何设置JVM会工作的很好&#xff0c;但对一些配置很好的Server和具体的应用必须仔细调优才能获得最佳性能。通过设置我们希望达到一些目标&#xff1a; GC的时间足够的小 GC的…

Homekit智能家居DIY一WIFI智能插座

WiFi智能插座对于新手接触智能家居产品更加友好&#xff0c;不需要额外购买网关设备 很多智能小配件也给我们得生活带来极大的便捷&#xff0c;智能插座就是其中之一&#xff0c;比如外出忘记关空调&#xff0c;可以拿起手机远程关闭。 简单说就是&#xff1a;插座可以连接wi…

如何外网登录管理云通信短信网关平台?——快解析映射方案

云通信&#xff08;Cloud Communications &#xff09;是基于云计算商业模式应用的通信平台服务&#xff0c;简单易用,满足企业一键群发场景,支持多种语言SDK和API 接入。各个通信平台软件都集中在云端&#xff0c;且互通兼容&#xff0c;用户只要登录云通信平台&#xff0c;不…

【玩家心得】Smurf Society 游戏攻略

在深林的深处&#xff0c;生活着一群无忧无虑、快乐的小精灵&#xff0c;浑身蓝色&#xff0c;叫做蓝精灵。蓝精灵住在自己村子里蘑菇屋里&#xff0c;精灵爸爸、精灵妹妹、笨笨、乐乐等使得精灵村每天都欢声笑语。可是&#xff0c;在森林深处的城堡里住着一个邪恶的巫师格格巫…

量化择时——资金流择时策略(第1部分—因子测算)

文章目录资金流模型概述资金流模型的有效性逻辑资金流向指标MFI&#xff08;Money Flow Index&#xff09;MFI指标测算测算规则测算结论资金流模型概述 通常&#xff0c;资金流是一种反映股票供给信息的指标&#xff0c;宏观上来讲&#xff0c;我们知道一个道理&#xff1a;僧…

仅花半年时间,他从外包月薪5K到阿里月薪15K,究竟经历了什么?

背景介绍&#xff1a;“渣渣”二本&#xff0c;95年Java程序员**外包类型&#xff1a;**传统外包公司**内容简介&#xff1a;**朋友从一个传统公司是如何修仙到阿里巴巴&#xff1f;分享一些他的真实经历&#xff0c;希望对你有帮助。**学习路线&#xff1a;**基础&#xff08;…

电阻串联的作用

电阻串联常见作用 第一个作用是&#xff1a;阻抗匹配&#xff1a; 因为信号源的阻抗很低&#xff0c;跟信号线之间阻抗不匹配&#xff0c;串上一个电阻后&#xff0c;可以改善匹配情况&#xff0c;以减少反射&#xff0c;避免振荡等。 常见的阻抗匹配方法 1、使用变压器来做…

(十)、kityminder支持富文本的编辑

前段时间&#xff0c;去试用了下processon 上的脑图功能&#xff0c;发现人家这块确实已经做的好强大了。而且他的节点竟然还可以支持单独某个文本的颜色字体的设置&#xff0c;这个可是连xmind&#xff0c;本身都没有实现的功能的。所以想着学习下人家的实现看看是否能够借鉴到…