IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Bitmap类02_进阶及缓存 -> 正文阅读

[移动开发]Bitmap类02_进阶及缓存

本文知识点大部分来自《Android开发艺术探索》,大佬yyds!

Bitmap的高效加载

高效加载 Bitmap 需要用到 BitmapFactory.Options 类

使用 ImageView 时,图片的原始尺寸与设定的大小通常情况下不会一致,假如图片大小为530X530,而设定的大小为100X100,这时把整个图片加载进来显然就浪费了资源。如果通过一定的采样率加载缩小后的图片,可以降低内存占用,在一定层度上也避免了OOM的出现

通过使用在 BitmapFactory.Options 中的参数 inSampleSize (采样率,图片缩放倍数)可以达到缩放图片的效果:

inSampleSize 为1时,宽高长度不变化,图片为原大小

inSampleSize 为2时,宽高各缩小 1/2,图片为原来大小的 1/4

以此类推…

注意点:

(1)inSampleSize 的值必须要大于1时才能起作用

(2)采样率同时作用在宽和高上,所以重新采样后,图片的大小以采样率的2次方递减(即 1/(inSampleSize的2次方)

(3)inSampleSize 的值小于1时作用相当于1,即无效果

(4)如果 inSampleSize 的值不为2的指数,系统会向下取整一个最接近2的指数的数值(书中说此结论并非在所有的Android版本上成立,仅作为建议)

实践

代码:

public static Bitmap decodeSampleFromResource(Resources resources, int resId, int reqWidth, int reqHeight) {
    // 第一次加载时,设置inJustDecodeBounds参数为true,不实际加载图片,但能得到宽高等信息
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(resources, resId, options);

    // 获取采样率
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // 返回压缩后的图片
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(resources, resId, options);
}

public static 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 halfHeight = height / 2;
        final int halfWidth = width / 2;
        // 计算最大的inSampleSize
        while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
            inSampleSize *= 2;
        }
    }
    
    return inSampleSize;
}

解析:

在上述代码的 decodeSampleFromResource() 方法中,主要分为四个步骤:

(1)将Options实例中的 inJustDecodeBounds 变量设置为true:当该变量为true时仅解析图片原始的宽高信息,并不会真正的加载图片

在第一个步骤中,BitmapFactory获取的宽高信息与图片的位置和程序运行设备的分辨率有关,不是固定的

(2)在 calculateInSampleSize() 方法中取出原始宽高信息

(3)通过设定的值计算出采样率 inSampleSize

(3)将 inJustDecodeBounds 设置为false,并使用采样率重新加载图片

使用:

imageView5.setImageBitmap(decodeSampleFromResource(getResources(), R.drawable.test, 25, 25));

效果呈现:

image-20211119223612329

使用Bitmap的缓存策略

概述

Android设备不同于PC端设备,如果缓存数量或体积较大的图片显然对用户的流(qian)量(bao)不太友好。并且手机的存储容量也有一定限制,这时候就需要有缓存策略来帮用户节省流量等开销

缓存通常分为三级:

(1)内存缓存:优先加载,速度最快

(2)磁盘缓存:次优先加载,速度快

(3)网络缓存:最后加载,速度慢,浪费流量

缓存策略

而缓存策略中至少应该包括缓存的添加、获取、删除三类操作

对于删除操作,如何定义缓存的新旧对应着不同的缓存算法,常用的算法之一是LRU(近期最少使用算法Least Recently Used),核心思想是当缓存满了时优先淘汰近期最少使用的缓存对象。

实现LRU算法的缓存类有 LruCache 类和 DiskLruCache 类,分别实现了内存缓存和磁盘缓存

LruCache内存缓存

参考API:

LruCache | Android Developers

LruCache - Android中文版 - API参考文档 (apiref.com)

LruCache 是一个泛型类,并且其是线程安全的,内部采用了一个 LinkedHashMap 以强引用的方式存储外界的缓存对象

image-20211120110546885

