2024年安卓最全Android:跟着实战项目学缓存策略之LruCache详谈(2),记录一次腾讯Android岗面试笔试总结

最后

我见过很多技术leader在面试的时候,遇到处于迷茫期的大龄程序员,比面试官年龄都大。这些人有一些共同特征:可能工作了7、8年,还是每天重复给业务部门写代码,工作内容的重复性比较高,没有什么技术含量的工作。问到这些人的职业规划时,他们也没有太多想法。

其实30岁到40岁是一个人职业发展的黄金阶段,一定要在业务范围内的扩张,技术广度和深度提升上有自己的计划,才有助于在职业发展上有持续的发展路径,而不至于停滞不前。

不断奔跑,你就知道学习的意义所在!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

    InputStream is = null;



    try {

        URL url = new URL(urlStr);

        HttpURLConnection connection = (HttpURLConnection) url.openConnection();

        is = new BufferedInputStream(connection.getInputStream());

        bitmap = BitmapFactory.decodeStream(is);

        connection.disconnect();

        return bitmap;

    } catch (java.io.IOException e) {

        e.printStackTrace();

    } finally {

        try {

            is.close();

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

    return null;

}

}




如果你看了上面的代码,你可能对下面这段判断代码有疑问:



//只有当前的ImageView所对应的URL的图片是一致的,才会设置图片

if (mImageView.getTag().equals(mIconUrl)) {

mImageView.setImageBitmap((Bitmap) msg.obj);

}




没关系,因为我还没说Adapter,现在来说。先看代码,就是简单的继承BaseAdapter,还有个ViewHolder,其他都是常规的东西。



/**

  • 新闻列表适配器

*/

public class NewsAdapter extends BaseAdapter {

private Context context;

private List<NewsBean> list;



public NewsAdapter(Context context, List<NewsBean> list, ListView lv) {

    this.context = context;

    this.list = list;

}

public int getCount() {

    return list.size();

}





public Object getItem(int position) {

    return list.get(position);

}





public long getItemId(int position) {

    return position;

}





public View getView(int position, View convertView, ViewGroup parent) {

    ViewHolder viewHolder;

    if (convertView == null) {

        convertView = View.inflate(context, R.layout.item_news, null);

    }

    // 得到一个ViewHolder

    viewHolder = ViewHolder.getViewHolder(convertView);

    //先加载默认图片 防止有的没有图

    viewHolder.iconImage.setImageResource(R.mipmap.ic_launcher);



    String iconUrl = list.get(position).newsIconUrl;

    //当前位置的ImageView与图片的URL绑定

    viewHolder.iconImage.setTag(iconUrl);

    //再加载联网图



    //第一种方式 通过子线程设置

    new ThreadUtil().showImageByThread(viewHolder.iconImage, iconUrl);



    viewHolder.titleText.setText(list.get(position).newsTitle);

    viewHolder.contentText.setText(list.get(position).newsContent);



    return convertView;

}



static class ViewHolder {

    ImageView iconImage;

    TextView titleText;

    TextView contentText;



    // 构造函数中就初始化View

    public ViewHolder(View convertView) {

        iconImage = (ImageView) convertView.findViewById(R.id.iv_icon);

        titleText = (TextView) convertView.findViewById(R.id.tv_title);

        contentText = (TextView) convertView.findViewById(R.id.tv_content);

    }



    // 得到一个ViewHolder

    public static ViewHolder getViewHolder(View convertView) {

        ViewHolder viewHolder = (ViewHolder) convertView.getTag();

        if (viewHolder == null) {

            viewHolder = new ViewHolder(convertView);

            convertView.setTag(viewHolder);

        }

        return viewHolder;

    }

}

}




值得注意的是,在得到图片的Url的时候,给当前item的ImageView与图片的URL进行绑定。



String iconUrl = list.get(position).newsIconUrl;

//当前位置的ImageView与图片的URL绑定

