Android内存泄漏分析

Java内存泄漏是什么

C/C++中内存泄漏定义为:一块内存没有引用了(也就是将来肯定用不到了),但是这块内存没被释放,还一直占用着内存空间。对于C/C++的这种内存泄漏,Android/Java中是不存在的,因为Android/Java会自动检查一块内存是否有引用,如果没有引用,Android/java会自动释放内存的。由此是否可以说Java中就没有内存泄漏问题呢?当然不是,Java中内存泄漏问题比C/C++还更容易发生。

与C/C++不同, Java中是没有显式声明释放内存的代码,所以程序员要释放内存, 就需要程序员断开所有对该块内存的引用。而这是一个技术活,在很多情况下,如果没有内存分析工具,程序员是根本保证他的引用都断开了。举个简单的例子:

程序员A的代码:

/* new一个对象,然后调用ClassLibB的func方法,而ClassLibB是程序员B提供的, 是看不到源码的  */

Object  a = new Object();

ClassLibB.fun(a);

a = null; //程序员A在这里建a设置为null, 认为没有引用了

程序员B的代码:

/*这是程序员B提供的库函数*/
class ClassLibB{
   static Object cache;

   static void func(Object para){ 
		cache = para; //这里静态变量引用了入参, 程序员A并不知道
    }
}

程序员A认为a已经没有引用了,但是这块内存却被ClassLibB中的cache引用的,这点程序员A是怎么也想不到的。

内存泄漏分析工具

从上面这个例子也可以看到,Java的内存泄漏用工具是无法检测到的。虽然程序员A执行了a=null, 但是这也只是表示一个内存引用断开了,并不能断定程序员要释放这块内存。正因为Java没有显式声明内存释放的语句,所以理论上任何Java内存分析工具都是无法断定一块内存是泄漏了。

估计有人说了,可以通过判断实例一直增加,并且没有减少来判断内存泄漏。比如如下这个例子:

class ClassA{

​ static ArrayList mArray = new ArrayList();

​ void add(Object para){ //只有add函数,没有delete

​ mArray.add(para);

​ }

}

这有一定有内存泄漏吗? 不一定,程序员可以说:我程序就是要这样写的啊,我这个列表就是要一直保存所有的实例啊。

所以说,所有的内存分析工具都只会列出内存的引用情况,然后给出怀疑的内存泄漏的点,包括Eclipse的MAT工具也是一样的。MAT工具分析后,如果有提示Leak Suspects,并不等于有内存泄漏; MAT工具没检查出Leak Suspects,也不等于你程序写的好,没有内存泄漏。而且MAT的Leak Suspects也只是检查内存占用比较大的类, 对于Android程序而言,Leak Suspects很多情况下都是没有参考意义的。

img

可以看出,内存分析工具是无法简单怀疑出一块内存有内存泄漏的,需要从实例的个数,引用的情况进行分析的。而实际上更重要的,还是需要从代码逻辑上进行协同分析。举个例子:

public class MainActivity extends AppCompatActivity {
    private static MainActivity activity;
    TextView saButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        saButton = (TextView) findViewById(R.id.text);
        saButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                setStaticActivity();
                nextActivity();
            }
        });
    }
    void setStaticActivity() {
        activity = this;
    }

    void nextActivity(){
        startActivity(new Intent(this,RegisterActivity.class));
        SystemClock.sleep(1000);
        finish();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
    }
}

这里MainActivity Destroy()后, 还有一个静态变量activity引用这MainActivity实例。从Java代码逻辑来说没什么问题,但是从Android层面分析,我们可以断定这个activity是泄漏了。MAT工具偏向Java内存泄漏分析,是无法检测出这种内存泄漏的。

从上面例子可以看出,要检查内存泄漏,往往是需要结合代码逻辑进行判断的。这里推荐介绍QTrace的HeapAnalyzer工具进行分析,相对于MAT,它最大的有点有两个:

1.它是基于脚本的,可以根据代码逻辑编写脚本检查各种内存泄漏。

2.它的root paths 可以更直接的查看内存引用情况。

QTrace分析实例引用

分析实例的引用,可以看清一个实例的引用情况如何,是分析内存是否有泄漏的基础。对于简单的情况,可以通过查看reference树来查看。而大部分实际情况时,都会发现reference树都会变得异常复杂,根本与源码对应不起来。

