加载大量网络图片会遇到的问题和原理性解决方案
我们在实际的项目过程中肯定会遇到需要加载大量网络图片的情况,这些图片经常是放在listview或者是GridView中显示。遇到OOM的问题也是很正常的。下面我分析下会遇到的问题:
1.滑动列表的时候开启很多线程,有些图片已经被移动到屏幕之外了,但线程仍旧还在进行。
2.法确保当前视图在结束时,分配的视图已经进入循环队列中给另外一个子视图进行重用,也就是图片显示错位了,不该显示到当前问题的图片却显示了,这个是经常遇到的问题。之前用框架加载的时候就因为参数设置不当而出现了这个问题
3.图片在内存中过多,出现内存溢出OOM
一、提出我们的解决思路前,我们先看看Google在BitmapFun中是怎么解决的:
1. ImageView和Task绑定准确的加载对应图片;
2. ImageView和Task无法对应时则取消任务;
但这里也会出现一个问题bitmapfun可能在加载图片的时候会出现加载缓慢的情况,这是为什么呢?原因是bitmapfun下载图片的时候会在下载过程中加一个锁,锁住了很多代码来确保图片是按顺序下载的,这样保证了按顺序显示。但一个图片在下载的时候,其他的都得在那等着,就出现了下载缓慢的问题。老版本的代码貌似没这个问题,我在别人的博客里找到了这个代码:https://files.cnblogs.com/xiaoQLu/bitmapfun_old.rar
PS:如果大家对bitmapfun项目有兴趣可以去看看这篇详解文章:http://blog.csdn.net/xu_fu/article/details/8269865
二、弱引用和软应用貌似不是很合适了
通过内存缓存可以快速加载缓存图片,但会消耗应用的内存空间。LruCache类(通过兼容包可以支持到sdk4)很适合做图片缓存,它通过LinkedHashMap保持图片的强引用方式存储图片,当缓存空间超过设置定的限值时会释放掉早期的缓存。
注:在过去,常用的内存缓存实现是通过SoftReference或WeakReference,但不建议这样做。从Android2.3(API等级9)垃圾收集器开始更积极收集软/弱引用,这使得它们相当无效。此外,在Android 3.0(API等级11)之前,存储在native内存中的可见的bitmap不会被释放,可能会导致应用程序暂时地超过其内存限制并崩溃。
三、设置合适的缓存大小,需要考虑以下几点因素:
-
你的应用中空闲内存是多大?
-
你要在屏幕中一次显示多少图片? 你准备多少张图片用于显示?
-
设备的屏幕大小与density 是多少?超高屏幕density的设备(xhdpi)像Galaxy Nexus 比 Nexus S (hdpi)这样的设备在缓存相同的图片时需要更大的Cache空间。
-
图片的大小和属性及其需要占用多少内存空间?
-
图片的访问频率是多少? 是否比其他的图片使用的频率高?如果这样你可能需要考虑将图片长期存放在内存中或者针对不同类型的图片使用不同的缓存策略。
-
如何平衡质量与数量,有事你可能会存储一些常用的低质量的图片用户显示,然后通过异步线程加载高质量的图片。
图片缓存方案没有固定的模式使用所有的的应用,你需要根据应用的具体应用场景进行分析,选择合适的方案来做,缓存太小不能发挥缓存的优势,太大可能占用过多的内存,降低应用性能,或者发生内存溢出异常,
现在我们来提出自己的解决思路:
1.在用户快速滑动时不加载图片,用户缓慢滑动时进行加载图片
这里说的加载是从网络或者是磁盘中加载,如果内存里有了还是要加载的,因为如果内存有还不加载用户会感觉这个程序很不流畅
2.减少bitmap的采样率
选择性的将要加载的图片进行压缩,在保证可以观看的情况降低质量。这样可以大大的减少图片的大小,减少OOM
3.利用二级缓存策略
加载时判断内存里有没有,如果没有就去硬盘里找,如果还没有就从网上下。网上下载后保存到内存和硬盘中各一份。这里需要注意的是内存的缓存别开太大,有人推荐16M,具体的还需要根据项目自行决定。现在的手机内存都上G了,所以看具体需求和系统版本的要求吧。
4.将图片用一个键值对进行引用,可以选择弱引用。
保证一个key对于的是一个value,这里的value可以使内存的图片或者是硬盘的图片地址。
5.加载合理尺寸的Bitmap;避免反复解码、重复加载Bitmap;控制Bitmap的生命周期,合理回收
下面是具体的解决办法,这个仅仅凸显了思路,仅仅为了方便理解。
一、设置bitmap采样率,压缩Bitmap
(转载自:http://blog.csdn.net/xu_fu/article/details/8262153)
在Android中显示图片一般使用ImageView,通过setImageBitmap()、setImageResource()等方法指定要显示的方法,而这些方法最终都会调用到BitmapFactory.decode()方法来生成一个Bitmap进行显示。
对于一般的小图片这样使用没有什么问题,因为垃圾回收器会及时将不用的图片进行回收,但连续加载大图片的时候就会发生典型的OOM问题,也就是内存溢出,这是因为在Android系统中虚拟机为每一个应用程序都分配了指定内存大小,如果使用超出了这个限制就会发生内存溢出导致程序崩溃。 因此要避免OOM的问题就需要对大图片的加载进行管理,主要通过缩放来减小图片的内存占用。
计算采样率
/** * 计算实际的采样率 * @param options * @param reqWidth 设定的宽度 * @param reqHeight 设定的高度 * @return 一个压缩的比例 */ public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { // Raw height and width of image final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { if (width > height) { inSampleSize = Math.round((float) height / (float) reqHeight); } else { inSampleSize = Math.round((float) width / (float) reqWidth); } } return inSampleSize; }
得到这个压缩后的Bitmap
/** * * @param filename * @param reqWidth * @param reqHeight * @return 一个设定好属性的Bitmap对象 */ public static Bitmap decodeSampledBitmapFromFile(String filename, int reqWidth, int reqHeight) { // First decode with inJustDecodeBounds=true to check dimensions final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(filename, options); // Calculate inSampleSize 计算大小 options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // Decode bitmap with inSampleSize set options.inJustDecodeBounds = false; return BitmapFactory.decodeFile(filename, options); }
这样就可以对图片实现缩放的功能了,缩放的内存大小是实际的1/(samplesize^2)。但这里还有一个问题,就是实际解析图片的时候是从硬盘上读取的,当图片较大的时候这个解析时间是不确定的,如果直接在UI线程中执行,会观察到明显的停顿,再时间长一点就ANR了,所以需要将图片解析的过程放在异步操作了,可以单开一个线程,也可以使用系统提供的异步任务类AsyncTask,示例如下:
class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap> { private final WeakReference<ImageView> imageViewReference; private String data = null; public BitmapWorkerTask(ImageView imageView) { // Use a WeakReference to ensure the ImageView can be garbage collected imageViewReference = new WeakReference<ImageView>(imageView); } // Decode image in background. @Override protected Bitmap doInBackground(String... params) { data = params[0]; return decodeSampledBitmapFromFile(data, 100, 100)); } // Once complete, see if ImageView is still around and set bitmap. @Override protected void onPostExecute(Bitmap bitmap) { if (imageViewReference != null && bitmap != null) { final ImageView imageView = imageViewReference.get(); if (imageView != null) { imageView.setImageBitmap(bitmap); } } } }
二、内存缓存
内存缓存是必须的,为什么这么说呢,因为内存的缓存可以直接将图片展现给用户,让用户感觉很流畅。硬盘的缓存就做不到这点,需要一定的加载时间(虽然加载时间也不长)。内存缓存怎么做呢?用一个很简单的弱引用或者是软应用就可以啦。将图片被一个map对象包裹,用键值对的形式将图片存到一个map中,多个map(也就是多个图片)装到一个list中,这样需要的时候直接去问list要对应position的map对象,再通过唯一的key得到图片对象。
下面的代码就是一个简单讲图片作为map对象存入和取出的方式
public class MemoryCache { private static final String TAG = "MemoryCache"; //WeakReference Map: key=string, value=Bitmap //弱引用对象,用键值对的方式保存Bitmap private WeakHashMap<String, Bitmap> cache = new WeakHashMap<String, Bitmap>(); /** * 通过一个唯一的key存放Bitmap对象 * @param key Should be unique. * @param value A bitmap. * */ public void put(String key, Bitmap value){ if(key != null && !"".equals(key) && value != null){ cache.put(key, value); //Log.i(TAG, "cache bitmap: " + key); Log.d(TAG, "size of memory cache: " + cache.size()); } } /** * 通过这个key来到内存里面找这个Bitmap对象 * @param key Should be unique. * @return The Bitmap object in memory cache corresponding to specific key. * */ public Bitmap get(String key){ if(key != null) return cache.get(key); return null; } /** * clear the memory cache. * */ public void clear() { cache.clear(); } }
三、硬盘缓存
硬盘缓存是将Bitmap对象存到硬盘里面,通过流的形式写入硬盘。
public static boolean saveBitmap(File file, Bitmap bitmap){ if(file == null || bitmap == null) return false; try { BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file)); return bitmap.compress(CompressFormat.JPEG, 100, out); } catch (FileNotFoundException e) { e.printStackTrace(); return false; } }
取出和存入的时候当然也是通过一个key进行操作的。这里就可以明白了,内存缓存是将图片放到一个对象中,这个对象可以被内存管理机制进行回收,磁盘的话就是我们手动管理的,因为磁盘很大我们可以放心的存放。
public class FileCache { private static final String TAG = "MemoryCache"; private File cacheDir; //the directory to save images /** * Constructor * @param context The context related to this cache. * */ public FileCache(Context context) { // Find the directory to save cached images if (android.os.Environment.getExternalStorageState() .equals(android.os.Environment.MEDIA_MOUNTED)) cacheDir = new File( android.os.Environment.getExternalStorageDirectory(), Config.CACHE_DIR); else cacheDir = context.getCacheDir(); if (!cacheDir.exists()) cacheDir.mkdirs(); Log.d(TAG, "cache dir: " + cacheDir.getAbsolutePath()); } /** * Search the specific image file with a unique key. * @param key Should be unique. * @return Returns the image file corresponding to the key. * */ public File getFile(String key) { File f = new File(cacheDir, key); if (f.exists()){ Log.i(TAG, "the file you wanted exists " + f.getAbsolutePath()); return f; }else{ Log.w(TAG, "the file you wanted does not exists: " + f.getAbsolutePath()); } return null; } /** * Put a bitmap into cache with a unique key. * @param key Should be unique. * @param value A bitmap. * */ public void put(String key, Bitmap value){ File f = new File(cacheDir, key); if(!f.exists()) try { f.createNewFile(); } catch (IOException e) { e.printStackTrace(); } //Use the util's function to save the bitmap. if(BitmapHelper.saveBitmap(f, value)) Log.d(TAG, "Save file to sdcard successfully!"); else Log.w(TAG, "Save file to sdcard failed!"); } /** * Clear the cache directory on sdcard. * */ public void clear() { File[] files = cacheDir.listFiles(); for (File f : files) f.delete(); } }
上面是核心代码。剩下的代码和使用方式在:http://blog.csdn.net/floodingfire/article/details/8249122
本文参考自:
http://www.cnblogs.com/purediy/p/3462822.html
http://blog.csdn.net/floodingfire/article/details/8249122
http://blog.csdn.net/floodingfire/article/details/8249117
http://blog.csdn.net/floodingfire/article/details/8247021
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?