Android性能优化系列之内存优化

版权声明:本文为博主原创文章,未经博主允许不得转载。

目录(?)[+]

  1. 内存的分配策略概述

  2. 内存泄露产生的原因

  3. 内存泄漏的检测

  4. Allocation TrackerDevice Monitor

  5. DDMS的Heap Viewer

  6. MAT工具

  7. LeakCanary

Java中,内存的分配是由程序完成的,而内存的释放是由垃圾收集器(Garbage Collection,GC)完成的,程序员不需要通过调用函数来释放内存,但也随之带来了内存泄漏的可能,上篇博客,我介绍了 Android性能优化系列之布局优化,本篇博客,我将介绍内存优化的相关知识。

内存的分配策略概述


程序运行时的内存分配有三种策略,分别是静态的,栈式的,和堆式的,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、堆区和栈区。

静态存储区(方法区):内存在程序编译的时候就已经分配好,这块内存在程序整个运行期间都存在。它主要存放静态数据、全局static数据和常量。

栈区:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

堆区:亦称动态内存分配。程序在运行的时候用malloc或new申请任意大小的内存,程序员自己负责在适当的时候用free或delete释放内存(Java则依赖垃圾回收器)。动态内存的生存期可以由我们决定,如果我们不释放内存,程序将在最后才释放掉动态内存。 但是,良好的编程习惯是:如果某动态内存不再使用,需要将其释放掉。

堆和栈的区别:

在函数中(说明是局部变量)定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用。

堆内存用于存放所有由new创建的对象(内容包括该对象其中的所有成员变量)和数组。在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,在栈中的这个特殊的变量就变成了数组或者对象的引用变量,以后就可以在程序中使用栈内存中的引用变量来访问堆中的数组或者对象,引用变量相当于为数组或者对象起的一个别名,或者代号

堆是不连续的内存区域(因为系统是用链表来存储空闲内存地址,自然不是连续的),堆大小受限于计算机系统中有效的虚拟内存(32bit系统理论上是4G),所以堆的空间比较灵活,比较大。栈是一块连续的内存区域,大小是操作系统预定好的,windows下栈大小是2M(也有是1M,在编译时确定,VC中可设置)。

对于堆,频繁的new/delete会造成大量内存碎片,使程序效率降低。对于栈,它是先进后出的队列,进出一一对应,不产生碎片,运行效率稳定高。

这里写图片描述

所以我们可以得出结论:

1.局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。因为它们属于方法中的变量,生命周期随方法而结束。

2.成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体),因为它们属于类,类对象终究是要被new出来使用的。

3.我们所说的内存泄露,只针对堆内存,他们存放的就是引用指向的对象实体。

内存泄露产生的原因


在Java中,内存的分配是由程序完成的,而内存的释放是由垃圾收集器(Garbage Collection,GC)完成的,程序员不需要通过调用函数来释放内存,但它只能回收无用并且不再被其它对象引用的那些对象所占用的空间。

Java的内存垃圾回收机制是从程序的主要运行对象(如静态对象/寄存器/栈上指向的堆内存对象等)开始检查引用链,当遍历一遍后得到上述这些无法回收的对象和他们所引用的对象链,组成无法回收的对象集合,而其他孤立对象(集)就作为垃圾回收。GC为了能够正确释放对象,必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。

在Java中,这些无用的对象都由GC负责回收,因此程序员不需要考虑这部分的内存泄露。虽然,我们有几个函数可以访问GC,例如运行GC的函数System.gc(),但是根据Java语言规范定义,该函数不保证JVM的垃圾收集器一定会执行。因为不同的JVM实现者可能使用不同的算法管理GC。通常GC的线程的优先级别较低。JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但通常来说,我们不需要关心这些。

但是我们仍然可以去监听系统的GC过程,以此来分析我们应用程序当前的内存状态。那么怎样才能去监听系统的GC过程呢?其实非常简单,系统每进行一次GC操作时,都会在LogCat中打印一条日志,我们只要去分析这条日志就可以了,日志的基本格式如下所示:

1.D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <Pause_time>

  • 1

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 1

首先第一部分GC_Reason,这个是触发这次GC操作的原因,一般情况下一共有以下几种触发GC操作的原因:

GC_CONCURRENT: 当我们应用程序的堆内存快要满的时候,系统会自动触发GC操作来释放内存。

GC_FOR_MALLOC: 当我们的应用程序需要分配更多内存,可是现有内存已经不足的时候,系统会进行GC操作来释放内存。

GC_HPROF_DUMP_HEAP: 当生成HPROF文件的时候,系统会进行GC操作,关于HPROF文件我们下面会讲到。

GC_EXPLICIT: 这种情况就是我们刚才提到过的,主动通知系统去进行GC操作,比如调用System.gc()方法来通知系统。或者在DDMS中,通过工具按钮也是可以显式地告诉系统进行GC操作的。