看实例是否可能发生泄漏,也就是看这些实例是否存在不该存在的引用。要分析不该存在的引用,就应该先看下这些实例被哪几个变量引用着

当有内存泄漏时,往往存在较多实例,如果采用MAT等工具逐个实例的看引用, 这工作量是非常巨大的。同时在很多较复杂情况,内存引用都是呈网状结构的,引用着引用着就回到起点了。

看下这个图,该图采用MAT分析KeyguardPINView@12fd8c08这个实例的incoming references,很显然从该图是很难找出: 这些实例被哪几个变量引用着。

mat_reference.png

对于**"这些实例被哪几个变量引用着** "这个问题,用QTrace可以较方便的解决。

1.QTrace打开菜单"视图->插件->内存分析", 在内存分析中打开hprof文件。

2.左侧查找对应的类,执行右键菜单"查看实例列表"得到实例列表

listinstances.png

3.选择所有KeyguardPINView实例,执行右键菜单"Root路径"

​ 在Root路径界面会对引用情况进行汇总,结果就简单的多了。

​ 默认情况下,Root路径也是按引用进行展示的。可以右键菜单选择"从上而下视图", 同时选择"优化显示路径". 这样结果就会按照正常的变量查看的方式进行展示,看起来会方便很多, 如下图。

topdownview.png

从该图,可以很容易看出KeyguardPINView这些实例被4个变量引用引用着。

QTrace查看变量

MAT中主要功能为分析内存的引用情况,QTrace除了分析内存引用情况,还可以方便的查看变量的值。

比如对上面的KeyguardPINView@12fd8c08这个实例,执行右键菜单"查看实例",就可以查看这个实例中包含的变量的值。

view_instance.png

QTrace分析内存泄漏

​ QTrace提供了分析hprof内存导出文件的接口,主要包括分析实例的引用关系,实例的值的分析。同时QTrace的HeapAnalyzer是基于脚本的,所以可以在脚本中编写检查脚本,结合实例的引用情况与实例的值,来判断内存是否泄漏。如下举几个内存泄漏检查的例子。

1.Activity泄漏

存在内存泄漏的代码如下:

public class MainActivity extends AppCompatActivity {
    static MainActivity mMainActivity = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mMainActivity = this;
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
    }
}

1).执行MainActivity后,按返回键退出,执行onDestory。但是静态mMainActivity的引用,导致MainActivity无法被释放。按逻辑来说,执行onDestory后,这个Activity本该可以被释放了。

2.打开QTrace中打开"视图->插件->内存分析", 执行菜单"文件->抓取Android内存", 选择要分析的进程, 完成对应进程的内存导出

