BitMap详解及加载

一、Bitmap的生成

Bitmap实现在android.graphics包中。但是Bitmap类的构造函数是私有的,外面并不能实例化,只能是通过JNI实例化。这必然是 某个辅助类提供了创建Bitmap的接口,而这个类的实现通过JNI接口来实例化Bitmap的,这个类就是BitmapFactory。
screenshot
利用 BitmapFactory可以从一个指定文件中,利用 decodeFile()解出 Bitmap;也可以定义
图片资源中,利用 decodeResource()解出 Bitmap

二、利用Bitmap和Matrix实现图像变换

Bitmap可以和Matrix结合实现图像的剪切、旋转、缩放等操作。
screenshot
用源Bitmap通过变换生成新的Bitmap的方法:

public static Bitmap createBitmap(Bitmap source, int x, int y, intwidth, int height, 
Matrix m, boolean filter)
public static Bitmap createBitmap(Bitmap source, int x, int y, intwidth, int height)
public static Bitmap createScaledBitmap(Bitmap src, int dstWidth,
int dstHeight,boolean filter)

第一个方法是最终的实现,后两种只是对第一种方法的封装。
第二个方法可以从源Bitmap中指定区域(x,y, width, height)中挖出一块来实现剪切;第三个方法可以把源Bitmap缩放为dstWidth x dstHeight的Bitmap。
设置Matrix的Rotate(通过setRotate())或者Scale(通过setScale()),传入第一个方法,可实现旋转或缩放。

三、保存图像文件

经过图像变换之后的Bitmap里的数据可以保存到图像压缩文件里(JPG/PNG)。
screenshot

这个操作过程中,Bitmap.compress()方法的参数format可设置JPEG或PNG格式;quality可选择压缩质量;fOut是输出流(OutputStream),这里的FileOutputStream是OutputStream的一个子类。

四、Bitmap在内存的存储

在Android2.3.3(API 10)及之前的版本中,Bitmap对象与其像素数据是分开存储的,Bitmap对象存储在Dalvik heap中,而Bitmap对象的像素数据则存储在Native Memory(本地内存)中或者说Derict Memory(直接内存)中,这使得存储在Native Memory中的像素数据的释放是不可预知的,我们可以调用recycle()方法来对Native Memory中的像素数据进行释放,前提是你可以清楚的确定Bitmap已不再使用了,如果你调用了Bitmap对象recycle()之后再将Bitmap绘制出来,就会出现"Canvas: trying to use a recycled bitmap"错误,而在Android3.0(API 11)之后,Bitmap的像素数据和Bitmap对象一起存储在Dalvik heap中,所以我们不用手动调用recycle()来释放Bitmap对象,内存的释放都交给垃圾回收器来做,也许你会问,为什么我在显示Bitmap对象的时候还是会出现OutOfMemoryError呢?
简单的了解垃圾收集器之后,我们对上面的问题举一个简单的例子,假如系统启动了垃圾回收线程去收集垃圾,而此时我们一下子产生大量的Bitmap对象,此时是有可能会产生OutOfMemoryError,因为垃圾回收器首先要判断某个对象是否还存活(JAVA语言判断对象是否存活使用的是根搜索算法 GC Root Tracing),然后利用垃圾回收算法来对垃圾进行回收,不同的垃圾回收器具有不同的回收算法,这些都是需要时间的, 发生OutOfMemoryError的时候,我们要明确到底是因为内存泄露(Memory Leak)引发的还是内存溢出(Memory overflow)引发的,如果是内存泄露我们需要利用工具(比如MAT)查明内存泄露的代码并进行改正,如果不存在泄露,换句话来说就是内存中的对象确实还必须活着,那我们可以看看是否可以通过某种途径,减少对象对内存的消耗,比如我们在使用Bitmap的时候,应该根据View的大小利用BitmapFactory.Options计算合适的inSimpleSize来对Bitmap进行相对应的裁剪,以减少Bitmap对内存的使用,如果上面都做好了还是存在OutOfMemoryError(一般这种情况很少发生)的话,那我们只能调大Dalvik heap的大小了,在Android 3.1以及更高的版本中,我们可以在AndroidManifest.xml的application标签中增加一个值等于“true”的android:largeHeap属性来通知Dalvik虚拟机应用程序需要使用较大的Java Heap,但是我们也不鼓励这么做。