LruCache 内部提供了 get 和 put 方法可以对缓存对象进行操作,当缓存要满的时候,其会先移除较早使用的缓存对象,再将之后的对象存储进去

扩展:

对象的引用被分为四个层次:

(1)强引用:直接的对象引用,垃圾回收器绝不会回收他

(2)软引用:当一个对象只存在软引用时,系统内存不足时此对象会被GC回收

(3)弱引用:当一个对象只有弱引用存在时,此对象会随时被GC回收

(4)虚引用:当一个对象只有虚引用时,随时会被GC回收

关于四者的区别可以参考 理解Java的强引用、软引用、弱引用和虚引用 - 掘金 (juejin.cn)

LruCahce使用举例:

int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override
    protected int sizeOf(String key, Bitmap value) {
        return value.getRowBytes() * value.getHeight() / 1024;
    }
};

解析:

通过 Runtime.getRuntime().maxMemory() 方法获取当前程序所能使用的总容量

之后设定缓存大小为最大容量的 1/8

最后定义LruCache的实例,并将缓存容量传入,重写 sizeOf() 方法用于计算缓存对象的大小

有些情况下需要重写 enrtyRemoved() 方法,LruCache在移除旧缓存时会调用该方法,可以在其中完成一些资源回收工作

使用:

获取一个缓存对象: mMemoryCache.get(key)

添加一个缓存对象:mMemoryCache.put(key, bitmap)

删除一个缓存对象:mMemoryCache.remove(key)

DiskLruCache磁盘缓存

参考:

源码参考:DiskLruCache.java - Git at Google

Github:JakeWharton/DiskLruCache

引用:implementation 'com.jakewharton:disklrucache:2.0.2'

创建

通过 DiskLruCache 中包含的 open() 方法创建一个 DiskLruCache 实例对象:

image-20211120115011750

第一个参数directory:表示该磁盘缓存在文件系统中的存储路径,默认地址时 /sdcaard/Android/data/package_name/cache 目录,当应用卸载时该目录下的内容也会一并删除,如果希望应用卸载后保留数据则可以选择SD卡的其他特定目录

第二个参数appVersion:从字面意思可以知道是应用的版本号,当版本号发生变更时会清空之前的所有缓存,一般没必要清除的情况下设置为1即可

第三个参数valueCount:表示单个节点对应数据的个数,设置为1即可

第四个参数maxSize:表示缓存的总大小,当缓存超过这个值时 DiskLruCache 会删除一些缓存保证总大小不超过这个值

创建代码示例:

获取文件位置,判断手机是否有SD卡:

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

创建 DiskLruCache :

DiskLruCache mDiskLruCache = null;
try {
    File cacheDir = getDiskCacheDir(this, "bitmap");
    if (!cacheDir.exists()) {
        cacheDir.mkdirs();
    }
    mDiskLruCache = DiskLruCache.open(cacheDir, 1, 1, 10 * 1024 * 1024);
} catch (IOException e) {
    e.printStackTrace();
}

如果需要获取版本号以便更新时删除之前的缓存,可以使用以下方法:

public int getAppVersion(Context context) {
    try {
        PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
        return info.versionCode;
    } catch (PackageManager.NameNotFoundException e) {
        e.printStackTrace();
    }
    return 1;
}
添加

使用 DiskLruCache 首先要通过网络请求获取图片 url:

private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
    HttpURLConnection urlConnection = null;
    BufferedOutputStream out = null;
    BufferedInputStream in = null;
    try {
        final URL url = new URL(urlString);
        urlConnection = (HttpURLConnection) url.openConnection();
        in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
        out = new BufferedOutputStream(outputStream, 8 * 1024);
        int b;
        while ((b = in.read()) != -1) {
            out.write(b);
        }
        return true;
    } catch (final IOException e) {
        e.printStackTrace();
    } finally {
        if (urlConnection != null) {
            urlConnection.disconnect();
        }
        try {
            if (out != null) {
                out.close();
            }
            if (in != null) {
                in.close();
            }
        } catch (final IOException e) {
            e.printStackTrace();
        }
    }
    return false;
}

