Ace教你一步一步做Android新闻客户端(四) 优化Bitmap大法

时间:2023-03-08 21:51:36

我计划着把需要用到的知识分解开来写,趁着我们要开发这款客户端的机会把安卓所有移动客户端开发中的技术贯穿其中,也是我自己成长的过程。By Ace in 20160121

我们开发一款新闻客户端程序,它的新闻列表中显示从网络获取的列表图片。可是如果用户发现每次进入新闻列表的时候,程序都要重新下载图片才能进行显示,甚至当把图片滑动到底部再重新滑动回顶部的时候,刚才已经加载完成了的图片竟然又变成了空白图片开始重新加载,这将是一种糟糕的用户体验,用户需要花费很多不必要的流量为了重新加载这些已经加载过得图片。为了解决这种问题,我们就需要—LruCache缓存。

什么是Cache?

Cache,高速缓存,原意是指计算机中一块比内存更高速容量更小的存储器。更广义地说,Cache指对于最近使用过的信息的可高速读取的存储块。而本文要讲的Cache技术,指的就是将最近使用过的Bitmap缓存在手机的内存与磁盘中,来实现再次使用Bitmap时的瞬时加载,以节省用户的时间和手机流量。

Android中的两种Cache类型Memory Cache和Disk Cache。下面来对这两种缓存类型进行介绍

有些代码 来自 http://developer.android.com(需要*,多说两句 比本地打开开发文档速度快多了 )

Memory Cache内存中的Cache

Memory Cache使用内存来为应用程序提供Cache。由于内存的读写速度非常快,所以我们应该优先使用它(相对于下面将介绍的Disk Cache来说)。

Android中提供了LruCache类来进行Memory Cache的管理(该类是在Android 3.1时推出的,但我们可以使用android -support-v4.jar的兼容包来对低版本的手机提供支持)。

下来来看一段Bitmap设置LruCache的代码:这是我们新闻客户端列表图片加载的代码:和前面的相接,但也可以独立去看

我先来写下LruCache的步骤

1 创建并初始化LruCache对象

2 给 LruCache分配内存 

3 创建内部类重写sizeOf方法 用value.getByteCount() 来告诉系统这张bitmap占用多少缓存

4创建addBitmapToCache方法 用来向 LruCache中存放bitmap,可以看出来LruCache机制很像map <key , value>

5创建getBitmapFromCache方法 用来从LruCache中读取

6在 showImageByAsyncTask 方法中加入判断 LruCache中是否有缓存 没有的话创建缓存,有的话直接设置Imagview

7在 MyIconSyncTaskdoInBackgroud方法中把得到的图片加入到缓存中

8 NewsAdapter中也要做相应的改变  mImageLoader.showImageByAsyncTask(viewHolder.iconimage, mlist.get(position).newsIconUrl); 不能使用 new ImageLoader(). 因为每new一次都创建一个ImageLoader()这样系统会创建很多的Lrucache来占用内存,在构造方法中创建对象并设置全局变量就行了。 好了 不难,下面就看代码吧! 还是那句话 不明白请问我,说不定哪一年火了呢 哈哈哈哈哈 如此逗比的程序猿 哎累死我了喝两口水先,下面的一会讲 DiskCache

