天天看點

Android 架構-ImageLoader 圖檔加載架構

Android 開發中我們會經常會加載網絡圖檔的需求,目前成熟的圖檔加載架構有 Fresco、Glide、Picasso, 比較老的還有UniversaclImageLoader(15年開發的時候還在用這個開源庫,可惜現在已停止維護了)。由于最近想分析下Glide 的源碼,之前有沒有分析過圖檔加載架構,是以就自己參考網上簡單的圖檔加載架構自己寫了一個ImageLoader,分析下圖檔加載的基本原理和流程,為以後分析更複雜架構打下基礎。

ImageLoader 基本架構

Android 架構-ImageLoader 圖檔加載架構

整個架構還是有點複雜,我們從底層往上層開始分析 。

第一部分 緩存

為了提高使用者體驗,提升圖檔的加載速度, 我們一般會将下載下傳的圖檔緩存起來,從緩存的位置來分,可以分為記憶體緩存和本地緩存(一般是Data或者Cache 分區緩存)。

緩存接口

public interface BitmapCache {

    Bitmap get(Target key);

    void put(Target key, Bitmap value);

    void remove(Target key);
}
           

為了可擴充,我們定義了BitmapCache接口, 該接口中隻定義了三個方法, 分别是 擷取、添加、移除等,該接口中有個名字為Target 的類,該類表示一張圖檔的請求,類中包含的屬性有Image、url、resourceId等,我們主要圖圖檔的url 做為key。

記憶體緩存

記憶體緩存實作了緩存接口,記憶體緩存将解碼後的Bitmap儲存到Map中,下次直接從記憶體中加載,這樣大大提升了圖檔重複加載的速度, 我們采用了LruCache 算法

代碼如下:

public class MemoryCache implements BitmapCache {

    private LruCache<String, Bitmap> mMemoryCache;

    public MemoryCache() {

        final int totalMemory = (int) (Runtime.getRuntime().totalMemory() / );

        int cacheSize = totalMemory / ;

        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / ;
            }
        };
    }

    @Override
    public synchronized Bitmap get(Target key) {
        return mMemoryCache.get(key.imgUri);
    }

    @Override
    public synchronized void put(Target key, Bitmap bitmap) {
        mMemoryCache.put(key.imgUri, bitmap);
    }

    @Override
    public synchronized void remove(Target key) {
        mMemoryCache.remove(key.imgUri);
    }
}
           

本地緩存

如果隻緩存到記憶體的話,下次啟動應用還需要從伺服器拉圖檔,這樣即費流量使用者體驗又差,一次我們增加了本地緩存,這裡我們采用了DiskLruCache類,同樣該類也實作了緩存接口

代碼如下

public class DiskCache implements BitmapCache {

    private static final String DISK_IMG_PATH = "disk_cache_path";

    private DiskLruCache mDiskLruCache;

    private static DiskCache sDiskCache;

    private DiskCache(Context context) {
        initial(context);
    }

    public static DiskCache getInstance(Context context) {
        if (sDiskCache == null) {
            synchronized (DiskCache.class) {
                if (sDiskCache == null) {
                    sDiskCache = new DiskCache(context);
                }
            }
        }
        return sDiskCache;
    }

