三、源码分析
==========
从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方法呢,因为需要加载的图片来源是不一样的,如:从网络加载或从系统联系人头像加载,所以分别让不同来源的类去实现这个接口,然后在该方法中处理逻辑。如图:
我们来看下这三个类的具体代码:
package com.loopj.android.image;
import android.content.Context;
import android.graphics.Bitmap;
/**
*/
public class BitmapImage implements SmartImage {
//定义Bitmap对象
private Bitmap bitmap;
//构造方法
public BitmapImage(Bitmap bitmap) {
this.bitmap = bitmap;
}
//实现getBitmap方法
public Bitmap getBitmap(Context context) {
return bitmap;
}
}
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;
//WebImage的构造方法,获取URL
private String url;
public WebImage(String url) {
this.url = url;
}
//实现方法,处理相应的业务逻辑
public Bitmap getBitmap(Context context) {
// Don’t leak context
if(webImageCache == null) {
webImageCache = new WebImageCache(context);
}
// Try getting bitmap from cache first
//此处做了简单的二级缓存(内存缓存和磁盘缓存)
Bitmap bitmap = null;
if(url != null) {
//先从缓存获取bitmap对象
bitmap = webImageCache.get(url);
if(bitmap == null) {
//未找到则从网络加载
bitmap = getBitmapFromUrl(url);
if(bitmap != null){
//加载后将bitmap对象put到缓存中
webImageCache.put(url, bitmap);
}
}
}
return bitmap;
}
/**
根据Url获取网络图片资源
@param url
@return
*/
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;
}
/**
提供移除缓存的方法
@param url
*/
public static void removeFromCache(String url) {
if(webImageCache != null) {
webImageCache.remove(url);
}
}
}
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 {
/****************************************************************************************************
注:Android系统中访问其他app的数据时,一般都是通过ContentProvider实现,
一个ContentProvider类实现了一组标准的方法接口,能够让其他app保存或者读取它提供的各种数据类型。
其他app通过ContentResolver接口就可以访问ContentProvider提供的数据。
备注:(获取的是手机的联系人头像,而不是Sim卡中的联系人头像的。Sim卡由于容量限制等原因,无联系人头像数据)
–使用时记得添加获取联系人头像的权限
*****************************************************************************************************/
//联系人头像ID
private long contactId;
public ContactImage(long contactId) {
this.contactId = contactId;
}
public Bitmap getBitmap(Context context) {
Bitmap bitmap = null;
//获取ContentResolver实例
ContentResolver contentResolver = context.getContentResolver();
try {
//根据ID生成查找联系人的Uri
/*
关于withAppendedId方法:
Open Declaration Uri android.content.ContentUris.withAppendedId(Uri contentUri, long id)
Appends the given ID to the end of the path.
Parameters:
contentUri – to start with
id – to append
Returns:
a new URI with the given ID appended to the end of the path */
Uri uri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId);
//调用Contact类中的openContactPhotoInputStream获得图像InputStream对象
InputStream input = ContactsContract.Contacts.openContactPhotoInputStream(contentResolver, uri);
if(input != null) {
//将数据流decode为bitmap对象并返回
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); // 联系人id
Long photoId = cursor.getLong(PHOTO_ID_INDEX); // 联系人头像id(photoId大于0时表示联系人有头像)
}
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) {
// Set up in-memory cache store
memoryCache = new ConcurrentHashMap
// Set up disk cache store
Context appContext = context.getApplicationContext();
diskCachePath = appContext.getCacheDir().getAbsolutePath() + DISK_CACHE_PATH;
//先根据URL在cache目录中生成对应的文件
File outFile = new File(diskCachePath);
outFile.mkdirs();
diskCacheEnabled = outFile.exists();
// Set up threadpool for image fetching tasks
writeThread = Executors.newSingleThreadExecutor();
}
具体从缓存获取Bitmap实例的实现方法:
/**
从Memory获取bitmap实例
@param url
@return
*/
private Bitmap getBitmapFromMemory(String url) {
Bitmap bitmap = null;
//通过memoryCache取出软引用
SoftReference softRef = memoryCache.get(getCacheKey(url));
//判断系统有无回收该引用
if(softRef != null){
//get
bitmap = softRef.get();
}
return bitmap;
}
/**
从Disk获取Bitmap实例
@param url
@return
*/
private Bitmap getBitmapFromDisk(String url) {
Bitmap bitmap = null;
if(diskCacheEnabled){
//根据URL在磁盘上查找对应的文件
String filePath = getFilePath(url);
File file = new File(filePath);
//若存在则decode为Bitmap实例
if(file.exists()) {
bitmap = BitmapFactory.decodeFile(filePath);
}
}
return bitmap;
}
/**
获取磁盘缓存路径
@param url
@return
*/
private String getFilePath(String url) {
return diskCachePath + getCacheKey(url);
}
/**
获取缓存Key
@param url
@return
*/
private String getCacheKey(String url) {
if(url == null){
throw new RuntimeException(“Null url passed in”);
} else {
//URL中可能包含一些特殊字符,在将URL转换成文件名时需要做预处理,过滤掉这些字符。
return url.replaceAll("[.,%?&=]", “+”).replaceAll("[+]+", “+”);
}
}
那是如何缓存的呢?我们再来看两个方法:
/**
将Bitmap存到内存缓存
@param url
@param bitmap
*/
private void cacheBitmapToMemory(final String url, final Bitmap bitmap) {
//将数据put到HashMap中,存入的是Bitmap的软引用
memoryCache.put(getCacheKey(url), new SoftReference(bitmap));
}
/**
将Bitmap存入到磁盘
@param url
@param bitmap
*/
private void cacheBitmapToDisk(final String url, final Bitmap bitmap) {
/*******************************************************
将Bitmap存入磁盘缓存是通过线程池ExecutorService实现
1.限制同时存在的线程个数
2.是解决同步问题。smart-image库使用的是只有一个线程的线程池,
*******************************************************/
writeThread.execute(new Runnable() {
@Override
public void run() {
if(diskCacheEnabled) {
BufferedOutputStream ostream = null;
try {
//调用Bitmap.compress函数按指定压缩格式和压缩质量将Bitmap写到磁盘文件输出流中(在构造方法中根据URL创建对应文件)
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;
/**
专注于后台图片加载处理的Task类
实现了Runnable接口
*/
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;
}
//图片加载完成的回调接口OnCompleteListener
public static class OnCompleteHandler extends Handler {
//将handler定义成static,是为了避免内存泄露,可参考博客:http://blog.csdn.net/gao_chun/article/details/46046637
@Override
public void handleMessage(Message msg) {
Bitmap bitmap = (Bitmap)msg.obj;
onComplete(bitmap);
}
public void onComplete(Bitmap bitmap){};
}
//用于SmartImageView类中加载完成后的回调接口
public abstract static class OnCompleteListener {
public abstract void onComplete();
//这里也说明了此方法的作用:加载图片回调的方法,重写此方法以获取位图的句柄,增加了重载的实现使其与以前版本兼容
/***
Convient method to get Bitmap after image is loaded.
Override this method to get handle of bitmap
Added overloaded implementation to make it backward compatible with previous versions
*/
public void onComplete(Bitmap bitmap){
onComplete();
}
}
//构造方法,将SmartImage作为参数
public SmartImageTask(Context context, SmartImage image) {
this.image = image;
this.context = context;
}
@Override
public void run() {
//使用SmartImage的getBitmap函数来获取URL的Bitmap实例
if(image != null) {
complete(image.getBitmap(context));
context = null;
}
}
public void cancel() {
cancelled = true;
}
/**
获取Bitmap实例
@param bitmap
*/
Android学习PDF+架构视频+面试文档+源码笔记
【Android开发核心知识点笔记】
【Android思维脑图(技能树)】
【Android核心高级技术PDF文档,BAT大厂面试真题解析】
【Android高级架构视频学习资源】
ible with previous versions
*/
public void onComplete(Bitmap bitmap){
onComplete();
}
}
//构造方法,将SmartImage作为参数
public SmartImageTask(Context context, SmartImage image) {
this.image = image;
this.context = context;
}
@Override
public void run() {
//使用SmartImage的getBitmap函数来获取URL的Bitmap实例
if(image != null) {
complete(image.getBitmap(context));
context = null;
}
}
public void cancel() {
cancelled = true;
}
/**
获取Bitmap实例
@param bitmap
*/
Android学习PDF+架构视频+面试文档+源码笔记
【Android开发核心知识点笔记】
[外链图片转存中…(img-OS8NFvjW-1645164421839)]
【Android思维脑图(技能树)】
[外链图片转存中…(img-lXR9NBJM-1645164421840)]
【Android核心高级技术PDF文档,BAT大厂面试真题解析】
[外链图片转存中…(img-VRIPpmjV-1645164421840)]
【Android高级架构视频学习资源】