6

Android 框架练成 教你打造高效的图片加载框架

 3 years ago
source link: https://blog.csdn.net/lmj623565791/article/details/41874561
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Android 框架练成 教你打造高效的图片加载框架_Hongyang-CSDN博客

转载请标明出处: http://blog.csdn.net/lmj623565791/article/details/41874561,本文出自: 【张鸿洋的博客】

优秀的图片加载框架不要太多,什么UIL , Volley ,Picasso,Imageloader等等。但是作为一名合格的程序猿,必须懂其中的实现原理,于是乎,今天我就带大家一起来设计一个加载网络、本地的图片框架。有人可能会说,自己写会不会很渣,运行效率,内存溢出神马的。放心,我们拿demo说话,拼得就是速度,奏事这么任性。

好了,如果你看过之前的博文,类似Android Handler 异步消息处理机制的妙用 创建强大的图片加载类,可能会对接下来文章理解会有很大的帮助。没有的话,就跟我往下继续走吧,也不要去看了。

关于加载本地图片,当然了,我手机图片比较少,7000来张:

1、首先肯定不能内存溢出,但是尼玛现在像素那么高,怎么才能保证呢?我相信利用LruCache统一管理你的图片是个不二的选择,所有的图片从LruCache里面取,保证所有的图片的内存不会超过预设的空间。

2、加载速度要刚刚的,我一用力,滑动到3000张的位置,你要是还在从第一张给我加载,尼玛,你以为我打dota呢。所以我们需要引入加载策略,我们不能FIFO,我们选择LIFO,当前呈现给用户的,最新加载;当前未呈现的,选择加载。

3、使用方便。一般图片都会使用GridView作为控件,在getView里面进行图片加载,当然了为了不错乱,可能还需要用户去自己setTag,自己写回调设置图片。当然了,我们不需要这么麻烦,一句话IoadImage(imageview,path)即可,剩下的请交给我们的图片加载框架处理。

做到以上几点,关于本地的图片加载应该就木有什么问题了。

关于加载网络图片,其实原理差不多,就多了个是否启用硬盘缓存的选项,如果启用了,加载时,先从内存中查找,然后从硬盘上找,最后去网络下载。下载完成后,别忘了写入硬盘,加入内存缓存。如果没有启用,那么就直接从网络压缩获取,加入内存即可。

2、效果图

终于扯完了,接下来,简单看个效果图,关于加载本地图片的效果图:可以从Android 超高仿微信图片选择器 图片该这么加载这篇博客中下载Demo运行。

下面演示一个网络加载图片的例子:

80多张从网络加载的图片,可以看到我直接拖到最后,基本是呈现在用户眼前的最先加载,要是从第一张到80多,估计也是醉了。

此外:图片来自老郭的博客,感谢!!!ps:如果你觉得图片不劲爆,Day Day Up找老郭去。

3、完全解析

1、关于图片的压缩

不管是从网络还是本地的图片,加载都需要进行压缩,然后显示:

用户要你压缩显示,会给我们什么?一个imageview,一个path,我们的职责就是压缩完成后显示上去。

1、本地图片的压缩

a、获得imageview想要显示的大小

想要压缩,我们第一步应该是获得imageview想要显示的大小,没大小肯定没办法压缩?

那么如何获得imageview想要显示的大小呢?

可以看到,我们拿到imageview以后:

首先企图通过getWidth获取显示的宽;有些时候,这个getWidth返回的是0;

那么我们再去看看它有没有在布局文件中书写宽;

如果布局文件中也没有精确值,那么我们再去看看它有没有设置最大值;

如果最大值也没设置,那么我们只有拿出我们的终极方案,使用我们的屏幕宽度;

总之,不能让它任性,我们一定要拿到一个合适的显示值。

可以看到这里或者最大宽度,我们用的反射,而不是getMaxWidth();维萨呢,因为getMaxWidth竟然要API 16,我也是醉了;为了兼容性,我们采用反射的方案。反射的代码就不贴了。

b、设置合适的inSampleSize

我们获得想要显示的大小,为了什么,还不是为了和图片的真正的宽高做比较,拿到一个合适的inSampleSize,去对图片进行压缩么。

那么首先应该是拿到图片的宽和高:

这三行就成功获取图片真正的宽和高了,存在我们的options里面;

然后我们就可以happy的去计算inSampleSize了:

options里面存了实际的宽和高;reqWidth和reqHeight就是我们之前得到的想要显示的大小;经过比较,得到一个合适的inSampleSize;

有了inSampleSize:

经过这几行,就完成图片的压缩了。

上述是本地图片的压缩,那么如果是网络图片呢?

2、网络图片的压缩

a、直接下载存到sd卡,然后采用本地的压缩方案。这种方式当前是在硬盘缓存开启的情况下,如果没有开启呢?

b、使用BitmapFactory.decodeStream(is, null, opts);

基本和本地压缩差不多,也是两次取样,当然需要注意一点,我们的is进行了包装,以便可以进行reset();直接返回的is是不能使用两次的。

到此,图片压缩说完了。

2、图片加载框架的架构

我们的图片压缩加载完了,那么就应该放入我们的LruCache,然后设置到我们的ImageView上。

好了,接下来我们来说说我们的这个框架的架构;

1、单例,包含一个LruCache用于管理我们的图片;

2、任务队列,我们每来一次加载图片的请求,我们会封装成Task存入我们的TaskQueue;