    private void initial(Context context) {
        try {
            File cacheDir =  getDiskCacheDir(context, DISK_IMG_PATH);
            if (!cacheDir.exists()) {
                boolean success = cacheDir.mkdir();
            }
            int versionCode = context.getPackageManager().getPackageInfo(context.getPackageName(), ).versionCode;
            mDiskLruCache = DiskLruCache.open(cacheDir, versionCode, ,  *  * );
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private File getDiskCacheDir(Context context, String uniqueName) {
        String cachePath = null;
        try {
            if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
                cachePath = context.getExternalCacheDir().getPath();
            } else {
                cachePath = context.getCacheDir().getPath();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new File(cachePath + File.separator + uniqueName);
    }

    @Override
    public Bitmap get(final Target request) {
        BitmapDecoder decoder = new BitmapDecoder() {
            @Override
            public Bitmap decodeBitmapWithOption(BitmapFactory.Options options) {
                final InputStream inputStream = getInputStream(request.imgUriMd5);
                Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
                try {
                    if (inputStream != null) {
                        inputStream.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return bitmap;
            }
        };
        return decoder.decodeBitmap(, );
    }

    @Override
    public void put(Target target, Bitmap value) {
        if (target.justCacheInMem) {
            return;
        }
        DiskLruCache.Editor editor = null;
        try {
            editor = mDiskLruCache.edit(target.imgUriMd5);
            if (editor != null) {
                OutputStream outputStream = editor.newOutputStream();
                if (writeBitmap2Disk(value, outputStream)) {
                    editor.commit();
                } else {
                    editor.abort();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void remove(Target key) {
        try {
            mDiskLruCache.remove(Md5Helper.toMD5(key.imgUriMd5));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private boolean writeBitmap2Disk(Bitmap bitmap, OutputStream outputStream) {
        BufferedOutputStream bos = new BufferedOutputStream(outputStream,  * );
        bitmap.compress(Bitmap.CompressFormat.JPEG, , bos);
        boolean result = true;
        try {
            bos.flush();
        } catch (IOException e) {
            e.printStackTrace();
            result = false;
        } finally {
            try {
                bos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return result;
    }

    private InputStream getInputStream (String md5)  {
        DiskLruCache.Snapshot snapshot;
        try {
            snapshot = mDiskLruCache.get(md5);
            if (snapshot != null) {
                return snapshot.getInputStream();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}
           

二級緩存

就是記憶體和本地同時緩存 代碼如下:

public class DoubleCache implements BitmapCache {

    private DiskCache mDiskCache;

    private MemoryCache mMemoryCache = new MemoryCache();

    public DoubleCache(Context context) {
        mDiskCache = DiskCache.getInstance(context);
    }

    @Override
    public synchronized Bitmap get(Target key) {
        Bitmap bitmap = mMemoryCache.get(key);
        if (bitmap == null) {
            bitmap = mDiskCache.get(key);
            if (bitmap != null) {
                mMemoryCache.put(key, bitmap);
            }
        }
        return bitmap;
    }

    @Override
    public synchronized void put(Target key, Bitmap value) {
        mDiskCache.put(key, value);
        mMemoryCache.put(key, value);
    }

    @Override
    public synchronized void remove(Target key) {
        mDiskCache.remove(key);
        mMemoryCache.remove(key);
    }
}
           

以上是圖檔緩存部分接下來是圖檔加載部分

圖檔的加載

圖檔加載分為兩大類,一類是加載網絡圖檔,一類是加載本地圖檔(Sd卡、Data 分區、Asset中的等)

圖檔加載Factory

根據不同的uri 産生不同不同的Loder,代碼如下:

public class LoaderFactory {

    private static LoaderFactory INSTANCE;

    public static final String HTTP = "http";

    public static final String FILE = "file";

    private BaseLoader mNullBaseLoader = new NullLoader();

    private static Map<String, BaseLoader> mLoaderMap = new HashMap<>();

    static {
        mLoaderMap.put(HTTP, new UrlLoader());
        mLoaderMap.put(FILE, new NativeLoader());
    }

    private LoaderFactory() {

    }

    public static LoaderFactory getInstance() {
        if (INSTANCE == null) {
            synchronized (LoaderFactory.class) {
                if (INSTANCE == null) {
                    INSTANCE = new LoaderFactory();
                }
            }
        }
        return INSTANCE;
    }

    public BaseLoader getLoader(String schema) {
        if (mLoaderMap.containsKey(schema)) {
            return mLoaderMap.get(schema);
        }
        return mNullBaseLoader;
    }
}
           

網絡圖檔

下載下傳網絡圖檔然後Decoder 成Bitmap, 代碼如下

public class UrlLoader extends Loader {

    @Override
    protected Bitmap onLoadBitmap(Target request) {
        final String imageUrl = request.imgUri;
        InputStream inputStream = null;
        Bitmap bitmap = null;
        HttpURLConnection connection = null;
        try {
            URL url = new URL(imageUrl);
            connection = (HttpURLConnection) url.openConnection();
            inputStream = new BufferedInputStream(connection.getInputStream());
            bitmap = BitmapFactory.decodeStream(inputStream, null, null);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            IOUtil.closeQuietly(inputStream);
            if (connection != null) {
                connection.disconnect();
            }
        }
        return bitmap;
    }
}
           

本地圖檔

public class NativeLoader extends AbsLoader {

    @Override
    protected Bitmap onLoadBitmap(Target request) {
        final String imagePath = Uri.parse(request.imgUri).getPath();
        File imgFile = new File(imagePath);
        if (!imgFile.exists()) {
            return null;
        }
        request.justCacheInMem = true;
        BitmapDecoder decoder = new BitmapDecoder() {
            @Override
            public Bitmap decodeBitmapWithOption(BitmapFactory.Options options) {
                return BitmapFactory.decodeFile(imagePath, options);
            }
        };
        return decoder.decodeBitmap(request.getImageWidth(), request.getImageHeight());
    }
}
           

最後就是啟動線程不斷的從隊列中取出請求然後加載圖檔代碼如下:

啟動線程

public final class TargetQueue {

    private BlockingQueue<Target> mRequestQueue = new PriorityBlockingQueue<>();

    private AtomicInteger mAtomicInteger = new AtomicInteger();

    private static int CORE_NUM = Runtime.getRuntime().availableProcessors();

    private int mDispatchNum = CORE_NUM;

    private Executor[] mDispatchers = null;


    public TargetQueue() {

    }

    private void startDispatchers() {
        mDispatchers = new Executor[mDispatchNum];
        for (int i = ; i < mDispatchNum; i++) {
            mDispatchers[i] = new Executor(mRequestQueue);
            mDispatchers[i].start();
        }
    }


    public void addRequest(Target request) {
        if (!mRequestQueue.contains(request)) {
            request.sequenceNumber = mAtomicInteger.incrementAndGet();
            mRequestQueue.add(request);
        }
    }

    /**
     * 開始請求圖檔
     */
    public void start() {
        stop();
        startDispatchers();
    }

    /**
     * 停止所有的請求
     */
    public void stop() {
        if (mDispatchers != null && mDispatchers.length > ) {
            for (Executor dispatcher : mDispatchers) {
                dispatcher.interrupt();
            }
        }
    }

    public BlockingQueue<Target> getAllRequest() {
        return mRequestQueue;
    }

    public void clear() {
        mRequestQueue.clear();
    }

}
           

線程中的循環

public class Executor extends Thread {

    private BlockingQueue<Target> mTargetQueue;

    Executor(BlockingQueue<Target> targetQueue) {
        mTargetQueue = targetQueue;
    }

    @Override
    public void run() {
        while (!this.isInterrupted()) {
            try {
                final Target request  = mTargetQueue.take();
                if (request.isCancelled) {
                    continue;
                }
                String target = parseSchema(request.imgUri);
                BaseLoader imgBaseLoader = LoaderFactory.getInstance().getLoader(target);
                imgBaseLoader.loadImage(request);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private String parseSchema(String uri) {
        if (uri.contains("://")) {
            return uri.split("://")[];
        }
        return "";
    }
}