接下来第二部分Amount_freed,表示系统通过这次GC操作释放了多少内存。

然后Heap_stats中会显示当前内存的空闲比例以及使用情况(活动对象所占内存 / 当前程序总内存)。

最后Pause_time表示这次GC操作导致应用程序暂停的时间。关于这个暂停的时间,Android在2.3的版本当中进行过一次优化,在2.3之前GC操作是不能并发进行的,也就是系统正在进行GC,那么应用程序就只能阻塞住等待GC结束。虽说这个阻塞的过程并不会很长,也就是几百毫秒,但是用户在使用我们的程序时还是有可能会感觉到略微的卡顿。而自2.3之后,GC操作改成了并发的方式进行,就是说GC的过程中不会影响到应用程序的正常运行,但是在GC操作的开始和结束的时候会短暂阻塞一段时间,不过优化到这种程度,用户已经是完全无法察觉到了。

我们来看看Java中需要被回收的垃圾:

{ Person p1 = new Person(); …… }

  • 1

  • 2

  • 3

  • 4

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 1

  • 2

  • 3

  • 4

引用句柄p1的作用域是从定义到“}”处,执行完这对大括号中的所有代码后,产生的Person对象就会变成垃圾,因为引用这个对象的句柄p1已超过其作用域,p1失效,在栈中被销毁,因此堆上的Person对象不再被任何句柄引用了。 因此person变为垃圾,会被回收。

这里我们需要讲述一个关键词:引用,通过A能调用并访问到B,那就说明A持有B的引用,或A就是B的引用,B的引用计数+1.

(1)比如 Person p1 = new Person();通过P1能操作Person对象,因此P1是Person的引用;

(2)比如类O中有一个成员变量是I类对象,因此我们可以使用o.i的方式来访问I类对象的成员,因此o持有一个i对象的引用。

GC过程与对象的引用类型是严重相关的,我们来看看Java对引用的分类Strong reference, SoftReference, WeakReference, PhatomReference

这里写图片描述

在Android应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周期较长的对象时候,可以尽量应用软引用和弱引用技术。

软/弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。利用这个队列可以得知被回收的软/弱引用的对象列表,从而为缓冲器清除已失效的软/弱引用。

假设我们的应用会用到大量的默认图片,比如应用中有默认的头像,默认游戏图标等等,这些图片很多地方会用到。如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低。所以我们考虑将图片缓存起来,需要的时候直接从内存中读取。但是,由于图片占用内存空间比较大,缓存很多图片需要很多的内存,就可能比较容易发生OutOfMemory异常。这时,我们可以考虑使用软/弱引用技术来避免这个问题发生。以下就是高速缓冲器的雏形:

首先定义一个HashMap,保存软引用对象。

1.private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();

  • 1

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 1

再来定义一个方法,保存Bitmap的软引用到HashMap

public class CacheSoftRef { //首先定义一个HashMap,保存引用对象 private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>(); //再来定义一个方法,保存Bitmap的软引用到HashMap public void addBitmapToCache(String path) { //强引用的Bitmap对象 Bitmap bitmap = BitmapFactory.decodeFile(path); //软引用的Bitmap对象 SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap); //添加该对象到Map中使其缓存 imageCache.put(path, softBitmap); } //获取的时候,可以通过SoftReference的get()方法得到Bitmap对象 public Bitmap getBitmapByPath(String path) { //从缓存中取软引用的Bitmap对象 SoftReference<Bitmap> softBitmap = imageCache.get(path); //判断是否存在软引用 if (softBitmap == null) { return null; } //通过软引用取出Bitmap对象,如果由于内存不足Bitmap被回收,将取得空,如果未被回收, //则可重复使用,提高速度。 Bitmap bitmap = softBitmap.get(); return bitmap; } }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

  • 22

  • 23

  • 24

  • 25

  • 26

  • 27

  • 28

  • 29

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

  • 22

  • 23

  • 24

  • 25

  • 26

  • 27

  • 28

