在android 应用中多数情况要和图片打交道,有时还要请求网络获取各种清晰度的图片,无论是处理本地还是网络图片,如果不能采取行之有效的处理机制,
当我们进行频繁操作或者仅展示几张高清图片时都难免碰到OOM问题。
是什么导致了OOM?
在Android中,一个JVM只能使用16M内存(如果framework未被定制),如果超过了这个限制就OOM。通常情况下我们可以简单的通过android性能测试工具dumpsys 来查看jvm内存使用情况。 输入命令dumpsys meminfo appprocessname
显示如下:
这是一张从moto手机获取的app内存分析图,可以明显的看出OOM上限显然超过了16M。下面给出模拟器内存分析图
值得一提的是:有些情况内存剩余明明大于下次分配大小但还是毫无例外的每次OOM
如何尽可能降低OOM?
一般可采取以下方式:
1 要及时回收Bitmap的内存
recycle方法不是必须调用,就算调了GC也不会立即回收Java层的Bitmap对象。这个跟把一个对象手动置空一个道理。可以看一下API说明:
This operation cannot be reversed, so it should only be called if you are sure there are no further uses for the bitmap. This is an advanced call, and normally need not be called, since the normal GC process will free up this memory when there are no more references to this bitmap.
那么为什么程序中一般还需要调用呢?我们可以看一下recycle方法到底干了什么,下面是它的调用链:
Bitmap.recycle()(java)->nativeRecycle(mNativeBitmap)(java native)->SKBitmap::setPixels(NULL, NULL)(C++)->freePixels()(C++)
可以看出,recycle方法就是把C层的像素内存给释放了。我们知道,C层的内存还是算到当前进程的内存里的,而构成一个Bitmap对象的内存大部分都是C层的像素数组。所以,手动调用Bitmap.recycle并把Bitmap对象的Java层引用手动置NULL可以瞬间释放像素所占的内存,并让虚拟机下次运行时回收Bitmap对象所占的内存。不要忘了,因为包含了一堆的像素数据,Bitmap对象通常都很大。当很容易判断出这个图片不再被使用的时候就把它recycle一下吧。
// 先判断是否已经回收
if(bitmap != null && !bitmap.isRecycled()){
// 回收并且置为null
bitmap.recycle();
bitmap = null;
}
//这里不建议使用 System.gc() 在调用时程序处于阻塞状态
2 捕获OOM异常
Bitmap bitmap = null;
try {
// 实例化Bitmap
bitmap = BitmapFactory.decodeFile(path);
} catch (OutOfMemoryError e) {
//
}
if (bitmap == null) {
// 如果实例化失败 返回默认的Bitmap对象
return defaultBitmapMap;
}
3 缓存通用的Bitmap对象
有时候,可能需要在一个Activity里多次用到同一张图片。比如一个Activity会展示一些用户的头像列表,而如果用户没有设置头像的话,则会显示一个默认头像,而这个头像是位于应用程序本身的资源文件中的。
4 bitmap转成drawable
用Drawable保存图片对象比Bitmap占用更小的内存空间。
5 采用低内存占用量的编码方式,比如Bitmap.Config.ARGB_4444比Bitmap.Config.ARGB_8888更省内存
6 让系统主导回收
Java从JDK1.2版本开始,就把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期。这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用。
(1)强引用(StrongReference)如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
(2)软引用(SoftReference)如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
(3)弱引用(WeakReference)弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
(4)虚引用(PhantomReference)“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。当我们有大量请求网络图片需求时,除了要合理的回收bitmap还要采取一定的缓存机制。
sdcard保存:
private void saveBmpToSd(Bitmap bm, Stringurl) {
if (bm == null) {
Log.w(TAG, " trying to savenull bitmap");
return;
}
//判断sdcard上的空间
if (FREE_SD_SPACE_NEEDED_TO_CACHE >freeSpaceOnSd()) {
Log.w(TAG, "Low free space onsd, do not cache");
return;
}
String filename =convertUrlToFileName(url);
String dir = getDirectory(filename);
File file = new File(dir +"/" + filename);
try {
file.createNewFile();
OutputStream outStream = newFileOutputStream(file);
bm.compress(Bitmap.CompressFormat.JPEG, 100, outStream);
outStream.flush();
outStream.close();
Log.i(TAG, "Image saved tosd");
} catch (FileNotFoundException e) {
Log.w(TAG,"FileNotFoundException");
} catch (IOException e) {
Log.w(TAG,"IOException");
}
}
/**
* 计算sdcard上的剩余空间
* @return
*/
private int freeSpaceOnSd() {
StatFs stat = newStatFs(Environment.getExternalStorageDirectory() .getPath());
double sdFreeMB = ((double)stat.getAvailableBlocks() * (double) stat.getBlockSize()) / MB;
return (int) sdFreeMB;
}
/**
* 修改文件的最后修改时间
* @param dir
* @param fileName
*/
private void updateFileTime(String dir,String fileName) {
File file = new File(dir,fileName);
long newModifiedTime =System.currentTimeMillis();
file.setLastModified(newModifiedTime);
}
/**
*计算存储目录下的文件大小,当文件总大小大于规定的CACHE_SIZE或者sdcard剩余空间小于FREE_SD_SPACE_NEEDED_TO_CACHE的规定
* 那么删除40%最近没有被使用的文件
* @param dirPath
* @param filename
*/
private void removeCache(String dirPath) {
File dir = new File(dirPath);
File[] files = dir.listFiles();
if (files == null) {
return;
}
int dirSize = 0;
for (int i = 0; i < files.length;i++) {
if(files[i].getName().contains(WHOLESALE_CONV)) {
dirSize += files[i].length();
}
}
if (dirSize > CACHE_SIZE * MB ||FREE_SD_SPACE_NEEDED_TO_CACHE > freeSpaceOnSd()) {
int removeFactor = (int) ((0.4 *files.length) + 1);
Arrays.sort(files, newFileLastModifSort());
Log.i(TAG, "Clear some expiredcache files ");
for (int i = 0; i mTimeDiff) {
Log.i(TAG, "Clear some expiredcache files ");
file.delete();
}
}
到这里结束
现在手机大内存已经很普遍了,存入sd卡 在程序中要考虑sd卡挂载异常和用户卸载情况,程序中暂且存放在内存缓存内。上面也提到了仅作本地缓存处理并不能改善OOM状况,
本地文件缓存过期策略可以参照sdk
为了更好的控制资源回收来满足实际要求,这里采用LRU策略来尽可能完全展现视野内风景
LRU是Least Recently Used最近最少使用算法。内存管理的一种算法,对于在内存中但最近又不用的内存块叫做LRU,Oracle会根据那些数据属于LRU而将其移出内存而腾出空间来加载另外的数据。
把最近最少使用的缓存对象给换走。总是需要去了解在什么时候,用了哪个缓存对象。如果有人想要了解为什么总能把最近最少使用的对象踢掉,是非常困难的。浏览器就是使用了LRU作为缓存算法。新的对象会被放在缓存的顶部,当缓存达到了容量极限,我会把底部的对象踢走,而技巧就是:我会把最新被访问的缓存对象,放到缓存池的顶部。
所以,经常被读取的缓存对象就会一直呆在缓存池中。可以用数据或者链表实现。其改进算法有LRU2 和 2Q。
Least Recently Used 2(LRU2)
把被两次访问过的对象放入缓存池,当缓存池满了之后,我会把有两次最少使用的缓存对象踢走。因为需要跟踪对象2次,访问负载就会随着缓存池的增加而增加。如果用在大容量的缓存池中,就会有问题。另外,还需跟踪那么不在缓存的对象,因为他们还没有被第二次读取。这比LRU好
public class ImageLoad
{
private static final String TAG = "ImageLoad";
private Handler mHandler;
private ImageCache mImageCache;
private FileCache fileCache;
private Context mContext;
public static SyncImageLoader instance;
private int bitmapType;
public static ImageLoad getInstance(Handler mHandler, Context context)
{
if (instance == null)
{
instance = new ImageLoad(mHandler, context);
}
return instance;
}
private ImageLoad(Handler mHandler, Context context)
{
this.mContext = context;
this.mHandler = mHandler;
mImageCache = ImageCache.getInstance();
fileCache = FileCache.getInstance(mContext);
}
/**
* *
* @param imageUrl
* @param headImage
*/
public synchronized void loadImage(String url,ImageView headImage)
{
// 内存缓存
if (url!= null)
{
Bitmap bit = mImageCache.getBitmapFromCache(url);
if (null != bit)
{
headImage.setImageBitmap(bit);
return;
} else
{
// 文件缓存
File file = fileCache.getFromFileCache(url);
if (null != file && file.exists())
{
Bitmap temp = BitmapFactory.decodeFile(file.getAbsolutePath());
if (null != temp)
{
headImage.setImageBitmap(temp);
mImageCache.addBitmapToCache(url, temp);
return;
}
}
// 下载
if (ConstString.Net_STATUS)
{
forceDownload(url, headImage);
}
}
}
}
/**
* 网络下载
* @param url
* @param imageView
*/
private void forceDownload(String url, ImageView imageView)
{
if (url == null)
return;
SyncDown mSyncDown= new SyncDown(imageView);
mSyncDown.execute(url);
}
class SyncDown extends AsyncTask
{
private String url;
private ImageView mImageView;
public BitmapDownloaderTask(ImageView imageView)
{
this.mImageView = imageView;
}
@Override
protected Bitmap doInBackground(String... params)
{
url = params[0];
if (isCancelled())
return null;
return downloadBitmap(url);
}
@Override
protected void onPostExecute(Bitmap result)
{
if (isCancelled())
return;
mImageCache.addBitmapToCache(url, result);
if (mImageView!= null && null != result){
if (url.equals(imageView.getTag())){
imageView.setImageBitmap(result);
}
}
super.onPostExecute(result);
}
}
/**
*
* 下载图片
* @param url
* @return
*/
private Bitmap downloadBitmap(String url)
{
Bitmap bitmap = null;
String realUrl = null;
InputStream inputStream;
try
{
inputStream = getInputStream(url);
realUrl = fileCache.addToFileCache(url, inputStream);
if (null != realUrl)
return BitmapFactory.decodeFile(realUrl);
} catch (MalformedURLException e)
{
e.printStackTrace();
} catch (IOException e)
{
e.printStackTrace();
}
return null;
}
}
//文件缓存类:
public class FileCache {
private static final String TAG = "FileCache";
public static SharedPreferences preferences = null;
private File cacheDir;
public static FileCache instance;
/**
* @param context
*/
private FileCache(Context context) {
init(context);
}
public static FileCache getInstance(Context context){
if(null==instance)
instance = new FileCache(context);
return instance;
}
private void init(Context context) {
cacheDir = new File(context.getCacheDir()+FileCachDir.listHeadDir) ;
if(!cacheDir.exists()) {
cacheDir.mkdirs();
}
preferences = context.getSharedPreferences("FileCacheList",Context.MODE_APPEND);;
}
/**添加文件缓存
*
* @param url
* @param bitmap
*/
public synchronized String addFileCache(String url, InputStream inputStream) {
//文件存在
String path = preferences.getString(url,null);
if(null!=path){
File file = new File(path);
if(file.exists())
return path;
}
//文件不存在
OutputStream outputStream = null;
try {
String fileNameIndex = Util.generateKey(url);
File createFile = new File(cacheDir,fileNameIndex);
if(!createFile.exists())
try {
createFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
outputStream = new FileOutputStream(createFile);
copyStream(inputStream, outputStream);
preferences.edit().putString(url, createFile.getAbsolutePath()).commit();
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if(outputStream != null) {
try {
inputStream.close();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return preferences.getString(url,null);
}
/**
* 获取缓存文件
* @param url
* @return
*/
public File getFromFileCache(String url) {
return getFile(url);
}
/**
* 清理缓存
* 缓存目录:cacheDir
*/
public void clearCache() {
File[] files=cacheDir.listFiles();
if(files==null)
return;
for(File f:files)
if(null!=f)
f.delete();
}
/**
* @param is
* @param os
*/
private void copyStream(InputStream is, OutputStream os){
final int buffer_size=1024;
try
{
byte[] bytes=new byte[buffer_size];
for(;;)
{
int count=is.read(bytes, 0, buffer_size);
if(count==-1)
break;
os.write(bytes, 0, count);
}
}
catch(Exception ex){}
}
public File getCacheDir() {
return cacheDir;
}
}
key 用唯一的url 表示,本地文件缓存有效减少网络请求数量,提高数据加载速度,降低用户使用网络流量。 还有更快速的加载方式就是直接从内存读取,当然这无疑在本来就紧张的内存 环境下增加了OOM发生的概率 ,真的是这样嘛?
内存保存:
public class ImageCache {
private static ImageCache instance;
private final String TAG = "ImageCache";
// private final int HARD_CACHE_CAPACITY = memClass*3/10;
private final int HARD_CACHE_CAPACITY = 4*1024*1024; //不宜过大可设置为总内存的1/n 容量越大读取效率越低,可设置一个折中如果缓存已满,我们需要采用合适的替换策略换掉一个已有的数据对象,并替之已一个新的数据对象 private LruCache sHardBitmapCache;
private static final int SOFT_CACHE_CAPACITY = 20;
private final static ConcurrentHashMap> sSoftBitmapCache =
new ConcurrentHashMap>(SOFT_CACHE_CAPACITY );
//
private ImageCache(){
init();
}
public static ImageCache getInstance(){
if(null==instance)
instance = new ImageCache();
return instance;
}
private void init() {
sHardBitmapCache = new LruCache(HARD_CACHE_CAPACITY) {
@Override
public int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight();
}
@Override
protected void entryRemoved(boolean evicted, String key,
Bitmap oldValue, Bitmap newValue) {
// 硬引用缓存区满,将一个最不经常使用的oldvalue推入到软引用缓存区
XYLog.e(TAG, "sHardBitmapCache 已经被回收");
sSoftBitmapCache.put(key, new SoftReference(oldValue));
sHardBitmapCache.remove(key);
}
};
//
}
/**
* 添加缓存 sHardBitmapCache
* @param bitmap
*/
public void addBitmapToCache(String url, Bitmap bitmap) {
if (bitmap != null) {
synchronized (sHardBitmapCache) {
if(null!=sHardBitmapCache.get(url)){
return;
}
sHardBitmapCache.put(url, bitmap);
}
//
/* synchronized (sSoftBitmapCache) {
if(null!=sSoftBitmapCache.get(url)){
XYLog.e(TAG, "save sSoftBitmapCache");
return;
}
sSoftBitmapCache.put(url, new SoftReference(bitmap));
} */
}
}
public Bitmap getBitmapFromCache(String url) {
// 先hardCache
synchronized (sHardBitmapCache) {
final Bitmap bitmap = sHardBitmapCache.get(url);
if (bitmap != null) {
// // LRU has
// sHardBitmapCache.remove(url);
// sHardBitmapCache.put(url, bitmap);
return bitmap;
}
}
synchronized (sSoftBitmapCache){
// 然softCache
SoftReference bitmapReference = sSoftBitmapCache.get(url);
if (bitmapReference != null) {
final Bitmap bitmap = bitmapReference.get();
if (bitmap != null) {
return bitmap;
} else {
sSoftBitmapCache.remove(url);
}
}
}
return null;
}
/**
* 清除缓存.
*/
public void clearCache() {
sHardBitmapCache.evictAll();
sSoftBitmapCache.clear();
}
/**
* 移除指定bitmapp
* @param key
*/
public void remove(String key){
Bitmap bitmap = sHardBitmapCache.get(key);
if(null!=bitmap&&bitmap.isRecycled()){
bitmap.recycle();
bitmap = null;
}
}
}