五、高效加载大图片

我们在编写Android程序的时候经常要用到许多图片,不同图片总是会有不同的形状、不同的大小,但在大多数情况下,这些图片都会大于我们程序所需要的大小。比如说系统图片库里展示的图片大都是用手机摄像头拍出来的,这些图片的分辨率会比我们手机屏幕的分辨率高得多。大家应该知道,我们编写的应用程序都是有一定内存限制的,程序占用了过高的内存就容易出现OOM(OutOfMemory)异常。我们可以通过下面的代码看出每个应用程序最高可用内存是多少。

[java] view plaincopyprint?
1. int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
2. Log.d("TAG", "Max memory is " + maxMemory + "KB");

因此在展示高分辨率图片的时候,最好先将图片进行压缩。压缩后的图片大小应该和用来展示它的控件大小相近,在一个很小的ImageView上显示一张超大的图片不会带来任何视觉上的好处,但却会占用我们相当多宝贵的内存,而且在性能上还可能会带来负面影响。下面我们就来看一看,如何对一张大图片进行适当的压缩,让它能够以最佳大小显示的同时,还能防止OOM的出现。
BitmapFactory这个类提供了多个解析方法(decodeByteArray,decodeFile,decodeResource等)用于创建Bitmap对象,我们应该根据图片的来源选择合适的方法。比如SD卡中的图片可以使用decodeFil
le方法,网络上的图片可以使用decodeStream方法,资源文件中的图片可以使用decodeResource方法。这些方法会尝试为已经构建的Bitmap分配内存,这时就会很容易导致OOM出现。为此每一种解析方法都提供了一个可选的BitmapFactory.Options参数,将这个参数的inJustDecodeBounds属性设置为true就可以让解析方法禁止为bitmap分配内存,返回值也不再是一个Bitmap对象,而是null。虽然Bitmap是null,但是BitmapFactory.Options的outWidth,outHeight和outMimeType属性都会被赋值。这个技巧可以让我们在加载图片之前就获取到图片的长宽值和MIME类型,从而根据情况对图片进行压缩。如下代码所示:
BitmapFactory.Options options=new BitmapFactory.Options();
options.inJustDecodeBounds=true;
BitmapFactory.decodeResource(getResources(),R.id.myimage.options);
int imageHeight=options.outHeight;
int imageWidth=options.outWidth;
String imageType=options.outMimeType;

为了避免OOM异常,最好在解析每张图片的时候都先检查一下图片的大小,除非你非常信任图片的来
源,保证这些图片都不会超出你程序的可用内存。
现在图片的大小已经知道了,我们就可以决定是把整张图片加载到内存中还是加载一个压缩版的图片到内存中。以下几个因素是我们需要考虑的:
•预估一下加载整张图片所需占用的内存。
•为了加载这一张图片你所愿意提供多少内存。
•用于展示这张图片的控件的实际大小。
•当前设备的屏幕尺寸和分辨率。
比如,你的ImageView只有128*96像素的大小,只是为了显示一张缩略图,这时候把一张1024*768像素的图片完全加载到内存中显然是不值得的。
那我们怎样才能对图片进行压缩呢?通过设置BitmapFactory.Options中inSampleSize的值就可以实现。比如我们有一张2048*1536像素的图片,将inSampleSize的值设置为4,就可以把这张图片压缩成512*384像素。原本加载这张图片需要占用13M的内存,压缩后就只需要占用0.75M了(假设图片是ARGB_8888类型,即每个像素点占用4个字节)。下面的方法可以根据传入的宽和高,计算出合适的inSampleSize值:
01.public static int calculateInSampleSize(BitmapFactory.Options options,
02. int reqWidth, int reqHeight) {
03. //获取长宽 //
04 final int height = options.outHeight;
05. final int width = options.outWidth;
06. int inSampleSize = 1;
07. if (height > reqHeight || width > reqWidth) {
08. // 计算出实际宽高和目标宽高的比率//
09. final int heightRatio = Math.round((float) height / (float) reqHeight);
10. final int widthRatio = Math.round((float) width / (float) reqWidth);
11. // 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高 //
12. // 一定都会大于等于目标的宽和高//
13. inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
14. }
15. return inSampleSize;
16.}

