Android中内存优化

159 阅读9分钟

1,内存分析常见的命令

1,dumpsys meminfo:查看进程的oom adj,或者davilk/native等区域内存情况,或者某个进程或apk的内存情况,功能非常强大
2,procrank:查看进程VSS/RSS/PSS/USS各个内存指标
3,cat/proc/meminfo:查看系统的详尽内存信息,包括内核情况
4,free:只查看系统的可用内存
5,showmap:查看进程的虚拟地址空间的内存分配情况
6,vmstat:周期性地打印出进程运行队列、系统切换、CPU时间占比等情况
7,top -n 1

2,常见的android中内存泄露的场景

1,资源性对象未关闭:对于资源性对象不再使用时,应该立即调用它的close()函数,将其关闭,然后再置为null,例如Bitmap等资源未关闭造成的内存泄露,此时我们应该再Activity销毁时及时关闭
2,注册对象未注销:例如BroadcastReceiver、EventBus未注销造成的内存泄露,我们应该在Activity销毁时及时注销
3,类的静态变量持有大数据对象:尽量避免使用静态变量存储数据,特别是大数据对象,建议使用数据库存储
4,单例造成的内存泄露:优先使用Application的Context,如需使用Activity的Context,可以在引用Context的时候使用弱引用进行封装,在使用到的地方从弱引用处获取Context,如果获取不到,直接return
5,非静态内部类的静态实例:该实例的生命周期和应用一样长,这就导致该静态实例一直持有Activity的引用,
Activity的内部资源不能正常回收,此时,我们可以将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如需使用Context,尽量使用Application的Context,如果使用Activity的Context,在使用完之后置null让GC可以回收,否则还是会内存泄露
6,handler临时性内存泄露:Message发出之后存储在MessageQueue中,在message中存在一个targe,它是handler的一个引用,Message在Queue中存在的时间过长,就会导致Handler无法被回收,如果Handler是非静态的,则会导致Activity或者Service不会被回收,并且消息队列是在一个Looper线程中不断地轮询处理消息,当Activity退出时,消息队列中还有未处理的消息或者正在处理的消息,并且消息队列中的Message持有Handler实例的引用,Handler又出游Activity的引用,所有导致Activity的内存资源无法及时回收,引发内存泄露,解决方案
    1,使用一个静态Handler内部类,然后对Handler持有的对象(一般时Activity)使用弱引用,这样回收时,也可以回收Handler持有的对象
    2,在Activity的Destory或者Stop时,应该移除消息队列中的消息,避免Looper线程的消息队列中有待处理的消息需要处理
    需要注意的是,AsyncTask内部也是Handler机制,同样存在内存泄露风险,但其一般是临时性的,对于类似AsyncTask或是线程造成的内存泄露,我们也可以将AsyncTask和Runnable类独立出来或者使用静态内部类
7,容器中的对象没有清理造成的内存泄露:在退出程序之前,将集合中的对象或者数据置null或者清楚,再退出程序

3,App内存组成以及限制

Android给每个App分配一个“VM”,让App运行在dalvik上,这样即使app崩溃也不会影响到系统,系统给VM分配了一定的内存大小,App可以申请使用的内存大小不能超过此硬性逻辑限制,就算物理内存富余,如果应用超出VM最大内存,就会出现内存溢出crash

由程序控制操作的内存空间在heap上,分为java heapsize和native heapsize

Java申请的内存在vm heap上,所以如果Java申请的内存大小超过VM的逻辑内存限制,就会出现内存溢出的异常

Native层内存申请不受其限制,native层受native process对内存大小的限制

内存抖动:内存波动图形呈锯齿状、GC导致卡顿

内存泄露:在当前应用周期内不再使用的对象被GC Roots引用,导致不能回收,使实际可使用内存变小

内存溢出:即OOM,OOM时会导致程序异常,Android设备出厂以后,java虚拟机对单个应用的最大内存分配就确定下来了,超出这个值就会OOM

image.png

image.png

使用命令查看内存情况

image.png

4,Android内存泄露分析工具

Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation

MAT(memory anallyzer tools)中的重要概念

incoming reference 引入

outgoing reference 引出

image.png

调用方法之后,可以打开文件查看

image.png

image.png

命令: hprof-conv dump.hprof converted-dump.hprof

image.png

MAT中的深堆和浅堆

shallow heap 浅堆

Retained heap 深堆 包含本身和所以引用的总大小

image.png

image.png

5,内存泄露中的Bitmap

public void recycle() //回收位图的内存空间,把位图标记为Dead
public final boolean isRecycled() //判断位图内存是否已释放
public final int getwidth() //获取位图的宽度
public final int getHeight() //获取位图的高度
public final boolean isMutable()  //图片是否可修改
public int getScalewidth(Canvas canvas)  //获取指定密度转换后的图像的宽度
public int getScaleHeight(Canvas canvas)  //获取指定密度转换后的图像的高度
public boolean compress(CompressFormat format, int quality, OutputStream stream) //按指定的图片格式以及画质,将图片转换为输出流
public static Bitmap createBitmap(Bitmap src)  //以src为原图生成不可变的新图像 
public static Bitmap createBitmap(Bitmap src, int dstwidth, int dstHeight, boolean filter) //以src原图,创建新的图像,指定新图像的宽高以及是否可变
public static Bitmap createBitmap(int width, int height, Config config) //创建指定格式、大小的位图
public static Bitmap createBitmap(Bitmap source, int x, int y, int width, int height) //以source为原图,创建新的图片,指定起始坐标以及新图像的宽高

