WebRTC源码分析:引用计数系统

引言

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<RTCStatsReport> statsReport = new RefCountedObject<RTCStatsReport>(...);

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<int> ref_count_;
};

正如上述源码可知,实际上该类使用了c++标准库的原子操作类std::atomic来保存引用计数:

扫描二维码关注公众号,回复: 14468966 查看本文章
  • 利用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 T>
class RefCountedObject : public T {
 public:
  RefCountedObject() {}

  template <class P0>
  explicit RefCountedObject(P0&& p0) : T(std::forward<P0>(p0)) {}

  template <class P0, class P1, class... Args>
  RefCountedObject(P0&& p0, P1&& p1, Args&&... args)
      : T(std::forward<P0>(p0),
          std::forward<P1>(p1),
          std::forward<Args>(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 T>
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<T>& r) : ptr_(r.ptr_) {
    if (ptr_)
      ptr_->AddRef();
  }
  // 1.4 拷贝构造,U必须是T的子类,同1.3
  template <typename U>
  scoped_refptr(const scoped_refptr<U>& r) : ptr_(r.get()) {
    if (ptr_)
      ptr_->AddRef();
  }
  // 1.5 移动构造(右值引用),表示对象的转移,亦即使用同一个对象,因此,需要保持
  //     对引用计数对象的引用次数不变。所以,此处调用scoped_refptr<T>的release()
  //     方法,将原来的scoped_refptr<T>对象内部引用计数指针置空,新的scoped_refptr<T>
  //     对象来保存引用计数对象,以达到转移的目的。   
  scoped_refptr(scoped_refptr<T>&& r) noexcept : ptr_(r.release()) {}
  // 1.6 移动构造,U必须是T的子类,同1.5
  template <typename U>
  scoped_refptr(scoped_refptr<U>&& 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<T>& 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<T>& operator=(const scoped_refptr<T>& r) {
  	// 取出智能指针的内部引用计数的指针,利用4.1的功能实现赋值
    return *this = r.ptr_;
  }
  // 4.3 赋值T的子类U的智能指针,新引用计数对象的引用计数+1,原来的-1
  //     具体使用过程中,这个赋值方法用得最多。
  template <typename U>
  scoped_refptr<T>& operator=(const scoped_refptr<U>& r) {
    // 使用get()方法取出智能指针的内部引用计数的指针,利用4.1的功能实现赋值
    return *this = r.get();
  }
  // 4.4 移动赋值右值智能指针,新引用计数对象的引用计数不变,原来引用计数对象的引用计数不变
  scoped_refptr<T>& operator=(scoped_refptr<T>&& r) noexcept {
    // 使用移动语义std::move + 移动构造 + swap进行引用计数对象的地址交换
    scoped_refptr<T>(std::move(r)).swap(*this);
    return *this;
  }
  // 4.5 移动赋值T的子类U的右值智能指针,新引用计数对象的引用计数不变,原来引用计数对象的引用计数不变
  template <typename U>
  scoped_refptr<T>& operator=(scoped_refptr<U>&& r) noexcept {
    // 使用移动语义std::move + 移动构造 + swap进行引用计数对象的地址交换
    scoped_refptr<T>(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<T>& 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<MyFoo> foo = new RefCountedObject<MyFoo>();
	foo->Method(param);
	//|foo| is released when this function returns
}

void some_other_function() {
	scoped_refptr<MyFoo> foo = new RefCountedObject<MyFoo>();
	...
	foo = nullptr;  // explicitly releases |foo|
	...
	if (foo)
		foo->Method(param);
}

上面的示例演示了scoped_refptr如何表现得像T*

6.2

{
	scoped_refptr<MyFoo> a = new RefCountedObject<MyFoo>();
	scoped_refptr<MyFoo> b;
	b.swap(a);
	// now, |b| references the MyFoo object, and |a| references null.
}

{
	scoped_refptr<MyFoo> a = new RefCountedObject<MyFoo>();
	scoped_refptr<MyFoo> 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↓↓↓↓↓↓见下面↓↓文章底部点击领取↓↓

猜你喜欢

转载自blog.csdn.net/m0_60259116/article/details/126290124
今日推荐