使用这个方法,首先你要将**BitmapFactory.Options**的**inJustDecodeBounds**属性设置为true,解析一次图片。然后将**BitmapFactory.Options**连同期望的宽度和高度一起传递到到**calculateInSampleSize**方法中,就可以得到合适的inSampleSize值了。之后再解析一次图片,使用新获取到的**inSampleSize**值,并把**inJustDecodeBounds**设置为false,就可以得到压缩后的图片了。
01.public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
02. int reqWidth, int reqHeight) {
03. // 第一次解析将inJustDecodeBounds设置为true,来获取图片大小
04. final BitmapFactory.Options options = new BitmapFactory.Options();
05. options.inJustDecodeBounds = true;
06. BitmapFactory.decodeResource(res, resId, options);
07. // 调用上面定义的方法计算inSampleSize值
08. options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
09. // 使用获取到的inSampleSize值再次解析图片
10. options.inJustDecodeBounds = false;
11. return BitmapFactory.decodeResource(res, resId, options);
12.}

下面的代码非常简单地将任意一张图片压缩成100*100的缩略图,并在ImageView上展示。
01.mImageView.setImageBitmap(
02.decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

六、使用图片内存缓存技术

   在你应用程序的UI界面加载一张图片是一件很简单的事情,但是当你需要在界面上加载一大堆图片的时候,情况就变得复杂起来。在很多情况下,(比如使用ListView, GridView 或者 ViewPager 这样的组件),屏幕上显示的图片可以通过滑动屏幕等事件不断地增加,最终导致OOM。

为了保证内存的使用始终维持在一个合理的范围,通常会把被移除屏幕的图片进行回收处理。此时垃圾回收器也会认为你不再持有这些图片的引用,从而对这些图片进行GC操作。用这种思路来解决问题是非常好的,可是为了能让程序快速运行,在界面上迅速地加载图片,你又必须要考虑到某些图片被回收之后,用户又将它重新滑入屏幕这种情况。这时重新去加载一遍刚刚加载过的图片无疑是性能的瓶颈,你需要想办法去避免这个情况的发生。
这个时候,使用内存缓存技术可以很好的解决这个问题,它可以让组件快速地重新加载和处理图片。下面我们就来看一看如何使用内存缓存技术来对图片进行缓存,从而让你的应用程序在加载很多图片的时候可以提高响应速度和流畅性。
内存缓存技术对那些大量占用应用程序宝贵内存的图片提供了快速访问的方法。其中最核心的类是LruCache (此类在android-support-v4的包中提供) 。这个类非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。
在过去,我们经常会使用一种非常流行的内存缓存技术的实现,即软引用或弱引用 (SoftReference or WeakReference)。但是现在已经不再推荐使用这种方式了,因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃。
为了能够选择一个合适的缓存大小给LruCache, 有以下多个因素应该放入考虑范围内,例如:
•你的设备可以为每个应用程序分配多大的内存?
•设备屏幕上一次最多能显示多少张图片?有多少图片需要进行预加载,因为有可能很快也会显示在屏幕上?
•你的设备的屏幕大小和分辨率分别是多少?一个超高分辨率的设备(例如 Galaxy Nexus) 比起一个较低分辨率的设备(例如 Nexus S),在持有相同数量图片的时候,需要更大的缓存空间。
•图片的尺寸和大小,还有每张图片会占据多少内存空间。
•图片被访问的频率有多高?会不会有一些图片的访问频率比其它图片要高?如果有的话,你也许应该让一些图片常驻在内存当中,或者使用多个LruCache 对象来区分不同组的图片。
•你能维持好数量和质量之间的平衡吗?有些时候,存储多个低像素的图片,而在后台去开线程加载高像素的图片会更加的有效。
下面是一个使用LruCache来缓存图片的例子:

private LurCachemMemoryCache;
protected void onCreate(Bundle savedInstanceState){
//获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常
//LruCache通过构造函数传入缓存值,以KB为单位
int maxMemory=(int)(Runtime.getRuntime().maxMemory()/1024);
//使用最大可用内存之值的1/8作为缓存的大小
int cacheSize=maxMemory/8;
memoryCache=new LruCache{
protected int sizeOf(String key,Bitmap bitmap){
//重写此方法来衡量每张图片的大小,默认返回图片的数量
return bitmap.getByteCount()/1024;
}
};
}
public void addBitmapToMemoryCache(String key,Bitmap bitmap){
if(getBitmapFromCache(key)==null){
mMemoryCache.put(key,bitmap);
}
}
public Bitmap getBitmapFromMemoryCache(String key){
return mMemoryCache.get(key);
}

在这个例子当中,使用了系统分配给应用程序的八分之一内存来作为缓存大小。在中高配置的手机当中,这大概会有4兆(32/8)的缓存空间。一个全屏幕的 GridView 使用4张 800x480分辨率的图片来填充,则大概会占用1.5兆的空间(800*480*4)。因此,这个缓存大小可以存储2.5页的图片。
当向 ImageView 中加载一张图片时,首先会在 LruCache 的缓存中进行检查。如果找到了相应的键值,则会立刻更新ImageView ,否则开启一个后台线程来加载这张图片。
01.public void loadBitmap(int resId, ImageView imageView) {
02. final String imageKey = String.valueOf(resId);
03. final Bitmap bitmap = getBitmapFromMemCache(imageKey);
04. if (bitmap != null) {
05. imageView.setImageBitmap(bitmap);
06. } else {
07. imageView.setImageResource(R.drawable.image_placeholder);
08. BitmapWorkerTask task = new BitmapWorkerTask(imageView);
09. task.execute(resId);
10. }
11.}
BitmapWorkerTask 还要把新加载的图片的键值对放到缓存中。
Class BitmapWorkerTask extends AsyncTask{
//在后台加载图片
protected Bitmap doInBackground(Integer...params){
final Bitmap bitmap=decodeSampledBitmapFromResource(
getReources(),params[0],100,100);
addBitmapToMemoryCache(String.valueOf(params[0]),bitmap);
return bitmap;
}
}

掌握了以上两种方法,不管是要在程序中加载超大图片,还是要加载大量图片,都不用担心OOM的问题了。

七、使用硬盘缓存

  一个内存缓存对加速访问最近浏览过的Bitmap非常有帮助,但是你不能局限于内存中的可用图片。

GridView这样有着更大的数据集的组件可以很轻易消耗掉内存缓存。你的应用有可能在执行其他任务(如打电话)的时候被打断,并且在后台的任务有可能被杀死或者缓存被释放掉。一旦用户重新聚焦(resume)到你的应用,你得再次处理每一张图片。
在这种情况下,硬盘缓存可以用来存储Bitmap并在图片被内存缓存释放后减小图片加载的时间(次数)。当然,从硬盘加载图片比内存要慢,并且应该在后台线程进行,因为硬盘读取的时间是不可预知的。
注意:如果访问图片的次数非常频繁,那么ContentProvider可能更适合用来存储缓存图片,例如Image Gallery这样的应用程序。
这个类中的示例代码使用DiskLruCache实现。在示例代码中,除了已有的内存缓存,还添加了硬盘缓存。


private DiskLruCache mDiskLruCache;
private final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails";
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// Initialize memory cache
...
// Initialize disk cache on background thread
File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
new InitDiskCacheTask().execute(cacheDir);
}
class InitDiskCacheTask extends AsyncTask {
@Override
protected Void doInBackground(File... params) {
synchronized (mDiskCacheLock) {
File cacheDir = params[0];
mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
mDiskCacheStarting = false; // Finished initialization
mDiskCacheLock.notifyAll(); // Wake any waiting threads
}
return null;
}
}



