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(除了)虚、弱引用、软引用之外,即只看强引用。
弹出分析结果界面,界面中 根据 图示(橙红小圆圈)
可以找到 引用选中实例实例对象 及其 所在的类文件名;**

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

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






参考自
相关文章
|
4天前
|
缓存 数据处理 Android开发
Android经典实战之Kotlin常用的 Flow 操作符
本文介绍 Kotlin 中 `Flow` 的多种实用操作符,包括转换、过滤、聚合等,通过简洁易懂的例子展示了每个操作符的功能,如 `map`、`filter` 和 `fold` 等,帮助开发者更好地理解和运用 `Flow` 来处理异步数据流。
33 4
|
4天前
|
API Android开发 开发者
Android经典实战之使用ViewCompat来处理View兼容性问题
本文介绍Android中的`ViewCompat`工具类,它是AndroidX库核心部分的重要兼容性组件,确保在不同Android版本间处理视图的一致性。文章列举了设置透明度、旋转、缩放、平移等功能,并提供了背景色、动画及用户交互等实用示例。通过`ViewCompat`,开发者可轻松实现跨版本视图操作,增强应用兼容性。
24 5
|
1天前
|
Linux Android开发 iOS开发
Android经典实战之Kotlin Multiplatform跨平台开发
KMP(Kotlin Multiplatform)是由JetBrains开发的开源技术,让开发者能在多平台间高效重用代码,保留原生编程优势。适用于Android/iOS应用、多平台库及桌面应用开发。KMP支持代码共享、预期与实际声明机制,具备灵活性、稳定性和性能优势。通过Compose Multiplatform可实现跨平台UI共享。开发者可访问官方文档开始学习。
7 1
|
9天前
|
缓存 API Android开发
Android经典实战之Kotlin Flow中的3个数据相关的操作符:debounce、buffer和conflate
本文介绍了Kotlin中`Flow`的`debounce`、`buffer`及`conflate`三个操作符。`debounce`过滤快速连续数据,仅保留指定时间内的最后一个;`buffer`引入缓存减轻背压;`conflate`仅保留最新数据。通过示例展示了如何在搜索输入和数据流处理中应用这些操作符以提高程序效率和用户体验。
22 6
|
7天前
|
API Android开发 开发者
Android经典实战之用WindowInsetsControllerCompat方便的显示和隐藏状态栏和导航栏
本文介绍 `WindowInsetsControllerCompat` 类,它是 Android 提供的一种现代化工具,用于处理窗口插入如状态栏和导航栏的显示与隐藏。此类位于 `androidx.core.view` 包中,增强了跨不同 Android 版本的兼容性。主要功能包括控制状态栏与导航栏的显示、设置系统窗口行为及调整样式。通过 Kotlin 代码示例展示了如何初始化并使用此类,以及如何设置系统栏的颜色样式。
30 2
|
7天前
|
API Android开发 Kotlin
Android实战经验分享之如何获取状态栏和导航栏的高度
在Android开发中,掌握状态栏和导航栏的高度对于优化UI布局至关重要。本文介绍两种主要方法:一是通过资源名称获取,简单且兼容性好;二是利用WindowInsets,适用于新版Android,准确性高。文中提供了Kotlin代码示例,并对比了两者的优缺点及适用场景。
51 1
|
1天前
|
编译器 API Android开发
Android经典实战之Kotlin Multiplatform 中,如何处理不同平台的 API 调用
本文介绍Kotlin Multiplatform (KMP) 中使用 `expect` 和 `actual` 关键字处理多平台API调用的方法。通过共通代码集定义预期API,各平台提供具体实现,编译器确保正确匹配,支持依赖注入、枚举类处理等,实现跨平台代码重用与原生性能。附带示例展示如何定义跨平台函数与类。
6 0
|
3天前
|
编译器 Android开发 开发者
Android经典实战之Kotlin 2.0 迁移指南:全方位优化与新特性解析
本文首发于公众号“AntDream”。Kotlin 2.0 已经到来,带来了 K2 编译器、多平台项目支持、智能转换等重大改进。本文提供全面迁移指南,涵盖编译器升级、多平台配置、Jetpack Compose 整合、性能优化等多个方面,帮助开发者顺利过渡到 Kotlin 2.0,开启高效开发新时代。
6 0
|
5天前
|
搜索推荐 Java API
Electron V8排查问题之分析 node-memwatch 提供的堆内存差异信息来定位内存泄漏对象如何解决
Electron V8排查问题之分析 node-memwatch 提供的堆内存差异信息来定位内存泄漏对象如何解决
9 0
|
5天前
|
XML 数据可视化 API
Android经典实战之约束布局ConstraintLayout的实用技巧和经验
ConstraintLayout是Android中一款强大的布局管理器,它通过视图间的约束轻松创建复杂灵活的界面。相较于传统布局,它提供更高灵活性与性能。基本用法涉及XML定义约束,如视图与父布局对齐。此外,它支持百分比尺寸、偏移量控制等高级功能,并配有ConstraintSet和编辑器辅助设计。合理运用可显著提高布局效率及性能。
16 0