Android | App内存优化 之 内存泄漏 要点概述 以及 解决实战

简介: Android | App内存优化 之 内存泄漏 要点概述 以及 解决实战

本文目录:

  • 内存泄漏的定义、表现、危害、情景,及避免OOM的技巧
  • Memory Analyzer Tool(MAT)简述、下载、安装
  • 内存泄漏解决实战
  • 解决方法小结

内存泄漏的定义、表现、危害、情景,及避免OOM的技巧

定义

  • **Android内存泄漏指的是进程中某些对象(垃圾对象)已经没有使用价值了,

但是它们却可以直接间接地引用到gc roots导致无法被GC回收

无用的对象占据着内存空间,使得实际可使用内存变小,形象地说法就是内存泄漏了。**

表现

  • **内存抖动、可用内存逐渐变少

上一篇博客写到,
内存抖动可能是
因为代码逻辑问题 导致内存被不断地进行分配回收
当然一个地方它的内存一直在抖动
还有可能是由于内存泄漏引起的,
比如说,内存泄漏 导致 可用内存逐渐减少
这时候系统为了增加可用内存,就会一直不断地进行GC
导致内存一直在抖动!!**

危害

内存不足 -- GC频繁 -- OOM

可能出现、需要注意的情景

  • .
    **1. 非静态内部类的静态实例

(“类”是这个类的类型,实例是new 出来的实例)**
非静态内部类会维持一个到外部类实例的引用,
如果非静态内部类的实例是静态的,
就会间接长期维持着外部类的引用,阻止被回收掉。
**解决办法
使用静态内部类,
静态内部类实例,不会维持一个到外部类实例的引用!**

**2.多线程相关的匿名内部类和非静态内部类**

匿名内部类同样会持有外部类的引用,
如果在线程中执行耗时操作
就有可能发生内存泄漏,导致外部类无法被回收,直到耗时任务结束,
**解决办法
在页面退出时结束线程中的任务**

**3. Handler临时性内存泄露**

Handler导致的内存泄漏也可以被归纳为非静态内部类实例(这里特指Handler实例)导致的;

Handler通过发送Message与主线程交互,
Message发出之后是存储MessageQueue中的,
有些Message也不是马上被处理的。
Message中存在一个 target,是指向Handler的一个引用,
如果MessageQueue存在的时间过长,
就会导致Handler无法被回收

如果Handler非静态的,
则会导致Handler外部类,如Activity或者Service不会被回收

由于AsyncTask内部也是Handler机制,同样存在内存泄漏的风险。
此种内存泄露,一般是临时性的。

**解决办法
A.使用静态handler,外部类引用使用弱引用处理
B.在退出页面时移除消息队列中的消息**

**4.Context导致内存泄漏**

根据场景确定使用ActivityContext还是ApplicationContext
因为二者生命周期不同,
对于不必须使用ActivityContext的场景(Dialog),一律采用ApplicationContext!!!
单例模式最常见的发生此泄漏的场景,
比如传入一个ActivityContext静态类引用,导致无法回收

**5.静态View导致泄漏**

使用静态View可以避免每次启动Activity都去读取渲染View!!!
但是静态View会持有Activity引用,导致无法回收!!!
**解决办法
Activity销毁的时候将静态View设置为null
View一旦被加载到界面中将会持有一个Context对象引用
在这里,这个context对象是我们的Activity,!!!
声明一个静态变量 引用这个View,也就引用activity)**

**6.WebView导致的内存泄漏**

WebView只要使用一次内存就不会被释放
所以WebView都存在内存泄漏的问题,!!!
**通常的解决办法:
WebView 单开一个进程
使用AIDL进行通信,根据业务需求在合适的时机释放掉!!**

**7. 资源对象未关闭**

资源性对象CursorFileSocket等,
内部往往都使用了缓冲,容易造成内存泄漏,
应该在使用后及时关闭。
未在finally中关闭,
会导致异常情况下资源对象未被释放的隐患。

**8.集合中的对象未清理**

我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,
当我们不需要集合中的某个对象时,
如果没有把它的引用从集合中清理掉,这个集合就会越来越大。
如果这个集合是static的话,那情况就更严重了。
**所以要在退出程序之前,
需要调用集合实例的clear() 将集合里的东西clear掉,
然后将集合实例置为null,再退出程序。**

