开发者社区> 岛哥手记> 正文

Android内存性能测试

简介: Android应用大部分性能问题归根结底都会成为内存的问题,今天我们就先以Out of Memory(OOM)为起点介绍一下Android内存的原理以及排查内存问题的方法。
+关注继续查看

原理



在讲OOM之前我们先来弄清楚几个概念:内存泄漏、内存抖动、内存溢出


  • 内存泄漏:内存泄漏是指没有用的对象资源仍与GC-Root保持可达路径,导致系统无法进行回收;
  • 内存抖动:内存抖动是因为大量的对象被创建又在短时间内马上被释放,导致频繁 GC;
  • 内存溢出:我们需要一定的内存大小,但是系统无法分配给我们,满足不了我们的需求,所以会导致OOM;


既然我们知道了什么是内存溢出,那它是什么时候发生的呢?答案是在虚拟机的Heap内存使用超过堆内存最大值(Max Memory Heap)的时候,那么在这里大家需要理解的第一个概念就是Dalvik(ART)虚拟机的最大堆内存。


虚拟机的堆内存最大值


在虚拟机中,Android系统给堆(Heap)内存设置了一个最大值,可以通过runtime.getruntime().maxmemory()获取。而因为游戏消耗内存特别大的原因,Android给开通了一个绿色通道,可以在manifest里面设置LargeHeap为true。


虽然我们的手机拥有动辄8GB、16GB的内存,但系统只会分给每个应用一小部分。比如Nexus7单个应用的最大可用内存是192MB,这个值一般在Android设备出厂以后就固定下来了,分这么小内存有一个重要的原因,是Android默认没有虚拟内存。在内存资源稀缺的大背景下,为了保证在极端情况下,前台App和系统还能稳定运行,就只有靠low memory killer机制。

Low Memory Killer


下面引出另一个重要概念Low Memory Killer,也是App消耗内存过大导致的另外一个结果。在手机剩余内存低于内存警戒线的时候,就会召唤Low Memory Killer这个劫富济贫的“杀手”在后台默默干活。这里需要记住一句:App占用内存越多,被Low Memory Killer处理掉的机会就越大。


如果OOM和Low Memory Killer都没有干掉你的App,那也不代表App就没有内存问题,因为还有一类问题,会直接导致App卡顿,那就是GC。


GC


最简单的理解就是没有被GC ROOT间接或直接引用的对象的内存会被回收。在具体执行中,ART和Dalvik会有很多不同,并发GC的时候ART会比Dalvik少一个stop-the-world的阶段,因此Dalvik比ART更容易产生Jank(卡顿),当然,无论ART还是Dalvik并发GC的stop-the-world的时间并不长。然而,糟糕的情况是GC for Alloc,这个情况在内存不足以分配给新的对象时触发,它stop-the-world的时间因为GC无法并发而变得更长。


那么说到底,我们还是要避免GC FOR ALLOC,跟要避免OOM一样,关键是要管理好内存。什么是管理好内存?除了减少内存的申请回收外,更重要的是减少常驻内存和避免内存泄漏,说起内存泄漏,就必须要提Activity内存泄漏。


Activity内存泄漏


因为Activity对象会间接或者直接引用View、Bitmap等,所以一旦无法释放,会占用大量内存,如下图:


微信图片_20220518230445.jpg


图片缓存


另外一个情况就是内存常驻了,而通常在常驻内存中最大的就是图片。现在很多互联网产品APP中都有大量的图片,但是这些图片在内存中的存储如果不合理就会导致Crash堆栈然后是疯狂GC,接着触发我们前面说到的GC for Alloc,导致Stop-the-world的“卡”,最后的结果就是导致功能异常,有损用户体验。


既然有这么多的损害,为什么不能把图片下载来都放到磁盘(SD Card)上呢?其实答案不难猜,放在内存中,展示起来会“快”那么一些,快的原因有如下两点:

  • 硬件快(内存本身读取、存入速度快);
  • 复用快(解码成果有效保存,复用时,直接使用解码后对象,而不是再做一次图片解码);


很多同学不知道所谓“解码”的概念,可以简单地理解,Android系统要在屏幕上展示图片的时候只认“像素缓冲”,而这也是大多数操作系统的特征。我们常见的jpg、png等图片格式,都是把“像素缓冲”使用不同的手段压缩后的结果,所以相对而言,这些格式的图片,要在设备上展示,就必须经过一次“解码”,它的执行速度会受图片压缩比、尺寸等因素影响,是影响图片展示速度的一个重要因素。


因此官方建议使用LRU算法来做图片缓存,而不是之前推荐的WeekReference,因为WeekReference会导致大量GC。另外官方也建议,把从内存淘汰的图片,降低压缩比存储到本地,以备后用。这样就可以最大限度地降低以后复用时的解码开销。


现在我们来归纳一下,内存问题主要包括常驻问题(主要是图片缓存)、泄漏问题(主要是Activity泄漏)、GC问题(关键是GC For Alloc),后果会导致App Crash、闪退、后台被杀、卡顿,而且这是各种资源类性能问题积压的最后一环。因此可见其重要性,下面,我们来介绍一下如何简单快速的检测和定位内存泄漏问题。

方案



这里介绍手工和自动两种检测方案


手工检测和定位


先介绍一个命令:


$ adb shell dumpsys meminfo (pid name)

