【Java&Android开源库代码剖析】のandroid-smart-image-view
Android应用开发已经进入到相对成熟的阶段,特别在国外,涌现出了各式各样的成熟稳定的开源库,供普通开发者使用。这种情况虽然极大加速了app开发的进程,但同时带来的问题是大多数普通开发者在使用这些开源库的时候只是止步于知道怎么使用它,但对开源库的底层实现原理并不清楚,或者不怎么深究,导致的问题很多:1)当开源库出现bug时,不能够很好很快的定位出问题;2)自己日常的代码编写只局限于实现app的业务逻辑,太上层,对技术水平的提升没有多大的好处;3)对追求完美的人来说,只有对自己项目中所有代码实现的原理都清楚的时候,才会安心,才会有成就感;4)当自己项目需要写基础库代码时,如果已经熟知各种开源库的实现,那么更能设计出好的架构,写出好的代码。
以上种种的解决方案就是多学习多研究开源库的源码,了解其运行机理,从而提升自身的技术积累,这就是本系列的初衷。本系列将选取各种常见或者不常见的开源库,只要它有剖析的价值,刚开始大部分将是基于Java语言的,后续会逐渐覆盖Objective C以及C、C++、PHP等语言。同时欢迎同学们推荐自己想了解的开源库,我会在甄别后排进本系列日程安排中。
James Smith,网名loopj,在Android平台上,因为android-async-http(https://github.com/loopj/android-async-http)这个开源库而知名的,本系列我们会仔细剖析这个库,但不是现在,刚开始我们稍微来个简单一点的,同样出自于loopj之手,名为android-smart-image-view(https://github.com/loopj/android-smart-image-view)
从github上将代码检出,我们可以看到整个项目的代码只包含7个Java源文件,这个库是对Android SDK中的ImageView控件的扩展,方便异步加载网络上指定URL的图片,以及系统联系人的头像等,同时,提供了简单可扩展的框架,方便使用者根据实际图片的来源进行扩展。SmartImageView的使用方法和ImageView类似,具体可参见http://loopj.com/android-smart-image-view/上面的说明。
android-smart-image-view扩展自ImageView,使其方便地显示不同来源的图片资源,因此,首先需要定义一个接口,来表示图片获取这样一个公共的行为。而在Android中,图片最终在绘制到画布canvas上的时候,都是以位图bitmap表示的,因此,接口定义如下:
public interface SmartImage {
public Bitmap getBitmap(Context context);
}
根据图片来源的不同,分别实现SmartImage接口,并在getBitmap函数中处理图片获取的逻辑,类图结构如下:
我们先看上半部分的实现结构,发现3个类实现了SmartImage接口,分别是BitmapImage、ContactImage和WebImage,下面分别介绍。
1)BitmapImage类,最简单的实现(可认为是dummy类),因为它仅仅是在构造函数中传入Bitmap实例,然后在调用getBitmap时返回它。
2)ContactImage类,实现系统联系人头像的获取,在构造函数中传入指定的联系人id,之后在getBitmap函数中查找指定id的联系人对应的头像,当然没有设置头像时返回null。
3)WebImage类,实现从指定URL获取图片资源,当然不是每次都从网络上加载,而是实现了一个简单的二级缓存,即内存缓存和磁盘缓存,每次加载时,都会先判断该图片是否存在于内存或者磁盘缓存中,缓存没有命中时,才到指定URL上下载。
【获取系统联系人头像】
获取联系人头像,也就是要访问系统通讯录这个app的数据,因此需要在AndroidManifest.xml文件中加入权限声明:
<uses-permission android:name="android.permission.READ_CONTACTS"/>
在Android系统中访问其他app的数据时,一般都是通过ContentProvider实现的,一个ContentProvider类实现了一组标准的方法接口,从而能够让其他app保存或者读取它提供的各种数据类型。其他app通过ContentResolver接口就可以访问ContentProvider提供的数据。在ContactImage类的getBitmap函数实现中,就是首先获取ContentResolver的实例,并根据联系人id生成查找的Uri,然后调用系统Contact类的openContactPhotoInputStream函数得到头像的数据流,最后使用BitmapFactory.decodeStream函数将数据流生成Bitmap实例。(需要说明的一点是,这里获取的是手机的联系人头像,而不是Sim卡中的联系人头像的,因为Sim卡由于容量限制等原因,是没有联系人头像数据的)。
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 变量contactId是怎么来的呢?contactId同样是通过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();
}
}
【从指定URL加载图片】
这里的指定URL通常指的是图片的外链,格式类似
http://farm6.staticflickr.com/5489/9272288811_286d003d9e_o.png
因此,简单的使用URLConnection的getContent方法就可以获取图片的数据,之后利用BitmapFactory将其转换为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;
}
【二级缓存实现】
为了加快图片的加载速度,smart-image库引入了简单的二级缓存,我们知道,数据获取速度取决于物理介质,一般是内存>磁盘>网络,因此,在加载某个URL的图片时,会优先判断是否命中内存缓存,没有则查找磁盘缓存,最终才会考虑从网络上加载,同时更新内存缓存和磁盘缓存记录。
考虑到缓存查找的速度问题,在实现内存缓存时一般都会使用类似哈希表这样查找时间复杂度低的数据结构。由于存在多个线程同时在哈希表中查找的情况,因此需要考虑多线程并发访问的问题,内存缓存的实现使用ConcurrentHashMap也就在情理之中了。Android平台上app的内存是有限制的,当内存超过这个限制时,会出现OOM(OutOfMemory),为了避免这个问题,内存缓存中我们不会直接持有Bitmap实例的引用,而是通过SoftReference来持有Bitmap对象的软引用,如果一个对象具有软引用,内存空间足够时,垃圾回收器不会回收它,只有在内存空间不足时,才会回收这些对象占用的内存。因此,软引用通常用来实现内存敏感的高速缓存。
Android系统上磁盘缓存可以放在内部存储空间,也可以放在外部存储空间(即SD卡)。对于小图片的缓存可以放在内部存储空间中,但当图片比较大,数量比较多时,那么就应该将图片缓存放到SD卡上,因为毕竟内部存储空间一般比SD卡空间要小很多。smart-image库的磁盘缓存是放在内部存储空间中的,也就是放在app的缓存目录中,该目录使用Context.getCacheDir()函数来获取,格式类似于:/data/data/app的包名/cache。cache目录主要用于存放缓存文件,当系统的内部存储空间不足时,该目录下面的文件会被删除;当然,我们不能依赖系统来清理这些缓存文件,而是应该对这些缓存文件设置最大存储空间,当实际占用空间超过这个最大值时,就需要对使用一定的算法对缓存文件进行清理。这一点在smart-image库的实现中并没有做考虑。
两级缓存空间的建立在WebImageCache类的构造函数中进行,代码如下:
public WebImageCache(Context context) {
// Set up in-memory cache store
memoryCache = new ConcurrentHashMap<String, SoftReference<Bitmap>>();
// Set up disk cache store
Context appContext = context.getApplicationContext();
diskCachePath = appContext.getCacheDir().getAbsolutePath() + DISK_CACHE_PATH;
File outFile = new File(diskCachePath);
outFile.mkdirs();
diskCacheEnabled = outFile.exists();
// Set up threadpool for image fetching tasks
writeThread = Executors.newSingleThreadExecutor();
}
判断Bitmap是否命中内存缓存的代码如下所示,就是先取出Bitmap的软引用,并判断是否已经被系统回收,如果没有就从软引用中取出Bitmap实例:
private Bitmap getBitmapFromMemory(String url) {
Bitmap bitmap = null;
SoftReference<Bitmap> softRef = memoryCache.get(getCacheKey(url));
if(softRef != null){
bitmap = softRef.get();
}
return bitmap;
}
判断Bitmap是否命中磁盘缓存的代码如下所示,基本原理就是根据URL在磁盘上查找对应的文件,如果存在,就将其转换成Bitmap实例返回。由于URL中可能包含一些不能出现在文件名中的特殊字符,因此,在讲URL转换成文件名时需要做预处理,过滤掉这些字符。
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("[+]+", "+");
}
}
将Bitmap存到内存缓存的步骤很简单,就是往HashMap中添加一个数据而已,不过要注意存的是Bitmap的软引用。代码如下所示。
private void cacheBitmapToMemory(final String url, final Bitmap bitmap) {
memoryCache.put(getCacheKey(url), new SoftReference<Bitmap>(bitmap));
}
往磁盘缓存中添加Bitmap是通过线程池ExecutorService实现的,一方面是限制同时存在的线程个数,另一方面是解决同步问题。smart-image库使用的是只有一个线程的线程池,在WebImageCache的构造函数中可以看到,因此,磁盘缓存的添加是顺序进行的。生成缓存的过程是先根据URL在cache目录中生成对应的文件,然后调用Bitmap.compress函数按指定压缩格式和压缩质量将Bitmap写到磁盘文件输出流中。
private void cacheBitmapToDisk(final String url, final Bitmap bitmap) {
writeThread.execute(new Runnable() {
@Override
public void run() {
if(diskCacheEnabled) {
BufferedOutputStream ostream = null;
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) {}
}
}
}
});
}
至此,总算将上面类图中相关类介绍完毕。接着就来看另外一个类图:
这个类图中有SmartImageTask和SmartImageView两个类以及onCompleteListener和onCompleteHandler两个接口,而SmartImage类在上文中已经介绍过了。可以很容易的看出SmartImageTask和SmartImageView是聚合的关系,task为view提供处理后台图片加载等操作,view则专注于ui的呈现。
一般这种后台task类会实现Runnable接口,特别是在和线程池配合使用的时候,SmartImageTask也不例外,因为在SmartImageView中就有一个线程池。
SmartImageTask既然实现了Runnable接口,那么它的主要逻辑实现就是在run方法中的。从类图结构中可以看到SmartImageTask聚合了SmartImage,使用SmartImage的getBitmap函数来获取指定URL的Bitmap实例。代码如下:
@Override
public void run() {
if(image != null) {
complete(image.getBitmap(context));
context = null;
}
}
除此之外,task类中还实现了回调机制,供view类使用。包括一个静态类型的handler类(将handler定义成static,是为了避免内存泄露),一个图片加载完成的回调接口OnCompleteListener,定义分别如下:
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();
}
当图片加载还未完成时,如果需要取消加载,那么可以设置标志位cancelled为false即可,这时就算图片加载成功了,也不会发送Message告知上层view类。
SmartImageView是ImageView的子类,定义了包含4个线程的线程池,用来执行SmartImageTask任务。在给ImageView设置图片资源时,可以选择是否设置默认图片,是否设置加载失败的图片,以及是否设置加载完成后的回调接口。在启用新的task任务前,得先判断是否已经存在给当前ImageView设置图片的task在运行中,如果是,就取消它,然后新建task任务并加入线程池中,永远保证一个ImageView有且只有一个最新的task在运行。
public void setImage(final SmartImage image, final Integer fallbackResource, final Integer loadingResource, final SmartImageTask.OnCompleteListener completeListener) {
// Set a loading resource
if(loadingResource != null){
setImageResource(loadingResource);
}
// Cancel any existing tasks for this image view
if(currentTask != null) {
currentTask.cancel();
currentTask = null;
}
// Set up the new task
currentTask = new SmartImageTask(getContext(), image);
currentTask.setOnCompleteHandler(new SmartImageTask.OnCompleteHandler() {
@Override
public void onComplete(Bitmap bitmap) {
if(bitmap != null) {
setImageBitmap(bitmap);
} else {
// Set fallback resource
if(fallbackResource != null) {
setImageResource(fallbackResource);
}
}
if(completeListener != null){
completeListener.onComplete();
}
}
});
// Run the task in a threadpool
threadPool.execute(currentTask);
}
最后,当要取消线程池中所有在等待和运行的task时,可调用ExecutorService的shutdownNow函数,线程池的创建和销毁如下代码所示:
private static final int LOADING_THREADS = 4;
private static ExecutorService threadPool = Executors.newFixedThreadPool(LOADING_THREADS);
public static void cancelAllTasks() {
threadPool.shutdownNow();
threadPool = Executors.newFixedThreadPool(LOADING_THREADS);
}
【扩展和优化】
前面说到如果图片有除了URL和联系人头像之外的其他来源的话,那么需要开发者实现SmartImage接口来进行扩展。国外另一位开发者commonsguy(以后会介绍他的开源项目)
Post了一个SmartImage的实现类VideoImage ,用于获取系统中视频的缩略图。
class VideoImage implements SmartImage {
private int videoId; // 视频id
private int thumbnailKind; // MICRO_KIND-微型缩略模式;MINI_KIND-迷你缩略模式,前者分辨率更低
public VideoImage(int videoId, int thumbnailKind) {
this.videoId = videoId;
this.thumbnailKind = thumbnailKind;
}
@Override
public Bitmap getBitmap(Context context) {
return (MediaStore.Video.Thumbnails.getThumbnail(
context.getContentResolver(), videoId, thumbnailKind, null));
}
}
在Android开发中,如果系统内存不足的情况下,继续创建Bitmap实例的话,会导致OutOfMemoryError,从而导致app crash。因此,是否需要在创建Bitmap之前判断系统可用的内存大小呢?是否应该捕获OOME呢,这一点在smart-image库中目前没有考虑,因为毕竟这个库只适用于小图片的加载。如果非要优化的话,那么可以在WebImage类的创建Bitmap对象的地方加入低内存的判断,如果内存过低,那么可以将图片的采样值inSample降低,从而降低图片质量,降低其占用的内存空间,改进后的getBitmapFromUrl函数如下所示:
private Bitmap getBitmapFromUrl(String url) {
Bitmap bitmap = null;
try {
URLConnection conn = new URL(url).openConnection();
conn.setConnectTimeout(CONNECT_TIMEOUT);
conn.setReadTimeout(READ_TIMEOUT);
ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo();
int inSample = 1;
if (memInfo.lowMemory) {
inSample = 12;
}
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = inSample;
bitmap = BitmapFactory.decodeStream((InputStream) conn.getContent(), null, options);
} catch(Exception e) {
e.printStackTrace();
}
return bitmap;
}
当然,降低质量后的图片还是超过可分配的内存大小时,还是会出现OutOfMemoryError,那么我们是否可以捕获这个异常呢?答案是可以,但不推荐。Java文档中明确说明的一点是java.lang.Error类是java.lang.Throwable的子类,java.lang.Exception也是Throwable的子类,Exception表示的是可以而且应该被捕获的异常,而Error表示的是会导致程序crash的致命错误,这个一般是不应该进行捕获的。但是,某些情况下,我们的程序在发生OutOfMemoryError异常后,可能需要做一些日志操作,或者能够做一些补救措施,例如释放内存或者降低申请的内存空间等等,那么还是可以catch住OutOfMemoryError异常的。
——欢迎转载,请注明出处 http://blog.csdn.net/asce1885 ,未经本人同意请勿用于商业用途,谢谢——