0%

Bitmap 的加载和 Cache(三):ImageLoader 的使用

1. 一个优秀的 ImageLoader 应该具备哪些功能

  • 图片的同步加载
  • 图片的异步加载
  • 图片压缩
  • 内存缓存
  • 磁盘缓存
  • 网络拉取
  • View 复用可能导致的列表图片错位

2. 实现一个高效且可靠的 ImageLoader

  1. 图片压缩功能的实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    public class ImageResizer {
    private static final String TAG = "ImageResizer";

    public ImaeResizer() {
    }

    public Bitmap decodeSampledBitmapFromResource(Resources res,
    int redId, int reqWidth, int reqHeight) {
    // First decode with inJustDecodeBounds=true to check dimentions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BItmapFactory.decodeResource(res, resId, options);

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
    }

    public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeFileDescriptor(fd, null, options);

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeFileDescriptor(fd, null, options);
    }

    public int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
    if (reqWidth == 0 || reqHeight == 0) {
    return 1;
    }

    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    Log.d(TAG, "origin, w=" + width + " h=" + height);
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {
    final int halfHeight = height / 2;
    final int halfWidth = width / 2;

    // Calculate the largest inSampleSize value that is a power of 2
    // and keeps both height and width larger than the requested height and width.
    while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
    inSampleSize *= 2;
    }
    }

    Log.d(TAG, "sampleSize:" + inSampleSize);
    return inSampleSize;
    }
    }
  2. 内存缓存磁盘缓存的实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // 创建 LruCache 和 DiskLruCache
    private LruCache<String, Bitmap> mMemoryCache;
    private DiskLruCache mDiskLruCache;

    private ImageLoader(Context context) {
    mContext = context.getApplicationContext();
    int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    int cacheSize = maxMemory / 8;
    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override
    protected int sizeOf(String key, Bitmap bitmap) {
    return bitmap.getRowBytes() * bitmap.getHeight() / 2014;
    }
    };

    File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
    if (!diskCacheDir.exists()) {
    diskCacheDir.mkdirs();
    }
    if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
    try {
    mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
    mIsDiskLruCacheCreated = true;
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 内存缓存的添加和获取
    private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
    mMemoryCache.put(key, bitmap);
    }
    }

    private Bitmap getBitmapFromMemCache() {
    return mMemoryCache.get(key);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    // 磁盘缓存的添加和读取
    private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight) throws IOException {
    if (Looper.myLooper() == Looper.getMainLooper()) {
    throw new RuntimeException("can not visit network from UI Thread.");
    }
    if (mDiskLruCache == null) {
    return null;
    }

    String key = hashKeyFromUrl(url);
    DiskLruCache.Editor editor = mDiskLruCache.edit(key);
    if (editor != null) {
    OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
    if (downloadUrlToStream(url, outputStream)) {
    editor.commit();
    } else {
    editor.abort();
    }
    mDiskLruCache.flush();
    }
    return loadBitmapFromDiskCache(url, reqWidth, reqHeight);
    }

    private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException {
    if (Looper.myLooper() == Looper.getMainLooper()) {
    Log.w(TAG, "load bitmap from UI Thread, it's not recommended!");
    }
    if (mDiskLruCache == null) {
    return null;
    }

    Bitmap bitmap = null;
    String key = hashKeyFromUrl(url);
    DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
    if (snapShot != null) {
    FileInputStream fileInputStream = (FileInputStream) snapShot.getInputStream(DISK_CACHE_INDEX);
    FileDescripto fileDescriptor = fileInputStream.getFD();
    bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);
    if (bitmap != null) {
    addBitmapToMemoryCache(key, bitmap);
    }
    }

    return bitmap;
    }
  3. 同步加载异步加载接口的设计

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    // 同步加载接口设计

    /**
    * load bitmap from memory cache or disk cache or network
    * @param uri http url
    * @param reqWidth the width ImageView desired
    * @param reqHeight the height ImageView desired
    * @return bitmap, maybe null.
    */
    public Bitmap loadBitmap(String uri, int reqWidth, int reqHeight) {
    Bitmap bitmap = loadBitmapFromMemCache(uri);
    if (bitmap != null) {
    Log.d(TAG, "loadBitmapFromMemCache, uri: " + uri);
    return bitmap;
    }

    try {
    bitmap = loadBitmapFromDiskCache(uri, reqWidth, reqHeight);
    if (bitmap != null) {
    Log.d(TAG, "loadBitmapFromDis, uri: " + uri);
    return bitmap;
    }
    bitmap = loadBitmapFromHttp(uri, reqWidth, ,reqHeight);
    Log.d(TAG, "loadBitmapFromHttp, uri: " + uri);
    } catch (IOException e) {
    e.printStackTrace();
    }

    if (bitmap == null && !mIsDiskLruCacheCreated) {
    Log.w(TAG, "encounter error, DiskLruCache is not created.");
    bitmap = downloadBitmapFromUrl(uri);
    }

    return bitmap;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 异步加载接口设计
    public void bindBitmap(final String uri, final ImageView imageView,
    final int reqWidth, final int reqHeight) {
    imageView.setTag(TAG_KEY_URL, uri);
    Bitmap bitmap = loadBitmapFromMemCache(uri);
    if (bitmap != null) {
    imageView.setImageBitmap(bitmap);
    return;
    }

    Runnable loadBitmapTask = new Runnable() {
    @Override
    public void run() {
    Bitmap bitmap = loadBitmap(uri, reqWidth, reqHeight);
    if (bitmap != null) {
    LoaderResult result = new LoaderResult(imageView, uri, bitmap);
    mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget();
    }
    }
    };
    THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
    }
  • ImageLoader 的完整实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    public class ImageLoader {

    private static final String TAG = "ImageLoader";

    public static final int MESSAGE_POST_RESULT = 1;

    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    private static final long KEEP_ALIVE = 10L;

    private static final int TAG_KEY_URI = R.id.image_loader_uri;
    private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50; // 磁盘缓存大小为 50M
    private static final int IO_BUFFER_SIZE = 8 * 1024;
    private static final int DISK_CACHE_INDEX = 0;
    private boolean mIsDiskLruCacheCreated = false;

    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
    private final AtomicInteger mCount = new AtomicInteger(1);

    public Thread newThread(Runnable r) {
    return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());
    }
    };

    public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
    CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS,
    new LinkedBlockingQueue<Runnable>(), sThreadFactory);
    private Handler mMainHandler = new Handler(Looper.getMainLooper()) {
    @Override
    public void handleMessage(Message msg) {
    LoaderResult result = (LoaderResult) msg.obj;
    ImageView imageView = result.imageView;
    // imageView.setImageBitmap(result.bitmap);
    String uri = (String) imageView.getTag(TAG_KEY_URI);
    if (uri.equals(result.uri)) {
    imageView.setImageBitmap(result.bitmap);
    } else {
    Log.w(TAG, "set image bitmap, but uri has changed, ignored!");
    }
    }
    };

    private Context context;
    private ImageResizer mImageResizer = new ImageResizer();
    private LruCache<String, Bitmap> mMemoryCache;
    private DiskLruCache mDiskLruCache;

    private ImageLoder(Context context) {
    mContext = context.getApplicationContext();
    int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    int cacheSize = maxMemory / 8;
    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override
    protected int sizeOf(String key, Bitmap bitmap) {
    return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
    }
    } ;
    File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
    if (!diskCacheDir.exists()) {
    diskCacheDir.mkdirs();
    }
    if (getUsableSpace(diskCacheDir) > DISK_CACE_SIZE) {
    try {
    mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
    mIsDiskLruCacheCreated = true;
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }

    /**
    * build a new instance of ImageLoader
    * @param context
    * @return a new instance of ImageLoader
    */
    public static ImageLoader build(Context context) {
    return new ImageLoader(context);
    }

    private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
    mMemoryCache.put(key, bitmap);
    }
    }

    private Bitmap getBitmapFromMemCache(String key) {
    return mMemoryCache.get(key);
    }

    /**
    * load bitmap from memory cache or disk cache or network async, then bind
    * imageView and bitmap.
    * NOTE THAT: should run in UI Thread
    * @param uri http url
    * @param imageView bitmap's bind object
    */
    public void bindBitmap(final String uri, final ImageView imageView) {
    bindBitmap(uri, imageView, 0, 0);
    }

    public void bindBitmap(final String uri, final ImageView imageView,
    final int reqWidth, final int reqHeight) {
    imageView.setTag(TAG_KEY_URI, uri);
    Bitmap bitmap = loadBitmapFromMemCache(uri);
    if (bimap != null) {
    imageView.setImageBitmap(bitmap);
    return;
    }

    Runnable loadBitmapTask = new Runnable() {
    @Override
    public void run() {
    Bitmap bitmap = loadBitmap(uri, reqWidth, reqHeight);
    if (bitmap != null) {
    LoaderResult result = new LoaderResult(imageView, uri, bitmap);
    mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sentToTarget();
    }
    }
    };

    THREAD_POOL_EXECUTOR.executor(loadBitmapTask);
    }

    /**
    * load bitmap from memory cache or disk cache or network.
    * @param uri http url
    * @param reqWidth the width ImageView desired
    * @param reqHeight the height ImageView desired
    * @return bitmap, maybe null
    */
    public Bitmap loadBitmap(String uri, int reqWidth, int reqHeight) {
    Bitmap bitmap = loadBitmapFrommemCache(uri);
    if (bitmap != null) {
    Log.d(TAG, "loadBitmapFromMemCache, url: " + uri);
    return bitmap;
    }

    try {
    bitmap = loadBitmapFromDiskCache(uri, reqWidth, reqHeight);
    if (bitmap != null) {
    Log.d(TAG, "loadBitmapFromDisk, url: " + uri);
    return bitmap;
    }
    bitmap = loadBitmapFromHttp(uri, reqWidth, reqHeight);
    Log.d(TAG, "loadBitmapFromHttp, url: " + uri);
    } catch (IOException e) {
    e.printStackTrace();
    }

    if (bitmap == null && !mIsDiskLruCacheCreated) {
    Log.w(TAG, "encounter error, DiskLruCache is not created.");
    bitmap = downloadBitmapFromUrl(uri);
    }

    return bitmap;
    }

    private Bitmap loadBitmapFromMemCache(String uri) {
    final String key = hashKeyFromUrl(url);
    Bitmap bitmap = getBitmapFromMemCache(key);
    return bitmap;
    }

    private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight) throws IOException {
    if (Looper.myLooper() == Looper.getMainLooer()) {
    throw new RuntimeException("can not visit network form UI Thread.");
    }
    if (mDiskLruCache == null) {
    return null;
    }

    String key = hashKeyFromUrl(url);
    DiskLruCache.Editor editor = mDiskLruCache.edit(key);
    if (editor != null) {
    OutputStream outputStream = editor.newOutputStream(DISK_CACEH_INDEX);
    if (downloadUrlToStream(url, outputStream)) {
    editor.commit();
    } else {
    editor.abort();
    }
    mDiskLruCache.flush();
    }
    return loadBitmapFromDiskCache(url, reqWidth, reqHeight);
    }

    private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException {
    if (Looper.myLooper() == Looper.getMainLooper()) {
    Log.w(TAG, "load bitmap from UI Thread, it's not recommend!");
    }
    if (mDiskLruCache == null) {
    return null;
    }

    Bitmap bitmap = null;
    String key = hashKeyFromUrl(url);
    DisLruCache.Snapshot snapShot = mDiskLruCache.get(key);
    if (snapShot != null) {
    FileInputStream fileInputStream = (FileInputStream) snapShot.getInputStream(DISK_CACHE_INDEX);
    FileDescriptor fileDescriptor = fileInputStream.getFD();
    bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);
    if (bitmap != null) {
    addBitmapToMemoryCache(key, bitmap);
    }
    }

    return bitmap;
    }

    public boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
    HttpURLConnection urlConnection = null;
    BufferedOutputStream out = null;
    BufferedInputStream in = null;
    try {
    final URL url = new URL(urlString);
    uriConnection = (HttpURLConnection) url.openConnection();
    in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
    out = BufferedOutputStream(outputStream, IO_BUFFER_SIZE);

    int b;
    while ((b = in.read()) != -1) {
    out.write(b);
    }
    return true;
    } catch (IOException e) {
    Log.e(TAG, "downloadBitmap failed." + e);
    } finally {
    if (urlConnection != null) {
    urlConnection.disconnect();
    }
    MyUtils.close(out);
    MyUtils.close(in);
    }
    return false;
    }

    private Bitmap downloadBitmapFromUrl(String urlString) {
    Bitmap bitmap = null;
    HttpURLConnection urlConnection = null;
    BufferedInputStream in = null;

    try {
    final URL url = new URL(urlString);
    urlConnection = (HttpURLConnection) url.openConnection();
    in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
    bitmap = BitmapFactory.decodeStream(in);
    } catch (final IOException e) {
    Log.e(TAG, "Error in downloadBitmap: " + e);
    } finally {
    if (urlConnection != null) {
    urlConnection.disconnect();
    }
    MyUtils.close(in);
    }
    return bitmap;
    }

    private String hashKeyFromUrl(String url) {
    String cacheKey;
    try {
    final MessageDigest mDigest = MessageDigest.getInstance("MD5");
    mDigest.update(url.getBytes());
    cacheKey = bytesToHexString(mDigest.digest());
    } catch (NoSuchAlgorithmException e) {
    cacheKey = String.valueOf(url.hashCode());
    }
    return cacheKey;
    }

    private String bytesToHexString(byte[] bytes) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < bytes.length; i++) {
    String hex = Integer.toHexString(oxFF & bytes[i]);
    if (hex.length() == 1) {
    sb.append('0');
    }
    sb.append(hex);
    }
    return sb.toString();
    }

    public File getDiskCacheDir(Context context, String uniquename) {
    boolean externalStorageAvailable = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
    final String cachePath;
    if (externalStorageAvailable) {
    cachePath = context.getExternalCacheDir().getPath();
    } else {
    cachePath = context.getCacheDir().getPath();
    }
    return new File(cachePath + File.separator + uniqueName);
    }

    @TargetApi(VERSION_CODES.GINGERBREAD)
    private long getUsableSpace(File path) {
    if (Build.VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {
    return path.getUsableSpace();
    }
    final StateFs stats = new StatFs(path.getPath());
    return (long) stats.getBlockSize * (long) stats.getAvailableBlocks();
    }

    private static class LoaderResult {
    public ImageView imageView;
    public String uri;
    public Bitmap bitmap;

    public LoaderResult(ImageView imageView, String uri, Bitmap bitmap) {
    this.iamgeView = imageView;
    this.uri = uri;
    this.bitmap = bitmap;
    }
    }
    }
    • ImageLoader 的经典使用场景是照片墙效果

3. 优化列表卡顿的思路

  • 不要在 ImageAdapter 适配器的 getView() 方法中执行耗时操作,应该采用异步加载方式加载图片

  • 控制异步任务的执行频率,比如用户在频繁上下滑动时会瞬时产生上百个异步任务,这些异步任务会造成线程池的拥堵并随机带来大量的 UI 更新操作,会造成一定程度的卡顿。可以考虑在列表滑动时停止加载图片

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 给 ListView 或者 GridView 设置 setOnScrollListener,判断是否处于滑动状态
    public void onScrollStateChanged(AbsListView view, int scrollState) {
    if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
    mIsGridViewIdle = true;
    mImageAdapter.notifyDataSetChanged();
    } else {
    mIsGridViewIdle = false;
    }
    }

    // 在 getView() 方法中,仅当列表静止时才能加载图片
    if (mIsGridViewIdle && mCanGetBitmapFromNetWork) {
    imageView.setTag(uri);
    mImageLoader.bindBitmap(uri, imageView, mImageWidth, mImageWidth);
    }
  • 开启硬件加速android:hardwareAccelerated=true

-------------------- 本文结束感谢您的阅读 --------------------