使用两个方法对图片 url 进行 MD5 加密得到需要存储的 key:

private String hashKeyFormUrl(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(0xFF & bytes[i]);
        if (hex.length() == 1) {
            sb.append('0');
        }
        sb.append(hex);
    }
    return sb.toString();
}

而 DiskLruCache 中对缓存的添加是通过 Editor 来实现的,Editor代表一个缓存对象的编辑对象。

根据 key 和 edit() 方法就可以获取一个Editor对象,如果这个缓存正在被编辑, edit() 方法会返回null

如果对于当前key不存在一个Editor对象,则会返回一个新的Editor对象,可以通过它获取一个文件输出流

因为在前面 open() 方法的第三个参数中设置一个节点只能有一个数据,所以在获取文件输出流的时候可以设置其中的常量为0

最后得到添加缓存的代码为:

try {
    String imageUrl = yourKey;
    String key = hashKeyFormUrl(imageUrl);
    DiskLruCache.Editor editor = mDiskLruCache.edit(key);
    if (editor != null) {
        OutputStream outputStream = editor.newOutputStream(0);
        if (downloadUrlToStream(imageUrl, outputStream)) {
            editor.commit();
        } else {
            editor.abort();
        }
    }
    mDiskLruCache.flush();
} catch (IOException e) {
    e.printStackTrace();
}
查找

通过 DiskLruCache 的 get 方法可以获取一个 Snapshot 对象,需要传入的参数是 key

接着通过 Snapshot 对象的 getInputStream() 方法得到缓存的输入输出流,从而得到Bitmap对象

代码演示:

try {
    String imageUrl = yourKey;
    String key = hashKeyFormUrl(imageUrl);
    DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
    if (snapShot != null) {
        InputStream is = snapShot.getInputStream(0);
        Bitmap bitmap = BitmapFactory.decodeStream(is);
        imageView1.setImageBitmap(bitmap);
    }
} catch (IOException e) {
    e.printStackTrace();
}
删除

与上述同理

try {
    String imageUrl = yourKey;
    String key = hashKeyFormUrl(imageUrl);
    mDiskLruCache.remove(key);
} catch (IOException e) {
    e.printStackTrace();
}
完整的使用示例:

代码:

public class ImageLoaderTestActivity extends AppCompatActivity {

