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

一,背景:

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的上下文,避免短生命周期的对象无法释放。

猜你喜欢

转载自blog.csdn.net/qq_29848853/article/details/129690899
今日推荐