**9.Bitmap导致内存泄漏

bitmap是比较占内存的,所以一定要在不使用的时候及时进行清理;
同时避免静态变量持有大的bitmap对象;**

**10.监听器未关闭,注册对象未反注册

很多需要registerunregister系统服务
要在合适的时候进行unregister,手动添加的listener也需要及时移除**

**11. 类的静态变量持有大数据对象**

静态变量长期维持到大数据对象的引用,阻止垃圾回收。

如何避免OOM?

1.Bitmap优化

Bitmap非常消耗内存,

而且在Android中,读取bitmap时,
一般分配给虚拟机的图片堆栈只有8M,所以经常造成OOM问题。
所以有必要针对Bitmap的使用作出优化:

**1.1. 图片显示:加载合适尺寸的图片,比如显示缩略图的地方不要加载大图。**<br>
**1.2. 图片回收:使用完`bitmap`,及时使用`Bitmap.recycle()`回收。
问题:Android不是自身具备垃圾回收机制吗?此处为何要手动回收。**

Bitmap对象不是new生成的,而是通过BitmapFactory生产的。
通过源码可发现是通过调用JNI生成Bitmap对象nativeDecodeStream()等方法)。
所以,
加载bitmap到内存里包括两部分
Dalvik(ART)内存Linux kernel内存
前者被虚拟机自动回收
后者必须通过recycle()方法,
内部调用nativeRecycle()linux kernel回收

**1.3. 捕获OOM异常:程序中设定如果发生OOM的应急处理方式。**<br>
**1.4. 图片缓存:内存缓存、硬盘缓存等**<br>
**1.5. 图片压缩:直接使用ImageView显示Bitmap时会占很多资源,

尤其当图片较大时容易发生OOM。
可以使用BitMapFactory.Options对图片进行压缩。**

**1.6. 图片像素(质量):android默认颜色模式为`ARGB_8888`,  显示质量最高,占用内存最大。

若要求不高时可采用RGB_565等模式。
还可以使用WebP
图片大小图片长度 * 宽度 * 单位像素 所占据字节数**
ARGB_4444:每个像素占用2byte内存
ARGB_8888:每个像素占用4byte内存 (默认)
RGB_565:每个像素占用2byte内存

