百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

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

bigegpt 2024-08-21 12:10 2 浏览

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

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

内存

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

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

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

这里我们就可以看到更多的内存信息了,统计了一些物理内存,虚拟内存使用情况以及统计,里面有三个参数,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);

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

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

也可以通过Android Monitor查看

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

内存泄漏

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

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走势图

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

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

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

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

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

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

MAT

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

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

下载后如图:

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

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

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

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

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

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

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

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

到这里你是否有一种恍然大悟的感觉,我们过滤之后只剩下一条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);

}

}

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

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

有兴趣的可以加入交流群:417046685

相关推荐

悠悠万事,吃饭为大(悠悠万事吃饭为大,什么意思)

新媒体编辑:杜岷赵蕾初审:程秀娟审核:汤小俊审签:周星...

高铁扒门事件升级版!婚宴上‘冲喜’老人团:我们抢的是社会资源

凌晨两点改方案时,突然收到婚庆团队发来的视频——胶东某酒店宴会厅,三个穿大红棉袄的中年妇女跟敢死队似的往前冲,眼瞅着就要扑到新娘的高额钻石项链上。要不是门口小伙及时阻拦,这婚礼造型团队熬了三个月的方案...

微服务架构实战:商家管理后台与sso设计,SSO客户端设计

SSO客户端设计下面通过模块merchant-security对SSO客户端安全认证部分的实现进行封装,以便各个接入SSO的客户端应用进行引用。安全认证的项目管理配置SSO客户端安全认证的项目管理使...

还在为 Spring Boot 配置类加载机制困惑?一文为你彻底解惑

在当今微服务架构盛行、项目复杂度不断攀升的开发环境下,SpringBoot作为Java后端开发的主流框架,无疑是我们手中的得力武器。然而,当我们在享受其自动配置带来的便捷时,是否曾被配置类加载...

Seata源码—6.Seata AT模式的数据源代理二

大纲1.Seata的Resource资源接口源码2.Seata数据源连接池代理的实现源码3.Client向Server发起注册RM的源码4.Client向Server注册RM时的交互源码5.数据源连接...

30分钟了解K8S(30分钟了解微积分)

微服务演进方向o面向分布式设计(Distribution):容器、微服务、API驱动的开发;o面向配置设计(Configuration):一个镜像,多个环境配置;o面向韧性设计(Resista...

SpringBoot条件化配置(@Conditional)全面解析与实战指南

一、条件化配置基础概念1.1什么是条件化配置条件化配置是Spring框架提供的一种基于特定条件来决定是否注册Bean或加载配置的机制。在SpringBoot中,这一机制通过@Conditional...

一招解决所有依赖冲突(克服依赖)

背景介绍最近遇到了这样一个问题,我们有一个jar包common-tool,作为基础工具包,被各个项目在引用。突然某一天发现日志很多报错。一看是NoSuchMethodError,意思是Dis...

你读过Mybatis的源码?说说它用到了几种设计模式

学习设计模式时,很多人都有类似的困扰——明明概念背得滚瓜烂熟,一到写代码就完全想不起来怎么用。就像学了一堆游泳技巧,却从没下过水实践,很难真正掌握。其实理解一个知识点,就像看立体模型,单角度观察总...

golang对接阿里云私有Bucket上传图片、授权访问图片

1、为什么要设置私有bucket公共读写:互联网上任何用户都可以对该Bucket内的文件进行访问,并且向该Bucket写入数据。这有可能造成您数据的外泄以及费用激增,若被人恶意写入违法信息还可...

spring中的资源的加载(spring加载原理)

最近在网上看到有人问@ContextConfiguration("classpath:/bean.xml")中除了classpath这种还有其他的写法么,看他的意思是想从本地文件...

Android资源使用(android资源文件)

Android资源管理机制在Android的开发中,需要使用到各式各样的资源,这些资源往往是一些静态资源,比如位图,颜色,布局定义,用户界面使用到的字符串,动画等。这些资源统统放在项目的res/独立子...

如何深度理解mybatis?(如何深度理解康乐服务质量管理的5个维度)

深度自定义mybatis回顾mybatis的操作的核心步骤编写核心类SqlSessionFacotryBuild进行解析配置文件深度分析解析SqlSessionFacotryBuild干的核心工作编写...

@Autowired与@Resource原理知识点详解

springIOCAOP的不多做赘述了,说下IOC:SpringIOC解决的是对象管理和对象依赖的问题,IOC容器可以理解为一个对象工厂,我们都把该对象交给工厂,工厂管理这些对象的创建以及依赖关系...

java的redis连接工具篇(java redis client)

在Java里,有不少用于连接Redis的工具,下面为你介绍一些主流的工具及其特点:JedisJedis是Redis官方推荐的Java连接工具,它提供了全面的Redis命令支持,且...