为什么需要弱引用 wp?

2023年 9月 28日 57.8k 0

Android 中的智能指针是通过引用计数的方式方式来实现内存自动回收的。在大多数情况下我们使用强指针 sp 就好了,那么弱指针 wp 的存在意义有是什么呢?

从使用的角度来说,wp 扮演的是一个指针缓存的角色,想用时候可以用,但不想因此阻止资源被释放。其实,简单的裸指针也能很好地完成指针缓存的功能,其功能性并不是 wp 存在的必要条件。

wp 存在的核心原因是:解决循环引用导致的死锁问题。

1. 循环引用导致的死锁问题

接下来,我们就通过一个简单的示例程序来演示循环引用导致的死锁问题:

首先有两个类,其内部都有一个智能指针指向对方,形成循环引用:

Class A : public RefBase
{
public:
    A() 
    {
        
    }

    virtual ~A() 
    {
       
    }

    void setB(sp& b) 
    { 
        mB = b; 
    }

private:
    sp mB;
}

Class B : public RefBase
{
public:
    B() 
    {
        
    }

    virtual ~B() 
    {
       
    }

    void setA(sp& a) 
    { 
        mA = a; 
    }

private:
    sp mA;
}

整体结构如下图所示:

接下来看 main 函数:

int main(int argc, char** argv)
{
    //初始化两个指针
    A *a = new A();
    B *b = new B();

    // 触发构造函数调用 spA 内部强弱计数值 (1,1)
    sp spA = a;
    // 触发构造函数调用 spB 内部强弱计数值 (1,1)
    sp spB = b;

    //setB 内部有赋值操作 mB = b,触发等于操作符函数重载
    //spB 内部强弱计数值 (2,2)
    spA->setB(spB);

    //setA 内部有赋值操作 mA = a,触发等于操作符函数重载
    //spA 内部强弱计数值 (2,2)
    spB->setA(spA);

    return 0;
    // spA 析构 内部强弱计数值 (1,1),内存无法回收
    // spB 析构 内部强弱计数值 (1,1),内存无法回收
}

//等于操作符函数重载
template
sp& sp::operator =(const sp& other) {
    // Force m_ptr to be read twice, to heuristically check for data races.
    T* oldPtr(*const_cast(&m_ptr));
    T* otherPtr(other.m_ptr);
    // 强弱引用计数分别加 1
    if (otherPtr) otherPtr->incStrong(this);
    if (oldPtr) oldPtr->decStrong(this);
    if (oldPtr != *const_cast(&m_ptr)) sp_report_race();
    m_ptr = otherPtr;
    return *this;
}

从这个示例可以看出,在循环引用的情况下,指针指针在作用域结束后,强弱引用计数值无法变回 (0,0),内存无法回收,导致内存泄漏;

2. 解决方案

只需要把其中一个智能指针改为弱引用即可解决上面的问题:

Class A : public RefBase
{
public:
    A() 
    {
        
    }

    virtual ~A() 
    {
       
    }

    void setB(sp& b) 
    { 
        mB = b; 
    }

private:
    sp mB;
}

Class B : public RefBase
{
public:
    B() 
    {
        
    }

    virtual ~B() 
    {
       
    }

    //函数参数也要变一下
    void setA(sp& a) 
    {   
        //触发另外的等于操作符函数重载 
        mA = a; 
    }

private:
    //这里改成 wp 弱引用
    wp mA;
}

主函数稍作修改:

int main(int argc, char** argv)
{
    //初始化两个指针
    A *a = new A();
    B *b = new B();

    // 触发构造函数调用  spA 内部强弱计数值 (1,1)
    sp spA = a;
    // 触发构造函数调用  spB 内部强弱计数值 (1,1)
    sp spB = b;

    //setB 内部有赋值操作 mB = b,触发等于操作符函数重载
    //spB 内部强弱计数值 (2,2)
    spA->setB(spB);

    //setA 内部有赋值操作 mA = a,触发等于操作符函数重载
    //spA 内部强弱计数值 (1,2)
    spB->setA(spA);

    return 0;
    // spB 析构 内部强弱计数值 (1,1),内存无法回收
    // spA 析构 内部强弱计数值 (0,1),强引用为 0 ,回收 sp spA 内部的目标对象 A,
    // 随着 A 的析构, A 的成员变量 mB 也开始析构, 目标对象 B 强弱引用计数减 1,内部强弱计数值变为 (0,0),回收目标对象 B 以及内部管理对象,B 对象的内存回收工作完成,接着触发 B 对象的成员 mA 的析构函数
    // mA 执行析构函数,弱引用计数减 1,内部强弱计数值变为 (0,0),回收 A 对象内部对应的管理对象,A 对象的内存回收工作完成
}

//等于操作符函数重载
template
wp& wp::operator = (const sp& other)
{
    weakref_type* newRefs =
        other != nullptr ? other->createWeak(this) : nullptr; //增加弱引用计数
    T* otherPtr(other.m_ptr);
    if (m_ptr) m_refs->decWeak(this);
    m_ptr = otherPtr;
    m_refs = newRefs;
    return *this;
}

当程序的一个引用修改为 wp 时,main 函数结束时:

  • spB 析构 内部强弱计数值 (1,1),内存无法回收
  • spA 析构 内部强弱计数值 (0,1),强引用为 0 ,回收 sp spA 内部的目标对象 A,
  • 随着 A 的析构, A 的成员变量 mB 也开始析构, 目标对象 B 强弱引用计数减 1,内部强弱计数值变为 (0,0),回收目标对象 B 以及内部管理对象,B 对象的内存回收工作完成,接着触发 B 对象的成员 mA 的析构函数
  • mA 执行析构函数,弱引用计数减 1,内部强弱计数值变为 (0,0),回收 A 对象内部对应的管理对象,A 对象的内存回收工作完成

这样就解决了上一节中提出的内存泄漏问题!

3. 总结

  • wp 的基本作用:wp 扮演了指针缓存的角色,想用时候可以用,但不想因此阻止资源被释放
  • wp 存在的根本原因:解决循环引用导致的死锁问题

关于

我叫阿豪,2015 年本科毕业于国防科学技术大学指挥信息系统专业,毕业后从事信息化装备的研发工作,主要研究方向是 Android Framework 与 Linux Kernel。

如果你对 Android Framework 感兴趣或者正在学习 Android Framework,可以关注我的微信公众号和抖音,我会持续分享我的学习经验,帮助正在学习的你少走一些弯路。学习过程中如果你有疑问或者你的经验想要分享给大家可以添加我的微信,我拉你进技术交流群。

相关文章

服务器端口转发,带你了解服务器端口转发
服务器开放端口,服务器开放端口的步骤
产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像
如何使用 WinGet 下载 Microsoft Store 应用
百度搜索:蓝易云 – 熟悉ubuntu apt-get命令详解
百度搜索:蓝易云 – 域名解析成功但ping不通解决方案

发布评论