    private DiskLruCache mDiskLruCache = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_image_loader_test);

        Button button1 = findViewById(R.id.btn3_1);
        Button button2 = findViewById(R.id.btn3_2);
        ImageView imageView1 = findViewById(R.id.image3_1);

        // 打开缓存
        try {
            File cacheDir = getDiskCacheDir(this, "bitmap");
            if (!cacheDir.exists()) {
                cacheDir.mkdirs();
            }
            mDiskLruCache = DiskLruCache.open(cacheDir, 1, 1, 10 * 1024 * 1024);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 添加缓存
        button1.setOnClickListener(v -> {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        String imageUrl = yourKey;
                        String key = hashKeyFormUrl(imageUrl);
                        DiskLruCache.Editor editor = mDiskLruCache.edit(key);
                        if (editor != null) {
                            OutputStream outputStream = editor.newOutputStream(0);
                            if (downloadUrlToStream(imageUrl, outputStream)) {
                                editor.commit();
                            } else {
                                editor.abort();
                            }
                        }
                        mDiskLruCache.flush();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        });

        // 查找缓存
        button2.setOnClickListener(v -> {
            try {
                String imageUrl = yourKey;
                String key = hashKeyFormUrl(imageUrl);
                DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
                if (snapShot != null) {
                    InputStream is = snapShot.getInputStream(0);
                    Bitmap bitmap = BitmapFactory.decodeStream(is);
                    imageView1.setImageBitmap(bitmap);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }

    // 查找缓存文件夹
    public File getDiskCacheDir(Context context, String uniqueName) {
        String cachePath;
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                || !Environment.isExternalStorageRemovable()) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + uniqueName);
    }

    // 获取版本号
    public int getAppVersion(Context context) {
        try {
            PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
            return info.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 1;
    }

    // MD5加密key
    private String hashKeyFormUrl(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(0xFF & bytes[i]);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }

    // 下载图片并缓存
    private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
        HttpURLConnection urlConnection = null;
        BufferedOutputStream out = null;
        BufferedInputStream in = null;
        try {
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
            out = new BufferedOutputStream(outputStream, 8 * 1024);
            int b;
            while ((b = in.read()) != -1) {
                out.write(b);
            }
            return true;
        } catch (final IOException e) {
            e.printStackTrace();
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            try {
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (final IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }
}

效果呈现:

点击LoadImage后,等待图片请求缓存完毕再点击FindImage按钮,即可加载图片

image-20211120225206738

在缓存文件目录下也可以看到两个缓存文件:

注:对 journal 的解析请参考文章末尾标注的郭神的文章

image-20211120225331282

使用Glide的缓存策略

概述

上文中提到:在Bitmap中的缓存策略分为三级(内存、磁盘、网络)

而在Glide中,缓存策略同样分为三级,但是略有不同,分别是:

**(1)活动缓存:**在某个Activity中,页面退出则该缓存不保存

作用:分担内存缓存的负担

活动缓存本质上仍是HashMap,相比内存缓存较小,如果活动缓存满了,会自动写到内存缓存中。同时系统也会对内存缓存进行管理,防止出现内存溢出

**(2)内存缓存:**某个App(模块)范围中,应用退出该缓存不保存

作用:加快数据读取

**(3)磁盘缓存:**整个系统范围内,与Bitmap的磁盘缓存相同

作用:进行持久化存储

Glide中仍使用了DiskLruCache框架进行数据保存和读取。

使用

Glide使用缓存的思路与之前的三级缓存相似:

(1)从活动缓存获取

(2)活动缓存没有则到内存缓存中寻找

(3)内存缓存没有,就去磁盘缓存读取

(4)磁盘缓存没有就去网络获取本地文件读取

代码使用:

因为Glide是默认开启内存缓存和磁盘缓存的,所以正常使用即可

Glide.with(context).load(url).into(imageView);

如果要关闭内存缓存则:

Glide.with(context)
        .load(url)
        .skipMemoryCache(true) // 关闭内存缓存
        .into(imageView);

对磁盘缓存的操作:

Glide.with(context)
        .load(url)
        .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
        .into(imageView);

diskCacheStrategy() 方法中可以传入以下参数:

DiskCacheStrategy.ALL       // 表示既缓存原始图片,也缓存转换过后的图片
DiskCacheStrategy.NONE      // 表示不缓存任何内容
DiskCacheStrategy.RESOURCE  // 表示只缓存原始图片
DiskCacheStrategy.DATA      // 表示只缓存转换过后的图片
DiskCacheStrategy.AUTOMATIC // 表示智能判断选择模式(默认选项)

Glide图片的缓存是最多存在两份:

(1)活动或内存缓存(所以这两个加起来相当于之前的内存缓存,但是Glide更快更好用)

(2)磁盘缓存

ImageLoader的实现

在文章上面的缓存策略概述中有写到关于图片的三级缓存,而实现一个优秀的 ImageLoader 所需要的可不仅是这些,而是以下六点:

(1)图片的同步加载:指能够以同步的方式向调用者提供所加载的图片,可以是从三级缓存中任意一个地方读取

(2)图片的异步加载:能够自己在线程中加载图片并将图片设置给所需要的ImageView

(3)图片压缩:压缩图片,降低OOM的概率

(4)内存缓存:三级缓存,是 ImageLoader 的意义所在

(5)磁盘缓存

(6)网络拉取

下面分部分实现:

图片压缩

将功能提取为 ImageResizer 类,其中的知识点已经在上文Bitmap的高效加载中解析过了:

public class ImageResizer {
    private static final String TAG = "ImageResizer";

    public ImageResizer() {
    }

    public Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
        // 将inJustDecodeBounds参数设置为true以获取图片的原始信息
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);
        // 计算获取inSampleSize采样率
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        // 将inJustDecodeBounds参数设置为false,并通过采样率重新加载图片
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }

    public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
        // 将inJustDecodeBounds参数设置为true以获取图片的原始信息
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fd, null, options);
        // 计算获取inSampleSize采样率
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        // 将inJustDecodeBounds参数设置为false,并通过采样率重新加载图片
        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;
        }
        // 图片的原始宽高信息
        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;
            // 计算最大的inSampleSize值
            while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
                inSampleSize *= 2;
            }
        }
        Log.d(TAG, "sampleSize:" + inSampleSize);
        return inSampleSize;
    }
}

