作者:feloveyu | 来源:互联网 | 2023-07-24 21:26
引言
WebRTC中自己实现了一套引用计数系统,在其基础库模块rtc_base/refcount中提供了相关实现,如下图所示:
主要由四个类RefCountInterface、RefCounter、RefCountedObject、scoped_refptr一起构建起WebRTC中的引用计数系统。
2. RefCountInterface——引用计数抽象接口
RefCountInterface是一个抽象接口类,位于rtc_base/ref_count.h目录下。源码如下:
enum class RefCountReleaseStatus { kDroppedLastRef, kOtherRefsRemained };
// Interfaces where refcounting is part of the public api should
// inherit this abstract interface. The implementation of these
// methods is usually provided by the RefCountedObject template class,
// applied as a leaf in the inheritance tree.
class RefCountInterface {
public:
virtual void AddRef() cOnst= 0;
virtual RefCountReleaseStatus Release() cOnst= 0;
// Non-public destructor, because Release() has exclusive responsibility for
// destroying the object.
protected:
virtual ~RefCountInterface() {}
};
正如该类的英文注释所说那样:需要使用引用计数的公共接口类需要继承RefCountInterface这个抽象接口。但公共接口类并不直接实现RefCountInterface的纯虚方法AddRef() && Release()。这两个方法由后文的模板对象RefCountedObject对象来提供具体的实现。
比如WebRTC中的类RTCStatsReport,它继承了RefCountInterface,但是RTCStatsReport类并没有实现AddRef()和Release()这两个纯虚方法,也即RTCStatsReport仍然是个抽象类。而使用该类时,不能直接new RTCStatsReport(因为它是抽象类)。 一般会如此使用:
rtc::scoped_refptr statsReport = new RefCountedObject(...);
RefCountedObject这个类继承了RTCStatsReport,并为AddRef()和Release()两个纯虚方法提供了实现。
本文福利, C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)↓↓↓↓↓↓见下面↓↓文章底部点击领取↓↓
如此设计后,这三个类的继承关系就变为:(RefCountInterface就是继承链上的叶子节点)
- AddRef():将对象的引用计数线+1
- Release():将对象的引用计数-1,若引用计数为0,即kDroppedLastRef。那么对象将在Release()中被delete。
RefCounter——引用计数
RefCounter是实现引用计数的核心类,代码位于rtc_base/ref_counter.h文件中。该类的对象用来保存被引用对象的计数值。
class RefCounter {
public:
explicit RefCounter(int ref_count) : ref_count_(ref_count) {}
RefCounter() = delete;
void IncRef() {
ref_count_.fetch_add(1, std::memory_order_relaxed);
}
rtc::RefCountReleaseStatus DecRef() {
int ref_count_after_subtract =
ref_count_.fetch_sub(1, std::memory_order_acq_rel) - 1;
return ref_count_after_subtract == 0
? rtc::RefCountReleaseStatus::kDroppedLastRef
: rtc::RefCountReleaseStatus::kOtherRefsRemained;
}
bool HasOneRef() const {
return ref_count_.load(std::memory_order_acquire) == 1;
}
private:
std::atomic ref_count_;
};
正如上述源码可知,实际上该类使用了c++标准库的原子操作类std::atomic来保存引用计数:
- 利用std::atomica类的 fetch_add() 方法来实现+1的操作函数IncRef();
- 利用std::atomic类的 fetch_sub() 方法来实现-1的操作函数DecRef();
- 利用std::atomic类的 load() 方法来实现是否刚好引用计数为1的操作函数HasOneRef();
- fetch_add()、fetch_sub() 、load() 方法都传入了指示CPU指令中插入内存屏障,防止某种指令重排序的参数,详细解释请查看 https://zh.cppreference.com/w/cpp/atomic/memory_order
4. RefCountedObject——引用计数对象
RefCountedObject模板类是正真实现应用计数增减方法 AddRef 和 Release的地方,实质上是通过持有成员RefCounter来实现的。那么RefCountedObject——>利用RefCounter——>利用std::atomic ref_count_来进行计数的。该类的UML类图如下所示
该类的源码如下:
template
class RefCountedObject : public T {
public:
RefCountedObject() {}
template
explicit RefCountedObject(P0&& p0) : T(std::forward(p0)) {}
template
RefCountedObject(P0&& p0, P1&& p1, Args&&... args)
: T(std::forward(p0),
std::forward(p1),
std::forward(args)...) {}
virtual void AddRef() const { ref_count_.IncRef(); }
virtual RefCountReleaseStatus Release() const {
const auto status = ref_count_.DecRef();
if (status == RefCountReleaseStatus::kDroppedLastRef) {
delete this;
}
return status;
}
virtual bool HasOneRef() const { return ref_count_.HasOneRef(); }
protected:
virtual ~RefCountedObject() {}
mutable webrtc::webrtc_impl::RefCounter ref_count_{0};
RTC_DISALLOW_COPY_AND_ASSIGN(RefCountedObject);
};
代码实现简单,也容易理解,有这么几点需要重点强调下:
- RefCountedObject是继承T的,是T的子类;
- RefCountedObject的构造函数使用了c++11中的右值引用&& 以及完美转发std::forward来提高效率;
- Release()函数的实现,当引用计数为0时,将delete掉自己!!!
- ref_count_成员被初始化为0,并且声明为mutable,表示该成员是可变的,将不受子类可能出现的const修饰符的影响;
- 最后,RTC_DISALLOW_COPY_AND_ASSIGN(RefCountedObject) 对 RefCountedObject进行了修饰,表示RefCountedObject不允许赋值和拷贝。注意RTC_DISALLOW_COPY_AND_ASSIGN所放位置是RefCountedObject声明的内部
PS1: 完美转发std:forward作用:在模板内,如果我们需要将一组参数原封不动的传递给另外一个参数,在没有完美转发的情况下,考虑参数会有多种类型的重载,因此在没有完美转发的情况下,重载函数个数将会达到2^n个,多么庞大的人工量。当使用std::forward辅以模板参数推导规则以保持参数属性不变,实现完美转发节省了大量的工作。std::forward分析可见:详解C++11中移动语义(std::move)和完美转发(std::forward)
PS2: RTC_DISALLOW_COPY_AND_ASSIGN(RefCountedObject) 宏展开后相当于两个语句:即移除了默认的拷贝构造和赋值运算符。
RefCountedObject(const RefCountedObject&) = delete;
RefCountedObject& operator=(const RefCountedObject&) = delete
5. rtc::scoped_refptr——智能指针
本文福利, C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)↓↓↓↓↓↓见下面↓↓文章底部点击领取↓↓
rtc::scoped_refptr是RTC中为了使用引用计数对象的智能指针,实现位于api层的api/scoped_refptr.h中。
使用智能指针,避免直接使用引用计数对象、手动调用引用计数对象的AddRef()、Release()。可以有效防止忘记Release一个对象所引发的内存泄漏。
template
class scoped_refptr {
public:
typedef T element_type;
// 1. 6个构造函数
// 1.1 默认构造,传入空的引用计数对象
scoped_refptr() : ptr_(nullptr) {}
// 1.2 构造,传入引用计数对象p的指针,调用其AddRef()方法,引用计数+1。
scoped_refptr(T* p) : ptr_(p) { // NOLINT(runtime/explicit)
if (ptr_)
ptr_->AddRef();
}
// 1.3 拷贝构造,对应的引用计数+1,调用引用计数对象的AddRef()方法。
scoped_refptr(const scoped_refptr& r) : ptr_(r.ptr_) {
if (ptr_)
ptr_->AddRef();
}
// 1.4 拷贝构造,U必须是T的子类,同1.3
template
scoped_refptr(const scoped_refptr& r) : ptr_(r.get()) {
if (ptr_)
ptr_->AddRef();
}
// 1.5 移动构造(右值引用),表示对象的转移,亦即使用同一个对象,因此,需要保持
// 对引用计数对象的引用次数不变。所以,此处调用scoped_refptr的release()
// 方法,将原来的scoped_refptr对象内部引用计数指针置空,新的scoped_refptr
// 对象来保存引用计数对象,以达到转移的目的。
scoped_refptr(scoped_refptr&& r) noexcept : ptr_(r.release()) {}
// 1.6 移动构造,U必须是T的子类,同1.5
template
scoped_refptr(scoped_refptr&& r) noexcept : ptr_(r.release()) {}
// 2 析构函数,调用引用计数对象Release(),引用计数-1
~scoped_refptr() {
if (ptr_)
ptr_->Release();
}
// 3. get、release、()、->()方法
T* get() const { return ptr_; }
operator T*() const { return ptr_; }
T* operator->() const { return ptr_; }
T* release() {
T* retVal = ptr_;
ptr_ = nullptr;
return retVal;
}
// 4. 重载赋值运算符
// 4.1 赋值新的引用计数对象的指针,新引用计数对象的引用计数+1,原来的-1
scoped_refptr& operator=(T* p) {
// AddRef first so that self assignment should work
// 先增加引用,再减小原来的引用,这样可以使得自赋值能正常工作
if (p)
p->AddRef();
if (ptr_)
ptr_->Release();
ptr_ = p;
return *this;
}
// 4.2 赋值智能指针,新引用计数对象的引用计数+1,原来的-1
scoped_refptr& operator=(const scoped_refptr& r) {
// 取出智能指针的内部引用计数的指针,利用4.1的功能实现赋值
return *this = r.ptr_;
}
// 4.3 赋值T的子类U的智能指针,新引用计数对象的引用计数+1,原来的-1
// 具体使用过程中,这个赋值方法用得最多。
template
scoped_refptr& operator=(const scoped_refptr& r) {
// 使用get()方法取出智能指针的内部引用计数的指针,利用4.1的功能实现赋值
return *this = r.get();
}
// 4.4 移动赋值右值智能指针,新引用计数对象的引用计数不变,原来引用计数对象的引用计数不变
scoped_refptr& operator=(scoped_refptr&& r) noexcept {
// 使用移动语义std::move + 移动构造 + swap进行引用计数对象的地址交换
scoped_refptr(std::move(r)).swap(*this);
return *this;
}
// 4.5 移动赋值T的子类U的右值智能指针,新引用计数对象的引用计数不变,原来引用计数对象的引用计数不变
template
scoped_refptr& operator=(scoped_refptr&& r) noexcept {
// 使用移动语义std::move + 移动构造 + swap进行引用计数对象的地址交换
scoped_refptr(std::move(r)).swap(*this);
return *this;
}
// 5 地址交换函数
// 5.1 引用计数对象的地址交换
void swap(T** pp) noexcept {
T* p = ptr_;
ptr_ = *pp;
*pp = p;
}
// 5.2 智能智能内部的引用计数对象的地址交换,利用5.1
void swap(scoped_refptr& r) noexcept { swap(&r.ptr_); }
protected:
T* ptr_;
};
rtc::scoped_refptr的成员方法大致按照源码的注释所述,分为5类。具体就不再详述,见注释即可。需要引起重视的有如下几个观点:
- rtc::scoped_refptr的构造 和 赋值 将调用引用计数对象的AddRef()方法,使得引用计数+1;
- rtc::scoped_refptr的析构 将调用引用计数对象的Release()方法,使得引用计数-1,引用计数减少为0,则引用计数对象将再Release()方法中被delete。
- rtc::scoped_refptr移动构造 和 移动赋值 将不会增加引用计数对象的计数,只会交换智能指针内部保存的引用计数对象的地址,右值引用的内部引用计数对象指针将被赋值为nullptr。
- 小写的release()方法不可轻易调用,需要在理解其作用的条件下谨慎使用。因为,release()方法会使智能指针内部存储的ptr_为空,失去对引用计数对象的引用,而引用计数没有-1。
6. 使用举例
在此,举两个WebRTC源码中介绍rtc::scoped_refptr时的小示例:
6.1
源码如下:
class MyFoo : public RefCountInterface {
...
};
void some_function() {
scoped_refptr foo = new RefCountedObject();
foo->Method(param);
//|foo| is released when this function returns
}
void some_other_function() {
scoped_refptr foo = new RefCountedObject();
...
foo = nullptr; // explicitly releases |foo|
...
if (foo)
foo->Method(param);
}
上面的示例演示了scoped_refptr如何表现得像T*
6.2
{
scoped_refptr a = new RefCountedObject();
scoped_refptr b;
b.swap(a);
// now, |b| references the MyFoo object, and |a| references null.
}
{
scoped_refptr a = new RefCountedObject();
scoped_refptr b;
b = a;
// now, |a| and |b| each own a reference to the same MyFoo object.
}
第一个{…}演示了两个智能指针对象之间如何交换底层保存的引用计数对象。通过swap(),最终b引用了a之间保存的引用计数对象,而a则引用nullptr,因为b之前就是引用nullptr。
第二个{…}演示了如何让智能指针b和a引用同一个引用计数对象,简单的赋值即可。上述示例将使得b也引用了之前a保存的引用计数对象,引用计数为2。若是语句b = a 改为 a = b,那么a和b都将引用空对象nullptr,之前a保存的引用计数对象RefCountedObject的引用计数将被减为0,从而被delete掉。
7 总结
行文至此,大致对WebRTC源码中引用计数系统实现以及使用方式做了简要的介绍,现在回顾下要点
- rtc_base中的RefCountInterface、RefCounter、RefCountedObject三个类构建起了引用计数系统。注意文章中所画的继承关系图,对理解引用计数系统的实现至关重要。
- 引用计数的本身是利用c++的原子操作类atomic的特性来实现的。
- 引用计数系统的使用需要配合智能指针rtc::scoped_refptr。一般不要直接使用引用计数对象的AddRef() 和 Release(),因为这种手动操作可能会由于人为编程失误引发内存泄漏。而应该通过rtc::scoped_refptr来保存、使用"引用计数对象"。
- rtc::scoped_refptr的代码值得反复看几遍,实现方式是非常优雅的。
本文福利, C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)↓↓↓↓↓↓见下面↓↓文章底部点击领取↓↓