Android之 内存泄漏问题检测和解决

2023-11-13

一,背景:

1.1,什么是内存泄漏

内存泄漏指程序在申请内存后,无法释放已申请的内存空间,导致系统无法及时回收内存并且分配给其他进程使用。

1.2,内存管理

49f973de611c49b99b6d44b725df01b3.png

1.3,垃圾回收

 上面可以看出GC回收的主要对象是java堆,也就是new出来的对象。垃圾回收算法

 

 标记-清除 算法

思想:

标记阶段:标记出所有需要回收的对象; 

清除阶段:统一清除(回收) 所有被标记的对象

优点:

实现简单

缺点:

 效率问题:标记和清除 两个过程效率不高

空间问题:标记-清除后,会产生大量不连续的内存碎片

场景:

对象存活率较低 & 垃圾回收行为频率低[如老年代)

复制算法

思想:

将内存分为大小相等的两块,每次使用其中一块,当使用的这块内存 用完,就将 这块内存上还存活的对象 复制到另一块还没试用过的内存上最终将使用的那块内存一次清理掉。

优点:

解决了标记-清除算法中 清除效率低的问题: 每次仅回收内存的一半区域

解决了标记-清除算法中 空间产生不连续内存碎片的问题:将已使用内存上的存活对象 移动到栈顶的指针,按顺序分配内存即可

缺点:

每次使用的内存缩小为原来的一半

当对象存活率较高的情况下需要做很多复制操作,即效率会变低

场景:

对象存活宰较低& 需要频繁进行垃圾回收 的区域(如新年代)》

标记 - 整理 算法

思想:

标记阶段:标记出所有需要回收的对象;

整理阶段:让所有存活的对象都向一移动

清除阶段:统一清除(回收) 端以外的对象

优点:

解决了标记-清除算法中 清除效率低的问题:一次清除端外区域

解决了标记-清除算法中 空间产生不连续内存碎片的问题:将已使用内存 步繁多:标记、整理、清除上的存活对象 移动到栈顶的指针,按顺序分配内存即可。

缺点:

步繁多:标记、整理、清除

场景:

对象存活率较低 & 垃圾回收行为频率低(如老年代)

分代收集 算法

思想:

根据对象存活周期的不同将Java堆内存分为:新生代&老年代

每块区域特点:

新生代:对象存活率较低& 垃圾回收行为频率高

老年代:对象存活率较低& 垃圾回收行为频率任

根据每块区域特点选择对应的垃圾收集算法《即上面介绍的算法)

新生代:采用复制算法

老年代:采用 标记-清除 算法、标记 - 整理 算法

优点:

效率高、空间利用率高:根据不同区域特点选择不同垃圾收集算法

缺点:

需要维护多个堆区域,增加了复杂性。可能会出现对象晋升的情况,导致老年代的内存压力增大。

场景:

虚拟机基本都采用这种算法

204bbecee1694fb7a81b3c2e49865758.png

1.4,上面可以看出对象能回收就不会造成垃圾,也不会占用大量内存,所以在对象管理上一定要注意GC能及时回收没用对象,不然内存就会慢慢被占满,最终导致内存溢出,发生程序崩溃和ANR异常。

总结起来主要原因就是长生命周期对象引用短生命周期对象,造成短生命周期对象不能及时释放回收

造成内存泄漏的常见原因:

单例持有activity短生命周期

非静态内部类会持有外部类的引用

外部类中持有非静态内部类的静态对象

Handler 或 Runnable 作为非静态内部类

资源需要关闭,BroadcastReceiver、ContentObserver、File、Cursor、Bitmap集合对象没有及时清理引起的内存泄漏

二,分析内存泄漏的工具

2.1 leakcanary 

leakcanary是一个监测android和java内存泄漏的工具。他能够在不影响程序正常运行的情况下,动态收集程序存在的内存泄漏问题

Github网站: https://github.com/square/leakcanary

使用,在app的build.gradle添加依赖

debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:2.10'

在application初始化leakcanary

LeakCanary.Config config = LeakCanary.getConfig().newBuilder()
                .retainedVisibleThreshold(3)
                .computeRetainedHeapSize(false)
                .build();
LeakCanary.setConfig(config);

 2.2 AndroidStudio 的Profiler

5cf2ee2f211e47efbeccff821a3fb1b7.png

 23d25de9d07e40c88d33d396c991424c.png

 三,实例,记一次内存泄漏问题

3.1 我们平时用的手机可能性能比较强,而且不会一直运行使用,一般内存泄漏也不会引起app瘫痪。但遇到开发板这种泄漏问题就会成为致命错误,由于内存小,运行时间长,万一发生内存泄漏就会一直增加内存,直至内存耗尽引发程序崩溃