三级缓存的实现

仍然是采用 LruCacheDiskLruCache 来实现内存缓存和磁盘缓存

(1)在 ImageLoader 初始化时的构造方法中实例化 LruCache 和 DiskLruCache

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() / 1024;
        }
    };
    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 / 8 == 50MB

同时在创建磁盘缓存之前先判断了是否有足够的空间

(2)添加对于内存缓存的读写方法:

// 内存缓存的添加和获取
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }
}

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

(3)在文章前面所述,获取磁盘缓存的 FileInputStream 输入流需要通过 Snapshot 类,而因为 FileInputStream 无法便捷的进行压缩,需要通过 FileDescriptor 来加载图片:

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 = hashKeyFormUrl(url);
    DiskLruCache.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;
}

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 = hashKeyFormUrl(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);
}

(4)完善一下,得到目前为止添加了缓存功能的 ImageLoader:

public class ImageLoader {

    private static final String TAG = "ImageLoader";

    private Context mContext;

    private static final int IO_BUFFER_SIZE = 8 * 1024;
    private static final int DISK_CACHE_INDEX = 0;
    private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;

    private boolean mIsDiskLruCacheCreated = false;

    private ImageResizer mImageResizer = new ImageResizer();

    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() / 1024;
            }
        };
        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();
            }
        }
    }

    // 内存缓存的添加和获取
    private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemCache(key) == null) {
            mMemoryCache.put(key, bitmap);
        }
    }

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

    // 加载三级缓存
    private Bitmap loadBitmapFromMemCache(String url) {
        final String key = hashKeyFormUrl(url);
        Bitmap bitmap = getBitmapFromMemCache(key);
        return bitmap;
    }

    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 = hashKeyFormUrl(url);
        DiskLruCache.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;
    }

    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 = hashKeyFormUrl(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);
    }

    // 下载文件
    public boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
        HttpURLConnection urlConnection = null;
        BufferedOutputStream out = null;
        BufferedInputStream in = null;
        try {
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
            out = new 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();
            }
            try {
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        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();
            }
            try {
                if (in != null) {
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return bitmap;
    }

    // 获取缓存目录
    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(Build.VERSION_CODES.GINGERBREAD)
    private long getUsableSpace(File path) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
            return path.getUsableSpace();
        }
        final StatFs stats = new StatFs(path.getPath());
        return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
    }

    // MD5加密key
    private String hashKeyFormUrl(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(0xFF & bytes[i]);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }
}

同步加载和异步加载

(1)先实现同步加载代码:

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;
}

loadBitmap 的代码首先从内存缓存中加载Bitmap,如果对象为空则从磁盘缓存中加载,磁盘缓存为空则从网络中加载

因为网络请求不能在主线程中执行,所以在之前的 loadBitmapFromHttp 中有判断是否为主线程的代码

(2)异步加载接口的设计:

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 (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);
}

bindBitmap的实现:

(1)方法首先从内存缓存中读取图片,如果找到了则直接返回

