C++是通过 new 和 delete 来管理动态内存的分配与释放,但是有时候开发者可能忘记使用 delete。Google 为了防止内存泄露以及加快开发效率,创建了 RefBase, sp, wp 这些类,这些类的路径如下。
- system/core/libutils/include/utils/RefBase.h
- system/core/libutils/RefBase.cpp
- system/core/libutils/include/utils/StrongPointer.h
- system/core/libutils/StrongPointer.cpp
有时候,我们需要站在巨人的肩膀上,才能看得更远。因此,我先看了下邓凡平老师写的<<深入理解Android卷1>>这一书。因此本文也借鉴书中的例子,代码如下
#include <utils/RefBase.h>
#include <utils/StrongPointer.h>
class A : public RefBase {};
int main()
{
A * pa = new A;
sp<A> spa(pa);
wp<A> wpa(pa);
// 这里不需要使用C++的方式来释放内存
// delet pa;
return 0;
}
从这个简单的例子中可以看出,虽然使用了 new 分配了动态内存,但是并没有使用 delete 来释放动态内存,这是因为这个例子做了如下两点操作
- 让类 A 继承自 RefBase 类。
- sp 或 wp 对象销毁时,自动释放了动态内存。
sp 是 Strong Pointer 的意思,wp 是 Weak Pointer 的意思。类似于 Java 的强引用,弱引用。
RefBase
例子中,先创建 A 对象,会调用 RefBase 构造函数
class RefBase
{
// ...
protected:
RefBase();
// ...
};
RefBase 只有一个构造函数,并且访问权限还是 protected,那么就说明,这个类只能用作基类。
现在看下 RefBase 构造函数的实现
RefBase::RefBase(): mRefs(new weakref_impl(this))
{
}
RefBase 构造函数,只初始化了它的成员变量 mRefs 指针,而它被初始化为 weakref_impl 对象。
class RefBase::weakref_impl : public RefBase::weakref_type
{
// ...
explicit weakref_impl(RefBase* base)
: mStrong(INITIAL_STRONG_VALUE) // mStrong 代表强引用计数,初始值为 1<<28
, mWeak(0) // mWeak 代表弱引用计数
, mBase(base) // mBase 指向 RefBase 派生类对象
, mFlags(0) // mFlags 代表 RefBase 派生类对象的生命周期
{
}
// ...
}
weakref_impl 是一个非常关键的类,它的成员变量的含义如下
- mStrong : 它表示强引用计数,初始值为 INITIAL_STRONG_VALUE ,也就是 1<<28。
- mWeak : 它表示弱引用计数,初始值为0.
- mBase : 是一个指针,指向 RefBase 派生类的对象,也就是例子中的 A 对象。
- mFlags : 主要控制了 RefBase 派生类的生命周期,初始值为0。mFlags 的有效值有如下几个
enum {
OBJECT_LIFETIME_STRONG = 0x0000,
OBJECT_LIFETIME_WEAK = 0x0001,
OBJECT_LIFETIME_MASK = 0x0001
};
我们来总结下,创建 RefBase 派生类对象,其实最主要就是为了创建管理强、弱引用计数,以及控制生命周期的 weakref_impl 对象。
sp
构造函数
例子中,通过sp<A> spa(pa);
创建一个 sp<A>
对象,构造函数实现如下
template<typename T>
sp<T>::sp(T* other)
: m_ptr(other) {
if (other)
other->incStrong(this);
}
指针 m_ptr 其实指向 RefBase 派生类对象,这里也就是例子中的 A 对象。构造函数中调用了 A 对象的 incStrong() 函数,它是由 RefBase 类实现的
void RefBase::incStrong(const void* id) const
{
weakref_impl* const refs = mRefs;
// 弱引用计数加1
refs->incWeak(id);
// 非debug版本,以add, remove, rename开关的函数都是空实现
refs->addStrongRef(id);
// 强引用加1,并返回旧值,这里返回的是 INITIAL_STRONG_VALUE
const int32_t c = refs->mStrong.fetch_add(1, std::memory_order_relaxed);
// 如果强引用的旧值,不等于初始值,那么代表现在的强引用计数为1,或2,或3,等等
if (c != INITIAL_STRONG_VALUE) {
return;
}
// 强引用减去 INITIAL_STRONG_VALUE,现在值为1
int32_t old __unused = refs->mStrong.fetch_sub(INITIAL_STRONG_VALUE, std::memory_order_relaxed);
// 回调通过 RefBase 派生类对象
refs->mBase->onFirstRef();
}
现在明白了,创建 sp 对象的过程,其实就是把强、弱引用都加1,现在强、弱引用的值都是1。
其实我们脑海中应该有个主要的观念,创建 sp 对象主要是为了把强引用加1。对应的,创建 wp 对象主要是为了把弱引用计数加1。文章后面不会分析 wp 代码,因此需要读者牢记这两个概念。
析构函数
现在来分析下,当 sp 对象销毁时, sp 的析构函数做了什么
template<typename T>
sp<T>::~sp() {
if (m_ptr)
m_ptr->decStrong(this);
}
通过刚才构造函数的分析,m_ptr 指针指向的其实就是 A 对象。sp 的析构函数调用了 A 对象的 decStrong() 函数,而这个函数是由 RefBase 实现的
void RefBase::decStrong(const void* id) const
{
// 复制了指针
weakref_impl* const refs = mRefs;
// 非debug版本,以remove, add, rename 开关的函数,实现都为空
refs->removeStrongRef(id);
// 1. 强引用减1
// 返回原始值1,现在强引用值为0
const int32_t c = refs->mStrong.fetch_sub(1, std::memory_order_release);
// c == 1 表示现在已经没有强引用了
if (c == 1) {
std::atomic_thread_fence(std::memory_order_acquire);
// 回调通知 RefBase 派生类对象
refs->mBase->onLastStrongRef(id);
// mFlags 初始化的值为0
int32_t flags = refs->mFlags.load(std::memory_order_relaxed);
// 2. 强生命周期下,释放 A 对象的动态内存
if ((flags&OBJECT_LIFETIME_MASK) == OBJECT_LIFETIME_STRONG) {
delete this;
}
}
// 3. 弱引用减1,并释放自己
refs->decWeak(id);
}
前面的分析指出,现在的强、弱引用计数都是1,并且 mFlags 的值为 OBJECT_LIFETIME_STRONG。
第一步,先把强引用计数减1,因此现在强引用计数为0。
第二步,当强引用计数为0,并且是强生命周期,就释放 A 对象的动态内存。因此这里会调用 A 类对象的析构函数,并且自动调用 RefBase 的析构函数,代码如下
RefBase::~RefBase()
{
// 获取 mFlags 值,现在值为0,也就是 OBJECT_LIFETIME_STRONG
int32_t flags = mRefs->mFlags.load(std::memory_order_relaxed);
if ((flags & OBJECT_LIFETIME_MASK) == OBJECT_LIFETIME_WEAK) {
/ ...
} else if (mRefs->mStrong.load(std::memory_order_relaxed) == INITIAL_STRONG_VALUE) {
// ...
}
// mRefs 指针重置为空指针
const_cast<weakref_impl*&>(mRefs) = nullptr;
}
由于 mFlags 值为 OBJECT_LIFETIME_STRONG,因此这里只把 RefBase 的 mRefs 指针重置为空指针。mRefs 指向的内存是通过 new 分配的动态内存,并且内存还没有释放,怎么就把 mRefs 重置为空指针了? 我刚开始分析到这里时,百思不得其解。其实在 decStrong() 方法的第一行,已经把指针进行了复制,代码如下
weakref_impl* const refs = mRefs;
因此,就算 mRefs 重置为了空指针,还是能通过 refs 进行释放动态内存。
第三步,调用 weakref_type 的 decWeak() 方法
void RefBase::weakref_type::decWeak(const void* id)
{
weakref_impl* const impl = static_cast<weakref_impl*>(this);
// 非debug版本,以remove, add, rename 开头的函数,实现都为空
impl->removeWeakRef(id);
// 1. 弱引用计数减1
// 现在弱引用计数的值为0,返回值为旧值1
const int32_t c = impl->mWeak.fetch_sub(1, std::memory_order_release);
if (c != 1) return;
atomic_thread_fence(std::memory_order_acquire);
int32_t flags = impl->mFlags.load(std::memory_order_relaxed);
if ((flags&OBJECT_LIFETIME_MASK) == OBJECT_LIFETIME_STRONG) {
// 强引用计数现在为0
if (impl->mStrong.load(std::memory_order_relaxed)
== INITIAL_STRONG_VALUE) {
// ...
} else {
//2. 删除原来 RefBase 的 mRefs 指向的动态内存
delete impl;
}
} else {
// ...
}
}
根据前面的分析,现在的情况是,强引用计数为0,弱引用计数为1,并且 mFlag 值OBJECT_LIFETIME_STRONG .
通过 RefBase 析构函数的分析,我们可以得出以下结论:
当 RefBase 派生类对象处理强生命周期下 ( mFlags 值为0 ),当强引用计数为0时,释放 RefBase 派生类对象,当弱引用计数为0时,释放 RefBase::mRefs 指针所指向的对象。
生命周期控制
通过前面的分析,RefBase 默认是强生命周期。在强生命周期下,当强引用计数为0时,才会释放 RefBase 派生类对象的动态内存。
那么如何调整 RefBase 的生命周期为弱生命周期呢?在弱生命周期下,何时才会释放 RefBase 的派生类对象呢?
首先来回来第一个问题,如何调整 RefBase 为弱生命周期。代码如下
class A : public RefBase
{
public:
A() { extendObjectLifetime(OBJECT_LIFETIME_WEAK); }
};
在创建 A 对象时,通过调用 extendObjectLifetime() 函数,来设置弱生命周期,其实就是设置了 mFlags 值为 OBJECT_LIFETIME_WEAK 。
现在来回答第二个问题,弱生命周期下,RefBase 派生类对象的动态内存何时被释放? 还是从 sp 的析构函数说起
void RefBase::decStrong(const void* id) const
{
weakref_impl* const refs = mRefs;
refs->removeStrongRef(id);
const int32_t c = refs->mStrong.fetch_sub(1, std::memory_order_release);
// c == 1 表示现在已经没有强引用了
if (c == 1) {
std::atomic_thread_fence(std::memory_order_acquire);
refs->mBase->onLastStrongRef(id);
int32_t flags = refs->mFlags.load(std::memory_order_relaxed);
// 强生命周期下的操作
if ((flags&OBJECT_LIFETIME_MASK) == OBJECT_LIFETIME_STRONG) {
delete this;
}
}
// 弱引用减1,并且释所有的放动态内存
refs->decWeak(id);
}
从代码可以看出,当处理弱生命周期下,强引用计数为0时,并不会释放 RefBase 派生类对象的动态内存,而只是调用了 weakref_type::decWeak()
函数,那么动态内存肯定就是这里释放的
void RefBase::weakref_type::decWeak(const void* id)
{
weakref_impl* const impl = static_cast<weakref_impl*>(this);
impl->removeWeakRef(id);
const int32_t c = impl->mWeak.fetch_sub(1, std::memory_order_release);
if (c != 1) return;
atomic_thread_fence(std::memory_order_acquire);
int32_t flags = impl->mFlags.load(std::memory_order_relaxed);
if ((flags&OBJECT_LIFETIME_MASK) == OBJECT_LIFETIME_STRONG) {
// ...
} else {
impl->mBase->onLastWeakRef(id);
// 走到这一步,表示弱引用计数为0,它会释放 RefBase 派生类对象的动态内存
delete impl->mBase;
}
}
从代码中可以看到,当弱引用计数为0时,它会释放 RefBase 派生类对象的动态内存,因此会调用 RefBase 的析构函数
RefBase::~RefBase()
{
int32_t flags = mRefs->mFlags.load(std::memory_order_relaxed);
if ((flags & OBJECT_LIFETIME_MASK) == OBJECT_LIFETIME_WEAK) {
if (mRefs->mWeak.load(std::memory_order_relaxed) == 0) {
// 处理弱生命周期下,并且弱引用计数为0,那么会释放 mRefs 指向的动态内存
delete mRefs;
}
} else if (mRefs->mStrong.load(std::memory_order_relaxed) == INITIAL_STRONG_VALUE) {
// ...
}
const_cast<weakref_impl*&>(mRefs) = nullptr;
}
RefBase 的析构函数,在弱生命周期下,并且弱引用计数为0时,会释放 RefBase::mRefs 指向的动态内存。至此,所有的动态内存都成功释放。
现在总结下
- 当 RefBase 派生类对象处于强生命周期下,只有当强引用计数为0时,RefBase 派生类对象的动态内存才会补释放。
- 当RefBase 派生类对象处于弱生命周期下,只有当弱引用计数为0时,RefBase 派生类对象的动态内存才会补释放。
如何使用sp, wp
sp, wp 可以帮我们自动释放动态内存,但是怎么安全地使用 sp, wp 来安全的操作 RefBase 派生类对象呢,因此 RefBase 动态内存在某一个时刻会被释放? 这得从 sp, wp 的公有接口中找答案
template<typename T>
class sp {
public:
// ...
inline T& operator* () const { return *m_ptr; }
inline T* operator-> () const { return m_ptr; }
inline T* get() const { return m_ptr; }
inline explicit operator bool () const { return m_ptr != nullptr; }
// ...
};
template <typename T>
class wp
{
public:
inline T* unsafe_get() const { return m_ptr; }
// ...
};
这一下就明白了,如何使用了,例子如下
#include <iostream>
class A : public RefBase
{
public:
void hello() const { std::cout << "hello" << std::endl; }
};
int main()
{
A * pa = new A();
sp<A> spa(pa);
wp<A> wpa(pa);
if (spa)
{
// 如下三种方式,可可以调用A的hello()函数
spa->hello();
(*spa).hello();
spa.get()->hello();
}
A * p = wpa.unsafe_get();
if (p)
{
p->hello();
}
return 0;
}
可以看到,为了安全使用 sp, wp,首先需要判空。然而是实际中,如果你能肯定动态内存没有被释放,那么可以不判空。例如刚刚创建了 sp 对象,此时就可以不判空。
LightRefBase
<<深入理解Android卷1>>中还提到一个类 LightRefBase (路径 为system/core/libutils/include/utils/LightRefBase.h),它与 RefBase 很像,只是 LightRefBase 只管理强引用计数。
那么怎么使用呢?与 RefBase 一样,先继承这个类
#include <utils/LightRefBase.h>
class A : public LightRefBase
{
public:
void hello() const;
};
由于只支持强引用计数,因此,只能使用 sp 来使用这个类
sp<A> sp = new A();
sp->hello();
想法
C++相比C语言,牺牲一点运行效率和内存,换取的是开发效率,而Java更是如此。在内存紧张的年代,像 RefBase 这种以内存和运行效率,来换开发效率的方式,应该是不被接受的。而在内存不是很紧张的现在,更看重的应该是开发效率,这应该就是 Google 设计 RefBase 这些类的初衷吧。