这个命令是用来查看指定进程所占用内存的具体情况,比如当前APP在手机中占用的具体的堆内存大小、View数量、Activity数量等:


微信图片_20220518230450.jpg


其中Activities的数量是一个非常关键的信息,可以帮助我们快速找出内存泄漏的页面,我们可以反复进入待测页面,如果反复进入退出后,查询内存的占用情况,Activity数量一直在增加,那说明一定是发生内存泄漏了。


在确定了哪个页面发生内存泄漏后,用Android Studio 自带工具就可以直接分析泄漏的Activity,完全没必要再单独安装MAT了,如下图打开Android Studio 的profile进入内存模块:


微信图片_20220518230455.png

点击Dump,反复进入退出发生内存泄漏的页面


微信图片_20220518230459.jpg

勾选下面的Activity/Fragment Leaks 就可以展示出具体哪些Activity或Fragment发生了内存泄漏,右边还有具体的引用情况


微信图片_20220518230507.jpg

自动检测和定位

这里主要是通过leakcanary来实现的,leakcanary具体在客户端的接入就不多说了,可以参考官网的文档:https://square.github.io/leakcanary/getting_started/


这里主要想讲一下如何自动收集leakcanary检测出的内存泄漏信息,因为在日常测试和开发过程中,即便客户端接了内存泄漏检测的工具,但也只是作为一个debug工具,很难系统的看出某个版本的应用内存泄漏情况是如何的。


于是我们需要在业务和开发同学平时使用的过程中顺带将这些信息收集上来,在同一的平台上以版本和页面为维度去展示,可以直观的看到某个版本发生了多少次内存泄漏以及哪些页面的哪些调用栈。


首先新建一个LeakUploadService类,用来格式化内存泄漏详情以及上传到日志服务器便于快速定位,具体代码如下:

public class LeakUploadService extends DisplayLeakService {
    @Override
    protected void afterDefaultHandling(HeapDump heapDump, AnalysisResult result, String leakInfo) {
        if (!result.leakFound || result.excludedLeak) {
            return;
        }
        String className = result.className;
        String pkgName = leakInfo.trim().split(":")[0].split(" ")[1];
        String pkgVer = leakInfo.trim().split(":")[1];
        String leakDetail = leakInfo.split("\n\n")[0] + "\n\n" + leakInfo.split("\n\n")[1];
        JSONObject json = new JSONObject();
        try {
            json.put("className", className);
            json.put("app", pkgName);
            json.put("appVersion", pkgVer);
            json.put("leakDetail", leakDetail);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        OkHttpClient okHttpClient = new OkHttpClient();
        MediaType mediaType = MediaType.parse("application/json; charset=utf-8");
        RequestBody requestBody = RequestBody.create(mediaType, json.toString());
        Request request = new Request.Builder()
                .url("日志上传接口")
                .post(requestBody)
                .build();
        try {
            okHttpClient.newCall(request).execute();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

然后在manifest中注册:

<service android:name="com.squareup.leakcanary.LeakUploadService"/>


最后将Service注册到监听接口:

LeakCanary.refWatcher(application).listenerServiceClass(LeakUploadService.class)


版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
23581 0
阿里云服务器ECS远程登录用户名密码查询方法
阿里云服务器ECS远程连接登录输入用户名和密码,阿里云没有默认密码,如果购买时没设置需要先重置实例密码,Windows用户名是administrator,Linux账号是root,阿小云来详细说下阿里云服务器远程登录连接用户名和密码查询方法
22359 0
阿里云ECS云服务器初始化设置教程方法
阿里云ECS云服务器初始化是指将云服务器系统恢复到最初状态的过程,阿里云的服务器初始化是通过更换系统盘来实现的,是免费的,阿里云百科网分享服务器初始化教程: 服务器初始化教程方法 本文的服务器初始化是指将ECS云服务器系统恢复到最初状态,服务器中的数据也会被清空,所以初始化之前一定要先备份好。
16657 0
阿里云服务器安全组设置内网互通的方法
虽然0.0.0.0/0使用非常方便,但是发现很多同学使用它来做内网互通,这是有安全风险的,实例有可能会在经典网络被内网IP访问到。下面介绍一下四种安全的内网互联设置方法。 购买前请先:领取阿里云幸运券,有很多优惠,可到下文中领取。
22539 0
如何设置阿里云服务器安全组?阿里云安全组规则详细解说
阿里云安全组设置详细图文教程(收藏起来) 阿里云服务器安全组设置规则分享,阿里云服务器安全组如何放行端口设置教程。阿里云会要求客户设置安全组,如果不设置,阿里云会指定默认的安全组。那么,这个安全组是什么呢?顾名思义,就是为了服务器安全设置的。安全组其实就是一个虚拟的防火墙,可以让用户从端口、IP的维度来筛选对应服务器的访问者,从而形成一个云上的安全域。
19808 0
windows server 2008阿里云ECS服务器安全设置
最近我们Sinesafe安全公司在为客户使用阿里云ecs服务器做安全的过程中,发现服务器基础安全性都没有做。为了为站长们提供更加有效的安全基础解决方案,我们Sinesafe将对阿里云服务器win2008 系统进行基础安全部署实战过程! 比较重要的几部分 1.
11998 0
+关注
81
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
JS零基础入门教程(上册)
立即下载
性能优化方法论
立即下载
手把手学习日志服务SLS,云启实验室实战指南
立即下载