(2)如果没找到结果则使用 loadBitmap 方法,当图片加载成功后将图片、图片地址和 ImageView 对象封装成一个 LoaderResult 对象

(3)最后通过 mMainHandler 向主线程发送一个消息,通知主线程设置 ImagView(主线程中更新UI)

线程池实现:

如果直接采用普通的线程加载图片,那么随着列表的滑动会产生大量的线程,显然会影响效率

而 AsyncTask 在安卓低版本和高版本的表现并不相同,在安卓3.0以上的版本中无法实现并发的效果,所以这里不考虑

从下列代码中分析线程池 THREAD_POOL_EXECUTOR 的实现:

(1)核心线程数为当前设备的CPU核心数+1

(2)最大容量是CPU核心数2倍+1

(3)线程闲置超时时长为10秒

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 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);

Handler实现:

(1)ImageLoader 直接采用主线程的 Looper 来构造 Handler 对象,使得 ImageLoader 对象可以在非主线程中构造

(2)为了解决在 ListView 等视图中快速滑动导致图片重复加载闪烁的问题,在给 ImageView 设置图片之前都会检查下它的 url 有没有发生变化,如果发生变化则不设置当前的图片,而是重新加载新的图片

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 url has changed,ignored!");
        }
    }
};

完整的 ImageLoader 代码

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.imageloader_uri;
    private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
    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 url has changed,ignored!");
            }
        }
    };
    
    private Context mContext;
    private ImageResizer mImageResizer = new ImageResizer();
    private LruCache<String, Bitmap> mMemoryCache;
    private DiskLruCache mDiskLruCache;

    // 在构造方法中创建LruCache和DiskLruCache
    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() / 1024;
            }
        };
        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();
            }
        }
    }

    // 返回一个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);
    }

    // 同步加载图片
    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;
    }

    // 异步加载图片
    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 (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);
    }
    
    // 加载三级缓存
    private Bitmap loadBitmapFromMemCache(String url) {
        final String key = hashKeyFormUrl(url);
        Bitmap bitmap = getBitmapFromMemCache(key);
        return bitmap;
    }

    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 = hashKeyFormUrl(url);
        DiskLruCache.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;
    }

    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 = hashKeyFormUrl(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);
    }

    // 下载文件
    public boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
        HttpURLConnection urlConnection = null;
        BufferedOutputStream out = null;
        BufferedInputStream in = null;
        try {
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
            out = new 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();
            }
            try {
                out.close();
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        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();
            }
            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return bitmap;
    }

    // 在版本之上运行
    @TargetApi(Build.VERSION_CODES.GINGERBREAD)
    private long getUsableSpace(File path) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
            return path.getUsableSpace();
        }
        final StatFs stats = new StatFs(path.getPath());
        return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
    }

    // 获取缓存目录
    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);
    }

    // MD5加密key
    private String hashKeyFormUrl(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(0xFF & bytes[i]);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }

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

        public LoaderResult(ImageView imageView, String uri, Bitmap bitmap) {
            this.imageView = imageView;
            this.uri = uri;
            this.bitmap = bitmap;
        }
    }
}

values目录下的ids文件代码:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <item name="imageloader_uri" type="id"/>
</resources>

实现照片墙

先创建一个SquareImageView类,以便待会使用。SquareImageView 顾名思义可以使加载的图片宽高相同,需要重写ImageView控件的 onMeasure() 方法

SquareImageView类代码:

public class SquareImageView extends ImageView {

    public SquareImageView(Context context) {
        super(context);
    }

    public SquareImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public SquareImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, widthMeasureSpec);
    }
}

主布局代码:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="5dp">

    <GridView
        android:id="@+id/gridView1"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:horizontalSpacing="5dp"
        android:listSelector="@android:color/transparent"
        android:numColumns="3"
        android:stretchMode="columnWidth"
        android:verticalSpacing="5dp"></GridView>

</LinearLayout>

item布局代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:orientation="vertical">

    <com.example.app2.ui.SquareImageView
        android:id="@+id/image"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:scaleType="centerCrop"
        android:src="@drawable/ic_launcher_background" />

</LinearLayout>

MyUtils代码:

public class MyUtils {

    public static String getProcessName(Context cxt, int pid) {
        ActivityManager am = (ActivityManager) cxt
                .getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.RunningAppProcessInfo> runningApps = am.getRunningAppProcesses();
        if (runningApps == null) {
            return null;
        }
        for (ActivityManager.RunningAppProcessInfo procInfo : runningApps) {
            if (procInfo.pid == pid) {
                return procInfo.processName;
            }
        }
        return null;
    }

    public static void close(Closeable closeable) {
        try {
            if (closeable != null) {
                closeable.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static DisplayMetrics getScreenMetrics(Context context) {
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics dm = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(dm);
        return dm;
    }

    public static float dp2px(Context context, float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
                context.getResources().getDisplayMetrics());
    }

    public static boolean isWifi(Context context) {
        ConnectivityManager connectivityManager = (ConnectivityManager) context
                .getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo activeNetInfo = connectivityManager.getActiveNetworkInfo();
        if (activeNetInfo != null
                && activeNetInfo.getType() == ConnectivityManager.TYPE_WIFI) {
            return true;
        }
        return false;
    }

    public static void executeInThread(Runnable runnable) {
        new Thread(runnable).start();
    }
}

主活动代码:

public class MainActivity extends Activity implements AbsListView.OnScrollListener {
    private static final String TAG = "MainActivity";

    private List<String> mUrList = new ArrayList<>();
    ImageLoader mImageLoader;
    private GridView mImageGridView;
    private BaseAdapter mImageAdapter;

    private boolean mIsGridViewIdle = true;
    private int mImageWidth = 0;
    private boolean mIsWifi = false;
    private boolean mCanGetBitmapFromNetWork = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initData();
        initView();
        mImageLoader = ImageLoader.build(MainActivity.this);
    }

    private void initData() {
        String key = "http://game.ming3.top/test/test";
        for (int i = 0; i < 30; i++) {
            String temp = key + i + ".jpg";
            mUrList.add(temp);
        }
        int screenWidth = MyUtils.getScreenMetrics(this).widthPixels;
        int space = (int) MyUtils.dp2px(this, 20f);
        mImageWidth = (screenWidth - space) / 3;
        mIsWifi = MyUtils.isWifi(this);
        if (mIsWifi) {
            mCanGetBitmapFromNetWork = true;
        }
    }

    private void initView() {
        mImageGridView = (GridView) findViewById(R.id.gridView1);
        mImageAdapter = new ImageAdapter(this);
        mImageGridView.setAdapter(mImageAdapter);
        mImageGridView.setOnScrollListener(this);

        if (!mIsWifi) {
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setMessage("初次使用会从网络下载图片,这将要消耗些许流量,确认要下载吗?");
            builder.setTitle("注意");
            builder.setPositiveButton("是", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    mCanGetBitmapFromNetWork = true;
                    mImageAdapter.notifyDataSetChanged();
                }
            });
            builder.setNegativeButton("否", null);
            builder.show();
        }
    }

    private class ImageAdapter extends BaseAdapter {
        private LayoutInflater mInflater;
        private Drawable mDefaultBitmapDrawable;

        private ImageAdapter(Context context) {
            mInflater = LayoutInflater.from(context);
            mDefaultBitmapDrawable = context.getResources().getDrawable(R.drawable.ic_launcher_background);
        }

        @Override
        public int getCount() {
            return mUrList.size();
        }

        @Override
        public String getItem(int position) {
            return mUrList.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder holder = null;
            if (convertView == null) {
                convertView = mInflater.inflate(R.layout.image_list_item, parent, false);
                holder = new ViewHolder();
                holder.imageView = (ImageView) convertView.findViewById(R.id.image);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }
            ImageView imageView = holder.imageView;
            final String tag = (String) imageView.getTag();
            final String uri = getItem(position);
            if (!uri.equals(tag)) {
                imageView.setImageDrawable(mDefaultBitmapDrawable);
            }
            if (mIsGridViewIdle && mCanGetBitmapFromNetWork) {
                imageView.setTag(uri);
                mImageLoader.bindBitmap(uri, imageView, mImageWidth, mImageWidth);
            }
            return convertView;
        }

    }

    private static class ViewHolder {
        public ImageView imageView;
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
            mIsGridViewIdle = true;
            mImageAdapter.notifyDataSetChanged();
        } else {
            mIsGridViewIdle = false;
        }
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
    }
}