(Hprof文件下载地址:https://github.com/jeffchau1979/QTraceDoc/blob/master/CheckMemoryLeak/ActivityLeak.hprof)

3.执行菜单"脚本->内存泄漏分析", 选择任务"Activities"

这里会列出已经被Destroryed,但是仍然存在的Activity, 也就是存在内存泄漏的Activities.

activity_check.png

4.查看Root路径,并且选择“自上而下视图”

可以看到该实例是被MainActiivty class引用的

activity_check_leak.png

2.Android SystemUI 监听导致的内存泄漏

如下这两个例子,将分析Android Q SystemUI的内存泄漏。

1.对Android SystemUI进行一段时间手动测试与monkey测试后,导出hprof文件

2.QTrace中打开"内存分析", 打开hprof文件

(hprof文件下载地址:https://github.com/jeffchau1979/QTraceDoc/blob/master/CheckMemoryLeak/MemoryLeakDetect.hprof)

3.执行"脚本->内存泄漏分析", 选择所有的任务

4.查看Multi Instances列表, 发现FalsingManagerImpl的实例数太多,不大正常

5.对FalsingManagerImpl执行右键菜单"查看实例列表",得到类FalsingManagerImpl所有的实例

6.选择所有FalsingManagerImpl实例,执行右键菜单"Root路径", 不用选择"自上而下视图".

FalsingManagerImpl_result.png

展开一个节点的引用情况,FlasingManagerImpl被内部类FlasingManagerImpl$2引用,FlasingManagerImpl$2这被ArrayList@0x14293060引用, ArrayList@0x14293060对应的是StatusBarStateControllerImpl中的mListeners变量, 同时ArrayList@0x14293060后面还有一个数字(::324), 这个数字表明有324个FalsingManagerImpl实例都被ArrayList@0x14293060引用着。

根据以上分析,可以很清晰的看出这部分内存引用是怎么回事了。

mStatusBarStateListener.mListeners引用着FlasingManagerImpl的内部类$2, 而这个内部类$2同时引用着FlasingManagerImpl类。

根据如上分析,查看FlasingManagerImpl对应的代码,可以很容易看出什么地方有内存泄漏了。

    public StateListener mStatusBarStateListener = new StateListener() {
        @Override
        public void onStateChanged(int newState) {
            ...
        }
    };

    FalsingManagerImpl(Context context) {
			...        		
		Dependency.get(StatusBarStateController.class)
			.addCallback(mStatusBarStateListener);
   }

排查代码发现,代码中又有添加addCallback(mStatusBarStateListener),没有对mStatusBarStateListener执行removeCallback。

3.Android SystemUI 监听导致的内存泄漏

这个内存泄漏分析例子会继续分析前面例子中的prof文件。

1,2,3: 这三个步骤同前面一个例子中1,2,3步骤

4.查看Multi Instances列表, 发现ConfigurableTexts$1的实例数太多,不大正常

5.对实例执行右键菜单"查看实例列表",得到类的所有的实例

6.选择所有实例,执行Root路径, 选择"自上而下视图"

cfgtextrootpath.png

(图 ConfigurableTexts$1 Root路径)

从这里已经能基本上能看出问题,ConfigurableTexts 1 都 是 被 8 个 T e x t V i e w 引 用 着 , 而 T e x t V i e w 又 都 是 被 V o l u m e D i a l o g I m p l 1都是被8个TextView引用着,而TextView又都是被VolumeDialogImpl 18TextViewTextViewVolumeDialogImplVolumeRow引用着。如果熟悉代码,已经可以看出内存引用的情况了。

如果还需要继续分析,可以看出,所有的实例都是被Object[]0x13018180引用着的。选择Object[]0x13018180节点,执行右键菜单"查看引用".

rootnoderef.png

(图 0x13018180引用)

可以看到所有的引用最终都是被VolumeDialogImpl@13a2be58实例引用着的。

这样,引用的情况就很清楚了,然后我们需要结合代码评估引用情况了:

1)8个TextView被VolumeDialogImpl, 这是正常的吗?

​ 结合代码分析,确认这点是正常的。这里不再讨论。

2)每个TextView都有26个ConfigurableTexts$1是否正常呢.

​ 从上面的“图 ConfigurableTexts$1 Root路径”的图中可以看到,ConfigurableTexts$1引用都是添加到mOnAttachStateCangeListeners的。AttachStateCangeListeners有26个, 这从经验来说,就很可能不正常了。

​ 打开ConfigurableTexts进一步分析

    public int add(final TextView text, final int labelResId) {
        ...
        mTexts.put(text, sp);
        text.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
            @Override
            public void onViewDetachedFromWindow(View v) {
            }

            @Override
            public void onViewAttachedToWindow(View v) {
               setTextSizeH(text, sp);
            }
        });
        mTextLabels.put(text, labelResId);
        return sp;
    }

这里存在一个问题,同一个TextView执行addOnAttachStateChangeListener会创建多个OnAttachStateChangeListener,导致同一个TextView存在26个onAttachStateChangeListener, 这是多余的; 如果继续执行这个add函数, 那么这个Listener还会持续增加。并且这里是会影响效率的,来一个event会对text执行26次setTextSizeH(text, sp), 但是最终只有最后一次执行才是有效的。

总结

​ Java/Android内存泄漏单单从实例引用的关系是无法确定内存泄漏的,往往需要结合程序逻辑来判断这些内存是泄漏的。采用QTrace的HeapAnalyzer工具可以较直观的看出实例的关系,并且提供了脚本可以检查特定情况的内存泄漏,比如Activity的内存泄漏;随着脚本的丰富,该工具对内存泄漏的检查会越来越精确。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值