3.2 近期引发上述问题的地方是一个扫码付款设备,功能是打开摄像头识别二维码,异步线程扫码二维码后退出扫码页面,最后定位到的内存泄漏地方是ActivityUtils页面管理工具类造成的内存泄漏

3.3 引起内存泄漏的源码:

mCamera.setDisplayOrientation(270);//竖屏显示
mCamera.setPreviewDisplay(mHolder);
mCamera.setPreviewCallback(previewCallback);
mCamera.startPreview();
mCamera.autoFocus(autoFocusCallback);

PreviewCallback previewCallback = new PreviewCallback() {
	public void onPreviewFrame(byte[] data, Camera camera) {
		if (data != null) {
			Camera.Parameters parameters = camera.getParameters();
			Size size = parameters.getPreviewSize();//获取预览分辨率

			//创建解码图像,并转换为原始灰度数据,注意图片是被旋转了90度的
			Image source = new Image(size.width, size.height, "Y800");
			//图片旋转了90度,将扫描框的TOP作为left裁剪
			source.setData(data);//填充数据

			ArrayList<HashMap<String, String>> result = new ArrayList<>();
			  //解码,返回值为0代表失败,>0表示成功
			int dataResult = mImageScanner.scanImage(source);
			if (dataResult != 0) {
				playBeepSoundAndVibrate();//解码成功播放提示音
				SymbolSet syms = mImageScanner.getResults();//获取解码结果
				for (Symbol sym : syms) {
					HashMap<String, String> temp = new HashMap<>();
					temp.put(ScanConfig.TYPE, sym.getSymbolName());
					temp.put(ScanConfig.VALUE, sym.getResult());
					result.add(temp);
				}
				if (result.size() > 0) {
					finish();
					EventBusUtil.sendEvent(new Event(EventCode.EVENT_QRCODE, result.get(0).get(ScanConfig.VALUE)));
				} 
			}
			AsyncDecode asyncDecode = new AsyncDecode();
			asyncDecode.execute(source);//调用异步执行解码
		}
	}
};  
private class AsyncDecode extends AsyncTask<Image, Void, ArrayList<HashMap<String, String>>> {
        @Override
        protected ArrayList<HashMap<String, String>> doInBackground(Image... params) {
            final ArrayList<HashMap<String, String>> result = new ArrayList<>();
            Image src_data = params[0];//获取灰度数据
            //解码,返回值为0代表失败,>0表示成功
            final int data = mImageScanner.scanImage(src_data);
            if (data != 0) {
                playBeepSoundAndVibrate();//解码成功播放提示音
                SymbolSet syms = mImageScanner.getResults();//获取解码结果
                for (Symbol sym : syms) {
                    HashMap<String, String> temp = new HashMap<>();
                    temp.put(ScanConfig.TYPE, sym.getSymbolName());
                    temp.put(ScanConfig.VALUE, sym.getResult());
                    result.add(temp);
                    if (!ScanConfig.IDENTIFY_MORE_CODE) {
                        break;
                    }
                }
            }
            return result;
        }

        @Override
        protected void onPostExecute(final ArrayList<HashMap<String, String>> result) {
            super.onPostExecute(result);
            if (!result.isEmpty()) {
                EventBusUtil.sendEvent(new Event(EventCode.EVENT_QRCODE, result.get(0).get(ScanConfig.VALUE)));
                finish();
            } else {
                isRUN.set(false);
            }
        }
    }
ActivityUtil.finishExcept(MainActivity.class);

 3.4 造成的结果:

Profiler分析结果,GC回收频繁,内存抖动严重

5ace84e7195f4c8f852eb488c1398dd2.png

 内存在5分钟内从128以下上到快200多,增加了近一倍

929712e3bbda4e29886d43ab334a1426.png

 分析结果,可以看到300多次内存泄漏

7052131251b14d1ab6cfd6bd232a63fb.png

 三个地方引发的内存泄漏d6ab7704ac174e49934b11c1e4125f42.png

leakcanary 检测结果 ActivityUtils引发的泄漏

76d9a344cb6a4f19959869bd1f5eb951.jpeg

0a6cdb3ea2d14d028dfccdf4db968d5e.jpeg

程序崩溃

 3.5 原因分析

异步任务持有activity的引用,activity关闭后但任务还没结束,导致ActivityUtils工具类不能finish结束页面,造成页面不能回收,从而引发内存泄露。

3.6 优化解决

第一步 摄像头扫描帧预览优化,改用有缓冲区的

mCamera.addCallbackBuffer(new byte[parameters.getPreviewSize().width * parameters.getPreviewSize().height * 3 / 2]);
mCamera.setPreviewCallbackWithBuffer(previewCallback);