优化列表的卡顿:

(1)不要再 getView() 方法中执行耗时的操作

(2)控制异步任务的执行频率,可以考虑在列表滑动的时候停止加载图片(因为不这样做可能会在一瞬间产生大量的异步任务,导致线程池拥堵)

(3)在 getView() 方法中设置为仅当列表静止时才加载图片

(4)如果经过上面的步骤仍有卡顿情况,可以开启硬件加速(设置 android:hardwareAccelerated="true"

参考文章:

缓存

Android DiskLruCache完全解析,硬盘缓存的最佳方案_郭霖的专栏-CSDN博客_disklrucache

记:getExternalCacheDir与getCacheDir的区别_CarsonWoo的博客-CSDN博客_getexternalcachedir

Glide

Glide三级缓存理解详细_wenzhi的博客-CSDN博客_glide三级缓存

Android 主流开源框架(七)Glide 的缓存机制 - 掘金 (juejin.cn)

面试官:简历上最好不要写Glide,不是问源码那么简单 - 掘金 (juejin.cn)

        if (!uri.equals(tag)) {
            imageView.setImageDrawable(mDefaultBitmapDrawable);
        }
        if (mIsGridViewIdle && mCanGetBitmapFromNetWork) {
            imageView.setTag(uri);
            mImageLoader.bindBitmap(uri, imageView, mImageWidth, mImageWidth);
        }
        return convertView;
    }

}

private static class ViewHolder {
    public ImageView imageView;
}

@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
    if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
        mIsGridViewIdle = true;
        mImageAdapter.notifyDataSetChanged();
    } else {
        mIsGridViewIdle = false;
    }
}

@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}

}


### 优化列表的卡顿:

(1)不要再 `getView()` 方法中执行耗时的操作

(2)控制异步任务的执行频率,可以考虑在列表滑动的时候停止加载图片(因为不这样做可能会在一瞬间产生大量的异步任务,导致线程池拥堵)

(3)在 `getView()` 方法中设置为仅当列表静止时才加载图片

(4)如果经过上面的步骤仍有卡顿情况,可以开启硬件加速(设置 `android:hardwareAccelerated="true"`)



## 参考文章:

### 缓存

[Android DiskLruCache完全解析,硬盘缓存的最佳方案_郭霖的专栏-CSDN博客_disklrucache](https://blog.csdn.net/guolin_blog/article/details/28863651)

[记:getExternalCacheDir与getCacheDir的区别_CarsonWoo的博客-CSDN博客_getexternalcachedir](https://blog.csdn.net/CarsonWoo/article/details/89142756)



### Glide

[Glide三级缓存理解详细_wenzhi的博客-CSDN博客_glide三级缓存](https://blog.csdn.net/wenzhi20102321/article/details/119337059)

[Android 主流开源框架(七)Glide 的缓存机制 - 掘金 (juejin.cn)](https://juejin.cn/post/6844904161327185927)

[面试官:简历上最好不要写Glide,不是问源码那么简单 - 掘金 (juejin.cn)](https://juejin.cn/post/6844903986412126216#heading-0)









  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-11-23 12:28:54  更:2021-11-23 12:30:57 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 5:50:08-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码