package asynctask.zb.com.asynctask_02;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.os.Message;
import android.util.Log;
import android.util.LruCache;
import android.widget.ImageView; import java.io.BufferedInputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL; /**
* Created by Administrator on 2016/1/20.
*/
public class ImageLoader {
private ImageView mImageView;
private String mUrl;
public LruCache<String,Bitmap> mCache;
public ImageLoader (){
int maxMemory = (int) Runtime.getRuntime().maxMemory(); //获取虚拟机可用内存(内存占用超过该值的时候,将报OOM异常导致程序崩溃)
int cacheSzie = maxMemory/4; //使用可用内存的1/4来作为Memory Cache
mCache = new LruCache<String,Bitmap>(cacheSzie) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount(); //返回Bitmap占用的空间,告诉系统我这张图片要用多少内存
}
};
}
//把bitmap加入到缓存
public void addBitmapToCache(String mUrl, Bitmap bitmap) {
if (getBitmapFromCache(mUrl) == null) {
mCache.put(mUrl, bitmap);
}
}
//从缓存中获取数据 public Bitmap getBitmapFromCache(String mUrl) {
return mCache.get(mUrl);
} android.os.Handler mHandler = new android.os.Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (mImageView.getTag().equals(mUrl))
mImageView.setImageBitmap((Bitmap)msg.obj);
}
}; public void showImageByThread(ImageView imageView, final String url) {
mImageView =imageView;
mUrl = url;//对传过来的imageView 和url进行缓存(为了避免程序逻辑顺序错误,和viewholder的机制差不多)
new Thread() {
@Override
public void run() {
super.run();
Bitmap bitmap = getBitmapFromURL(url);
Message message = Message.obtain();
message.obj = bitmap;
mHandler.sendMessage(message);
}
}.start(); } //创建从URL获取Bitmap的放方法
public Bitmap getBitmapFromURL(String stringUrl) { Bitmap bitmap;
BufferedInputStream bis = null; URL url1 = null;
try {
url1 = new URL(stringUrl);
} catch (MalformedURLException e) {
e.printStackTrace();
} HttpURLConnection connection = null;
try {
connection = (HttpURLConnection) url1.openConnection();
} catch (IOException e) {
e.printStackTrace();
}
try {
bis = new BufferedInputStream(connection.getInputStream()); bitmap = BitmapFactory.decodeStream(bis);
connection.disconnect();
return bitmap;
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
} }return null;
} public void showImageByAsyncTask (ImageView imageView, String url){
Bitmap bitmap = getBitmapFromCache(url);
if (bitmap == null){
new MyIconSyncTask(imageView,url).execute(url);
}else{
imageView.setImageBitmap(bitmap);
}
} class MyIconSyncTask extends AsyncTask<String,Void,Bitmap> {
private ImageView mImageView;
private String mUrl;
public MyIconSyncTask(ImageView imageView,String url){
mUrl = url;
mImageView = imageView;
}
@Override
protected Bitmap doInBackground(String... params) {
String mUrl = params[0];
//从网络获取图片,并存入缓存中
Bitmap bitmap = getBitmapFromURL(mUrl);
if (bitmap != null){
addBitmapToCache(mUrl,bitmap);
}
return bitmap;
} @Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
if (mImageView.getTag().equals(mUrl)) {
mImageView.setImageBitmap(bitmap);
}
}
}
}

newAdapter 也要做些改变

package asynctask.zb.com.asynctask_02;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView; import java.net.URL;
import java.util.List; /**
* Created by Ace on 2016/1/20.
*/
public class NewsAdapter extends BaseAdapter {
private List<NewsBean> mlist;
private LayoutInflater mInflater;
private ImageLoader mImageLoader; public NewsAdapter(Context context,List<NewsBean>data){
//映射下 把data传给mlist
mlist = data;
//从一个上下文中(这里的上下文是MainActivity),获得一个布局填充器,这样你就可以使用这个填充器的inflater.inflate()来把xml布局文件转为View对象了,然后利用view对象,findViewById就可以找到布局中的组件
mInflater = LayoutInflater.from(context);
mImageLoader = new ImageLoader(); //再适配器初始化ImageLoader
}
@Override
public Object getItem(int position) {
return mlist.get(position);
} @Override
public int getCount() { return mlist.size();
} @Override
public long getItemId(int position) {
return position;
} @Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder= null;
if (convertView == null){
viewHolder = new ViewHolder();
convertView = mInflater.inflate(R.layout.adapter_item,null);
viewHolder.iconimage = (ImageView)convertView.findViewById(R.id.tvimage);
viewHolder.title = (TextView)convertView.findViewById(R.id.tvtitle);
viewHolder.content = (TextView)convertView.findViewById(R.id.tvcontent);
convertView.setTag(viewHolder);
}else{
viewHolder = (ViewHolder)convertView.getTag();
viewHolder.iconimage.setImageResource(R.mipmap.ic_launcher);
String url = mlist.get(position).newsIconUrl;
viewHolder.iconimage.setTag(url);//给imageview设置标签 是为了增加一个判断的标准(再imageloader类里),只有URL地址和当前位置的Item的图片相匹配才显示
// new ImageLoader().showImageByThread(viewHolder.iconimage, mlist.get(position).newsIconUrl);
mImageLoader.showImageByAsyncTask(viewHolder.iconimage, mlist.get(position).newsIconUrl);//不能使用 new ImageLoader(). 因为每new一次都创建一个ImageLoader()这样就会有很多的lru
viewHolder.title.setText(mlist.get(position).newsTitle);
viewHolder.content.setText(mlist.get(position).newsContent);
}
return convertView;
} class ViewHolder{
public TextView title;
public ImageView iconimage;
public TextView content; }
}