**1.7. 考虑使用`inBitmap`;[图片优化之inBitmap](https://www.jianshu.com/p/45fbef7a58ba)**<br>

2. 巧用对象引用类型

  • **强引用 strong:Object object=new Object()。

当内存不足时,Java虚拟机宁愿抛出OOM内存溢出异常,
也不会轻易回收强引用对象来解决内存不足问题;
软引用 soft:只有当内存达到某个阈值时才会去回收,常用于缓存;
弱引用 weak :只要被GC线程扫描到了就进行回收;
虚引用**

  • **如果想要避免OOM发生,则使用软引用对象,即当内存快不足时进行回收;

如果想尽快回收某些占用内存较大的对象,例如bitmap,可以使用弱引用,能被快速回收。
不过如果要对bitmap作缓存就不要使用弱引用,因为很快就会被GC回收,导致缓存失败。**

3. 使用 池 pool 内存对象重复利用

  • 对象池:如果某个对象在创建时,需要较大的资源开销,

那么可以将其放入对象池,
即将对象保存起来,下次需要时直接取出使用,
而不用再次创建对象。
当然,维护对象池也需要一定开销,故要衡量。

a. ListView/GridView源码可以看到重用的情况 ConvertView的复用。
RecyclerViewRecycler源码。
b.**Bitmap的复用
Listview等要显示大量图片。
需要使用 LRU缓存机制来复用图片。**
  • 线程池:与对象池差不多,

将线程对象放在池中供反复使用,减少反复创建线程的开销。

**4. 使用更加轻量的数据结构:
考虑适当的情况下,
使用更加高效的安卓专门为手机研发的数据结构类
ArrayMap/SparseArray/SparseLongMap/SparseIntMap/SparseBoolMap
替代HashMap等传统数据结构。
HashMap.put(string,Object);Object o = map.get(string);会导致一些没必要的自动装箱拆箱
HashMap,HashMap更耗内存,
因为它需要额外的实例对象来记录Mapping操作,
SparseArray更加高效
因为它避免了Key Value自动装箱,和装箱后解箱操作;**

**5. StringBuilder替代String: 在有些时候,
代码中会需要使用到大量的字符串拼接的操作,
这种时候有必要考虑使用StringBuilder来替代频繁的“+”**

**6.避免在类似onDraw这样的方法中创建对象
因为它会迅速占用大量内存,引起频繁的GC甚至内存抖动**

参考文章


Memory Analyzer Tool(MAT)简述、下载、安装

  • **一个强大的Java Heap 工具

相对于Memory Profiler(MP)的简单分析,
MAT可以对Java内存做一个深入的分析;**

这里可以下载独立版MAT工具;**

** MAT安装及使用教程
文件下载下来的是一个zip压缩包, 解压压缩包,
进入到如下目录,
双击对应的exe文件,
即可开始使用:
工具初始界面如下: **
  • 转换:hprof-conv 原文件路径 转换后文件路径


内存泄漏解决实战

  • 程序使用Bitmap来举例,

因为对于一般程序,最大的问题往往都在Bitmap上,
因为它消耗的内存非常多,
将其作为内存泄漏的案例,效果会比较明显;

**定义类似观察者的一个接口CallBack
以及类似被观察者的一个实现类CallBackManager:**

public interface CallBack {
    void dpOperate();
}

----

public class CallBackManager {

    public static ArrayList<CallBack> sCallBacks = new ArrayList<>();

    public static void addCallBack(CallBack callBack) {
        sCallBacks.add(callBack);
    }

    public static void removeCallBack(CallBack callBack) {
        sCallBacks.remove(callBack);
    }

}

activity_memoryleak.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/iv_memoryleak"
        android:layout_width="50dp"
        android:layout_height="50dp" />

</LinearLayout>

MemoryLeakActivity.java:

/**
 * 模拟内存泄露的Activity
 */
public class MemoryLeakActivity extends AppCompatActivity implements CallBack{

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memoryleak);
        ImageView imageView = findViewById(R.id.iv_memoryleak);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.splash);
        imageView.setImageBitmap(bitmap);

        CallBackManager.addCallBack(this);
    }

    @Override
    public void dpOperate() {
        // do sth
    }
}
  • **dpOperate()模拟操作业务的逻辑方法;

在反复地退出关闭和打开进入 以上界面的时候,
就可能引起内存泄漏;**

  • 运行程序,打开MP,初始曲线图是平稳的:
  • **建立一个简单的界面,如MainActivity

可以点击进入MemoryLeakActivity
然后不断地在MainActivity和MemoryLeakActivity之间切换,
即反复地退出关闭和打开进入 MemoryLeakActivity,
这时候可以看到MP的图示,内存曲线在阶梯状上升,
也就是说我们的可用内存在逐渐减少了,!!!
出现了这种情况,我们基本就可以断定,界面可能就是出现了内存泄漏!!!:**

  • MP工具这里,

只能帮我们大致断定这个界面是出现了内存泄漏,
但是它没有办法帮助我们 断定那个地方有 内存泄漏,
来让我们有的放矢 修改代码;
这里就需要MAT上场了;

  • **首先需要点击堆转储按钮,

MP工具会记录一段时间的内存分配情况,
然后我们可以对这段记录进行Dump,
下载成文件保存在本地:**

  • 保存成功:
  • **接着,

在AS的下方的Terminal终端栏,**

Android studio 进入 adb 命令 ---使用terminal 终端 进入sdk 找到 platform-tools 目录进入即可
  • **使用cd指令,

进入到配套SDK目录下的platform-tools目录,
回车,到达工具目录;**

  • **接着在使用platform-tools目录目录下,

使用hprof-conv工具指令,
转化堆转储保存下来的文件:**

  • 回车后,转换成功:

  • **接着如文首下载好独立版本的MAT工具,

使用MAT工具,打开我们刚刚转化完的文件
(工具界面左上角File ---> openFile ---> 选择文件打开):**

  • 打开之后,MAT 就会对我们的 堆转储转换后的文件 进行分析:
  • **接下来目的是通过MAT来找到内存泄漏的位置,