viewHolder.iconImage.setTag(iconUrl);




为什么呢?因为我们知道,ListView中的Item是属于复用机制的,所以在滑动过程中,滑动很快的话,有可能出现一个item中ImageView刚刚加载好一张图后突然加载另一张图,因为前面那张图还是上一个item所对应的图,由于滑动太快,刚加载就被复用了。也就是可能出现图片闪烁的现象,体验十分差,所以就将当然位置与URL绑定,在Hanlder中设置的时候判断标记,防止出现这种现象。这也算ListView的一种优化吧。



好了到此,就是一个很小的新闻项目,没有任何难度。下面我们就对这个项目不断进行优化。主要就是通过缓存策略,所以先来看缓存策略的介绍。



[]( )LruCache用法详解

------------------------------------------------------------------------------



### []( )初识Cache



对于缓存我想大家都应该了解,通俗的说就是把一些经常使用但需要联网获取文件,通过一种策略持久的保存在内存或者存储设备中,当下一次需要用到这些文件的时候,不需要联网,直接从内存或存储设备中获取就可以了。这种策略就是缓存策略。



缓存策略一般来说包含缓存的 **添加、获取、删除** 。至于删除,其实是指缓存的大小已经超过定义的缓存的大小后移除已有的一部分缓存。比如LRU算法,最近最少使用算法,会移除最近最少使用的那一部分缓存,以此来添加新的缓存。



关于缓存的好处,开篇已经说过了,无非就是两点:



*   节省流量

*   提高用户体验