BitmapFactory工厂类

Option 参数类

public boolean inJustDecodeBounds //如果设置为true,不获取图片,不分配内存,但会返回图片的高度宽度信息,如果将这个值置为true,那么在解码的时候将不会返回bitmap,只会返回这个bitmap的尺寸,这个属性的目的是,如果你只想知道一个bitmap的尺寸,但又不想将其加载到内存时,这是一个非常有用的属性
public int inSampleSize //图片缩放的倍数,这个值是一个int, 当它小于1的时候,将会被当做1处理,如果大于1,那么就会按照比例(1/inSampleSize)缩小bitmap的宽和高,降低分辨率,大于1时这个值将会被处置为2的倍数,例如width = 100, height = 100, inSampleSize = 2,那么就会将bitmap处理为,width = 50, height = 50,宽高降为1/2,像素降为1/4
public int outwidth //获取图片的宽度值
public int outHeight //获取图片的高度值,表示这个bitmap的宽和高,一般和inJustDecodeBounds一起使用来获得Bitmap的宽高,但是不加载到内存
public int inDensity //用于位图的像素压缩比
public int inTargetDensity //用于目标位图的像素压缩比(要生成的位图)
public byte[] inTemstorage //创建临时文件,将图片存储
public boolean inScaled //设置为true时进行图片压缩,从inDensity到inTargetDensity
public boolean inDither //设置为true,解码器尝试抖动解码
public Bitmap.Config inPreferredConfig //设置解码器,这个值是设置色彩模式,默认值是ARGB_8888,在这个模式下,一个像素点占用4bytes空间,一般对透明不做要求的话,一般采用RGB_565模式,这个模式下一个像素点占用2bytes
public String outMimeType //设置解码图像
public boolean inPureable //当存储Pixel的内存空间在系统内存不足时是否可以被回收
public boolean inInputShareable //inPurgeable为true情况下才生效,是否可以共享一个InputStream

工厂方法

public static Bitmap decodeFile(String pathName. Options opts) //从文件读取图片
public static Bitmap decodeFile(String pathName)
public static Bitmap decodeStream(InputStream is) //从输入流读取图片
public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts)
public static Bitmap decodeResource(Resource res, int id) //从资源文件读取图片
public static Bitmap decodeResource(Resource res, int id, Options opts)
public static Bitmap decodeByteArray(byte[] data, int offset, int length) //从数组读取图片
public static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts)
public static Bitmap decodeFileDescriptor(FileDescriptor fd)  // 从文件读取文件与decodeFile不同的时这个直接调用JNI函数进行读取,效率比较高
public static Bitmap decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Option opts)

单个像素的占用内存大小

image.png

Bitmap的内存回收

在Android2.3.3之前推荐使用Bitmap.recycle()方法进行Bitmap的内存回收

只有当确定这个Bitmap不能被引用的时候才能调用此方法,否则会有“Canvas:trying to use a recycled bitmap”这个错误

Android3.0之后 android3.0之后,并没有强调Bitmap.recycle(),而是强调Bitmap的复用

save a bitmap for later use

使用LruCache对Bitmap进行缓存,当再次使用到这个Bitmap的时候直接获取,而不用重走编码流程

use an existing bitmap
Android3.0之后引入了BitmapFactory.Options.inBitmap字段,设置此字段之后解码方法会尝试复用一张存在的Bitmap。这意味着Bitmap的内存被复用,避免了内存的回收即使申请过程,显然性能表现更佳,不过,使用这个字段有几点限制
声明可被复用的Bitmap必须设置inMutable为true
android4.4之前只有格式为jpg、png;同等宽高(要求苛刻),inSampleSize为1的Bitmap才可以复用
android4.4之前被复用的Bitmap的inPreferredConfig会副高分配内存的Bitmap设置为inPreferredConfig
android4.4之后被复用的Bitmap的内存必须大于需要申请内存的Bitmap的内存
android4.4之前待加载Bitmap的Options.inSampleSize必须明确指定为1

6,LeakCanary内存检测框架

LeakCanary的安装

加入LeakCanary依赖包之后,在LeakCanary的源码中的Manifest清单文件中,有contentprovider

apk打包时会mergeAndroidManifest.xml,就是合并所以AndroidManifest中的内容,这样就会把contentprovider打进去

安装apk时会调用installContentProviders方法

image.png

image.png

image.png

ReferenceQueue作用

通常我们将ReferenceQueue翻译为引用队列,就是存放引用的队列,保存的时Reference对象,其作用在于Reference对象所引用的对象被GC回收时,该Reference对象将会被加入引用队列(ReferenceQueue)的队列末尾

ReferenceQueue常用的方法:

public Reference pool(): 从队列中取出一个元素,队列为空则返回null
public Reference remove(): 从队列中移除一个元素,若没有则阻塞,直到有可移除队列的元素
public Reference remove(long timeout):从队列中出对一个元素,若没有则阻塞直到有可出队元素或阻塞至超过timeout毫秒

LeakCanary原理图解

image.png

image.png