点击左下角有个Histogram:**

  • 进入Histogram界面后,可以看到最上面有个匹配搜索:

  • **这里搜索我们Activity的名字:MemoryLeak可以看到搜索结果,

可以看到这里Objects这里显示着有18个,
也就是说现在内存当中,竟存在着18MemoryLeakActivity实例
(emmmmm,也就是我们刚刚在试验的时候,
反复退出进入了18次MemoryLeakActivity),
这是非常不合理的!

后面是
Shallow Heap:堆中 此类型所有实例 的总大小(以字节为单位)
Retained Heap:为此 类型的所有实例 而 保留的内存总大小(以字节为单位)

接下来,点击搜索出来的实例,右键,
选择List objects -> with incoming references,
(with incoming reference
incoming 指过来
即指的是 引用到选中实例实例 ,即查看本实例被谁引用;

with outcoming references
outcoming 指出去
该选中实例引用的实例,即查看本实例引用了谁;)
下面是点击后弹出的搜索结果:
选择第一个结果Item,右键,
Merge Shortest Path To GC Roots ---> exclude all phantom/weak/soft etc. references:
查看这个对象GC Root 之间的一个路径---exclude(除了)虚、弱引用、软引用之外,即只看强引用
(从GC上说,除了强引用外,
其他的引用在JVM需要的情况下是都可以 被GC掉的!!!
所以!!!
如果一个对象始终无法被GC,就是因为强引用的存在,!!!
从而导致在GC的过程中一直得不到回收,
因此就内存溢出了)
下面是分析结果:我们可以看到,
它从左上角往下是一个GC路径,
可以看到最后一个行的item其图标左下角有一个小圆圈
这是我们真正要关注的地方,
如上图所示,也就是说最终MAT这里给了我们的一个信息,
即上述所说的17个 MemoryLeakActivity 实例实际上
是被CallBackManager这个类中的sCallBack这个实例(对象)引用了,
至此,我们便可以回到代码中,寻找这个CallBackManager,去修改对应的代码;**

  • **可以看到果然我们一开始便定义了一个sCallBacks实例,

MemoryLeakActivity 实例就一直被它所引用着!!!!
因为这里sCallBacks实例它被static修饰着,!!!
Android中被static修饰着的变量,它的生命周期是跟APP的整个周期 一样长的,

所以我们打开进入MemoryLeakActivity的时候,
onCreate()中我们就把当前的一个MemoryLeakActivity实例加到sCallBacks这个List实例中去,而每次退出销毁MemoryLeakActivity的时候,
却没有在sCallBacks中移除刚刚添加的这个MemoryLeakActivity实例,
而且MemoryLeakActivity被销毁的时候,我们没有退出APP,
所以sCallBacks实例也一直存在,
如此一来,
每次反复进入打开退出销毁 MemoryLeakActivity的,
sCallBacks实例就会多持有一个MemoryLeakActivity实例,
多次进出MemoryLeakActivity累积下来,
sCallBacks实例持有的引用就只增不减,
而且这里的持有都是强应用持有,不会被GC回收的,
无用的对象占据的内存空间就越来越大,实际可使用内存逐渐变小,导致内存泄漏了!!


解决方法的话,
就是在onDestroy()中,
MemoryLeakActivity自身从sCallBacks实例中移除了,
这样每次退出销毁MemoryLeakActivity的时候,
onCreate()中添加进sCallBacks实例的MemoryLeakActivity自身,就会在销毁前被移除,
由此解决内存泄漏:**

/**
 * 模拟内存泄露的Activity
 */
public class MemoryLeakActivity extends AppCompatActivity implements CallBack{

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memoryleak);
        ImageView imageView = findViewById(R.id.iv_memoryleak);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.splash);
        imageView.setImageBitmap(bitmap);

        CallBackManager.addCallBack(this);
    }

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

    @Override
    public void dpOperate() {
        // do sth
    }
}

解决方法小结

  • **使用MP初步观察,

发现不断上升或者居高不下内存曲线
可用内存逐渐减少现象
便可以判断这个地方是可能出现了内存泄漏;**

  • **使用MP堆转储

将一段时间内的分配情况记录成文件
导出并保存这份文件
基于AS的Terminal终端栏,
使用hprof-conv工具指令
转化堆转储保存下来的文件;**

  • 使用MAT打开(OpenFile)并分析hprof-conv的转化生成的文件
  • **点击进入Histogram界面,