[![image](http://upload-images.jianshu.io/upload_images/15405197-cbc445c8c9e46d96.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)](https://img.colabug.com/2018/07/8d2982d5e9a2f6bd74a362dcadc31f90.png "Android:跟着实战项目学缓存策略之LruCache详谈")



而接下来要说的LruCache和DiskLruCache就是基于LRU算法的缓存策略。LruCache是用于实现内存缓存的,而DiskLruCache实现存储设备缓存,也就是直接缓存到本地。其中LruCache在Android中已经封装成了类,直接用就可以了。而DiskLruCache需要下载对应的文件才能用,本项目中也有集成好的。如果需要可以直接拷贝来用。



### []( )LruCache介绍



关于什么是LruCache,在开发艺术探索上任老师说的不能再清楚了:



LruCache是一个泛型类,它内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,其提供了get与set方法来完成缓存的添加与获取操作。当缓存满时,LruCache会移除较早使用的缓存对象,然后再添加新的缓存对象。



这里面提到了一个概念——强引用。也就是Java中的四种引用,不久前电话面试中面试官也问到了这个,可惜当时答的太烂。由于不作为重点,这里仅仅给出定义,点到为止:



*   强引用:直接的对象引用,gc绝不会回收它

*   软引用:当对象只具有软引用时,系统内存不足时才会被gc回收

*   弱引用:当对象只具有弱引用时,对象随时会被gc

*   虚引用:当对象只具有虚引用时,对象随时会被gc,但是必须与引用队列一起使用



那么如何使用LruCache呢?首先需要直接定义一个LruCache,注意内部实现是Map,所以要设置key和value的类型:



//LRU缓存

private LruCache<String, Bitmap> mCache;




然后就是初始化LruCache,来看下面这段代码:



//返回Java虚拟机将尝试使用的最大内存

int maxMemory = (int) Runtime.getRuntime().maxMemory();

//指定缓存大小

int cacheSize = maxMemory / 4;

mCache = new LruCache<String, Bitmap>(cacheSize) {

protected int sizeOf(String key, Bitmap value) {

    //Bitmap的实际大小 注意单位与maxMemory一致

    return value.getByteCount();



    //也可以这样返回 结果是一样的

    //return value.getRowBytes()*value.getHeight();

}

};




可以看到上面这段代码规定了LruCache的缓存大小,它是通过返回Java虚拟机将尝试使用的最大内存来确定的。这就初始化了一个LruCache,现在就是简单的添加获取了。因为是Map机制,所以与Map的添加获取是一样的道理。



//添加到缓存

mCache.get(key);

//从缓存中获取

mCache.put(key,value);




这也就是LruCache的基本使用了,当然还有其他方法,这里暂且不考虑。而且上面的添加与获取在项目中可以封装成相关的方法。接下来,我们就对上一个项目进行优化,来看看如何将联网获取的图片缓存到内存。



### []( )LruCache实战运用



为了与之前的ThreadUtil对比,这里讲LruCache方法剥离成LruCacheUtil。先看效果图,第一次打开需要加载图片,全部加载完成后,再滑动的时候,已经不需要加载图片了。



[![image](http://upload-images.jianshu.io/upload_images/15405197-ddf03a0859673f31.gif?imageMogr2/auto-orient/strip)](https://img.colabug.com/2018/07/84049e6cdad431975ac2067baee38942.gif "Android:跟着实战项目学缓存策略之LruCache详谈")



来看LruCacheUtil,为了方便讲解,我将各个部分分开来说明。完整代码在下一个部分 **进一步优化ListView** 中。



首先我们定义出相关需要的变量,然后在构造函数中,初始化LruCache:



//LRU缓存



private LruCache<String, Bitmap> mCache;



private ListView mListView;

public LruCacheUtil(ListView listView) {

this.mListView = listView;

//返回Java虚拟机将尝试使用的最大内存

int maxMemory = (int) Runtime.getRuntime().maxMemory();

//指定缓存大小

int cacheSize = maxMemory / 4;

mCache = new LruCache<String, Bitmap>(cacheSize) {

   

    protected int sizeOf(String key, Bitmap value) {

        //Bitmap的实际大小 注意单位与maxMemory一致

        return value.getByteCount();

    }

};

}




注意这个地方,LurCache的key是字符串类型的图片Url地址,value当然就是图片的Bitmap对象,所以我们很轻易的封装出缓存的添加删除方法:



/**

  • 将Bitmap存入缓存

  • url Bitmap对象的key

  • bitmap 对象的key

*/

public void addBitmapToCache(String url, Bitmap bitmap) {

//如果缓存中没有

if (getBitmapFromCache(url) == null) {

    //保存到缓存中

    mCache.put(url, bitmap);

}

}

/**

  • 从缓存中获取Bitmap对象

  • url Bitmap对象的key

  • 缓存中Bitmap对象

*/

public Bitmap getBitmapFromCache(String url) {

return mCache.get(url);

}




其次呢,由于前几天说了异步任务AsyncTask,所以我们这里就改成异步任务。所以我们先定义一个AsyncTask类,比较简单,不多解释。



/**

  • 异步任务类

*/

private class NewsAsyncTask extends AsyncTask<String, Void, Bitmap> {

private String url;



public NewsAsyncTask(String url) {

    this.url = url;

}





protected Bitmap doInBackground(String... params) {

    Bitmap bitmap = getBitmapFromURL(params[0]);

    //保存到缓存中

    if (bitmap != null) {

        addBitmapToCache(params[0], bitmap);

    }

    return bitmap;

}





protected void onPostExecute(Bitmap bitmap) {

    super.onPostExecute(bitmap);

    //只有当前的ImageView所对应的UR的图片是一致的,才会设置图片

    ImageView imageView = (ImageView) mListView.findViewWithTag(url);

    if (imageView != null && bitmap != null) {

        imageView.setImageBitmap(bitmap);

    }

}

}




剩下的当然就是展示了,先从缓存中找,没找到才联网获取,联网获取的方法getBitmapFromURL在之前已经介绍了,不再多说了。代码如下:



/**

  • 通过异步任务的方式加载数据

  • iv 图片的控件

  • url 图片的URL

*/

public void showImageByAsyncTask(ImageView iv, final String url) {

//从缓存中取出图片

Bitmap bitmap = getBitmapFromCache(url);

//如果缓存中没有,先设为默认图片

if (bitmap == null) {

    iv.setImageResource(R.mipmap.ic_launcher);

} else {

    //如果缓存中有 直接设置

    iv.setImageBitmap(bitmap);

}

}




到此,这个类就算完成了,当然不要忘了,注意我们在自定义的AsyncTask中传递了一个类型为ListView的参数,所以要改一下自定义Adapter的参数,并在改一下图片的加载方式:



private LruCacheUtil lruCacheUtil;

public NewsAdapter(Context context, List list, ListView lv) {

...

//初始化

lruCacheUtil = new LruCacheUtil(lv);

...

}


public View getView(int position, View convertView, ViewGroup parent) {

...

//第二种方式 通过异步任务方式设置 且利用LruCache存储到内存缓存中

lruCacheUtil.showImageByAsyncTask(viewHolder.iconImage, iconUrl);

}




好了,到这我们就算实现了LruCache,再总结一下思路就是在加载图片的时候,先从缓存中找图,如果没有才从网络中获取。而且在获取后存入到缓存中,以方便下一次加载。



进一步优化ListView  

不知道大家注意到没有,上面一个动态图中有一个细节。就是我在滑动ListView的时候,并没有加载图片,等到列表停止滑动的时候才加载图片。对,这也是一个优化ListView的方式之一。



那它是怎么实现的呢?其实就是实现一个列表滚动监听,也就是 OnScrollListener ,它的核心方法是



public void onScrollStateChanged(AbsListView view, int scrollState)  

public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)  

其中参数的意思等会代码中有详细解释,先来说一下思路。



首先在滑动过程中需要记录滑动的起止位置,根据起止位置判断中间有多少个item,然后再加载图片。那么如何加载呢?有了起止位置,要得到起止位置之间的元素,这跟数组不是很相似嘛,所以干脆我们用数据把图片的URL记录起来,然后直接从数组中取就好了。



思路大体就是这样,但是实现起来还是比较复杂的,主要有很多细节问题。



先来改造一下自定义的Adapter:



/**

  • 新闻列表适配器

*/

public class NewsAdapter extends BaseAdapter implements AbsListView.OnScrollListener {

private Context context;

private List<NewsBean> list;



private LruCacheUtil lruCacheUtil;



private int mStart, mEnd;//滑动的起始位置

public static String[] urls; //用来保存当前获取到的所有图片的Url地址



//是否是第一次进入

private boolean mFirstIn;



public NewsAdapter(Context context, List<NewsBean> list, ListView lv) {

    this.context = context;

    this.list = list;



    lruCacheUtil = new LruCacheUtil(lv);



    //存入url地址

    urls = new String[list.size()];

    for (int i = 0; i < list.size(); i++) {

        urls[i] = list.get(i).newsIconUrl;

    }



    mFirstIn = true;



    //注册监听事件

    lv.setOnScrollListener(this);

}

/**

 * 滑动状态改变的时候才会去调用此方法

 *

 * view        滚动的View

 * scrollState 滚动的状态

 */



public void onScrollStateChanged(AbsListView view, int scrollState) {

    if (scrollState == SCROLL_STATE_IDLE) {

        //加载可见项

        lruCacheUtil.loadImages(mStart, mEnd);

    } else {

        //停止加载任务

        lruCacheUtil.cancelAllTask();

    }

}



/**

 * 滑动过程中 一直会调用此方法

 *

 * view             滚动的View

 * firstVisibleItem 第一个可见的item

 * visibleItemCount 可见的item的长度

 * totalItemCount   总共item的个数

 */



public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {

    mStart = firstVisibleItem;

    mEnd = firstVisibleItem + visibleItemCount;

    //如果是第一次进入 且可见item大于0 预加载

    if (mFirstIn && visibleItemCount > 0) {

        try {

            lruCacheUtil.loadImages(mStart, mEnd);

        } catch (IOException e) {

            e.printStackTrace();

        }

        mFirstIn = false;

    }

}

}




也许你有个疑问?因为LruCacheUtil中自定义的AsyncTask类是用来加载图片的。但是你在滑动过程中,停止了这些Task,那么在滑动停止的时候如何得到这些没执行的Task呢?没错,解决办法就是把这些滑动过程中产生的Task放在集合中。当滑动的时候,停止这些Task的执行;滑动停止的时候,再执行这些Task。也就是如下这样定义:



private Set mTaskSet



好了,现在回头看滑动停止、静止加载的主要两个核心方法:



/**

  • 加载从start到end的所有的Image

  • start

  • end

*/

public void loadImages(int start, int end) {

for (int i = start; i < end; i++) {

    String url = NewsAdapter.urls[i];

    //从缓存中取出图片

    Bitmap bitmap = getBitmapFromCache(url);

    //如果缓存中没有,则需要从网络中下载

    if (bitmap == null) {

        NewsAsyncTask task = new NewsAsyncTask(url);

        task.execute(url);

        mTaskSet.add(task);

    } else {

        //如果缓存中有 直接设置

        ImageView imageView = (ImageView) mListView.findViewWithTag(url);

        imageView.setImageBitmap(bitmap);

    }

}

}

/**

  • 停止所有当前正在运行的任务

*/

public void cancelAllTask() {

if (mTaskSet != null) {

    for (NewsAsyncTask task : mTaskSet) {

        task.cancel(false);

    }

}

}




说到这里,现在来看整个LruCacheUtil类,是不是清晰很多呢?



/**

  • 异步加载图片的工具类

*/

public class LruCacheUtil {

//LRU缓存

private LruCache<String, Bitmap> mCache;

最后

那我们该怎么做才能做到年薪60万+呢,对于程序员来说,只有不断学习,不断提升自己的实力。我之前有篇文章提到过,感兴趣的可以看看,到底要学习哪些知识才能达到年薪60万+。

通过职友集数据可以查看,以北京 Android 相关岗位为例,其中 【20k-30k】 薪酬的 Android 工程师,占到了整体从业者的 30.8%!

北京 Android 工程师「工资收入水平 」

今天重点内容是怎么去学,怎么提高自己的技术。

1.合理安排时间

2.找对好的系统的学习资料

3.有老师带,可以随时解决问题

4.有明确的学习路线

当然图中有什么需要补充的或者是需要改善的,可以在评论区写下来,一起交流学习。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

在运行的任务

*/

public void cancelAllTask() {

if (mTaskSet != null) {

    for (NewsAsyncTask task : mTaskSet) {

        task.cancel(false);

    }

}

}




说到这里,现在来看整个LruCacheUtil类,是不是清晰很多呢?



/**

  • 异步加载图片的工具类

*/

public class LruCacheUtil {

//LRU缓存

private LruCache<String, Bitmap> mCache;

最后

那我们该怎么做才能做到年薪60万+呢,对于程序员来说,只有不断学习,不断提升自己的实力。我之前有篇文章提到过,感兴趣的可以看看,到底要学习哪些知识才能达到年薪60万+。

通过职友集数据可以查看,以北京 Android 相关岗位为例,其中 【20k-30k】 薪酬的 Android 工程师,占到了整体从业者的 30.8%!

北京 Android 工程师「工资收入水平 」

[外链图片转存中…(img-r0Jb3ZA0-1715745629326)]

今天重点内容是怎么去学,怎么提高自己的技术。

1.合理安排时间

2.找对好的系统的学习资料

3.有老师带,可以随时解决问题

4.有明确的学习路线

当然图中有什么需要补充的或者是需要改善的,可以在评论区写下来,一起交流学习。

[外链图片转存中…(img-pwe2LdE6-1715745629327)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值