内存泄露产生的原因:
短生命周期对象被长生命周期对象引用,短生命周期对象功能周期结束后,长生命周期对象还没有释放该引用。
内存抖动问题:
短时间内反复的开辟内存,导致频繁GC。
优化内存良好的编码习惯
- 数据类型 不要使用比需求更占用空间的基本数据类型
- 数据结构与算法的处理
数据量千级以内可以使用Sparse数组、ArrayMap,性能不如HashMap但节约内存。 - 枚举优化
枚举的缺点:
每一个枚举值都是一个单例对象,使用它会增加额外的内存消耗,所以枚举相比于Integer和String会占用更多的内存。
较多使用Enum会增加DEX文件的大小,会造成运行时更多的IO开销,使我们的用用需要更多的空间。
特别是分DEX的大型APP,枚举的初始化很容易导致ANR。
枚举可以进行改进:
webView的内存泄露问题基本无解,所以webView尽量在独立的进程中开启。因此,多进程也是解决性能问题的一个方向。
算法优化,时间、空间的互换提高CPU性能。
优化内存的良好编码习惯:
1.数据类型,不要使用比需求更占空间的基本数据类型。
2.循环尽量使用foreach少用iterator,自动装箱尽量少用。
3.数据结构与算法的处理。数据量千级内可以使用sparse数组,arrayMap,性能不如HashMap但节约内存。
枚举优化:
一个枚举对象就是一个静态对象,占用内存高,可以考虑少用。
static 和 static final 的问题:
static会由虚拟机调用classInit方法进行初始化。
static final不需要进行初始化工作,打包在dex文件中可以直接调用,并不会在类初始化时申请内存,所以基本数据类型的成员,可以全写成static final 。对象不建议写成static final ,因为对象可能导致dex包过大。
字符串的拼接尽量少用加号。
重复new对象问题:
循环中、递归中、onDraw方法中等,不要去new对象。
不要在onMeasure、onLayout、onDraw中刷新UI(requestLayout)。
避免GC回收将来要重用的对象:
内存设计模式对象池+LRU算法。
图片压缩的思路:
1.AndroidSDK提供了
android.graphics.Bitmap#compress
但是它侧重点是压缩的时间效率,压缩质量和文件大小效果不好。
2.NDK方式引用第三方的libturbojpeg库的压缩api,编译方式自行百度。压缩之前可以先舍弃alpha信息,只保留RGB信息,提高压缩效率。压缩时,要手动开启哈夫曼压缩功能:optimize_coding = true 。
===== 以下内容来自http://blog.csdn.net/talkxin/article/details/50696511 ======
为何Android图片压缩效率比IOS低质量差
为什么Android的图片压缩质量要比iPhone的压缩质量差很多,这是因为Android底层犯的一个小错误:libjpeg。并且这个错误一直持续到了今天。
libjpeg是广泛使用的开源JPEG图像库(参考 http://en.wikipedia.org/wiki/Libjpeg ),安卓也依赖libjpeg来压缩图片。通过查看源码,我们会发现安卓并不是直接封装的libjpeg,而是基于了另一个叫Skia的开源项目 (http://en.wikipedia.org/wiki/Skia_Graphics_Engine)来作为的图像处理引擎。Skia是谷歌自己维 护着的一个大而全的引擎,各种图像处理功能均在其中予以实现,并且广泛的应用于谷歌自己和其它公司的产品中(如:Chrome、Firefox、 Android等)。Skia对libjpeg进行了良好的封装,基于这个引擎可以很方便为操作系统、浏览器等开发图像处理功能。 libjpeg在压缩图像时,有一个参数叫optimize_coding,关于这个参数,libjpeg.doc有如下解释:
boolean optimize_coding
TRUE causes the compressor to compute optimal Huffman coding tables
for the image. This requires an extra pass over the data and
therefore costs a good deal of space and time. The default is
FALSE, which tells the compressor to use the supplied or default
Huffman tables. In most cases optimal tables save only a few percent
of file size compared to the default tables. Note that when this is
TRUE, you need not supply Huffman tables at all, and any you do
supply will be overwritten.
这段话大概的意思就是如果设置optimize_coding为TRUE,将会使得压缩图像过程中基于图像数据计算哈弗曼表(关于图片压缩中的哈弗曼表,请自行查阅相关资料),由于这个计算会显著消耗空间和时间,默认值被设置为FALSE。
这段解释乍看起来没有任何问题,libjpeg的代码也经受了十多年的考验,健壮而高效。但很多人忽略了这一点,那就是,这段解释是十多年前写的,对于当 时的计算设备来说,空间和时间的消耗可能是显著的,但到今天,这似乎不应再是问题,相反,我们应该更多的考虑图片的品质(越来越好的显示技术)和图片的大 小(越来越依赖于云服务)。
谷歌的Skia项目工程师们最终没有设置这个参数,optimize_coding在Skia中默认的等于了FALSE,这就意味着更差的图片质量和更大的图片文件,而压缩图片过程中所耗费的时间和空间其实反而是可以忽略不计的。那么,这个参数的影响究竟会有多大呢?
经我们实测,使用相同的原始图片,分别设置optimize_coding=TRUE和FALSE进行压缩,想达到接近的图片质量(用Photoshop 放大到像素级逐块对比),FALSE时的图片大小大约是TRUE时的5-10倍。换句话说,如果我们想在FALSE和TRUE时压缩成相同大小的JPEG 图片,FALSE的品质将大大逊色于TRUE的(虽然品质很难量化,但我们不妨说成是差5-10倍)。
我们又对Android和iOS进行了对比(均使用标准的JPEG压缩方法),两个系统都没有提供设置optimize_coding的接口(通过阅读源 码,我们已经知道Android是FALSE,iOS不详),当压缩相同的原始图片时,结果也是一样,iOS完胜。想要品质接近,文件大小就会差出 5-10倍,而如果要压缩出相同大小的文件,Android的压缩品质简直就是惨不忍睹。
结果说明,苹果很清楚optimize_coding参数和哈弗曼表的意义,这里需要特别指出,苹果使用的哈弗曼表算法与libjpeg(及我们后来自行 采用的libjpeg-turbo)不同,像素级可以看出区别,苹果似乎基于libjpeg又进行了进一步的优化,压缩出来的图片细节上更柔和、更平滑。
Android中图片资源加载的内存问题:
图片放在drawable目录下时,如果直接使用
android.graphics.BitmapFactory#decodeResource(android.content.res.Resources, int)
加载图片资源,Android系统会自动根据屏幕dpi进行缩放,这就可能会导致一张磁盘占用只有200KB的图片,加载到内存中需要占用6MB的内存空间,如果不做处理,很有可能导致oom。
解决方案:
1.图片放到适合的drawable-dpi目录下
项目下文件mdpi、hdpi、xdpi、xxdpi、xxxdpi
最近再项目开发的时候遇到了一个内存溢出的问题,害怕被祭天,所以赶紧检查出问题的地方,在网上查到了很多资料,说是由切图的放置位置导致,原来我把一张大图直接放到了drawble文件夹下面,没有放到相应的drawble分辨率下面,界面View加载图片的时候,会造成图片占用内存过大,然后就导致部分机型打开引用这张大图这个界面的时候特别卡,甚至有的会直接Crash掉。
那么导致这个问题的根本原因是什么呢?通过在网络上面查资料,和自己亲自实验,得出了一些结论,下面给大家分享一下
实验内容
因为网上有人说是因为图片没有放到相应的分辨率包下面,加载内存过大导致的,所以我觉得把同一张图片放到不同的分辨率的包下面,然后运行再同一设备上,看看对图片的大小以及内存占用有什么影响。
测试环境
采用华为荣耀5C手机(1080*1960,xxhdpi)进行测试
研究过程
下面就是测试的过程,我会用截图来说明过程
我测试用的是一张750 *1334分辨率的png图片,占用磁盘内存260.89kb,
1.没有设置图片的情况下 APP图片资源占用内存是11.81MB截图如下
2.将图片放到drawable文件夹下面,然后图片的大小为22504002 ,图片资源占用内存是46.98MB
3.将图片当道drawable-hdpi文件夹下面,图片的大小为15002668,图片资源Graphics占用内存是27.39MB
4.将图片当道drawable-xhdpi文件夹下面,图片的大小为11252001,图片资源Graphics占用内存是20.66MB
5.将图片当道drawable-xxhdpi文件夹下面,图片的大小为7501334(原图大小一致),Graphics占用内存是15.7MB
6.将图片当道drawable-xxxhdpi文件夹下面,图片的大小为563*1001,Graphics占用内存14.1MB
结果分析
从上面的测试结果,我们可以得出如下结论:
同一张图片,放在不同目录下,会生成不同大小的Bitmap
Bitmap的长度和宽度越大,占用的内存就越大
图片在硬盘上占用的大小,与在内存中占用的大小完全不一样
下面我会对上面几个问题一一解释。
我们以放在drawable文件夹下面的图片为例,加载到内存之后,2250*4002 大小的Bitmap占用的内存为
2250*4002 * 4 = 3601,8000 byte = 3,5173kb = 34.34944 M
所以drawable文件夹下的App内存占用 = 原始内存11.81MB+图片内存34.34944MB= 46.16MB ,与实际内存占用46.98MB存在0.1755%的误差,在误差范围之内。
先简单解释一下上面的计算公式,长*宽是图片的像素总数,乘以4则是因为一个像素占用A、R、G、B四个通道,每个通道占用8位,所以描述一个像素需要32位即4个字节。
一个颜色通道需要8位描述,2^8=256,所以每个颜色通道就有256种状态。如果把彩色图转化成灰阶图的话,也有256种状态分割从白色到黑色之间的过渡颜色。
当然,也并不是所有格式的图片每个像素占用4字节,这和图片在加载时设置的Bitmap.Config有关,默认的是Bitmap.Config.ARGB_8888,其他类型如下:
Bitmap.Config.ALPHA_8 此时图片只有alpha值,没有RGB值,
一1个像素占用一个字节
Bitmap.Config.ARGB_4444 一个像素占用2个字节,alpha(A)值,Red(R)值,Green(G)值,Blue(B)值各占4个bites共16bites,即2个字节
Bitmap.Config.ARGB_8888 一个像素占用4个字节,alpha(A)值,Red(R)值,Green(G)值,Blue(B)值各占8个bites,共32bites,即4个字节。这是一种高质量的图片格式,在电脑上普通采用。它也是Android手机上一个Bitmap的默认格式。
Bitmap.Config.RGB_565 一个像素占用2个字节,没有alpha(A)值,即不支持透明和半透明,Red(R)值占5个bites ,Green(G)值占6个bites ,Blue(B)值占5个bites,共16bites,即2个字节。对于没有透明和半透明颜色的图片来说,该格式的图片能够达到比较的呈现效果,相对于ARGB_8888来说也能减少一半的内存开销。因此它是一个不错的选择。
那么为啥在硬盘上存储只需要200多k,放到内存里面就需要30多M呢?
因为这根本不是一回事呀~
存放在硬盘上的图片文件,会根据各自的压缩规则进行压缩,比如Jpeg这种有损压缩的图片格式,最常使用可变字长编码的哈弗曼编码,会使用哈弗曼树,也就是最优二叉树,根据某些数据出现的频率对数据段编码,从而减少占用的硬盘大小。
比如说“10111”这个序列在图片的二进制数据中出现的概率最大,那我们可以用“01”来代替这一段数据,原来5位的数据,用2位就可以表示了,这就是压缩率60%。当然这只是打个比方,在实际操作中需要考虑“异前缀原则”等编码的基本原则。
而如果把图像读取到内存中就不一样了,因为我们需要每一个像素都能在屏幕上显示,所以会把每个像素点都加载至内存中,不会对相同像素进行压缩或者是替换,所以你也应该能明白前面提到的Bitmap占用内存大小的计算公式的由来了。
说到这里,其实后两个结论已经解释清楚了,那么为什么“同一张图片,放在不同目录下,会生成不同大小的Bitmap”呢?
我的测试设备为华为荣耀5C,1080*1960,xxhdpi,所以说,如果把这张放置在xxhdpi的话,应该不会对图像进行放缩,也就是原始大小,所以我们在前面得到drawable-xxhdpi文件夹下,图片大小为750 * 1334是完全可以理解的,就是图片本身的大小。
当图片放置在drawable-hdpi中时,图片大小为1500*2668,长宽变为原来的两倍,这是因为不同分辨率之间的倍数关系导致的。
我们可以很明显的看到xxhdpi是hdpi的2倍,所以如果单独放置在某个drawable文件夹,手机会自动根据当前的屏幕密度对图片进行放缩。
比如上面,当把图片放置在xxxhdpi里面的时候,在xxhdpi的设备上,图片长 = 750 * (3/4) =563,图片宽 = 1280 * (3/4) = 1001,这与上面的测试结果是完全一致的。
至于为什么在前面的测试中,drawable和drawable-mdpi是一样的大小,是因为drawable-mdpi是系统默认的像素密度,其他像素密度都以它为基数,当只在drawable中存在图片时,如果使用该图片,那么将按照drawable-mdpi的放缩比例进行放缩。
最后的结论
从上面的测试我们可以得出以下几个结论:
当图片放置在不同drawable文件夹中,且只有这一张图片时,运行设备会根据自身的屏幕密度,对图片进行放缩,放缩比例符合前面图上的规则
图片文件的大小与在内存中占用的大小没关系,内存中实际占用大小与图片分辨率、像素显示参数有关
所以,在一个App里面使用一套UI理论上应该是没有问题的,但是要注意
最好使用较高分辨率的切图,并且放置在正确的drawable文件夹中,比如按照xxhdpi的分辨率进行切图,放置在drawable-xxhdpi中
对于可以使用.9格式的图片,最好使用.9,减少资源大小
如果有条件,最好提供多套UI切图。如果只有一套切图,系统需要对图片进行压缩,会进行大量运算,影响设备性能。同时,在某些情况下,系统对图片的压缩会可能会出现锯齿,造成信息的丢失
如果是多套切图的话,最好不要直接用工具按照比例放缩,这样小图标会丢失一些细节。当然,这部分是美工来做的,
说到这里大家就明白我上面说的那个Crash问题了,思考一下,如果把一个本来应该放在drawable-xxhdpi里面的图片放在了drawable文件夹中会出现什么问题呢?
在xxhdpi设备上,图片会被放大3倍,图片内存占用就会变为原来的9倍!
2.根据View控件的大小,缩放图片。
SDK提供了
android.graphics.BitmapFactory#decodeResource(android.content.res.Resources, int, android.graphics.BitmapFactory.Options)
使用方法如下:
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
public class ImageResize {
public static Bitmap resizeBitmap(Context context, int id, int maxW, int maxH, boolean hasAlpha) {
Resources resources = context.getResources();
BitmapFactory.Options options = new BitmapFactory.Options();
//需要拿得到系统处理的信息 比如解码出宽高,....
options.inJustDecodeBounds = true;
//我们把原来的解码参数改了再去生成bitmap
BitmapFactory.decodeResource(resources, id, options);
//取到宽高
int w = options.outWidth;
int h = options.outHeight;
//设置缩放系数
options.inSampleSize = calcuteInSampleSize(w, h, maxW, maxH);
if(!hasAlpha){
options.inPreferredConfig=Bitmap.Config.RGB_565;
}
options.inJustDecodeBounds=false;
return BitmapFactory.decodeResource(resources,id,options);
}
//返回结果是原来解码的图片的大小 是我们需要的大小的 最接近2的几次方倍
private static int calcuteInSampleSize(int w, int h, int maxW, int maxH) {
int inSampleSize = 1;
if (w > maxW && h > maxH) {
inSampleSize = 2;
while (w / inSampleSize > maxW && h / inSampleSize > maxH){
inSampleSize*=2;
}
}
inSampleSize/=2;
return inSampleSize;
}
}
这样可以减少图片加载到内存中的内存浪费。
Bitmap四级缓存实现思路:
通常来说,缓存的实现三种:内存、磁盘、网络。为什么Bitmap可以有四级?多出的一级是什么?
其实多出的一级也是内存缓存,但是Bitmap有些特别。
普通的内存缓存可以使用LruCache实现,LruCache是一种最近最少使用移出的缓存策略,内部实现是一个双向链表。
Bitmap在SDK>=11及<=SDK23时,Bitmap会有一个叫做mFinalizer的BitmapFinalizer成员变量,BitmapFinalizer是Bitmap的内部静态类,它重写了finalize方法
private static class BitmapFinalizer {
private long mNativeBitmap;
// Native memory allocated for the duration of the Bitmap,
// if pixel data allocated into native memory, instead of java byte[]
private int mNativeAllocationByteCount;
BitmapFinalizer(long nativeBitmap) {
mNativeBitmap = nativeBitmap;
}
public void setNativeAllocationByteCount(int nativeByteCount) {
if (mNativeAllocationByteCount != 0) {
VMRuntime.getRuntime().registerNativeFree(mNativeAllocationByteCount);
}
mNativeAllocationByteCount = nativeByteCount;
if (mNativeAllocationByteCount != 0) {
VMRuntime.getRuntime().registerNativeAllocation(mNativeAllocationByteCount);
}
}
@Override
public void finalize() {
try {
super.finalize();
} catch (Throwable t) {
// Ignore
} finally {
setNativeAllocationByteCount(0);
nativeDestructor(mNativeBitmap);
mNativeBitmap = 0;
}
}
}
FinalizerDaemon:析构守护线程。对于重写了成员函数finalize的对象,它们被GC决定回收时,并没有马上被回收,而是被放入到一个队列中,等待FinalizerDaemon守护线程去调用它们的成员函数finalize,然后再被回收。这也是为什么如果对象实现了finalize函数,不仅会使其生命周期至少延长一个GC过程,而且也会延长其所引用到的对象的生命周期,从而给内存造成了不必要的压力。
再来看下SDK>=11及<=SDK23时Bitmap的recycle实现:
public void recycle() {
if (!mRecycled && mFinalizer.mNativeBitmap != 0) {
if (nativeRecycle(mFinalizer.mNativeBitmap)) {
// return value indicates whether native pixel object was actually recycled.
// false indicates that it is still in use at the native level and these
// objects should not be collected now. They will be collected later when the
// Bitmap itself is collected.
mBuffer = null;
mNinePatchChunk = null;
}
mRecycled = true;
}
}
所以,SDK>=11及<=SDK23时,Bitmap在第一次GC时,会因为mFinalizer的原因
当APP需要往LruCache中存放Bitmap时,如果LruCache满了,就要移出一个Bitmap对象,我们把这个要移出的对象先不立即显式调用Bitmap.recycler() 通知native层立即释放Bitmap像素点信息,而是先放到一个起到复用池效果的容器中:
Set<WeakReference<Bitmap>> reuseablePool;
WeakReference创建的时候可以指定一个引用队列,监控WeakReference修饰的Bitmap回收情况。
ReferenceQueue referenceQueue;
只要复用池中的对象还没有被回收,就可以继续取出来,复用这个Bitmap对象的内存空间。
复用Bitmap对象的内存空间有一些限制:
1.原Bitmap是可变的,即Bitmap.mIsMutable 参数为true。
2.KITKAT及其之后,新图的大小(可以是新图本身固有大小,也可以是设置了缩放后的大小,具体看BitmapFactory.Options#inSampleSize 的值)<=原图的Bitmap#getAllocationByteCount()。
3.KITKAT之前,新图必须是jpeg或者png格式,新图尺寸必须和原图一致,且新图不能开启缩放(BitmapFactory.Options#inSampleSize = 1)。新图的BitmapFactory.Options#inPreferredConfig会被旧图的值覆盖。