============================================================================逗比的分割符===============================

回来了~下面对Lrucache做些补充

在使用 LruCache 的时候,需要考虑如下一些因素来选择一个合适的缓存数量参数:

1 程序中还有多少内存可用

2 同时在屏幕上显示多少图片?要先缓存多少图片用来显示到即将看到的屏幕上?

3 设备的屏幕尺寸和屏幕密度是多少?超高的屏幕密度(xhdpi 例如 Galaxy note5)
设备显示同样的图片要比低屏幕密度(hdpi 例如 小米2S milestone2 哈哈哈哈暴露年龄了)设备需要更多的内存。

4 图片的尺寸和格式决定了每个图片需要占用多少内存

5 图片访问的频率如何?一些图片的访问频率要比其他图片高很多?如果是这样的话,建议最好把这些经常访问的图片放到内存中。

6 在质量和数量上如何平衡?有些情况下保存大量的低质量的图片是非常有用的,当需要的情况下使用后台线程来加入一个高质量版本的图片。

这里没有万能配方可以适合所有的程序,你需要分析你的使用情况并在指定自己的缓存策略。使用太小的缓存并不能起到应有的效果,而使用太大的缓存会消耗更多
的内存从而有可能导致 java.lang.OutOfMemory 异常或者留下很少的内存供你的程序其他功能使用。

Disk Cache(磁盘中的Cache)

前面已经提到,Memory Cache的优点是读写非常快。但它的缺点就是容量太小了,而且不能持久化,所以在用户在滑动GridView时它很快会被用完,而且切换多个界面时或者是关闭程序重新打开后,再次进入原来的界面,Memory Cache是无能为力的。这个时候,我们就要用到Disk Cache了。

Disk Cache将缓存的数据放在磁盘中,因此不论用户是频繁切换界面,还是关闭程序,Disk Cache是不会消失的。

实际上,Android SDK中并没有一个类来实现Disk Cache这样的功能。但google其实已经提供了实现代码:DiskLruCache。我们只要把它搬到自己的项目中就可以了。

在我们清理手机的时候不知道你有没有注意到 手机里很多缓存图片,比如微信,浏览器,网易新闻等等 他们会把缓存图片存在本地,没错 用Lrucache太占内存了 他们图片多而且大 用lru会经常OOM的,所以他们会存到本地,

来看我们的双缓冲大法:DiskLruCache来配合Memory Cache来缓冲:代码来自Android开发者网站

