转自:http://blog.csdn.net/gao_chun/article/details/47610137
SmartImageView源码分析
一、描述
目前Android应用开发涌出了各种各样出自大牛之手的成熟稳定的开源库,供开发者使用,虽然很明显的提高了App的开发效率,也同样凸显出部分问题:
- 我只知道如何去使用(也许还并不能随心所欲的使用,只知道简单的使用方式),并不清楚内部实现原理。
- 出了问题解决不了,冒出想法:这玩意真伤脑筋,怎么老出问题。好慌 ~
相信用过一些开源库的同志都遇到过类似揪心的问题。开源库不是自己写的,也不清楚里面是啥原理,遇到问题无从下手解决显然也是很正常的事情。不过从另一方面想,解读或了解开源库的内部实现原理也对我们技术提升有不小的进步:
- 日常的代码编写只局限于实现app的业务逻辑,对提升技术水平益处不大。
- 熟知和了解各种开源库的实现,自己在开发中也能设计出好的架构,写出好的代码。
本文将和大家一起分析一个简单的开源项目 android-smart-image-view
GitHub地址:https://github.com/loopj/android-smart-image-view
出自James Smith之手,网名loopj,曾写过Android中 android-async-http 开源库,大家应该不陌生。
二、原理和技术实现
这里我们先来谈谈关于SmartImageView的实现原理,既然是原理,那肯定是简单的了解了解关于这个控件的实现了,SmartImageView可完全替代ImageView,毫无疑问,因为其本身便是继承了ImageView,并对其进行了扩展,在ImageView的基础上添加了许多新的功能,一个开源库的成型,并非一朝一夕就可以完成,它需要经过大量时间去实践、升级和维护。比如A程序员写了个开源库发布到网上,程序员B下载并使用,发现其功能不够完善,联系了程序员A后便进去改造,对一些方法进行了升级优化并发布到网上,这时程序员C发现他俩写的功能不能适用于我的需求,然后又进行了复写和改造……,这样一个好的开源库或框架就流传出来了。
下面我们也自定义一个SmartImageView控件来简单看下SmartImageView的实现原理,首先继承ImageView类,并重写ImageView的构造方法,下图为ImageView类中的构造方法:
我们自定义控件后重写其构造方法:
接着在类中写个访问网络请求的方法,如 setImageUrl 方法,让该方法可根据URL路径下载图片,如:
布局文件中引入我们定义的控件:
- <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <Button
- android:id="@+id/btn"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:onClick="onClick"
- android:text="获取网络图片" />
-
- <com.example.smartimageview.SmartImageView
- android:id="@+id/iv_coco"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_below="@+id/btn"
- android:scaleType="fitXY" />
-
- </RelativeLayout>
主Activity中使用我们刚刚定义的控件:
- package com.example.smartimageview;
-
- import android.os.Bundle;
- import android.support.v7.app.ActionBarActivity;
- import android.view.View;
- import android.view.View.OnClickListener;
-
- public class MainActivity extends ActionBarActivity implements OnClickListener{
-
- String mUrl = "http://e.hiphotos.baidu.com/baike/c0%3Dbaike150%2C5%2C5%2C150%2C50/sign=79b94a3be21190ef15f69a8daf72f673/bd3eb13533fa828b5a920735f81f4134960a5ab7.jpg";
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
-
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
-
- }
-
-
- @Override
- public void onClick(View v) {
- if (v.getId() == R.id.btn) {
-
- SmartImageView siv = (SmartImageView) findViewById(R.id.iv_coco);
- siv.setImageUrl(mUrl);
- }
-
- }
-
- }
效果图如下:
OK!已经完成主要功能。不过只是简单的完成了下载图片和显示图片的功能,但是并未涉及到图片的缓存,压缩,以及错处理等情况。所以需要一步一步去拓展。比如可以在返回的Code不为200时,加个else并发个消息给Handler,在Handler中去处理加载失败显示的错误图片资源就好。
三、源码分析
从github上clone该项目,可以看到整个项目的代码只包含7个Java源文件,同时,还可进行扩展,方便使用者根据实际图片的来源进行扩展。我们来看看Class逻辑图:
上面有提到,SmartImageView继承自ImageView并自定义了一些方法,能够方便的显示网络图片。在Android中,图片的显示最终都绘制到画布canvas上以位图的形式显示,所以通过逻辑图可以看出定义了一个 SmartImage 接口,而里面有一个返回值为Bitmap的getBitmap方法:
- package com.loopj.android.image;
- import android.content.Context;
- import android.graphics.Bitmap;
- public interface SmartImage {
- public Bitmap getBitmap(Context context);
- }
为什么会定义这个getBitmap方法呢,因为需要加载的图片来源是不一样的,如:从网络加载或从系统联系人头像加载,所以分别让不同来源的类去实现这个接口,然后在该方法中处理逻辑。如图:
我们来看下这三个类的具体代码:
- 【BitmapImage(仅仅是传入Bitmap的实例然后返回)】---->
- package com.loopj.android.image;
-
- import android.content.Context;
- import android.graphics.Bitmap;
-
-
-
-
- public class BitmapImage implements SmartImage {
-
-
- private Bitmap bitmap;
-
-
- public BitmapImage(Bitmap bitmap) {
- this.bitmap = bitmap;
- }
-
-
- public Bitmap getBitmap(Context context) {
- return bitmap;
- }
- }
- 【WebImage(根据Url获取图片资源,需要注意的是这里用到了缓存,注意代码注释)】---->
- package com.loopj.android.image;
-
- import java.io.InputStream;
- import java.net.URL;
- import java.net.URLConnection;
-
- import android.content.Context;
- import android.graphics.Bitmap;
- import android.graphics.BitmapFactory;
-
- public class WebImage implements SmartImage {
-
-
- private static final int CONNECT_TIMEOUT = 5000;
- private static final int READ_TIMEOUT = 10000;
-
-
- private static WebImageCache webImageCache;
-
-
- private String url;
- public WebImage(String url) {
- this.url = url;
- }
-
-
- public Bitmap getBitmap(Context context) {
-
- if(webImageCache == null) {
- webImageCache = new WebImageCache(context);
- }
-
-
-
- Bitmap bitmap = null;
-
- if(url != null) {
-
- bitmap = webImageCache.get(url);
- if(bitmap == null) {
-
- bitmap = getBitmapFromUrl(url);
-
- if(bitmap != null){
-
- webImageCache.put(url, bitmap);
- }
- }
- }
-
- return bitmap;
- }
-
-
-
-
-
-
- private Bitmap getBitmapFromUrl(String url) {
- Bitmap bitmap = null;
-
- try {
- URLConnection conn = new URL(url).openConnection();
- conn.setConnectTimeout(CONNECT_TIMEOUT);
- conn.setReadTimeout(READ_TIMEOUT);
- bitmap = BitmapFactory.decodeStream((InputStream) conn.getContent());
- } catch(Exception e) {
- e.printStackTrace();
- }
-
- return bitmap;
- }
-
-
-
-
-
- public static void removeFromCache(String url) {
- if(webImageCache != null) {
- webImageCache.remove(url);
- }
- }
- }
- 【ContactImage(系统联系人头像的获取,通过在构造函数传入联系人的ID,然后在getBitmap方法中根据ID查找对应的联系人头像)】---->
- package com.loopj.android.image;
-
- import java.io.InputStream;
-
- import android.content.ContentUris;
- import android.content.ContentResolver;
- import android.content.Context;
- import android.provider.ContactsContract;
- import android.graphics.Bitmap;
- import android.graphics.BitmapFactory;
- import android.net.Uri;
-
-
-
-
- public class ContactImage implements SmartImage {
-
-
-
-
-
-
-
-
-
-
-
- private long contactId;
-
- public ContactImage(long contactId) {
- this.contactId = contactId;
- }
-
- public Bitmap getBitmap(Context context) {
- Bitmap bitmap = null;
-
- ContentResolver contentResolver = context.getContentResolver();
-
- try {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Uri uri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId);
-
- InputStream input = ContactsContract.Contacts.openContactPhotoInputStream(contentResolver, uri);
- if(input != null) {
-
- bitmap = BitmapFactory.decodeStream(input);
- }
- } catch(Exception e) {
- e.printStackTrace();
- }
-
- return bitmap;
- }
- }
那ContactImage中联系人ID是怎么来的呢,其实也是通过ContentResolver查出来的,下面示例代码:
- private static final int DISPLAY_NAME_INDEX = 0;
- private static final int PHONE_NUMBER_INDEX = 1;
- private static final int PHOTO_ID_INDEX = 2;
- private static final int CONTACT_ID_INDEX = 3;
- private static final String[] PHONES_PROJECTION = new String[] {
- Phone.DISPLAY_NAME, Phone.NUMBER, Phone.PHOTO_ID,Phone.CONTACT_ID };
-
- private void getPhoneContact(Context context) {
- ContentResolver contentResolver = context.getContentResolver();
- Cursor cursor = contentResolver.query(Phone.CONTENT_URI, PHONES_PROJECTION, null, null, null);
- if (cursor != null) {
- while(cursor.moveToNext()) {
- String displayName = cursor.getString(DISPLAY_NAME_INDEX);
- String phoneNum = cursor.getString(PHONE_NUMBER_INDEX);
- Long contactId = cursor.getLong(CONTACT_ID_INDEX);
- Long photoId = cursor.getLong(PHOTO_ID_INDEX);
- }
-
- cursor.close();
- }
- }
好了,讲到这里,相信大家通过注释可以对这三个类有一定的了解了。可能有同学注意到了,那个WebImage中的
WebImageCache
是什么东西,里面是如何实现的呢?好,我们下面来说说关于这个类的源码实现,相信大家看了会对“缓存”这个高大上的词有一定认识。在WebImageCache中其实是实现了二级缓存,
接着我们就来说说这个类。
SmartImageView中的二级缓存
相信大家都知道,在开发中,为了加快图片的访问速度,避免系统资源的浪费,用户体验上的流畅,都会引入缓存的机制,由于App内存有限,若超过了这个限制,系统便会报错OutOfMemory,这是个很头疼的问题。引入缓存的机制目的就是为了让App在使用中更加流畅,体验更好,减少不必要的资源开销。SmartImageView库中也引入了简单的二级缓存,数据获取速度取决于物理介质,一般是 内存>磁盘>网络,故在加载图片时,会优先判断是否命中内存缓存,没有则查找磁盘缓存,最终才会考虑从网络上加载,同时更新内存缓存和磁盘缓存记录。
考虑到缓存查找的速度问题,在实现内存缓存时一般都会使用类似哈希表这样查找时间复杂度低的数据结构。由于存在多个线程同时在哈希表中查找的情况,需要考虑多线程并发访问的问题。故使用 ConcurrentHashMap。内存缓存中我们不会直接持有Bitmap实例的引用,而是通过SoftReference来持有Bitmap对象的软引用,如果一个对象具有软引用,内存空间足够时,垃圾回收器不会回收它,只有在内存空间不足时,才会回收这些对象占用的内存。因此,软引用通常用来实现内存敏感的高速缓存。关于引用问题,可具体参见博文java中对象的引用(强引用、软引用、弱引用、虚引用)
Android系统上磁盘缓存可以放在内部存储空间,也可以放在外部存储空间(即SD卡)。对于小图片的缓存可以放在内部存储空间中,但当图片比较大,数量比较多时,那么就应该将图片缓存放到SD卡上,毕竟内部存储空间一般比SD卡空间要小很多。SmartImageView库的磁盘缓存是放在内部存储空间中的,也就是app的缓存目录,该目录使用 Context.getCacheDir() 函数来获取,格式类似于:/data/data/app的包名/cache。cache目录主要用于存放缓存文件,当系统的内部存储空间不足时,该目录下面的文件会被删除;当然,不能依赖系统来清理这些缓存文件,而是应该对这些缓存文件设置最大存储空间,当实际占用空间超过这个最大值时,就需要对使用一定的算法对缓存文件进行清理。这一点在SmartImage库中并没有做考虑。
我们来看看WebImageCache类中对内存缓存和磁盘缓存的实现,先看其构造方法:
-
- public WebImageCache(Context context) {
-
-
- memoryCache = new ConcurrentHashMap<String, SoftReference<Bitmap>>();
-
-
- Context appContext = context.getApplicationContext();
- diskCachePath = appContext.getCacheDir().getAbsolutePath() + DISK_CACHE_PATH;
-
- File outFile = new File(diskCachePath);
- outFile.mkdirs();
- diskCacheEnabled = outFile.exists();
-
-
- writeThread = Executors.newSingleThreadExecutor();
- }
具体从缓存获取Bitmap实例的实现方法:
-
-
-
-
-
- private Bitmap getBitmapFromMemory(String url) {
- Bitmap bitmap = null;
-
- SoftReference<Bitmap> softRef = memoryCache.get(getCacheKey(url));
-
- if(softRef != null){
-
- bitmap = softRef.get();
- }
-
- return bitmap;
- }
-
-
-
-
-
-
- private Bitmap getBitmapFromDisk(String url) {
- Bitmap bitmap = null;
- if(diskCacheEnabled){
-
- String filePath = getFilePath(url);
- File file = new File(filePath);
-
- if(file.exists()) {
- bitmap = BitmapFactory.decodeFile(filePath);
- }
- }
- return bitmap;
- }
-
-
-
-
-
-
- private String getFilePath(String url) {
- return diskCachePath + getCacheKey(url);
- }
-
-
-
-
-
-
- private String getCacheKey(String url) {
- if(url == null){
- throw new RuntimeException("Null url passed in");
- } else {
-
- return url.replaceAll("[.:/,%?&=]", "+").replaceAll("[+]+", "+");
- }
- }
那是如何缓存的呢?我们再来看两个方法:
-
-
-
-
-
- private void cacheBitmapToMemory(final String url, final Bitmap bitmap) {
-
- memoryCache.put(getCacheKey(url), new SoftReference<Bitmap>(bitmap));
- }
-
-
-
-
-
-
- private void cacheBitmapToDisk(final String url, final Bitmap bitmap) {
-
-
-
-
-
-
-
-
- writeThread.execute(new Runnable() {
- @Override
-
- public void run() {
-
- if(diskCacheEnabled) {
- BufferedOutputStream ostream = null;
- <span style="white-space:pre"> </span>try {
-
- ostream = new BufferedOutputStream(new FileOutputStream(new File(diskCachePath, getCacheKey(url))), 2*1024);
- bitmap.compress(CompressFormat.PNG, 100, ostream);
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- } finally {
- try {
- if(ostream != null) {
- ostream.flush();
- ostream.close();
- }
- } catch (IOException e) {}
- }
- }
- }
- });
- }
到此,关于其缓存原理大概就是这么个样子了,如果大家还有不明白的,我后面给出源码,可根据注释进行学习。
我们接着往下看关于SmartImageView和SmartImageTask两个类的类图结构:
从类图可以看到,有两个类:SmartImageTask、SmartImageView,另外还有两个静态类,一个是继承Handler的静态类:
- public static class OnCompleteHandler extends Handler{}
另一个是抽象的静态类:
- public abstract static class OnCompleteListener{}
那么如何理解这两个类呢?可能有同学已经从关系图和字面上理解了,咦~ ,难道是一个类专注于后台图片加载处理、另一个则专注于UI处理。哟西~,这位同学,你说对了!确实就是如此。来来来,脸挪过来 ……
SmartImageTask实现了Runnable接口,我们来看看这个类的源码:
- package com.loopj.android.image;
- import android.content.Context;
- import android.graphics.Bitmap;
- import android.os.Handler;
- import android.os.Message;
-
-
-
-
- public class SmartImageTask implements Runnable {
- private static final int BITMAP_READY = 0;
- private boolean cancelled = false;
- private SmartImage image;
- private Context context;
- private OnCompleteHandler onCompleteHandler;
- public void setOnCompleteHandler(OnCompleteHandler handler){
- this.onCompleteHandler = handler;
- }
-
-
- public static class OnCompleteHandler extends Handler {
-
- @Override
- public void handleMessage(Message msg) {
- Bitmap bitmap = (Bitmap)msg.obj;
- onComplete(bitmap);
- }
- public void onComplete(Bitmap bitmap){};
- }
-
-
- public abstract static class OnCompleteListener {
- public abstract void onComplete();
-
-
-
-
-
-
- public void onComplete(Bitmap bitmap){
- onComplete();
- }
- }
-
-
- public SmartImageTask(Context context, SmartImage image) {
- this.image = image;
- this.context = context;
- }
-
- @Override
- public void run() {
-
- if(image != null) {
- complete(image.getBitmap(context));
- context = null;
- }
- }
-
- public void cancel() {
- cancelled = true;
- }
-
-
-
-
-
- public void complete(Bitmap bitmap){
- if(onCompleteHandler != null && !cancelled) {
-
- onCompleteHandler.sendMessage(onCompleteHandler.obtainMessage(BITMAP_READY, bitmap));
- }
- }
- }
可以看到,在SmartImageTask中实现了回调机制供SmartImageView使用,大家可以结合注释进行阅读。回调主要是以下实现:
- public static class OnCompleteHandler extends Handler {
-
- @Override
- public void handleMessage(Message msg) {
- Bitmap bitmap = (Bitmap)msg.obj;
- onComplete(bitmap);
- }
- public void onComplete(Bitmap bitmap){};
- }
-
- public abstract static class OnCompleteListener {
- public abstract void onComplete();
-
-
-
-
-
-
- public void onComplete(Bitmap bitmap){
- onComplete();
- }
- }
下面我们看看SmartImageView中的核心方法:
-
-
-
-
-
-
-
- public void setImage(final SmartImage image, final Integer fallbackResource, final Integer loadingResource, final SmartImageTask.OnCompleteListener completeListener) {
-
- if(loadingResource != null){
-
- setImageResource(loadingResource);
- }
-
-
- if(currentTask != null) {
-
- currentTask.cancel();
- currentTask = null;
- }
-
-
- currentTask = new SmartImageTask(getContext(), image);
-
- currentTask.setOnCompleteHandler(new SmartImageTask.OnCompleteHandler() {
- @Override
- public void onComplete(Bitmap bitmap) {
-
- if(bitmap != null) {
- setImageBitmap(bitmap);
- } else {
-
- if(fallbackResource != null) {
- setImageResource(fallbackResource);
- }
- }
-
- if(completeListener != null){
- completeListener.onComplete(bitmap);
- }
- }
- });
-
-
- threadPool.execute(currentTask);
- }
当需要取消线程池中所有等待和运行的Tast时,刻调用该方法销毁:
-
-
-
- public static void cancelAllTasks() {
- threadPool.shutdownNow();
- threadPool = Executors.newFixedThreadPool(LOADING_THREADS);
- }
四、总结
到此,该项目的大致源码就是这么回事了,源码比较简单,个人觉得对于初学者还是有一定帮助的,也建议大家抽时间自己试着去分析一些其他的开源库。记录记录一些东西,正所谓,好记性不如烂博客。
原理Demo链接:http://download.csdn.net/download/gao_chun/9005341
源码分析链接:http://download.csdn.net/download/gao_chun/9005229