筛选可疑实例;
观察Objects栏值是否异常;**

  • **点击对应的实例,右键,

选择List objects -> with incoming references
查看本实例被谁引用
弹出的搜索结果界面

在上述弹出的搜索结果界面中,
选择一个结果Item,右键,
Merge Shortest Path To GC Roots ---> exclude all phantom/weak/soft etc. references:
查看这个对象跟 GC Root 之间的一个路径---exclude(除了)虚、弱引用、软引用之外,即只看强引用。
弹出分析结果界面,界面中 根据 图示(橙红小圆圈)
可以找到 引用选中实例实例对象 及其 所在的类文件名;**

  • **根据上述定位的 类文件名 以及 持有引用对象名

找到相应的位置
排查并修改代码
解决问题;**






参考自
相关文章
|
3月前
|
存储 前端开发 Java
Android MVVM架构模式下如何避免内存泄漏
Android采用MVVM架构开发项目,如何避免内存泄漏风险?怎样避免内存泄漏?
123 1
|
3月前
|
XML Java 数据库
安卓项目:app注册/登录界面设计
本文介绍了如何设计一个Android应用的注册/登录界面,包括布局文件的创建、登录和注册逻辑的实现,以及运行效果的展示。
266 0
安卓项目:app注册/登录界面设计
|
14天前
|
存储 监控 API
app开发之安卓Android+苹果ios打包所有权限对应解释列表【长期更新】-以及默认打包自动添加权限列表和简化后的基本打包权限列表以uniapp为例-优雅草央千澈
app开发之安卓Android+苹果ios打包所有权限对应解释列表【长期更新】-以及默认打包自动添加权限列表和简化后的基本打包权限列表以uniapp为例-优雅草央千澈
|
1月前
|
监控 Java Android开发
深入探索Android系统的内存管理机制
本文旨在全面解析Android系统的内存管理机制,包括其工作原理、常见问题及其解决方案。通过对Android内存模型的深入分析,本文将帮助开发者更好地理解内存分配、回收以及优化策略,从而提高应用性能和用户体验。
|
2月前
|
监控 Java Android开发
深入探讨Android系统的内存管理机制
本文将深入分析Android系统的内存管理机制,包括其内存分配、回收策略以及常见的内存泄漏问题。通过对这些方面的详细讨论,读者可以更好地理解Android系统如何高效地管理内存资源,从而提高应用程序的性能和稳定性。
94 16
|
2月前
|
C# Windows
【Azure App Service】在App Service for Windows上验证能占用的内存最大值
根据以上测验,当使用App Service内存没有达到预期的值,且应用异常日志出现OutOfMemory时,就需要检查Platform的设置是否位64bit。
52 11
|
2月前
|
开发框架 监控 .NET
【Azure App Service】部署在App Service上的.NET应用内存消耗不能超过2GB的情况分析
x64 dotnet runtime is not installed on the app service by default. Since we had the app service running in x64, it was proxying the request to a 32 bit dotnet process which was throwing an OutOfMemoryException with requests >100MB. It worked on the IaaS servers because we had the x64 runtime install
|
2月前
|
Android开发 开发者
Android性能优化——内存管理的艺术
Android性能优化——内存管理的艺术
|
3月前
|
编解码 Android开发 UED
构建高效Android应用:从内存优化到用户体验
【10月更文挑战第11天】本文探讨了如何通过内存优化和用户体验改进来构建高效的Android应用。介绍了使用弱引用来减少内存占用、懒加载资源以降低启动时内存消耗、利用Kotlin协程进行异步处理以保持UI流畅,以及采用响应式设计适配不同屏幕尺寸等具体技术手段。
60 2
|
3月前
|
安全 网络安全 Android开发
深度解析:利用Universal Links与Android App Links实现无缝网页至应用跳转的安全考量
【10月更文挑战第2天】在移动互联网时代,用户经常需要从网页无缝跳转到移动应用中。这种跳转不仅需要提供流畅的用户体验,还要确保安全性。本文将深入探讨如何利用Universal Links(仅限于iOS)和Android App Links技术实现这一目标,并分析其安全性。
465 0