class BitmapWorkerTask extends AsyncTask {
...
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
final String imageKey = String.valueOf(params[0]);
    // Check disk cache in background thread
    Bitmap bitmap = getBitmapFromDiskCache(imageKey);

    if (bitmap == null) { // Not found in disk cache
        // Process as normal
        final Bitmap bitmap = decodeSampledBitmapFromResource(
                getResources(), params[0], 100, 100));
    }

    // Add final bitmap to caches
    addBitmapToCache(imageKey, bitmap);

    return bitmap;
}
...

}



public void addBitmapToCache(String key, Bitmap bitmap) {
// Add to memory cache as before
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
// Also add to disk cache
synchronized (mDiskCacheLock) {
if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
mDiskLruCache.put(key, bitmap);
}
}
}
public Bitmap getBitmapFromDiskCache(String key) {
synchronized (mDiskCacheLock) {
// Wait while disk cache is started from background thread
while (mDiskCacheStarting) {
try {
mDiskCacheLock.wait();
} catch (InterruptedException e) {}
}
if (mDiskLruCache != null) {
return mDiskLruCache.get(key);
}
}
return null;
}
// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
public static File getDiskCacheDir(Context context, String uniqueName) {
// Check if media is mounted or storage is built-in, if so, try and use external cache dir
// otherwise use internal cache dir
final String cachePath =
Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
!isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
context.getCacheDir().getPath();
return new File(cachePath + File.separator + uniqueName);
}

