1. 程式人生 > >android教你打造獨一無二的圖片載入框架

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的深入瞭解了。後續我會更上。我們需要知道其中的原理來解決這些問題,雖然網上都有現成的。但我們如果不分析直接拿來用的話,對自己水平提高的實在是太少了。今天就這樣吧~~~