單一職責原則
- 讀《Android源碼設計模式》
- 單一職責的定義為:就一個類而言,應該僅有一個引起它變化的原因,簡單來說,一個類中應該是一組相關性很高的函數,數據的封裝
- 我們從最入門的方式入手
入手
- 假設現在要實現圖片加載的功能,并且能將圖片緩存,我們可能寫出的代碼是這樣的
public class ImageLoader {
//圖片緩存
LruCache<String,Bitmap> 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<String,Bitmap>(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<String,Bitmap> mImageCache;
public ImageCache() {
initImageCache();
}
private void initImageCache(){
//計算可使用的最大內存
final int maxMemory = (int) (Runtime.getRuntime().maxMemory()/1024);
//取四分之一的可用內存作為緩存
final int cacheSize = maxMemory / 4;
mImageCache = new LruCache<String,Bitmap>(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);
- 這就是開閉原則,我們的圖片加載機制對于擴展式開放的,我們可以任意去擴展我們的緩存機制,而不用去管一點點圖片加載的細節,就可以實現代碼的開閉原則
- 這就是六大原則的前兩種,預知后面如何,且聽下回分解