Android 性能优化之内存泄漏,使用MAT&LeakCanary解决问题

本文较长,阅读大约5分钟


App进行到最终的测试的时候,往往会出现一些性能上,以及内存上的问题,需要优化,这也是一个Android高级工程师所需要了解并且掌握的知识点,内存这个小妮子比较调皮,每个月总有那么几次泄漏或者溢出(OOM),这篇文章所讲的是内存溢出,这里要注意,内存溢出和内存泄漏是两个概念,这点大家要清楚,当然,内存泄漏过多会导致内存泄漏,至于什么是内存泄漏呢,大家都知道我们的内存回收机制是GC,所以用一句话来概括:GC回收机制所无法回收的垃圾对象。


如果把垃圾回收机制比喻你在用餐,而服务员会来收盘的话,那么理想中的状态便是你吃完饭一走,服务员就把盘子收走了,即对象用完GC自动回收,但是这里却只是理想中的样子,实际上,对于Android的内存管理机制和回收机制,Android系统的一个内存管理机制,被称为Low Memorry Killer的一种管理机制,其实就是根据优先级去kill掉一些优先级较低的程序,而回收机制就比较佛系了,叫做GC,采用的也是懒人机制,你不需要用的变量,对象等,你放那里就好,系统会在Heap剩余空间不够的时候去回收,并且有一个隐患,即GC触发后,所有的线程都会被暂停。


内存


要了解内存泄漏,我们首先了解内存,我们都知道Android系统的底层是Linux,并且他运行是一个沙箱机制,即每个App对应独立运行在一个虚拟机中,并且有一个进程,这也延伸出了多任务机制,并且每个App都是独立的,即使你崩溃了也不会对系统造成影响,如果想看进程,可以使用ps命令:

      640?wx_fmt=png      

并且每个进程都有一个pid,按照顺序分配的,可以发现,init进程就是第一个,这个我们不做深究,你知道有这么一回事儿就行了


我们可以再次输入一个命令:dumpsys meminfo packagename

      640?wx_fmt=png      

这里我们就可以看到更多的内存信息了,统计了一些物理内存,虚拟内存使用情况以及统计,里面有三个参数,Heap Size , Heap Alloc ,Heap Free ,指分配了多少内存,使用了多少内存,剩余多少内存,一般 Heap Size = Heap Alloc + Heap Free (1985 = 1374 + 611),这里单位是K。


讲完系统,我们再化大为小,说一下App,其实App在内存中安装后,系统会预分配一个最大内存,这跟沙箱机制有一定关系,每家的系统都是不一样的,我们可以通过代码去读取出来:


ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);

//最大内存

int mc = am.getMemoryClass();

//Large最大内存

int lm = am.getLargeMemoryClass();


这里有一个Large,实际上我们之前就用过,也就是开启硬件加速后的最大内存,如何开启硬件加速,则需要在清单文件的Application根节点添加android:largeHeap="true"


不过大部分厂商会禁止此功能


我们把应用跑在模拟器上可以得到如下的数据:mc = 96 lm = 256


也得到了一个结论,即我这套模拟器给我的这个Demo App分配的最大内存为96,开启硬件加速后为256,这里的单位是M,但是大部分真机,这两个数值都是一样的,这里不曾考究,自行探索下。


其实这里要提一下,现在大部分的App其实所考虑的什么所谓的内存优化,都是因为图片过多,所以我们真正考虑的,还是如何有效率的优化图片给App带来的负荷,图片吃内存比较大。


我们继续来说一下Low Memorry Killer内存管理机制,这里涉及要当你的App切入后台后的管理方式,实际上可以用四个字概括:先进先出,当你的应用进入后台并且开始启动kill机制或者内存不够的时候,会优先清理任务栈最底层的应用,也就是最先开启的应用,而近期应用则相当于保护起来。这种机制叫做:LRU Cache (缓存淘汰)算法。


并且当系统内存存在变化的时候,可以通过Application的onTrimMemory方法监听


@Override

public void onTrimMemory(int level) {

// level 等级

       super.onTrimMemory(level);

}


这里的内存等级是这样划分的:


int TRIM_MEMORY_BACKGROUND = 40;

int TRIM_MEMORY_COMPLETE = 80;

int TRIM_MEMORY_MODERATE = 60;

int TRIM_MEMORY_RUNNING_CRITICAL = 15;

int TRIM_MEMORY_RUNNING_LOW = 10;

int TRIM_MEMORY_RUNNING_MODERATE = 5;

int TRIM_MEMORY_UI_HIDDEN = 20;


我们有好几种方式可以监听到内存的使用情况和波动,这里我一一道来,首先,我们知道,当我们打开手机的设置 - 应用 - 对应的某一个App的时候就可以看到这个App的使用情况,实际上我们可以通过代码的方式来获取:


float totalMemory = Runtime.getRuntime().totalMemory() * 1.0f / (1024 * 1024);

float freeMemory = Runtime.getRuntime().freeMemory() * 1.0f / (1024 * 1024);

float maxMemory = Runtime.getRuntime().maxMemory() * 1.0f / (1024 * 1024);


这样获取到运行时的内存情况了,我们可以看下数据:



640?wx_fmt=png

     


当然,你也可以通过Android Profile 查看


     640?wx_fmt=png      