  • 29

使用软引用以后,在OutOfMemory异常发生之前,这些缓存的图片资源的内存空间可以被释放掉的,从而避免内存达到上限,避免Crash发生。

如果只是想避免OutOfMemory异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。

另外可以根据对象是否经常使用来判断选择软引用还是弱引用。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。

所以我们得出内存泄漏的原因:堆内存中的长生命周期的对象持有短生命周期对象的强/软引用,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是Java中内存泄露的根本原因。

内存泄漏的检测


说了那么多关于内存分配的知识,接下来我们就看看Android给我们提供了哪些工具来解决内存泄漏的问题

Allocation Tracker(Device Monitor)


Allocation Tracker位于Android Device Monitor中

这里写图片描述

Allocation Tracker面板

这里写图片描述

各名称的含义如下:

这里写图片描述

Allocation Tracker操作

1.首先进入你要追踪的界面

2.点击Start Tracking按钮,开始跟踪内存分配轨迹

3.操作你的界面,尽量时间短点

4.点击Get Allocations按钮,抓去内存分配轨迹信息,显示在右边的面板中,默认以内存大小排序,你可以以分配顺序排序或者仍以列排序。

5.logcat中会显示出这次的轨迹共抓到内存分配轨迹记录数,可以简单的理解分配了多少次内存,这个数值和Alloc order的最大值是相等的

6.如果你不想看那么多乱七八糟的,你可以使用Filter来过滤,输入包名就可以了。

跟踪内存轨迹

如果这个时候我们想单独获取某次操作的内存轨迹,首先一定要记得Stop Tracking再Start Tracking一下,让追踪点初始化一下,然后就进行我们需要观察内存变化的操作,然后点击Get Allocations,这个时候我们从首页进入一个详情页,看一下我们的内存分配轨迹:

这里写图片描述

追踪到的内存分配20293次,看着是不是有点无从下手,没关系,用Filter过滤下:

这里写图片描述

过滤后,就剩下了跟我们App源码有关系的分配轨迹,我们随便选择一栏,可以看到其trace信息:

这里写图片描述

上图中,我们可以看出来,在第635次内存分配中,分配的是IntroduceFragment对象,占用内存224字节,处理线程Id为3245,在java.lang.Class的newInstance方法中分配的。从trace信息可以看出来该方法一步一步被调用的信息。

DDMS的Heap Viewer


Heap Viewer能做什么?

实时查看App分配的内存大小和空闲内存大小

发现Memory Leaks

在Devices 中,点击要监控的程序。

点击Devices视图界面中最上方一排图标中的“Update Heap”

点击Heap视图

点击Heap视图中的“Cause GC”按钮

到此为止需检测的进程就可以被监视。

这里写图片描述

按上图的标记顺序按下,我们就能看到内存的具体数据,右边面板中数值会在每次GC时发生改变,包括App自动触发或者你来手动触发。

ok,现在来解释下面板中的名词

总览

这里写图片描述

这里写图片描述

详情

这里写图片描述

这里写图片描述

下面是每一个对象都有的列名含义:

这里写图片描述

当我们点击某一行时,可以看到如下的柱状图:

这里写图片描述

横坐标是对象的内存大小,这些值随着不同对象是不同的,纵坐标是在某个内存大小上的对象的数量

Heap Viewer的使用

我们说Heap Viewer适合发现内存泄漏的问题,那么如何检测呢?

那么如何检测呢?

进入某应用,不断的操作该应用,同时注意观察data object的Total Size值,正常情况下Total Size值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾回收的情况。

所以说虽然我们不断的操作会不断的生成很多对象,而在虚拟机不断的进行GC的过程中,这些对象都被回收了,内存占用量会会落到一个稳定的水平;反之如果代码中存在没有释放对象引用的情况,则data object的Total Size值在每次GC后不会有明显的回落。随着操作次数的增多Total Size的值会越来越大,直到到达一个上限后导致进程被杀掉。

MAT工具


那么通过上面DDMS工具,现在我们已经可以比较轻松地发现应用程序中是否存在内存泄露的现象了。

我们应该怎么定位到具体是哪里出的内存泄露呢?这就需要借助一个内存分析工具了,叫做Eclipse Memory Analyzer(MAT)。下载地址是:http://eclipse.org/mat/downloads.php

为了使用该工具,我们需要hprof文件。但是该文件不能直接被MAT使用,需要进行一步转化,可以使用hprof-conv命令来转化,但是Android Studio可以直接转化,转化方法如下:

总结

其实要轻松掌握很简单,要点就两个:

  1. 找到一套好的视频资料,紧跟大牛梳理好的知识框架进行学习。
  2. 多练。 (视频优势是互动感强,容易集中注意力)

你不需要是天才,也不需要具备强悍的天赋,只要做到这两点,短期内成功的概率是非常高的。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。

以上就是总结的关于在面试的一些总结,希望对大家能有些帮助,除了这些面试中需要注意的问题,当然最重要的就是刷题了,这里放上我之前整理的一份超全的面试专题PDF

还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

这里只是整理出来的部分面试题,后续会持续更新,希望通过这些高级面试题能够降低面试Android岗位的门槛,让更多的Android工程师理解Android系统,掌握Android系统。喜欢的话麻烦点击一个喜欢在关注一下~

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。

以上就是总结的关于在面试的一些总结,希望对大家能有些帮助,除了这些面试中需要注意的问题,当然最重要的就是刷题了,这里放上我之前整理的一份超全的面试专题PDF

还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

[外链图片转存中…(img-w4SqFVNX-1714426609265)]

这里只是整理出来的部分面试题,后续会持续更新,希望通过这些高级面试题能够降低面试Android岗位的门槛,让更多的Android工程师理解Android系统,掌握Android系统。喜欢的话麻烦点击一个喜欢在关注一下~

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值