3、包含一个后台线程,这个线程在第一次初始化实例的时候启动,然后会一直在后台运行;任务呢?还记得我们有个任务队列么,有队列存任务,得有人干活呀;所以,当每来一次加载图片请求的时候,我们同时发一个消息到后台线程,后台线程去使用线程池去TaskQueue去取一个任务执行;

4、调度策略;3中说了,后台线程去TaskQueue去取一个任务,这个任务不是随便取的,有策略可以选择,一个是FIFO,一个是LIFO,我倾向于后者。

好了,基本就这些结构,接下来看我们具体的实现。

3、具体的实现

1、构造方法

这个就不用说了,重点看我们的构造方法


在贴构造的时候,顺便贴出所有的成员变量;

在构造中我们调用init,init中可以设置后台加载图片线程数量和加载策略;init中首先初始化后台线程initBackThread(),可以看到这个后台线程,实际上是个Looper最终在那不断的loop,我们还初始化了一个mPoolThreadHandler用于发送消息到此线程;

接下来就是初始化mLruCache  , mThreadPool ,mTaskQueue 等;

2、loadImage

构造完成以后,当然是使用了,用户调用loadImage传入(final String path, final ImageView imageView,final boolean isFromNet)就可以完成本地或者网络图片的加载。

首先我们为imageview.setTag;然后初始化一个mUIHandler,不用猜,这个mUIHandler用户更新我们的imageview,因为这个方法肯定是主线程调用的。

然后调用:getBitmapFromLruCache(path);根据path在缓存中获取bitmap;如果找到那么直接去设置我们的图片;

可以看到,如果找到图片,则直接使用UIHandler去发送一个消息,当然了携带了一些必要的参数,然后UIHandler的handleMessage中完成图片的设置;

handleMessage中拿到path,bitmap,imageview;记得必须要:

// 将path与getTag存储路径进行比较
if (imageview.getTag().toString().equals(path))
{
imageview.setImageBitmap(bm);
}

否则会造成图片混乱。

如果没找到,则通过buildTask去新建一个任务,在addTask到任务队列。

buildTask就比较复杂了,因为还涉及到本地和网络,所以我们先看addTask代码:

很简单,就是runnable加入TaskQueue,与此同时使用mPoolThreadHandler(这个handler还记得么,用于和我们后台线程交互。)去发送一个消息给后台线程,叫它去取出一个任务执行;具体代码:


直接使用mThreadPool线程池,然后使用getTask去取一个任务。


getTask代码也比较简单,就是根据Type从任务队列头或者尾进行取任务。

现在你会不会好奇,任务里面到底什么代码?其实我们也就剩最后一段代码了buildTask

我们新建任务,说明在内存中没有找到缓存的bitmap;我们的任务就是去根据path加载压缩后的bitmap返回即可,然后加入LruCache,设置回调显示。

首先我们判断是否是网络任务?

如果是,首先去硬盘缓存中找一下,(硬盘中文件名为:根据path生成的md5为名称)。

如果硬盘缓存中没有,那么去判断是否开启了硬盘缓存:

开启了的话:下载图片,使用loadImageFromLocal本地加载图片的方式进行加载(压缩的代码前面已经详细说过);

         如果没有开启:则直接从网络获取(压缩获取的代码,前面详细说过);

如果不是网络图片:直接loadImageFromLocal本地加载图片的方式进行加载

经过上面,就获得了bitmap;然后加入addBitmapToLruCache,refreashBitmap回调显示图片。

到此,我们所有的代码就分析完成了;

缓存的图片位置:在SD卡的Android/data/项目packageName/cache中:

不过有些地方需要注意:就是在代码中,你会看到一些信号量的身影:

第一个:mSemaphorePoolThreadHandler = new Semaphore(0); 用于控制我们的mPoolThreadHandler的初始化完成,我们在使用mPoolThreadHandler会进行判空,如果为null,会通过mSemaphorePoolThreadHandler.acquire()进行阻塞;当mPoolThreadHandler初始化结束,我们会调用.release();解除阻塞。

第二个:mSemaphoreThreadPool = new Semaphore(threadCount);这个信号量的数量和我们加载图片的线程个数一致;每取一个任务去执行,我们会让信号量减一;每完成一个任务,会让信号量+1,再去取任务;目的是什么呢?为什么当我们的任务到来时,如果此时在没有空闲线程,任务则一直添加到TaskQueue中,当线程完成任务,可以根据策略去TaskQueue中去取任务,只有这样,我们的LIFO才有意义。

到此,我们的图片加载框架就结束了,你可以尝试下加载本地,或者去加载网络大量的图片,拼一拼加载速度~~~

4、MainActivity

现在是使用的时刻~~

我在MainActivity中,我使用了Fragment,下面我贴下Fragment和布局文件的代码,具体的,大家自己看代码:

可以看到我们在getView中,使用mImageLoader.loadImage一行即完成了图片的加载。

fragment_list_imgs.xml

item_fragment_list_imgs.xml

好了,到此结束~~~有任何bug或者意见欢迎留言~

源码点击下载

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

博主部分视频已经上线,如果你不喜欢枯燥的文本,请猛戳(初录,期待您的支持):

1、Android 自定义控件实战 电商活动中的刮刮卡

2、Android自定义控件实战  打造Android流式布局和热门标签

3、Android智能机器人“小慕”的实现

4、高仿QQ5.0侧滑

5、高仿微信5.2.1主界面及消息提醒


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK