图形与动画高效显示位图.docx
- 文档编号:12950770
- 上传时间:2023-06-09
- 格式:DOCX
- 页数:35
- 大小:36.87KB
图形与动画高效显示位图.docx
《图形与动画高效显示位图.docx》由会员分享,可在线阅读,更多相关《图形与动画高效显示位图.docx(35页珍藏版)》请在冰点文库上搜索。
图形与动画高效显示位图
Android官方中文教程
构建应用——图形与动画
高效显示位图
学习如何使用常用技术处理和加载位图对象,并让你的用户界面(UI)保持响应,同时避免超过应用程序的内存限制。
如果你不谨慎处理,位图会快速地消耗可用内存预算,并因为可怕的异常导致应用程序崩溃:
java.lang.OutofMemoryError:
bitmapsizeexceedsVMbudget.
有几个原因导致在Android应用程序中加载位图很棘手:
●移动设备的系统资源通常有限制,Android设备用于单个应用程序的内存只有16M。
Android兼容定义文档(CDD)3.7节“虚拟机的兼容性”,为不同屏幕大小和密度给出了所需的最低应用程序内存。
应用程序应该在这个最低内存限制下优化执行。
无论如何,记住很多设备配置都有较高地限制。
●位图占用大量内存,尤其是丰富的图像照片。
例如,GalaxyNexus上的照相机可以拍摄高达2592x1936像素(500万像素)的照片。
如果位图结构采用ARGB_8888(默认从Android2.3开始),把图像加载到内存中需要19M内存(2592x1936x4字节),这会立即超过某些设备对App的限制。
●Android应用程序的UI经常需要同时加载几幅位图,ListView、GridView和ViewPager通常都会同时包含多幅显示在屏幕上的位图,以及更多潜在地随着手指动作准备显示出来的离屏位图。
高效加载大位图
图像有各种形状和大小。
很多情况下它们都比典型的应用程序UI所需要的更大。
例如,系统的“画廊”应用程序显示使用Android设备上的相机拍摄的照片,通常比设备的屏幕密度有更高的分辨率。
鉴于使用内存有限,最理想的是只加载低分辨率的版本到内存中。
低分辨率的版本应该匹配显示它的UI组件的大小。
一个高分辨率的图像不会提供任何可见的好处,但仍然占用宝贵的内存,并由于额外的缩放而导致额外性能开销。
这节课将引导你完成解码大位图,通过加载内存中较小的二次采样版本,而不超过每个应用程序的内存限制。
读取位图大小和类型
BitmapFactory类提供了几个解码方法(decodeByteArray(),decodeFile(),decodeResource()等等)从多种资源中创建位图,基于图像数据来源选择最恰当的解码方法。
这些方法尝试为构造位图分配内存,因而很容易导致OutOnMemory异常。
每种解码方法都有一个附加签名让你通过BitmapFactory.Options类指定解码选项。
解码时设置inJustDecodeBounds属性为true以避免内存分配,这会给位图对象返回null,但设置了outWidth,outHeight和outMimeType的值。
这种技术允许在位图构建(以及分配内存)之前读取图像数据的大小和类型。
BitmapFactory.Optionsoptions=newBitmapFactory.Options();
options.inJustDecodeBounds=true;
BitmapFactory.decodeResource(getResources(),R.id.myimage,options);
intimageHeight=options.outHeight;
intimageWidth=options.outWidth;
StringimageType=options.outMimeType;
要避免java.lang.OutofMemory异常,在解码之前检查位图大小。
除非你绝对相信随可预计大小的图像数据提供给你的来源,正好适合可用内存。
加载缩小版到内存中
现在图像的大小已经知道了,这可以决定是加载完整的图像到内存中,还是加载抽样版到内存中。
这要考虑某些因素:
●预计加载完整图像要占用的内存。
●考虑到你的应用程序中任何其它内存需求,你愿意提交给加载这个图像的内存数量。
●ImageView目标或是将要加载图像的UI组件的大小。
●当前设备的屏幕大小和密度。
例如,如果最终要在ImageView中显示128x96像素的缩略图,那就不值得把一个1024x768像素的图像加载到内存中。
要告诉解码器对图像二次抽样,加载一个较小的版本到内存中,在你的BitmapFactory.Options对象中设置inSampleSize为true。
例如,一个分辨率为2048x1536的图像,inSampleSize等于4,解码后生成大约512x384的位图。
相比加载完整图像需要12MB内存,加载这个只需要0.75MB内存(假设位图结构是ARGB_8888)。
下面是一个计算样本大小的方法,样本大小的值是2的幂,基于目标宽度和高度。
publicstaticintcalculateInSampleSize(
BitmapFactory.Optionsoptions,intreqWidth,intreqHeight){
//图像原始高度和宽度
finalintheight=options.outHeight;
finalintwidth=options.outWidth;
intinSampleSize=1;
if(height>reqHeight||width>reqWidth){
finalinthalfHeight=height/2;
finalinthalfWidth=width/2;
//计算inSampleSize的最大值,它是2的幂,
//同时保持高度和宽度大于所需的高度和宽度。
while((halfHeight/inSampleSize)>reqHeight
&&(halfWidth/inSampleSize)>reqWidth){
inSampleSize*=2;
}
}
returninSampleSize;
}
注意:
按照isSampleSize的文档说明,计算结果是2的幂,因为解码器使用的最终值会被四舍五入到最接近的2的幂值。
要使用这个方法,首次解码时把inJustDecodeBounds的值设为true,并传递options参数,然后把inJustDecodeBounds设为false,使用新的isSampleSize值再次解码:
publicstaticBitmapdecodeSampledBitmapFromResource(Resourcesres,intresId,
intreqWidth,intreqHeight){
//用inJustDecodeBounds=true进行首次解码以检查大小
finalBitmapFactory.Optionsoptions=newBitmapFactory.Options();
options.inJustDecodeBounds=true;
BitmapFactory.decodeResource(res,resId,options);
//计算inSampleSize
options.inSampleSize=calculateInSampleSize(options,reqWidth,reqHeight);
//用inSampleSize的设定解码位图
options.inJustDecodeBounds=false;
returnBitmapFactory.decodeResource(res,resId,options);
}
这种方法可以把任意大小的位图加载到ImageView中,显示为100x100的缩略图,如下例所示:
mImageView.setImageBitmap(
decodeSampledBitmapFromResource(getResources(),R.id.myimage,100,100));
你可以采用类似的过程解码来自其它资源的位图,以取代BitmapFactory.decode*方法。
在UI线程之外处理位图
如果资源数据来自于磁盘和网络(或任何除了内存之外的数据源),在上节课中讨论的BitmapFactory.decode*方法不应该在UI主线程中执行。
加载数据所需的时间是不可预测的,取决于多种因素(磁盘或网络的读取速度、图像大小、CPU的能力等等)。
如果其中一个任务阻塞UI线程,系统会标记你的应用程序为无响应,用户会选择关闭它。
这节课将引导你完成在后台线程中处理位图,并演示如何处理并发问题。
使用AsyncTask
AsyncTask类对在后台执行某些工作及在UI线程上公布结果提供了一种容易的方法。
要使用它,创建一个它的子类并重写提供的方法。
下面一个使用AsyncTask和decodeSampleBitmapFromResource()方法把大位图加载到ImageView中的例子:
classBitmapWorkerTaskextendsAsyncTask
privatefinalWeakReference
privateintdata=0;
publicBitmapWorkerTask(ImageViewimageView){
//使用WeakReference以确保ImageView能被垃圾回收
imageViewReference=newWeakReference
}
//在后台解码图像
@Override
protectedBitmapdoInBackground(Integer...params){
data=params[0];
returndecodeSampledBitmapFromResource(getResources(),data,100,100));
}
//一旦完成,如果ImageView仍然存在则设置位图
@Override
protectedvoidonPostExecute(Bitmapbitmap){
if(imageViewReference!
=null&&bitmap!
=null){
finalImageViewimageView=imageViewReference.get();
if(imageView!
=null){
imageView.setImageBitmap(bitmap);
}
}
}
}
指定给ImageView的WeakReference确保AsyncTask不会阻止ImageView以及它引用的任何东西被垃圾回收。
这不保证当任务完成时ImageView仍然存在,所以你必须在onPostExecute中检查引用。
如果用户从Activity中导航走,或是在任务完成之前配置发生变化,ImageView可能不再存在。
开始异步加载位图,直接创建新的任务并执行它。
publicvoidloadBitmap(intresId,ImageViewimageView){
BitmapWorkerTasktask=newBitmapWorkerTask(imageView);
task.execute(resId);
}
处理并发
在上一节中讨论的,把通用视图组件如GridView和ListView与AsyncTask结合使用时出现另一个问题。
为了有效利用内存,这些组件在用户滚屏时回收子视图。
如果每个子视图触发一个AsyncTask,当它完成时不能保证,为了在另一个子视图中使用,关联的视图没有被回收。
此外,也不能保证异步任务开始的顺序和完成的顺序一致。
博文“用多线程优化性能”深入探讨了并发行为,并且提供了一个解决方案:
当任务完成时,ImageView储存一个引用到最近的并在稍后能检查的AsyncTask中。
使用类似的方法,上一节的AsyncTask可以被扩展为下面的样例。
创建一个专用的Drawable子类来储存关联到工作任务的引用。
在这个案例中,当任务完成时,ImageView可以显示用BitmapDrawable做占位符的图像。
staticclassAsyncDrawableextendsBitmapDrawable{
privatefinalWeakReference
publicAsyncDrawable(Resourcesres,Bitmapbitmap,
BitmapWorkerTaskbitmapWorkerTask){
super(res,bitmap);
bitmapWorkerTaskReference=
newWeakReference
}
publicBitmapWorkerTaskgetBitmapWorkerTask(){
returnbitmapWorkerTaskReference.get();
}
}
在执行BitmapWorkerTask之前,创建一个AsyncDrawable并把它绑定到目标ImageView。
publicvoidloadBitmap(intresId,ImageViewimageView){
if(cancelPotentialWork(resId,imageView)){
finalBitmapWorkerTasktask=newBitmapWorkerTask(imageView);
finalAsyncDrawableasyncDrawable=
newAsyncDrawable(getResources(),mPlaceHolderBitmap,task);
imageView.setImageDrawable(asyncDrawable);
task.execute(resId);
}
}
上面示例代码中引用的cancelPotentialWork方法用来检查另一个运行中的任务是否已经关联到ImageView,如果是,它尝试调用cancel()方法中止它。
少数情况下,新的任务数据会匹配已经存在的任务,并且不需要进一步发生。
下面是一个cancelPotentialWork的实现:
publicstaticbooleancancelPotentialWork(intdata,ImageViewimageView){
finalBitmapWorkerTaskbitmapWorkerTask=getBitmapWorkerTask(imageView);
if(bitmapWorkerTask!
=null){
finalintbitmapData=bitmapWorkerTask.data;
if(bitmapData!
=data){
//取消上一个任务
bitmapWorkerTask.cancel(true);
}else{
//相同工作已经在处理中
returnfalse;
}
}
//没有任务关联到ImageView,或存在的任务被取消
returntrue;
}
辅助方法getBitmapWorkerTask用来接收ImageView关联的任务:
privatestaticBitmapWorkerTaskgetBitmapWorkerTask(ImageViewimageView){
if(imageView!
=null){
finalDrawabledrawable=imageView.getDrawable();
if(drawableinstanceofAsyncDrawable){
finalAsyncDrawableasyncDrawable=(AsyncDrawable)drawable;
returnasyncDrawable.getBitmapWorkerTask();
}
}
returnnull;
}
最后一步是更新BitmapWorkerTask中的onPostExecute(),检查任务是否被取消以及当前任务是否匹配ImageView关联的那个。
这个实现适用于ListView和GridView组件及其它回收子视图的任何组件。
直接调用loadBitmap设置ImageView的图像。
例如:
在GridView的实现中,应该在内部适配器的getView()方法中调用loadBitmap。
缓存位图
加载单幅位图比较简单,但是要同时加载一组大位图就比较复杂了。
在很多情况下(比如ListView、GridView和ViewPager组件),当前屏幕上的图像和随着滚屏出现在屏幕上的图像,其总数本质上是无限的。
内存的使用由组件控制,例如子视图移出屏幕时回收它们。
假如你不需要任何长期引用,垃圾回收也会释放你加载的位图。
这非常不错,但是为了保持UI的流畅和快速加载,你希望避免在图像回到屏幕时频繁处理这些图像。
内存和磁盘缓存可以提供这方面的帮助,允许组件快速加载已经处理过的图像。
这节课将引导你完成当加载多幅位图时,使用内存和磁盘缓存改善UI的响应性和流畅度。
使用内存缓存
内存缓存花费宝贵的应用程序内存提供快速访问位图。
LruCache(API级别4也在支持库中允许使用)特别适合缓存位图的任务。
在一个强引用的LinkedHashMap中保存最近引用的对象并在缓存超过它的设计大小之前移除最近最少使用的成员。
注意:
过去,流行的内存缓存是用SoftReference或WeakReference位图缓存实现的,但是不建议这样做。
开始于Android2.3(API级别9)的垃圾回收对软引用和弱引用的回收更加积极,这使得它们相应无效。
另外,在Android3.0(API级别11)之前,位图的支持数据储存在本机内存中,不会以可预见的方式释放,这可能导致应用程序超过其内存限制并崩溃。
为了给LruCache选择合适的大小,应该考虑以下几个因素:
●有多少内存集中在你其余的Activity或App中?
●有多少图像同时显示在屏幕上?
有多少图像准备显示在屏幕上?
●设备的屏幕大小和密度是什么?
一个超高密度(xhdpi)的设备比如GalaxyNexus比高密度(hdpi)设备比如NexusS,保存相同数量的图像需要更大的缓存。
●位图的尺寸和结构是什么?
每幅图像占用的内存是多少?
●图像的访问有多频繁?
某些图像是否比其它图像访问得更加频繁?
如果是,也许你可以始终在内存中保留一定的缓存项,甚至为不同的图像组保存多个LruCache对象。
●你能平衡质量与数量吗?
有时候存储大量低质量的位图更有用,在其它后台任务中加载高质量版本。
没有特定的大小或公式适合于所有的应用程序,你应该自己来分析你的使用并得出一个合适的解决方案。
缓存太小会导致额外的开销并且没有任何好处,缓存太大会再次导致java.lang.OutOfMemory异常并给你其余的App留下较少的内存。
下面是一个为位图设置LruCache的例子:
privateLruCache
@Override
protectedvoidonCreate(BundlesavedInstanceState){
...
//获取最大可用VM内存,超过这个数量将抛出OutOfMemory异常
//以千字节储存,LruCache在构造函数中获取一个整数
finalintmaxMemory=(int)(Runtime.getRuntime().maxMemory()/1024);
//使用可用内存的1/8做内存缓存
finalintcacheSize=maxMemory/8;
mMemoryCache=newLruCache
@Override
protectedintsizeOf(Stringkey,Bitmapbitmap){
//缓存大小以千字节计算而不是字节数
//numberofitems.
returnbitmap.getByteCount()/1024;
}
};
...
}
publicvoidaddBitmapToMemoryCache(Stringkey,Bitmapbitmap){
if(getBitmapFromMemCache(key)==null){
mMemoryCache.put(key,bitmap);
}
}
publicBitmapgetBitmapFromMemCache(Stringkey){
returnmMemoryCache.get(key);
}
注意:
在这个例子中,应用程序内存的八分之一被分配为缓存。
在一个正常/hdpi设备上最低约为4MB(32/8)。
在800x480分辨率的设备上,一个用图像填充的满屏GridView大约要使用1.5MB(800*480*4字节)内存,所以,这最少能在内存中缓存大约2.5页图像。
当加载位图到ImageView时,首先检查LruCache,如果找到对应项,则立即用它更新ImageView,否则一个后台线程被
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 图形 动画 高效 显示 位图