• ZT Android的引用计数(强弱指针)技术及一些问题


    Android的引用计数(强弱指针)技术及一些问题

    分类: Android 844人阅读 评论(4) 收藏 举报

    目录(?)[+]

    Android C++框架层的引用计数技术

    C++ 中对指针的使用时很头疼的事情,一个是经常会忘记free 指针,造成内存泄露,另外一个就是野指针问题:访问已经free掉的指针。程序debug工作的相当大部分,都是花费在这。Android中通过引用计数 来自动管理指针的生命周期,动态申请的内存将会在不再需要时被自动释放(有点类似Java的垃圾回收),不用程序员明确使用delete来释放对象,也不 需要考虑一个对象是否已经在其它地方被释放了,从而使程序编写工作减轻不少,而程序的稳定性也大大提高。

    Android提供的引用计数技术,主要是通过RefBase类及其子类sp (strong pointer)和wp(weak pointer)实现的,具体的原理和细节就不说了,可以参看《深入理解Android:卷1》,说的还是比较清楚。

    引用计数的问题

    任何东西都不会是万能的,Android C++中的引用计数问题,和Java一样,并不能完全避免内存泄露,另外还有一个问题就是性能(overhead)问题也很突出,本文就不说了。

    使用了android的sp指针,也不能说就不需要程序员去关心指针的细节了。通常由于设计或使用的不良,更有可能导致内存无法回收,也就是内存泄 露问题,甚至于还可能导致不明就里的野指针问题。而此时导致的内存使用问题,由于其具有更多的欺骗性和隐蔽性,往往更难发觉和调试。

    循环引用及其解法

    使 用引用计数的智能指针管理方法中,常见的java内存泄露问题在C++中一样存在。在Android的强指针引用中,一个最常见的就是强指针的循环引用问 题。而这又是程序员比较容易犯的问题:在程序员对强弱指针的理解不是很深入的情况下,想当然的认为使用了强指针,系统会根据引用计数自动收回。

    循 环引用,就是对象A有个强指针,引用对象B;对象B中,也有个强指针,引用对象A;这样A和B就互锁。A对象释放B对象的引用是在本身被析构回收时,而析 构回收的前提是A对象没有被引用,则需要B对象先释放,B对象释放的前提是A对象释放...如此则A和B都无法释放,这样即产生了内存泄露。


    我们可以写个程序看一下这种泄露情况。

    先定义一个类,这里假定叫Bigclass

    1. namespace android{  
    2.   
    3. class Bigclass : public RefBase   
    4. {   
    5. public:  
    6.     Bigclass(char *name){  
    7.         strcpy(mName, name);  
    8.         ALOGD("Construct: %s", mName);  
    9.     }  
    10.   
    11.     ~Bigclass(){  
    12.         ALOGD("destruct: %s", mName);  
    13.     }  
    14.       
    15.     void setStrongRefs(sp<Bigclass> b){  
    16.         spB = b;  
    17.     }         
    18.   
    19. private:      
    20.     sp<Bigclass> spB;  
    21.         char mName[64];   
    22. };  
    23.   
    24. }  

    该类非常简单,只有一个sp指针和一个name成员。循环引用示例:

    1. void testStrongCrossRef(){  
    2.     sp<Bigclass> A = new Bigclass("testStrongClassA");  
    3.     sp<Bigclass> B = new Bigclass("testStrongClassB");  
    4.     
    5.     A->setStrongRefs(B);  
    6.     B->setStrongRefs(A);  
    7. }  
    8.   
    9. int main(){  
    10.     ALOGD("Start testStrongClasses..");    
    11.     testStrongCrossRef();  
    12.     ALOGD("testStrongClasses Should be destructed!!");  
    13.   
    14.     return 0;  
    15. }  

    输出的结果,如预期,对象没有被释放,泄露了:

    1. D/TEST    ( 1552): Start testStrongClasses..  
    2. D/TEST    ( 1552): Construct: testStrongClassA  
    3. D/TEST    ( 1552): Construct: testStrongClassB  
    4. D/TEST    ( 1552): testStrongClasses Should be destructed!!  

    为了解决这一问题,Android在又引入了弱指针,弱指针并不能通过引用计数来控制所引用对象的生命周期,这样就可以消除上例中的引用环路问题,使得问题解决。我们将上述的类稍作修改,增加了弱引用的接口:

    1. namespace android{  
    2.   
    3. class Bigclass : public RefBase   
    4. {   
    5. public:  
    6.     Bigclass(char *name){  
    7.         strcpy(mName, name);  
    8.         ALOGD("Construct: %s", mName);  
    9.     }  
    10.   
    11.     ~Bigclass(){  
    12.         ALOGD("destruct: %s", mName);  
    13.     }  
    14.       
    15.     void setStrongRefs(sp<Bigclass> b){  
    16.         spB = b;  
    17.     }         
    18.   
    19.     void setWeakRefs(sp<Bigclass> b){  
    20.         wpB = b;  
    21.     }    
    22.   
    23. private   
    24.     sp<Bigclass> spB;  
    25.     wp<Bigclass> wpB;  
    26.         char mName[64];   
    27. };  
    28.   
    29. }  

    先来测试一下,将上例中的强指针换成弱指针,会是什么情况:

    1. void testWeakCrossRef(){  
    2.  sp<Bigclass> A = new Bigclass("testWeakClassA");  
    3.  sp<Bigclass> B = new Bigclass("testWeakClassB");  
    4.     
    5.   A->setWeakRefs(B);  
    6.   B->setWeakRefs(A);  
    7. }  

    输出结果:

    1. D/TEST    ( 2889): Start testWeakClass ..  
    2. D/TEST    ( 2889): Construct: testWeakClassA  
    3. D/TEST    ( 2889): Construct: testWeakClassB  
    4. D/TEST    ( 2889): destruct: testWeakClassB  
    5. D/TEST    ( 2889): destruct: testWeakClassA  
    6. D/TEST    ( 2889): testWeakClass Should be destructed!!  

    在出了testWeakClassA和testWeakClassB在对象A和B出了作用域后,没有强引用了,两个对象都释放了,这个符合预期。

    这里testWeakClassA和testWeakClassB之间的引用关系,全部都是弱引用,因此二者间的生命周期互不相干,这里二者用 sp<Bigclass>对象A和B与创建一般的栈对象 Bigclass A, Bigclass B 的生命周期一样。

    Android中,最常用的肯定不是上面两种:

    • 强强引用——互不相让,互相绑死,这是绝对禁止的。
    • 弱弱引用——互不相干,各管生死。这个对于想要使用引用计数自动管理对象生命周期来说,没什么用处。

    最常用的一般是强弱引用关系。强弱引用需要是有从属关系的,具体那个类是用sp引用,哪个是用wp引用,要看设计的逻辑了。

    测试示例:

    1. void testCrossRef(){  
    2.  sp<Bigclass> A = new Bigclass("testNormalClassA");  
    3.  sp<Bigclass> B = new Bigclass("testNormalClassB");  
    4.    
    5.   A->setStrongRefs(B);  
    6.   B->setWeakRefs(A);  
    7. }  

    输出结果:

    1. D/TEST    ( 2889): Start test Normal pointer reference ..  
    2. D/TEST    ( 2889): Construct: testNormalClassA  
    3. D/TEST    ( 2889): Construct: testNormalClassB  
    4. D/TEST    ( 2889): destruct: testNormalClassA  
    5. D/TEST    ( 2889): destruct: testNormalClassB  
    6. D/TEST    ( 2889): Test Normal pointer reference Should be destructed!!  

    这种情况下,消除了循环引用,没有了内存泄露问题。 和上一个弱弱引用的例子比较,这里testNormalClassB的析构在testWeakClassA之后,testWeakClassB的生命周期 是受testWeakClassA控制的,只有testWeakClassA析构,testWeakClassB才会析构。(上面的弱弱引用的测例,说明 在无干预的情况下,应该是testWeakClassB先析构的)

    对于强弱指针的使用,使用弱指针是需要特别注意,弱指针指向的对象,可能已经被销毁了,使用前需要通过promote()方法探测一下,详细信息可参考《深入理解Android》

    野指针问题

    强弱引用的问题,相信大多数Android程序员都明白,这里主要要强调一点就是:使用的时候要小心,一不小心可能就出错,举个例子:

    我们在刚才定义的BigClass中增加另外一个构造函数:

    1. Bigclass(char *name, char * other){  
    2.     strcpy(mName, name);  
    3.     ALOGD("Construct another: %s", mName);  
    4.     setWeakRefs(this);  
    5. }  

    这个构造函数,是将本对象中wp类型成员变量的用自己构造,也就是wp指针指向自己,这是允许的。

    在写个测试用例:

    1. void testCrash(){  
    2.     sp<Bigclass> A = new Bigclass("testCrashA""testCrash");  
    3.     sp<Bigclass> B = new Bigclass("testCrashB");  
    4.     A->setWeakRefs(B);  
    5.     B->setWeakRefs(A);  
    6. }  


    输出结果:

    1. D/TEST    ( 3709): Construct another: testCrashA  
    2. D/TEST    ( 3709): destruct: testCrashA  
    3. D/TEST    ( 3709): Construct: testCrashB  
    4. D/TEST    ( 3709): destruct: testCrashB  

    好像没有什么问题,程序也没有崩溃呀?

    没有崩溃,那是幸运,因为这个测试代码和上下文太简单了。 我们看下输出就知道了:testCrashB对象构造的时候, testClassA已经析构了!!!!

    也就是说,A对象,在其创建后,马上就消亡了,testCrash()方法中操作的A对象所指向的,都是野指针!!!

    为何会出现野指针?问题出在刚才定义的构造函数Bigclass(char *name, char * other)中。

    1. setWeakRefs(this);  

    这里的this是一个Bigclass *类型的,这样在参数压栈的时候需要构建一个临时的sp<Bigclass>强指针对象,调用完成后,该对象析构。该sp对象通过sp的sp<T*>构造函数构造的。

    这里我们看一下这个临时sp对象的构造和析构过程:

    构造完成后:new出来的这个Bigclass对象testCrashA,这个RefBase的子类对象,其强引用计数为1(从INITIAL_STRONG_VALUE增加到1)

    析构完毕后:sp的析构会减小该Bigclass对象指针(即对象 testCrashA,也是这里的this指针指向的对象)的强引用的计数,这里是从1减小到INITIAL_STRONG_VALUE,当一个对象的引 用计数减小到INITIAL_STRONG_VALUE时,会触发该Bigclass对象的delete操作,也就析构了该this指针指向的对象了。

    这里,构造函数中就删除构造的对象,比较难想象吧!有兴趣可以验证一下看看,将刚才的构造函数修改一下:

    1. Bigclass(char *name, char * other){  
    2.     ALOGD("start Construct another: %s,", mName);  
    3.     strcpy(mName, name);  
    4.     setWeakRefs(this);  
    5.     ALOGD("end Construct another: %s,", mName);  
    6. }  

    看一下析构是否在start和end之间。跑一下,你会有意想不到的打印 :)


    这里可以给一条规则:绝对不能在创建的RefBase对象还没有被一个确定的长作用域sp对象引用前,通过局部短作用域sp指针引用。

    建议

    没什么好建议,搞明白设计意图和工作原理对调试和写代码都非常有好处。


  • 相关阅读:
    测试开发工资为什么这么高?
    测试开发工程师技能图谱 V1.0 版 | 福利
    完成这 10+ 企业级项目实战,你也能进阶中高级测试开发
    我们准备了50000现金,给爱学习的你!手慢无~
    Git实战(四)| Git分支管理实操,搞定在线合并和本地合并
    公开课|互联网测试技术体系详解&职业发展规划
    测试面试 | 某互联网大厂测试面试真题,你能回答出多少?
    2020 中秋国庆,阖家快乐!
    测试面试 | 某BAT大厂测试开发面试真题与重点解析
    jsp_1
  • 原文地址:https://www.cnblogs.com/jeanschen/p/3507501.html
Copyright © 2020-2023  润新知