private DiskLruCache mDiskLruCache;
private final Object mDiskCacheLock = new Object();//声明锁对象
private boolean mDiskCacheStarting = true;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB ,分配Disk内存
private static final String DISK_CACHE_SUBDIR = "thumbnails";//缓存文件夹 @Override
protected void onCreate(Bundle savedInstanceState) {
...
// 初始化memory cache
...
// 开启后台线程初始化disk cache
File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);//指定缓冲到哪^ ^
new InitDiskCacheTask().execute(cacheDir);
...
} class InitDiskCacheTask extends AsyncTask<File, Void, Void> {
@Override
protected Void doInBackground(File... params) {
synchronized (mDiskCacheLock) {
File cacheDir = params[0];
mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
mDiskCacheStarting = false; // 初始化完成
mDiskCacheLock.notifyAll(); // 唤醒被hold住的线程
}
return null;
}
} class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
// 在后台加载图片
@Override
protected Bitmap doInBackground(Integer... params) {
final String imageKey = String.valueOf(params[0]); // 通过后台线程检查disk cache
Bitmap bitmap = getBitmapFromDiskCache(imageKey); if (bitmap == null) { // 如果没有在disk cache中发现这个bitmap
// 加载这个bitmap
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100));
} // 把这个bitmap加入cache
addBitmapToCache(imageKey, bitmap); return bitmap;
}
...
} public void addBitmapToCache(String key, Bitmap bitmap) {
// 把bitmap加入memory cache
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
} // 同样,也加入disk cache
synchronized (mDiskCacheLock) {
if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
mDiskLruCache.put(key, bitmap);
}
}
} public Bitmap getBitmapFromDiskCache(String key) {
synchronized (mDiskCacheLock) {
// 等待disk cache初始化完毕
while (mDiskCacheStarting) {
try {
mDiskCacheLock.wait();
} catch (InterruptedException e) {}
}
if (mDiskLruCache != null) {
return mDiskLruCache.get(key);
}
}
return null;
} // 在自带的cache目录下建立一个独立的子目录。优先使用外置存储。但如果外置存储不存在,使用内置存储。
public static File getDiskCacheDir(Context context, String uniqueName) {
// 如果MEDIA目录已经挂载或者外置存储是手机自带的(Nexus设备都这么干),使用外置存储;否则使用内置存储
final String cachePath =
Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
!isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
context.getCacheDir().getPath(); return new File(cachePath + File.separator + uniqueName);
}

上面这段代码看起来比较多,但大致读一下就会发现,它的思路非常简单:

1.读取cache的时候,优先读取memory cache,读不到的时候再读取disk cache;

2.把bitmap保存到cache中的时候,memory cache和disk cache都要保存;

可以看到 官方在初始化mDiskLruCache的 InitDiskCacheTask异步线程里 里用了锁 synchronized(mDiskCacheLock) 再初始化完成后再 mDiskCacheLock.notifyAll(); 唤醒

延伸:什么是synchronized?

概念:为了防止多个后台并发线程同时对同一个对象进行写操作时发成错误,java使用synchronized关键字对一个对象“加锁”,以保证同时只有一个线程可以访问该对象。(百度哒)

举个例子:快过年了,咱俩在12306买票,咱俩再同一时间买同一趟车,而这趟车现在只剩一张票了。然后咱俩同时按下购票按钮,那么后台系统接到两个请求,两个线程同时进行处理,执行了这么两行代码:

if (tickedCount > 0) { // 如果还有票
tickedCount -= 1; // 票数减一
printTicket(); // 出票
}

12306服务器线程1和线程2几乎同时运行,并且几乎同时执行到第一行代码,线程1一看,哦还有票,行,出票吧!然后执行了第二行代码,票数减一。但它不知道,在他执行第二行代码之前,线程2也执行到了第一行,这线程2也一看,哦还有票,行,出票吧!于是在线程1出票之后,线程2在已经没票的情况下依然把票数减到了-1,并且执行printTicket()方法尝试出票。到了这里,如果出了两张一样的票,这就是你们俩的猿粪了 哈哈哈哈  ,猿粪不猿粪吧你俩肯定觉得对方的假票 如果对方是个漂亮妹子还好解决,如果是汉子会不会打架呢

12306那么牛逼肯定为了不让你们打架也要解决啊,那么怎么解决呢?加锁:

synchronized(this) {
if (tickedCount > 0) { // 如果还有票
tickedCount -= 1; // 票数减一
printTicket(); // 出票
}
}

上面这段代码由于加了锁,同一时间只有一个线程可以进入这个代码块,当一个线程进入后,其他线程必须等这个线程执行完这段代码后释放了锁,才能进入这个代码块,12306每天访问量那么大虽然卡虽然有个很BT的验证码但是他的机制肯定不会这么简单的

我也就随便说两句,具体什么事锁百度吧。优化讲完了么?当然没有! 今天就到此为止吧 阿冰Ace与你共同学习共同进步~