//不管有没有数据都在预览最后加进缓冲区
PreviewCallback previewCallback = new PreviewCallback() {
    public void onPreviewFrame(byte[] data, Camera camera) {
        camera.addCallbackBuffer(data);
    }
}
mCamera.setDisplayOrientation(270);//竖屏显示
mCamera.setPreviewDisplay(mHolder);
//mCamera.setPreviewCallback(previewCallback);
mCamera.addCallbackBuffer(new byte[parameters.getPreviewSize().width * parameters.getPreviewSize().height * 3 / 2]);
mCamera.setPreviewCallbackWithBuffer(previewCallback);
mCamera.startPreview();
mCamera.autoFocus(autoFocusCallback);
PreviewCallback previewCallback = new PreviewCallback() {
   public void onPreviewFrame(byte[] data, Camera camera) {    
	if (data != null) {
		if (isRUN.compareAndSet(false, true)) {
			Camera.Parameters parameters = camera.getParameters();
			Size size = parameters.getPreviewSize();//获取预览分辨率

			//创建解码图像,并转换为原始灰度数据,注意图片是被旋转了90度的
			Image source = new Image(size.width, size.height, "Y800");
			//图片旋转了90度,将扫描框的TOP作为left裁剪
			source.setData(data);//填充数据

			ArrayList<HashMap<String, String>> result = new ArrayList<>();
			//解码,返回值为0代表失败,>0表示成功
			int dataResult = mImageScanner.scanImage(source);
			if (dataResult != 0) {
				playBeepSoundAndVibrate();//解码成功播放提示音
				SymbolSet syms = mImageScanner.getResults();//获取解码结果
				for (Symbol sym : syms) {
					HashMap<String, String> temp = new HashMap<>();
					temp.put(ScanConfig.TYPE, sym.getSymbolName());
					temp.put(ScanConfig.VALUE, sym.getResult());
					result.add(temp);
					if (!ScanConfig.IDENTIFY_MORE_CODE) {
						break;
					}
				}
				syms.destroy();
				syms = null;
				if (result.size() > 0) {
					isRUN.set(true);
					finish();
					EventBusUtil.sendEvent(new Event(EventCode.EVENT_QRCODE, result.get(0).get(ScanConfig.VALUE)));
				} else {
					isRUN.set(false);
				}
			} else {
				isRUN.set(false);
			}
		}
	}
    camera.addCallbackBuffer(data);
}

第二步,去掉异步任务和ActivityUtils相关的源码

优化后结果:

可以看到30分钟内存始终维持在128M以下,而且非常平稳

004ef0ccf9fa4da294d0ae619e4bcae9.png

 最后分析结果也是0个泄漏

448ef4d107ce46fb82c6d743d829bb20.png

 后面经过连续4个小时测试,内存始终还是在128M以下,完美解决

3.7 总结:

  • 发生内存泄漏,先要找到泄漏的地方,leakcanary和Profiler同时分析,快速定位地方。
  • 泄漏的主要特征就是内存不断飙升,容易引发的就是长生命周期持有短生命周期对象,导致短生命周期无法释放。
  • 所以在开发中要注意这点,短生命周期结束时候一定要先结束长生命周期的任务,让长生命周期的对象释放掉短生命周期的引用,才能使短生命周期的对象顺利结束和回收
  • 对于长生命周期尽量使用application的上下文,避免短生命周期的对象无法释放。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android之 内存泄漏问题检测和解决 的相关文章

  • 使用 Android 前台服务为 MediaPlayer 创建通知

    问题就在这里 我目前正在开发一个应用程序 该应用程序必须提供 A 广播播放器 来自 URL 的 AAC 直播 还有一个播客播放器 来自 URL 的 MP3 流 该应用程序必须能够在后台运行 Android 服务 并通过以下方式向用户公开持续
  • Android 应用程序在后台运行时保存数据

    目前我正在开发 xmmp 客户端 当应用程序位于前台时 该客户端工作得很好 但由于事实上 当应用程序处于后台时 我在 Application 类中保存了大量数据 复杂的 ArrayList 字符串和布尔值作为公共静态 每个字段都被垃圾收集
  • 在自定义对象中创建时粘性服务不会重新启动

    我有一个具有绑定服务的单例对象 我希望它重新启动 当我从启动器启动应用程序时 单例对象将初始化并绑定到这个现有的服务实例 以下是在单例中创建和绑定服务的代码 public class MyState private static MySta
  • Delphi XE7 Android 全屏(隐藏软键)

    如何在XE7中全屏显示 隐藏顶部 标题 和底部 软键 工具栏 在 XE6 中 我可以通过在应用程序部分写入来调整 AndroidManifest 以使我的应用程序全屏显示并且没有操作栏 android theme android style
  • 如果我们使用后退按钮退出,为什么 Android 应用程序会重新启动?

    按住主页按钮并返回应用程序时 应用程序不会重新启动 为什么使用后退按钮会重新启动 如果我们使用后退按钮退出 有什么方法可以解决在不重新启动的情况下获取应用程序的问题吗 请帮忙 当您按下Home按钮 应用程序将暂停并保存当前状态 最后应用程序
  • 在意图过滤器中使用多个操作时的默认值

    尝试理解 Android 中的意图和操作并查看文档 http developer android com guide topics intents intents filters html 但我一直看到的一件事是定义了多个操作的意图过滤器
  • 已经使用 AsyncTask doInBackground 但新数据未显示

    我使用 AsyncTask 创建一个聊天室来接收消息 因此它总是检查即将到来的消息并将其显示给客户端 但代码似乎无法按我希望的方式工作 在客户端只显示所有旧数据 新数据不显示 因为当我尝试从服务器发送消息时 新数据没有显示在客户端中 我对这
  • 如何在 sqlite 中将 2 列合并为新列

    我有一个包含 3 列的表 我必须将 2 列中的值按降序排列到一列中 A B C z 1 2 f 5 7 s 9 5 使用此示例 输出会将 B 列和 C 列中的值放入其中 如下所示 A B s 9 f 7 f 5 s 5 z 2 z 1 我当
  • Firebase:如何在Android应用程序中设置默认通知渠道?

    如何设置default通知渠道通知消息当应用程序在后台运行时会出现什么情况 默认情况下 这些消息使用 杂项 通道 如你看到的在官方文档中 https firebase google com docs cloud messaging andr
  • 请求位置更新参数

    这就是 requestLocationUpdates 的样子 我使用它的方式 requestLocationUpdates String provider long minTime float minDistance LocationLis
  • minHeight 有什么作用吗?

    在附图中 我希望按钮列与图像的高度相匹配 但我也希望按钮列有一个最小高度 它正确匹配图像的高度 但不遵守 minHeight 并且会使按钮向下滑动 我正在为按钮列设置这些属性
  • 带有自定义阵列适配器的微调器不允许选择项目

    我使用自定义阵列适配器作为微调器 但是 当在下拉列表中选择一个项目时 下拉列表保留在那里 并且微调器不会更新 这是错误行为 与使用带有字符串的通用数组适配器相比 这是自定义类 我错过了什么吗 谢谢 public class Calendar
  • 从 android 简单上传到 S3

    我在网上搜索了从 android 上传简单文件到 s3 的方法 但找不到任何有效的方法 我认为这是因为缺乏具体步骤 1 https mobile awsblog com post Tx1V588RKX5XPQB TransferManage
  • 使用 Matrix.setPolyToPoly 选择位图上具有 4 个点的区域

    我正在 Android 上使用位图 在使用 4 个点选择位图上的区域时遇到问题 并非所有 4 点组都适合我 在某些情况下 结果只是一个空白位图 而不是裁剪后的位图 如图所示 并且 logcat 中没有任何错误 甚至是内存错误 这是我用来进行
  • Android构建apk:控制MANIFEST.MF

    Android 构建 APK 假设一个 apk 包含一个库 jar 例如 foo jar 该库具有 META INF MANIFEST MF 这对于它的运行很重要 但在APK中有一个包含签名数据的MANIFEST MF 并且lib jar
  • 材质设计图标颜色

    应该是哪种颜色 暗 材质图标 在官方文档上 https www google com design spec style icons html icons system icons https www google com design s
  • 在 Android 上按下电源按钮时,如何防止先调用 onDestroy() 再调用 onCreate()

    我正在记录每个 onCreate 和 onDestroy 调用 我发现 一旦我单击 Android 上的电源按钮 以及模拟器上的电源按钮 我的活动中就会拨打电话 gt onDestroy gt onCreate 这会杀死我的游戏 然后立即从
  • Android 如何聚焦当前位置

    您好 我有一个 Android 应用程序 可以在谷歌地图上找到您的位置 但是当我启动该应用程序时 它从非洲开始 而不是在我当前的城市 国家 位置等 我已经在developer android com上检查了信息与位置问题有关 但问题仍然存在
  • 使用 Espresso 检查 EditText 的字体大小、高度和宽度

    如何使用 Espresso 检查 EditText 的字体大小 高度和宽度 目前要分割我使用的文本 onView withId R id editText1 perform clearText typeText Amr 并阅读文本 onVi
  • 用于推送通知的设备令牌

    我正在实施推送通知服务 我需要创建一个数据库来存储 4 个移动平台的所有设备令牌 我想根据他们的平台 iOS Android BlackBerry WP7 来组织它们 但是有什么方法可以区分平台 这样如果我只想向 Android 用户发送消

随机推荐