八、处理配置更改

 运行时的配置会发生变化,例如屏幕方向的改变,会导致Android销毁并以新的配置重新启动Activity(关于此问题的更多信息,请参阅Handling Runtime Changes)。为了让用户有着流畅而快速的体验,你需要在配置发生改变的时候避免再次处理所有的图片。
 幸运的是,你在“使用内存缓存”一节中为Bitmap构造了很好的内存缓存。这些内存可以通过使用Fragment传递到信的Activity(活动)实例,这个Fragment可以调用setRetainInstance(true)方法保留下来。在Activity(活动)被重新创建后,你可以在上面的Fragment中访问到已经存在的缓存对象,使得图片能快加载并重新填充到ImageView对象中。
 下面是一个使用Fragment将LruCache对象保留在配置更改中的示例:

private LruCache mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
RetainFragment mRetainFragment =
RetainFragment.findOrCreateRetainFragment(getFragmentManager());
mMemoryCache = RetainFragment.mRetainedCache;
if (mMemoryCache == null) {
mMemoryCache = new LruCache(cacheSize) {
... // Initialize cache here as usual
}
mRetainFragment.mRetainedCache = mMemoryCache;
}
...
}
class RetainFragment extends Fragment {
private static final String TAG = "RetainFragment";
public LruCache mRetainedCache;
public RetainFragment() {}
public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
if (fragment == null) {
fragment = new RetainFragment();
}
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
}

为了测试这个,可以在不适用Fragment的情况下旋转设备屏幕。在保留缓存的情况下,你应该能发现填充图片到Activity中几乎是瞬间从内存中取出而没有任何延迟的感觉。任何图片优先从内存缓存获取,没有的话再到硬盘缓存中找,如果都没有,那就以普通方式加载图片。 
Bitmap是Android系统中的图像处理的最重要类之一。用它可以获取图像文件信息,进行图像剪切、旋转、缩放等操作,并可以指定格式保存图像文件。本文从应用的角度,着重介绍怎么用Bitmap来实现这些功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值