android教你打造獨一無二的圖片載入框架
前言
首先,最近是在忙okhttp沒錯。不過或許有人問為什麼忙著okhttp怎麼又扯到了圖片載入上了。其實,最近想實現下斷點續傳以及多檔案下載,但並不知道怎麼搞。群裡有小夥伴提出了控制執行緒池來實現。然後我就想到了圖片載入需要控制執行緒池,所以在此鞏固下。
概述
好了,進入正題了。優秀的圖片載入框架不要太多,什麼UIL,Picasso,Glide等等。但我們需要了解其中的原理。所以今天我來介紹下如何自己寫一個圖片載入框架。有人可能會說,自己寫會不會很渣,執行效率,記憶體溢位神馬的。至於這個,等你看完這篇部落格自己試試就知道了。
快取
載入圖片肯定是需要用到快取的,這樣可以提高我們的載入效率,而且還可以省流量。所有的圖片從快取讀取,保證圖片的記憶體不會超過預期的空間。
壓縮
除了快取,更重要的就是壓縮了。不然別人用起來。直接oom,肯定要破口大罵,尼瑪炸了!!
載入速度
好了。談完壓縮,快取,基本沒問題了把?不不不,你載入速度不快誰用?載入速度分兩種,先進先出和後進先出。如果一個列表有幾千甚至上萬張圖片,我一次性滑倒底,你要是在從第一張開始載入。我估計我一把lol打完你還沒載入完把。所以我們選擇後進先出,當前呈現給使用者的,最新載入;當前未呈現的,選擇載入。
小結
關於載入網路圖片,其實原理差不多,就多了個是否啟用硬碟快取的選項,如果啟用了,載入時,先從記憶體中查詢,然後從硬碟上找,最後去網路下載。下載完成後,別忘了寫入硬碟,加入記憶體快取。如果沒有啟用,那麼就直接從網路壓縮獲取,加入記憶體即可。
效果圖
終於扯完了,我們先來看下效果圖:
載入本地
很流暢對吧,我們在看看網路圖片.
載入網路
效果還是不錯的,之前花了好久找的100個妹紙圖。想錄長一點的,結果要求必須小於2M,也是醉了。整體效果應該可以看出來。很流暢。perfect~~~
整體實現
整體實現我採用了建造者模式,對建造者模式不瞭解的。去看android建造者模式
圖片壓縮
不管是從網路還是本地的圖片,載入都需要進行壓縮,然後顯示。
首先壓縮。我們是需要得到ImageView的寬和高,也就是大小。沒大小怎麼壓縮?
//壓縮
protected Bitmap SampledBitmap(String path, Edit edit ) {
// 獲得圖片的寬和高,並把圖片載入到記憶體中
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);
options.inSampleSize = calculateInSampleSize(options, edit.imageView.getWidth(), edit.imageView.getHeight());
// 使用獲得到的InSampleSize再次解析圖片
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeFile(path, options);
if (edit.orientation != 0) {
int drawablewidth = bitmap.getWidth();
int drawableheight = bitmap.getHeight();
Matrix matrix = new Matrix();
matrix.setRotate(edit.orientation);
bitmap = Bitmap.createBitmap(bitmap, 0, 0, drawablewidth, drawableheight,
matrix, true);
}
return bitmap;
}
我們需要設定恰當的insamplesize。根據需求的寬和高以及圖片實際的寬和高計算SampleSize 。
public int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
//源圖片的高度和寬度
final int height = options.outHeight;//得到要載入的圖片高度
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
//計算出實際寬高和目標寬高的比例
final int heightRatio = Math.round((float) height / (float) reqHeight);
final int widthRatio = Math.round((float) width / (float) reqWidth);
// 選擇寬和高中最小的比率作為inSampleSize的值,這樣可以保證最終圖片的寬和高
// 一定都會大於等於目標的寬和高。
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
return inSampleSize;
}
上面是進行本地圖片的壓縮。網路圖片就方便了。只要把網路圖片下載到sd卡,然後在進行本地壓縮。
public static boolean downloadImgByUrl(String urlStr, File file) {
boolean isok = false;
FileOutputStream fos = null;
InputStream is = null;
try {
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
is = conn.getInputStream();
fos = new FileOutputStream(file);
byte[] buf = new byte[1024 * 4];
int len = 0;
while ((len = is.read(buf)) != -1) {
fos.write(buf, 0, len);
}
isok = true;
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (is != null)
is.close();
} catch (IOException e) {
}
try {
if (fos != null)
fos.close();
} catch (IOException e) {
}
}
return isok;
}
到這邊圖片壓縮是結束了,接下來是快取。
快取
// 圖片快取的核心物件
private LruCache<String, Bitmap> lruCache;
private Context context;
//任務佇列
private LinkedList<Runnable> taskQueue = new LinkedList<>();
//執行緒池
private ExecutorService threadPool;
private Semaphore semaphoreThreadPool;
private Semaphore semaphorePoolThreadHandler = new Semaphore(0);
//UI執行緒
private Handler uiHandler;
//後臺執行緒
private Thread backthread;
private Handler backthreadhandler;
//執行緒數量
private static final int THREAD_POOL_COUNT = 3;
private void BackGroundThread() {
// 後臺輪詢執行緒
backthread = new Thread() {
public void run() {
Looper.prepare();
backthreadhandler = new Handler() {
public void handleMessage(Message msg) {
// 執行緒池去取出一個任務進行執行
threadPool.execute(taskQueue.removeLast());
try {
semaphoreThreadPool.acquire();
} catch (InterruptedException e) {
}
}
};
semaphorePoolThreadHandler.release();
Looper.loop();
}
};
backthread.start();
}
private void setLruCache() {
int maxMemory = (int) Runtime.getRuntime().maxMemory();
int cacheSize = maxMemory / 8;
lruCache = new LruCache<String, Bitmap>(cacheSize) {
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight();
}
};
// 建立執行緒池
threadPool = Executors.newFixedThreadPool(THREAD_POOL_COUNT);
semaphoreThreadPool = new Semaphore(THREAD_POOL_COUNT);
}
對圖片進行完壓縮以及快取後,下面就是載入圖片了。
載入圖片
我們需要呼叫一個方法來執行載入圖片的操作:
public void loadImage(Edit edit) {
edit.imageView.setTag(edit.loadUrl);
edit.imageView.setImageResource(edit.load_pic);
if (uiHandler == null) {
uiHandler = new Handler() {
public void handleMessage(Message msg) {
myImageBean myImageBean = (myImageBean) msg.obj;
Bitmap bm = myImageBean.bitmap;
ImageView imageview = myImageBean.imageView;
String path = myImageBean.path;
if (imageview.getTag().toString().equals(path)) {
imageview.setImageBitmap(bm);
}
}
};
}
// 根據path在快取中獲取bitmap
Bitmap bitmap = getBitmapFromLruCache(edit.loadUrl);
if (bitmap != null) {
refreashBitmap(edit, bitmap);
} else {
addTask(buildTask(edit));
}
}
我們需要得到他的快取,所以:
private Bitmap getBitmapFromLruCache(String key) {
return lruCache.get(key);
}
如果快取存在的話,那麼我們需要重新整理bitmap。代表這個圖片已經完成載入了。
private void refreashBitmap(Edit edit, Bitmap bitmap) {
Message message = Message.obtain();
myImageBean holder = new myImageBean();
holder.bitmap = bitmap;
holder.path = edit.loadUrl;
holder.imageView = edit.imageView;
message.obj = holder;
uiHandler.sendMessage(message);
}
否則,我們需要去把他新增到lurcache中,以便之後快捷的載入。
private void addTask(Runnable runnable) {
taskQueue.add(runnable);
try {
if (backthreadhandler == null)
semaphorePoolThreadHandler.acquire();
} catch (InterruptedException e) {
}
backthreadhandler.sendEmptyMessage(0x250);
}
private Runnable buildTask(final Edit edit) {
return new Runnable() {
public void run() {
Bitmap bitmap = null;
if (edit.isNet) {
File file = getDiskCacheDir(context, MD5Utils.hashKeyFromUrl(edit.loadUrl));
if (file.exists()) {
bitmap = loadImageFromLocal(file.getAbsolutePath(), edit);
} else {
boolean success = downloadImgByUrl(edit.loadUrl, file);
if (success) {
bitmap = loadImageFromLocal(file.getAbsolutePath(), edit);
}
}
} else {
bitmap = loadImageFromLocal(edit.loadUrl, edit);
}
if (lruCache.get(edit.loadUrl) == null) {
if (bitmap != null)
lruCache.put(edit.loadUrl, bitmap);
}
addBitmapToLruCache(edit.loadUrl, bitmap);
refreashBitmap(edit, bitmap);
semaphoreThreadPool.release();
}
};
}
下載到本地之後,其實也是執行一個壓縮操作,程式碼很簡單:
private Bitmap loadImageFromLocal(String path, Edit edit) {
Bitmap bm;
bm = SampledBitmap(path, edit);
return bm;
}
到此,我們的程式碼應該分析完了,不過好像還缺少了什麼,對了。Edit這個。到底做了什麼處理呢。
public class Edit {
private boolean isNet = true;
private int orientation = 0;
private String loadUrl;
private ImageView imageView;
private int load_pic=R.drawable.ic_loading;
public Edit ic_loading(int load_pic) {
this.load_pic = load_pic;
return this;
}
public Edit into(ImageView imageView) {
this.imageView = imageView;
return this;
}
public Edit setUrl(String loadUrl) {
this.loadUrl = loadUrl;
return this;
}
public Edit isNet(boolean net) {
isNet = net;
return this;
}
public void load() {
loadImage(this);
}
public Edit setOrientation(int orientation) {
this.orientation = orientation;
return this;
}
}
一個個介紹。isnet代表是否從網路載入,預設是從網路載入,至於orientation是什麼呢,這個是圖片的旋轉角度,在座的小夥伴如果有三星手機的,你開啟boss直聘選擇頭像,然後讀取本地圖片會發現,一些圖片是經過旋轉的,其實這是三星手機的一個bug。當然我們也知道了。他的圖片載入其實也是自己寫的,並沒有用三方的jar。其他屬性應該不用介紹了把。
參考
總結
圖片載入主要考慮的就是壓縮,快取,載入速度以及執行緒池(一次載入幾張)。只要掌握好這些,你也可以寫一個圖片載入框架。
對於開發最常用的就是,網路載入,圖片載入以及佈局的重新整理載入。現在就剩對okhttp的深入瞭解了。後續我會更上。我們需要知道其中的原理來解決這些問題,雖然網上都有現成的。但我們如果不分析直接拿來用的話,對自己水平提高的實在是太少了。今天就這樣吧~~~