Bitmap对象占用内存大小: bitmap.getByteCount()
图片所占内存大小计算方式:图片长度 x 图片宽度 x 一个像素点占用的字节数。
Android Bitmap使用的三种颜色格式:
- ALPHA_8–每个像素占1个字节,存储透明度信息,没有颜色信息。
- RGB_565–每个像素占2个字节存储颜色信息,R 5位,G 6位,B 5位,能表示2^16种颜色。
- ARGB_8888–每个像素占4个字节存储颜色信息,A R G B各一个字节,能表示2^24种颜色,还有一个字节存储透明度信息。
BitmapFactory 类提供了几种用于从各种来源创建 Bitmap 的解码方法(decodeStream()、decodeByteArray()、decodeFile()、decodeResource()等)。根据您的图片数据源选择最合适的解码方法。这些方法尝试为构造的位图分配内存,因此很容易导致 OutOfMemory 异常。每种类型的解码方法都有额外的签名,允许您通过 BitmapFactory.Options 类指定解码选项。在解码时将inJustDecodeBounds 属性设置为 true 可避免内存分配,为位图对象返回 null,但 outWidth、outHeight 和 outMimeType会被赋值。此方法可让您在构造位图并为其分配内存之前读取图片数据的尺寸和类型。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.image, options); //获取res资源的图片
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;
压缩方式:
质量压缩:不改变图片尺寸的情况下,通过损失颜色精度减少图片的大小。图片的长,宽,像素都不变,bitmap所占内存大小是不会变的。
bitmap.compress(
Bitmap.CompressFormat format, //图像的压缩格式;
int quality, //图像压缩率,0-100。 0 压缩100%,100意味着不压缩;
OutputStream stream) ; //写入压缩数据的输出流;
- CompressFormat.JPEG
- CompressFormat.PNG,因为 PNG 格式是无损的,它无法再进行质量压缩,quality这个参数就没有作用了,会被忽略,所以最后图片保存成的文件大小不会有变化;
- CompressFormat.WEBP,这个格式是 google 推出的图片格式,它会比 JPEG 更加省空间。官方表示能节省 25%-34% 的空间;
采样率压缩: 降低图像尺寸,改变图片的存储体积。
图片尺寸的修改其实就是通过修改像素数,放大的过程称之为上采样,缩小的过程称之为下采样。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;
bm = BitmapFactory.decodeFile(imageFilePath, options);
设置inSampleSize的值(int类型)后,假如设为2,则宽和高都为原来的1/2,宽高都减少了,自然内存也降低了。
设置inPreferredConfig:
BitmapFactory.Options options2 = new BitmapFactory.Options();
options2.inPreferredConfig = Bitmap.Config.RGB_565;
图片大小直接缩小了一半,长度和宽度没有变,相比argb_8888减少了一半的内存。
createScaledBitmap:
bm = Bitmap.createScaledBitmap(bit, 150, 150, true);
将图片压缩成用户所期望的长度和宽度,但是这里要说,如果用户期望的长度和宽度和原图长度宽度相差太多的话,图片会很不清晰。
Matrix:
我们在自定义 View 控时随处件可见 Matrix 的身影,主要用于坐标转换映射,我们可以通过 Matrix 矩阵来控制视图的变换。
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 0.5f);
bm = Bitmap.createBitmap(bit, 0, 0, bit.getWidth(),bit.getHeight(), matrix, true);
内存缓存:LruCache算法
LruCache的算法核心 = LRU 算法 + LinkedHashMap数据结构
- LRU(Least Recently Used)最近最少使用
- LinkedHashMap 哈希表+双链表 用双链表保证了HashMap的顺序。
/**
* 使用流程(以加载图片为例)
**/
private LruCache<String, Bitmap> mMemoryCache;
// 1. 获得虚拟机能提供的最大内存
// 注:超过该大小会抛出OutOfMemory的异常
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// 2. 设置LruCache缓存的大小 = 一般为当前进程可用容量的1/8
// 注:单位 = Kb
// 设置准则
// a. 还剩余多少内存给你的activity或应用使用
// b. 屏幕上需要一次性显示多少张图片和多少图片在等待显示
// c. 手机的大小和密度是多少(密度越高的设备需要越大的 缓存)
// d. 图片的尺寸(决定了所占用的内存大小)
// e. 图片的访问频率(频率高的在内存中一直保存)
// f. 保存图片的质量(不同像素的在不同情况下显示)
final int cacheSize = maxMemory / 8;
// 3. 重写sizeOf方法:计算缓存对象的大小(此处的缓存对象 = 图片)
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getByteCount() / 1024;
// 此处返回的是缓存对象的缓存大小(单位 = Kb) ,而不是item的个数
// 注:缓存的总容量和每个缓存对象的大小所用单位要一致
// 此处除1024是为了让缓存对象的大小单位 = Kb
}
};
// 4. 将需缓存的图片 加入到缓存
mMemoryCache.put(key, bitmap);
// 5. 当 ImageView 加载图片时,会先在LruCache中看有没有缓存该图片:若有,则直接获取
mMemoryCache.get(key);
硬盘缓存:DiskLruCache
打开缓存
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) throws IOException
缓存目录;当前应用程序的版本号;同一个key可以对应多少个缓存文件,基本都是1;最多可以缓存多少字节的数据;
注:每当版本号改变,缓存路径下存储的所有数据都会被清除掉,因为DiskLruCache认为当应用程序有版本更新的时候,所有的数据都应该从网上重新获取。
获取editor对象
String key = turnToMD5(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
key会成为缓存文件的文件名,并且必须要和URL是一一对应的,而URL可能包含特殊字符,不能用作文件名,所以对URL进行MD5编码,编码后的字符串是唯一的,并且只会包含0-F字符,符合文件命名规则 。newOutputStream()方法需要传一个index参数,这里传入0就好。
转为MD5方法:
public String turnToMD5(String key) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(key.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(key.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();
}
获得缓存地址对应的输出流
OutputStream os = editor.newOutputStream(0);
// 下载图片到缓存的输出流
downloadUrlToStream(imageUrl, outputStream);
如果已经是已有的bitmap对象,通过前面学到的bitmap.compress方法,第三个参数输出流就可以指定为os从而输出到缓存路径。
从网上下载的图片:
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;
}
写完缓存后,调用commit(),来提交缓存;调用abort(),放弃写入的缓存。
editor.commit();
editor.abort();
读取缓存:
借助DiskLruCache的get()方法实现的
public synchronized Snapshot get(String key) throws IOException
get()方法返回DiskLruCache.Snapshot对象,只需要调用它的getInputStream()方法就可以得到缓存文件的输入流了。
try {
String imageUrl = "https://..............";
String key = hashKeyForDisk(imageUrl);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot != null) {
InputStream is = snapShot.getInputStream(0);
Bitmap bitmap = BitmapFactory.decodeStream(is);
mImage.setImageBitmap(bitmap);
}
} catch (IOException e) {
e.printStackTrace();
}
DiskLruCache对象其他方法:
- delete() 这个方法用于将所有的缓存数据全部删除
- close() 这个方法用于将DiskLruCache关闭掉,是和open()方法对应的一个方法。
- flush() 这个方法用于将内存中的操作记录同步到日志文件(也就是journal文件)当中。
- size() 这个方法会返回当前缓存路径下所有缓存数据的总字节数,以byte为单位,如果应用程序中需要在界面上显示当前缓存数据的总大小,就可以通过调用这个方法计算出来。