也可以通过Android Monitor查看

640?wx_fmt=png      

其实在面试中也经常有人会被问到内存优化的方法,只能说内存控制方面有很多的小技巧,但是归根结底还是要你自己有一个良好的代码习惯,当然,如果真发生了错误,比如内存泄漏或者溢出,那么你也应该知道如何去解决这些问题。


内存泄漏


如果想解决内存泄露,那么我们应该如何找到问题的根源尼?如果你只是一味的看内存增长是找不到问题所在的,应该内存泄漏如果不严重是察觉不到的,这里我们可以来写一段这样的代码:


public class MainActivity extends AppCompatActivity {


   @Override

   protected void onCreate(Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);

       setContentView(R.layout.activity_main);


       findViewById(R.id.finish).setOnClickListener(new View.OnClickListener() {

           @Override

           public void onClick(View v) {

               finish();

           }

       });


       new Thread(new Runnable() {

           @Override

           public void run() {

               try {

                   Thread.sleep(15000);

               } catch (InterruptedException e) {

                   e.printStackTrace();

               }

           }

       }).start();

   }

}


这段代码我只需要启动后点击退出按钮,再启动,再点击,那么Activity就会每次都finish掉,但是子线程却一在运行,Runnable是持有Activity对象的,这样我们就可以看到如下的Memory走势图

640?wx_fmt=png      

我反反复复的启动后finish,最终的结果将原本15.7MB的内存变成25.2MB,并且还会无限增加,最终导致内存溢出


640?wx_fmt=png      

那好,如果项目庞大的话,光这样看是定位不到的,我们可以这样来:先点击Profile app


       

640?wx_fmt=png

     


然后在下面的Profiler中点击Record,然后开始使用App,当看到波形变动的时候再点击Stop


       

640?wx_fmt=png

     


这样就会出现如下的文件列表,这里可以选择按照包名分类

  640?wx_fmt=png      

可以看到,这里的MainActivity出现了3个实例,这肯定是有问题的,也就定位到了发生溢出的界面为MainActivity。但是到这里还只是定位到了Activity,我们还可以更加精确一点,我们点击Record按钮旁边的下载按钮,然后点击保存hprof文件

  640?wx_fmt=png      

有了这个文件之后我们就可以进一步使用MAT工具来分析了


MAT


mat工具是Eclipse的,没有的话可以到这里去下载:


MAT工具下载:http://www.eclipse.org/mat/downloads.php


下载后如图:

  640?wx_fmt=png      

打开之后就是我i们久违的Eclipse风格了

640?wx_fmt=png

但是这里还不能直接导入,因为Android Studio导出的hprof文件并不是MAT标准的文件,所以我们需要用到SDK目录下的platform-tools下hprof-conv.exe工具,在此目录下进入cmd,通过命令:hprof-conv old.hprof new.hprof 来转换文件:

  640?wx_fmt=png      

现在我们可以回到MAT点击菜单栏的File - Open Heap Dump 导入new.hpfof

  640?wx_fmt=png      

只需要点击Create a historam from an arbitray set of objects 也就是这个小图标,即可生成分析表


       

640?wx_fmt=png

     


然后我们在这里输入过滤:

     640?wx_fmt=png      

到这里就很明朗了,我们继续缩小范围


640?wx_fmt=png      

右键选择 List object - with outgoing references ,这个的意思是查看外部所引用的对象

  

640?wx_fmt=png

     


然后继续过滤一下,并且右键 选择 Merge Shortest Paths to GC Roots - exclude all phantom/weak/soft etc .references 这个的意思是排查所有的弱引用,虚引用

640?wx_fmt=png

到这里你是否有一种恍然大悟的感觉,我们过滤之后只剩下一条Thread的错误,而所指向的对象为this$0,也就是他本身,意思是 子线程中所持有本类对象,那联想到内存溢出是我们退出后所引起的,所以最终得到的结论:Activity已经退出,但是子线程仍然持有本类对象所导致内存泄漏。


LeakCanary


当然,上述的方法我更多的倾向于你所了解这个一个追述的过程,毕竟有些繁琐,所以这里再教大家使用一款工具 —— LeakCanary


Github:https://github.com/square/leakcanary


我们在app/build.gradle下配置:


implementation 'com.squareup.leakcanary:leakcanary-android:1.6.3'


最新的v2.0-alpha-1貌似有些问题,所以我还是使用稳定版本,在Application中增加


public class BaseApp extends Application {


   @Override

   public void onCreate() {

       super.onCreate();


       if (LeakCanary.isInAnalyzerProcess(this)) {

           return;

       }

       LeakCanary.install(this);

   }

}


这样我们就可以正常的运行了,当发生内存泄漏的时候就会通知栏提示:


       

640?wx_fmt=png

     


到这里,基本上本章内容也讲完了,当然,这也只是一些皮毛而已,当你的项目足够大的时候,做这项优化工作还是比较繁琐的,所以最好还是尽量保持良好的编码习惯才是最重要的。


最新的精品文章我都会第一时间发表在我的星球中,文章内容比较扎实,欢迎加入!


640?wx_fmt=jpeg

我会继续保持文章的更新,也会继续给大家带来高质量的精品,谢谢大家!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值