进军Android底层,首先掌握 RefBase, sp, wp

2,765 阅读9分钟

C++是通过 new 和 delete 来管理动态内存的分配与释放,但是有时候开发者可能忘记使用 delete。Google 为了防止内存泄露以及加快开发效率,创建了 RefBase, sp, wp 这些类,这些类的路径如下。

  1. system/core/libutils/include/utils/RefBase.h
  2. system/core/libutils/RefBase.cpp
  3. system/core/libutils/include/utils/StrongPointer.h
  4. 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 来释放动态内存,这是因为这个例子做了如下两点操作

  1. 让类 A 继承自 RefBase 类。
  2. 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 是一个非常关键的类,它的成员变量的含义如下

  1. mStrong : 它表示强引用计数,初始值为 INITIAL_STRONG_VALUE ,也就是 1<<28。
  2. mWeak : 它表示弱引用计数,初始值为0.
  3. mBase : 是一个指针,指向 RefBase 派生类的对象,也就是例子中的 A 对象。
  4. 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 指向的动态内存。至此,所有的动态内存都成功释放。

现在总结下

  1. 当 RefBase 派生类对象处于强生命周期下,只有当强引用计数为0时,RefBase 派生类对象的动态内存才会补释放。
  2. 当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 这些类的初衷吧。