单一职责原则
- 读《Android源码设计模式》
- 单一职责的定义为:就一个类而言,应该仅有一个引起它变化的原因,简单来说,一个类中应该是一组相关性很高的函数,数据的封装
- 我们从最入门的方式入手
入手
- 假设现在要实现图片加载的功能,并且能将图片缓存,我们可能写出的代码是这样的
public class ImageLoader {
//图片缓存
LruCache mImageCache;
//线程池,线程数量为CPU的数量
ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
//UI Handler
Handler mUIHandler = new Handler(Looper.getMainLooper());
public ImageLoader() {
initImageCache();
}
private void initImageCache(){
//计算可使用的最大内存
final int maxMemory = (int) (Runtime.getRuntime().maxMemory()/1024);
//取四分之一的可用内存作为缓存
final int cacheSize = maxMemory / 4;
mImageCache = new LruCache(cacheSize){
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
}
public void displayImage(final String url, final ImageView imageView){
imageView.setTag(url);
mExecutorService.submit(new Runnable() {
@Override
public void run() {
Bitmap bitmap = downloadImage(url);
if(bitmap == null){
return;
}
if(imageView.getTag().equals(url)){
updataImageView(imageView,bitmap);
}
mImageCache.put(url,bitmap);
}
});
}
private void updataImageView(final ImageView imageView,final Bitmap bmp){
mUIHandler.post(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(bmp);
}
});
}
public Bitmap downloadImage(String imageUrl){
Bitmap bitmap = null;
try{
URL url = new URL(imageUrl);
final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
bitmap = BitmapFactory.decodeStream(conn.getInputStream());
conn.disconnect();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}
}
- 代码很简单,可是大概分析一下不难看出,我们的所有功能都聚合在一个类里面,当我们的需求增多的时候,所有的代码都挤在一个类里面,这将给我们的维护带来了很大的麻烦
- 那么怎么解决呢?
改进
- 我们提出的方法是:将ImageLoader类拆分一下,把各个功能独立出来
- 各个功能独立?我们原本的这个ImageLoader类有什么功能?图片加载和图片缓存?那好吧,就把图片缓存提出来吧?我们单独写一个图片缓存的类
public class ImageCache {
//图片缓存
LruCache mImageCache;
public ImageCache() {
initImageCache();
}
private void initImageCache(){
//计算可使用的最大内存
final int maxMemory = (int) (Runtime.getRuntime().maxMemory()/1024);
//取四分之一的可用内存作为缓存
final int cacheSize = maxMemory / 4;
mImageCache = new LruCache(cacheSize){
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
}
public void put(String url , Bitmap bitmap){
mImageCache.put(url,bitmap);
}
public Bitmap get(String url){
return mImageCache.get(url);
}
}
- 可以看到,我们只是将缓存类的put和get方法抽出去而已,
- 然后看一下我们的图片加载类怎么改的
public class ImageLoader {
//图片缓存
ImageCache mImageCache = new ImageCache();
//线程池,线程数量为CPU的数量
ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
//UI Handler
Handler mUIHandler = new Handler(Looper.getMainLooper());
public void displayImage(final String url, final ImageView imageView){
Bitmap bitmap = mImageCache.get(url);
if(bitmap == null){
imageView.setImageBitmap(bitmap);
return;
}
imageView.setTag(url);
mExecutorService.submit(new Runnable() {
@Override
public void run() {
Bitmap bitmap = downloadImage(url);
if(bitmap == null){
return;
}
if(imageView.getTag().equals(url)){
updataImageView(imageView,bitmap);
}
mImageCache.put(url,bitmap);
}
});
}
private void updataImageView(final ImageView imageView,final Bitmap bmp){
mUIHandler.post(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(bmp);
}
});
}
public Bitmap downloadImage(String imageUrl){
Bitmap bitmap = null;
try{
URL url = new URL(imageUrl);
final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
bitmap = BitmapFactory.decodeStream(conn.getInputStream());
conn.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
}
- 具体也没什么大的改动,就是用到了缓存类的对象去调用相关方法
- 这样拆分之后,每个类的功能很明确,且代码量也得到了减少,虽然可扩展性还是没那么好,但是最起码思路,代码结构变得清晰许多
总结
- 其实上面的改进思想就是单一职责的思想:根据不同的功能,合理的划分一个类,或者一个函数的职责,关于这个划分倒是没有一个特别强制的概念,每个人都对功能的划分有自己的理解,具体项目中的代码就需要根据个人经验与具体逻辑而定,
开闭原则
- 开闭原则的定义:软件中的对象(类,模块,函数等)应该对于扩展是开放的,但是对于修改是封闭的,在软件的生命周期内,因为变化,升级和维护等原因需要对软件原有代码进行修改时,可能会将错误引入原本经过测试的旧代码中,破坏原有系统,因此,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过破坏已有的代码来实现
- 当然,一定的不改变原有代码是不现实的,不过,我们在开发过程中,应尽量遵循这个开闭原则
入门
- 还是之前的那个例子,通过使用不难发现,我们虽然写的这个类具有缓存图片的功能,但是当程序重启的时候我们之前的缓存都会丢掉,因为我们的缓存全都是简单的缓存在运行内存中,这样不就会影响Android系统的性能,(因为Android手机的运行内存始终有限,我们无法让一个App占用手机太多运行内存),具有易失性,重启程序的时候又会重新下载,浪费用户流量,基于此,我们打算将缓存做成缓存在SD卡当中
- 先写缓存到SD卡中的类
public class DiskCache {
private static final String TAG = "DiskCache";
static String cacheDir = null;
public DiskCache() {
cacheDir = getSDPath() + "/sadsaf";
}
public String getSDPath(){
File sdDir = null;
boolean sdCardExist = Environment.getExternalStorageState()
.equals(android.os.Environment.MEDIA_MOUNTED);//判断sd卡是否存在
if(sdCardExist) {
//这里得到的是手机内置存储空间的根目录
sdDir = Environment.getExternalStorageDirectory();
Log.d(TAG, "getSDPath: " + sdDir.toString());
}else {
//而这个得到的是手机外部SD卡的根目录,但是一般Android 是不允许我们对此目录下文件进行读写操作
sdDir = Environment.getDataDirectory();
Log.d(TAG, "getSDPath: " + sdDir.toString());
}
return sdDir.toString();
}
public Bitmap get(String url){
Log.d(TAG, "get: 在这里" + url);
String fileName = creatFileName(url);
return BitmapFactory.decodeFile(cacheDir + fileName);
}
public void put(String url, Bitmap bmp){
FileOutputStream fileOutputStream = null;
try{
File file = new File(cacheDir);
if(!file.exists()){
Log.d(TAG, "put: 文件夹不存在,先创建出文件夹");
if(!file.mkdirs()){
Log.d(TAG, "put: 文件夹创建失败");
}
if (file.exists()){
Log.d(TAG, "put: 文件夹已经存在");
}
}
String s = cacheDir + creatFileName(url);
Log.d(TAG, "put: 准备打开文件流 " + s);
fileOutputStream = new FileOutputStream(s);
bmp.compress(Bitmap.CompressFormat.PNG,100,fileOutputStream);
} catch (FileNotFoundException e) {
e.printStackTrace();
}finally {
if (fileOutputStream != null){
try{
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//因为我们直接获取网络图片的话,图片的地址会含有斜杠,而这个斜杠在写文件的时候会被当成文件夹目录,故此会出错,所以这里我将
//文件的url处理,让他的名字取网络图片url的最后一个斜杠之后的东西
private String creatFileName(String url){
StringBuilder builder = new StringBuilder(url);
String s;
if(url.contains("/")){
int i = builder.lastIndexOf("/");
s = builder.substring(i, builder.length());
}else {
s = builder.toString();
}
return s;
}
- 那么接下来改一下我们ImageLoader,让他具有设置SD卡缓存的能力
- 注:这里的SD卡写入问题,以及权限问题,就不在这里细说了,如果在这里有问题的话,自行百度
- 看ImageLoader改动的内容
//图片 内存 缓存
ImageCache mImageCache = new ImageCache();
//图片SD卡或手机内存缓存
DiskCache mDiskCache = new DiskCache();
//是否使用SD卡缓存
boolean isUseDiskCache = false;
public void displayImage(final String url, final ImageView imageView){
//判断使用的是哪种缓存,并将缓存中的东西取出来(如果有的话)
Bitmap bitmap = isUseDiskCache ? mDiskCache.get(url) : mImageCache.get(url);
if(bitmap != null){
Log.d(TAG, "displayImage: 获取到缓存");
imageView.setImageBitmap(bitmap);
return;
}
//如果没有缓存,就去线程池中请求下载
imageView.setTag(url);
mExecutorService.submit(new Runnable() {
@Override
public void run() {
Bitmap bitmap = downloadImage(url);
if(bitmap == null){
return;
}
if(imageView.getTag().equals(url)){
updataImageView(imageView,bitmap);
}
if(isUseDiskCache){
mDiskCache.put(url,bitmap);
}else {
mImageCache.put(url,bitmap);
}
}
});
}
//是否使用SD卡缓存
public void useDiskCache(boolean useDiskCache){
isUseDiskCache = useDiskCache;
}
- 这里我们在Activity里面设置使用SD卡缓存就ok啦
String url = "http://img2.imgtn.bdimg.com/it/u=4060216298,1329589408&fm=27&gp=0.jpg";
mImageView = findViewById(R.id.main_IV);
ImageLoader loader = new ImageLoader();
loader.useDiskCache(true);//设置用SD卡缓存
loader.displayImage(url,mImageView);
- 写成这样,我们就可以很方便的选择缓存方式,非常方便
- 但是不知道大家思考过没?如果我想两种缓存都使用呢?
- 如果这样的话,用目前的代码去达到这种要求是不是太过复杂?那怎么办?
- 我们可以提供这样一个思路,当要获取图片的时候,我们先看看内存缓存里面有没有,如果没有,再看看SD卡缓存里面有没有,如果都没有,再去网络上获取是不是更加人性化一些呢?
继续探索
- 这里有两种方案,一种是我们直接在原来代码上面改,一种是创建一个新的可以实现同时两种缓存都支持的类
- 那么想想看?我们刚才说的开闭原则?好吧,直接选择第二种方案
- 来看看我们这个双缓存类(DoubleCache)的实现
public class DoubleCache {
ImageCache mMemoryCache = new ImageCache();
DiskCache mDiskCache = new DiskCache();
//先从内存缓存中获取,如果没有,再从SD中获取
public Bitmap get(String url){
Bitmap bitmap = mMemoryCache.get(url);
if(bitmap == null){
bitmap = mDiskCache.get(url);
}
return bitmap;
}
//把图片缓存到内存和SD中
public void put(String url,Bitmap bitmap){
mMemoryCache.put(url,bitmap);
mDiskCache.put(url,bitmap);
}
}
- 代码没有任何难度,当提供了双缓存机制之后,我们就可以去修改下我们的加载类了(ImageLoader)
//双缓存
DoubleCache mDoubleCache = new DoubleCache();
//是否使用双缓存
boolean isUseDoubleCache = false;
public void displayImage(final String url, final ImageView imageView){
//判断使用的是哪种缓存,并将缓存中的东西取出来(如果有的话)
Bitmap bitmap;
if(isUseDoubleCache){
bitmap = mDoubleCache.get(url);
}else if(isUseDiskCache){
bitmap = mDiskCache.get(url);
}else {
bitmap = mImageCache.get(url);
}
if(bitmap != null){
Log.d(TAG, "displayImage: 获取到缓存");
imageView.setImageBitmap(bitmap);
return;
}
//如果没有缓存,就去线程池中请求下载
imageView.setTag(url);
mExecutorService.submit(new Runnable() {
@Override
public void run() {
Bitmap bitmap = downloadImage(url);
if(bitmap == null){
return;
}
if(imageView.getTag().equals(url)){
updataImageView(imageView,bitmap);
}
if(isUseDiskCache){
mDiskCache.put(url,bitmap);
}else {
mImageCache.put(url,bitmap);
}
}
});
}
//是否使用双缓存
public void UseDoubleCache(boolean useDoubleCache) {
isUseDoubleCache = useDoubleCache;
}
- 貌似?蛮好的?好像是符合开闭原则来着?
- 来,回过头想一下,我们刚才为了添加双缓存机制,修改了几个类的代码?好像基本上都修改了吧
- 问题:每次加入新的缓存方法都要修改原来的代码,这样可能引来新的bug,而且,照这样的实现方法,用户是不能自己实现自定义缓存实现的
- 这里基于这个问题,我们再来看一下开闭原则的定义:软件中的对象(类,模块,函数等)应该对于扩展是开放的,但是对于修改是封闭的,也就是说,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通修改已有的代码来实现
- 如果要实现用户自定义缓存机制实现的话,我们是不是应该抽出一个缓存接口?
public interface IImageCache {
public Bitmap get(String url);
public void put(String url,Bitmap bitmap);
}
- 然后让我们之前写的三个缓存类实现这个接口,这里就不贴代码,接下来我们去看看图片加载类怎么做的(ImageLoader)
public class ImageLoader {
private final static String TAG = "ImageLoader";
//默认为内存缓存
IImageCache mImageCache = new MemeryCache();
//线程池,线程数量为CPU的数量
ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
//UI Handler
Handler mUIHandler = new Handler(Looper.getMainLooper());
//外部注入缓存
public void setImageCache(IImageCache mImageCache){
this.mImageCache = mImageCache;
}
public void displayImage(final String url, final ImageView imageView){
//直接来获取缓存
Bitmap bitmap = mImageCache .get(url);
if(bitmap != null){
Log.d(TAG, "displayImage: 获取到缓存");
imageView.setImageBitmap(bitmap);
return;
}
//如果没有缓存,就去线程池中请求下载网络图片
submitLoadRequest(url,imageView);
}
private void submitLoadRequest(final String url, final ImageView imageView) {
imageView.setTag(url);
mExecutorService.submit(new Runnable() {
@Override
public void run() {
Bitmap bitmap = downloadImage(url);
if(bitmap == null){
Log.d(TAG, "run: 网络图片下载失败");
return;
}
if (imageView.getTag().equals(url)){
updataImageView(imageView,bitmap);
}
//设置缓存
mImageCache.put(url,bitmap);
}
});
}
//更新UI
private void updataImageView(final ImageView imageView,final Bitmap bmp){
mUIHandler.post(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(bmp);
}
});
}
//下载网络图片
public Bitmap downloadImage(String imageUrl){
Bitmap bitmap = null;
try{
URL url = new URL(imageUrl);
final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
bitmap = BitmapFactory.decodeStream(conn.getInputStream());
conn.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
}
- 从这个ImageLoader里面我们可以看出来,这个类已经相当成熟了,里面的东西我们基本不需要再去改变,想用哪种缓存,哪怕是我们自己的缓存方式,只要我们实现了那个接口,然后调用set方法将我们的缓存注入进去即可,
- 现在想想看?如果我们现在需要使用一种新的缓存方式该怎么做呢?只需实现我们自己的缓存逻辑,然后在调用一下set方法,即可完美使用,如下:
String url = "http://img2.imgtn.bdimg.com/it/u=4060216298,1329589408&fm=27&gp=0.jpg";
mImageView = findViewById(R.id.main_IV);
ImageLoader loader = new ImageLoader();
DoubleCache doubleCache = new DoubleCache();
loader.setImageCache(doubleCache);
loader.displayImage(url,mImageView);
- 这就是开闭原则,我们的图片加载机制对于扩展式开放的,我们可以任意去扩展我们的缓存机制,而不用去管一点点图片加载的细节,就可以实现代码的开闭原则
- 这就是六大原则的前两种,预知后面如何,且听下回分解