1.框架的实现简单介绍
先来说一下图片加载框架的实现原理,就是我们使用手机获取网络图片时,我们不可能每一次都去网络获取图片,那么你要被用户打死,那么贵的流量,hhh,所以我们要做好图片的缓存工作,图片加载的流程就是下图描述的那样。
流程清楚了,我们就可以开始写代码了,大致可以看出需要实现三个功能,内存缓存的读取和加入,外部缓存的读取和加入,网络图片的加载。
2.单一职责的使用
单一职责的概念是:就一个类而言,应该仅有一个引起它变化的原因。简单来说,一个类应该是一组相关性很高的函数、数据的封装。
从上面的流程图中,我们可以清楚的看出图片获取分为三种策略,内存缓存获取,外部缓存获取,网络获取,按照单一职责原则,我们就需要将上述的三个功能封装到三个类中,还是用代码来说明一切:
MemoryCache.java
package bitmaputils;
import android.graphics.Bitmap;
import android.util.Log;
import android.util.LruCache;
import com.example.administrator.bitmaputils.Md5Utils;
/**
* 内存缓存类
* 将内存缓存类和外部缓存类、网络请求类分开,做到了单一职责原则
*/
public class MemoryCache {
public static LruCache<String, Bitmap> mMemoryCache;
static {
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1204 / 8); //得到手机最大的内存的1/8,即超过1/8时开始内存回收
mMemoryCache = new LruCache<String, Bitmap>((int) maxMemory) {
//用来计算每一个条目的大小
@Override
protected int sizeOf(String key, Bitmap value) {
int byteCount = value.getRowBytes();
return byteCount * value.getHeight() / 1024;
}
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
Log.i("path", "移除" + key);
}
};
}
/**
* 从内存中读取图片
*/
public static Bitmap getBitmapFromMemory(String url) {
String mUrl = Md5Utils.MD5(url);
Log.i("mUrl", mUrl);
return mMemoryCache.get(mUrl);
}
/**
* 将图片保存到内存中
*/
public static void putBitmapToMemoy(String url, Bitmap bitmap) {
String mUrl = Md5Utils.MD5(url);
if (mMemoryCache.get(mUrl) == null) {
mMemoryCache.put(mUrl, bitmap);
Log.i("path", "添加成功");
} else {
Log.i("path", "已经存在");
}
}
}
上述代码还是很简单,主要就是就是两个功能,一个是将bitmap添加到内存中,一个将bitmap从内存中得到,上面我们使用了LruCache,这是一个最近最少使用的缓存数据结构,使用LinkedHashmap实现,我们将图片的URL的MD5加密作为key,bitmap当做value。有时间分析一下Lrucache的源码。
DiskCache.java
**
* 三级缓存-本地SD缓存
* 在初次使用网络获取图片,我们可以在本地SD卡中将图片保存起来
* 可以使用MD5加密图片的网络地址,来作为图片的名称保存
*/
public class DiskCache{
private static final String CACHE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/WerbNew";
/**
* 从本地读取图片
*/
public static Bitmap getBitmapFromLocal(String url) {
String filename = Md5Utils.MD5(url);
File parentFile = new File(CACHE_PATH);
if (!parentFile.exists()) {
parentFile.mkdirs();
}
File file = new File(CACHE_PATH, filename);
if (file.exists()) {
try {
FileInputStream fileInputStream = new FileInputStream(file);
FileDescriptor fileDescriptor = fileInputStream.getFD();
Bitmap bitmap = BitmapUtils.decodeSampleBitmapFromFileDescriptor(fileDescriptor, 100, 100);
MemoryCache.putBitmapToMemoy(url, bitmap);
return bitmap;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
/**
* 将网络获取到的图片存储到本地SD中
*/
public static void setBitmapToLocal(String url, Bitmap bitmap) {
String filename = Md5Utils.MD5(url);
File parentFile = new File(CACHE_PATH);
if (!parentFile.exists()) {
parentFile.mkdirs();
}
File file = new File(CACHE_PATH, filename);
if (!file.exists())
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
try {
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, new FileOutputStream(file));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
上面是本地sd缓存实现,实现的思路就是:将请求的url的md5值作为文件名,将图片的文件流存储到文件中。
NetCacheUtils.java
/**
* 网络请求图片
*/
public class NetCacheUtils {
/**
* 从网络下载图片
*/
public static void getBitmapFromNet(ImageView imageview, String url) {
new BitmapTask().execute(imageview,url);
}
public static class BitmapTask extends AsyncTask<Object, Void, Bitmap> {
ImageView imageview;
String url;
@Override
protected Bitmap doInBackground(Object... params) {
imageview = (ImageView) params[0];
url = (String) params[1];
return dowmLoadBitmap(url);
}
@Override
protected void onPostExecute(Bitmap bitmap) {
if (bitmap != null) {
imageview.setImageBitmap(bitmap);
}
}
}
public static Bitmap dowmLoadBitmap(String url) {
try {
HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection();
urlConnection.setConnectTimeout(5000);
urlConnection.setReadTimeout(5000);
urlConnection.setRequestMethod("GET");
int responseCode = urlConnection.getResponseCode();
if (responseCode == 200) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;
options.inPreferredConfig = Bitmap.Config.ARGB_4444;
Bitmap bitmap = BitmapFactory.decodeStream(urlConnection.getInputStream());
MemoryCache.putBitmapToMemoy(url,bitmap);
DiskCache.setBitmapToLocal(url, bitmap);
return bitmap;
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
实现使用了一个异步任务获取网络图片,获取成功之后将bitmap存储到内部缓存中和外部缓存中,这样我们的三个功能模块都实现完了,看起来还是蛮简单的。
我们来用一下看看喽:
/**
* 同步加载图片
*
* @param imageview
* @param url
*/
public static void disPlay(ImageView imageview, String url) {
imageview.setBackgroundResource(R.drawable.yujiazai);
Bitmap mMemoryBitmap = MemoryCache.getBitmapFromMemory(url);
if (mMemoryBitmap != null) {
imageview.setImageBitmap(mMemoryBitmap);
Log.i("path", "从内存中加载");
return;
}
Bitmap mLocalBitmap = DiskCache.getBitmapFromLocal(url);
if (mLocalBitmap != null) {
imageview.setImageBitmap(mLocalBitmap);
Log.i("path", "从文件中加载");
return;
}
Log.i("mUrl", "path");
NetCacheUtils.getBitmapFromNet(imageview, url);
}
现在我们一个不完善的三级缓存图片加载机制就可以了,最坑的一点现在的图片加载时同步的,但是我们目前的实现还是有意义的,我们严格按照单一职责的设计原则分割了加载框架的功能模块。
3.开闭原则
开闭原则的概念是:软件中的对象应该对对修改关闭,对扩展开放。
上面的代码其实可以再进行一次优化,我们可以把缓存抽象出一个接口出来,封装好它的获取和写入的两个方法,这其实就有一点里氏替换的思想在里面了,这个我们后面会在继续介绍。
CacheInterface.java
public interface CacheInterface {
//获取已经缓存的Bitmap
Bitmap getBitmap(String url);
//将bitmap添加到缓存中
void putBitmap(String url,Bitmap bitmap);
}
MemoryCache.java
package bitmaputils;
import android.graphics.Bitmap;
import android.util.Log;
import android.util.LruCache;
import com.example.administrator.bitmaputils.Md5Utils;
/**
* 内存缓存类
* 将内存缓存类和外部缓存类、网络请求类分开,做到了接口隔离原则
*/
public class MemoryCache implements CacheInterface {
public static LruCache<String, Bitmap> mMemoryCache;
static{
int maxMemory = (int)(Runtime.getRuntime().maxMemory()/1204/8); //得到手机最大的内存的1/8,即超过1/8时开始内存回收
Log.i("path","maxMemory"+(maxMemory/1024));
mMemoryCache = new LruCache<String, Bitmap>((int) maxMemory) {
//用来计算每一个条目的大小
@Override
protected int sizeOf(String key, Bitmap value) {
int byteCount = value.getRowBytes();
return byteCount*value.getHeight()/1024;
}
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
Log.i("path","移除"+key);
}
};
}
/**
* 从内存中读取图片
*/
public static Bitmap getBitmapFromMemory(String url) {
String mUrl= Md5Utils.MD5(url);
Log.i("mUrl",mUrl);
return mMemoryCache.get(mUrl);
}
/**
* 将图片保存到内存中
*/
public static void putBitmapToMemoy(String url, Bitmap bitmap) {
String mUrl=Md5Utils.MD5(url);
if(mMemoryCache.get(mUrl)==null) {
mMemoryCache.put(mUrl, bitmap);
Log.i("path", "添加成功");
}else
{
Log.i("path", "已经存在");
}
}
@Override
public Bitmap getBitmap(String url) {
return getBitmapFromMemory(url);
}
@Override
public void putBitmap(String url, Bitmap bitmap) {
putBitmapToMemoy(url,bitmap);
}
}
DiskCache.java
package bitmaputils;
import android.graphics.Bitmap;
import android.os.Environment;
import com.example.administrator.bitmaputils.BitmapUtils;
import com.example.administrator.bitmaputils.Md5Utils;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* 三级缓存-本地SD缓存
* 在初次使用网络获取图片,我们可以在本地SD卡中将图片保存起来
* 可以使用MD5加密图片的网络地址,来作为图片的名称保存
*/
public class DiskCache implements CacheInterface {
private static final String CACHE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/WerbNew";
/**
* 从本地读取图片
*/
public static Bitmap getBitmapFromLocal(String url) {
String filename = Md5Utils.MD5(url);
File parentFile = new File(CACHE_PATH);
if (!parentFile.exists()) {
parentFile.mkdirs();
}
File file = new File(CACHE_PATH, filename);
if (file.exists()) {
try {
FileInputStream fileInputStream = new FileInputStream(file);
FileDescriptor fileDescriptor = fileInputStream.getFD();
Bitmap bitmap = BitmapUtils.decodeSampleBitmapFromFileDescriptor(fileDescriptor, 100, 100);
MemoryCache.putBitmapToMemoy(url, bitmap);
return bitmap;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
/**
* 将网络获取到的图片存储到本地SD中
*/
public static void setBitmapToLocal(String url, Bitmap bitmap) {
String filename = Md5Utils.MD5(url);
File parentFile = new File(CACHE_PATH);
if (!parentFile.exists()) {
parentFile.mkdirs();
}
File file = new File(CACHE_PATH, filename);
if (!file.exists())
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
try {
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, new FileOutputStream(file));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
@Override
public Bitmap getBitmap(String url) {
return getBitmapFromLocal(url);
}
@Override
public void putBitmap(String url, Bitmap bitmap) {
setBitmapToLocal(url, bitmap);
}
}
DoubleCache.java
/**
* 双缓存
*/
public class DoubleCache implements CacheInterface {
private CacheInterface mMemoryCache = new MemoryCache();
private CacheInterface mDiskCache = new DiskCache();
@Override
public Bitmap getBitmap(String url) {
Bitmap bitmap = mMemoryCache.getBitmap(url);
if (bitmap == null) {
bitmap = mDiskCache.getBitmap(url);
}
return bitmap;
}
@Override
public void putBitmap(String url, Bitmap bitmap) {
mMemoryCache.putBitmap(url,bitmap);
mDiskCache.putBitmap(url,bitmap);
}
}
还是通过我们上面的图片加载框架中来学习这个模式,假如我们需要给这个图片加载框架指定缓存的方式,但是这个需求一般没有什么实际的意义,只会加重用户的负担而已,这里只是举例:
public class ImageLoader {
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "MyBitmapUtils#" + mCount.getAndIncrement());
}
};
public void setCacheInterface(CacheInterface cacheInterface) {
this.mCacheInterface = cacheInterface;
}
private static CacheInterface mCacheInterface=new DoubleCache();
private static final int TAG_KEY_URI = R.id.imageview;
private static final String TAG = "ImageLoader";
public static final int MESSAGE_POST_RESULT = 1;
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final long KEEP_ALIVE = 10L;
//配置线程池
public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(), sThreadFactory);
private static Handler handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
LoaderResult loaderResult = (LoaderResult) msg.obj;
Log.i("handle",loaderResult.uri);
ImageView imageView = loaderResult.imageView;
imageView.setImageBitmap(loaderResult.bitmap);
String imageUri = (String) imageView.getTag(TAG_KEY_URI);
if (imageUri.equals(loaderResult.uri)) {
imageView.setImageBitmap(loaderResult.bitmap);
}
}
};
public static void bindBitmap(final String uri, final ImageView imageView) {
imageView.setTag(TAG_KEY_URI, uri);
imageView.setBackgroundResource(R.drawable.yujiazai);
Bitmap mBitmap = mCacheInterface.getBitmap(uri);
if (mBitmap != null) {
imageView.setImageBitmap(mBitmap);
return;
}
Runnable loadBitmapTask=new Runnable() {
@Override
public void run() {
Bitmap bitmap=loadBitmap(uri);
if(bitmap!=null)
{
LoaderResult result=new LoaderResult(imageView,uri,bitmap);
handler.obtainMessage(MESSAGE_POST_RESULT,result).sendToTarget();
}
}
};
THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
}
private static class LoaderResult {
public ImageView imageView;
public String uri;
public Bitmap bitmap;
public LoaderResult(ImageView imageView, String uri, Bitmap bitmap) {
this.imageView = imageView;
this.uri = uri;
this.bitmap = bitmap;
}
}
}
通过上面的public void setCacheInterface(CacheInterface cacheInterface)就可以在不修改代码的基础上修改缓存的方法,完全契合开闭原则。
4.里氏替换原则
里氏替换的概念:所有引用基类的地方必须透明的使用其子类的对象,面向对象的三大特点是:继承、封装、多态,里氏替换就是依赖与继承和多态两大特性。简单来说:所有引用基类的地方必须透明地使用其子类的对象,通俗的讲法就是,只要父类出现的地方子类就可以出现,并且替换成子类也不会出现任何错误,使用者可能根本不需要知道是子类还是父类。
里氏替换的核心就是抽象,抽象又依赖于继承,在OOP当中,继承的优缺点都相当明显
优点:
1.代码复用,减少创建类的成本,每个子类都拥有父类的方法和属性。
2.子类与父类基本相似,当和父类又有所区别。
3.提高代码的可扩展性。
缺点:
1.继承是侵入性的,只要继承就必须拥有父类的方法和属性。
2.可能造成子类代码冗余。
从上面的代码中我们就可以看出我们不知不觉就契合了里氏替换的原则,我们提出了CacheInterface的抽象接口,然后实现了MemoryCache、DiskCache、DoubleCache三个子类,在ImageLoader中,这三个子类都可以替代父类工作,并且确保行为的正确性,提高了扩展性。
5.依赖倒置原则
依赖倒置原则的概念:模块之间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象产生的。
里氏替换和依赖倒置都强调了面向接口编程的思想。
public class ImageLoader {
private static CacheInterface mCacheInterface=new DoubleCache();
public void setCacheInterface(CacheInterface cacheInterface) {
this.mCacheInterface = cacheInterface;
}
//省略了其他代码
}
从上面代码中可以看出ImageLoader依赖于CacheInterface的抽象,而不依赖于具体实现。
6.接口隔离原则
接口隔离原则的概念:客户端不应该依赖他不需要的接口,或者类间的依赖关系应该建立在最小的接口上。接口隔离原则将给非常庞大、臃肿的接口拆分成更小的和更具体的接口
public class ImageLoader {
private static CacheInterface mCacheInterface=new DoubleCache();
public void setCacheInterface(CacheInterface cacheInterface) {
this.mCacheInterface = cacheInterface;
}
//省略了其他代码
}
将图片的缓存策略建立在了最小的接口CacheInterface上,符合接口隔离策略。
7.迪米特原则
迪米特原则的概念:最小知识原则,一个对象应该对其他对象有最少的了解。通俗的讲,一个类应该对自己需要耦合或者调用的类知道的最少,类的内部如何实现与调用者或者依赖者没有关系,调用者只需要知道他需要的方法即可。
public class ImageLoader {
private static CacheInterface mCacheInterface=new DoubleCache();
public void setCacheInterface(CacheInterface cacheInterface) {
this.mCacheInterface = cacheInterface;
}
//省略了其他代码
}
比如这句代码同样符合该原则,图片加载类只需要知道一个加载策略的接口就可以了,而不必在乎具体如何去加载。