Android高效安全加载图片的方法详解
1.概述
在Android应用程序的设计中,几乎不可避免地都需要加载和显示图片,由于不同的图片在大小上千差万别,有些图片可能只需要几十KB的内存空间,有些图片却需要占用几十MB的内存空间;或者一张图片不需要占用太多的内存,但是需要同时加载和显示多张图片。
在这些情况下,加载图片都需要占用大量的内存,而Android系统分配给每个进程的内存空间是有限的,如果加载的图片所需要的内存超过了限制,进程就会出现OOM,即内存溢出。
本文针对加载大图片或者一次加载多张图片等两种不同的场景,采用不同的加载方式,以尽量避免可能导致的内存溢出问题。
下面话不多说了,来一起看看详细的介绍吧
2.加载大图片
有时一张图片的加载和显示就需要占用大量的内存,例如图片的大小是2592x1936,同时采用的位图配置是ARGB_8888,其在内存中需要的大小是2592x1936x4字节,大概是19MB。仅仅加载这样一张图片就可能会超过进程的内存限制,进而导致内存溢出,所以在实际使用时肯定无法直接加载到内存中。
为了避免内存溢出,根据不同的显示需求,采取不同的加载方式:
- 显示一张图片的全部内容:对原图片进行压缩显示。
- 显示一张图片的部分内容:对原图片进行局部显示。
2.1图片压缩显示
图片的压缩显示指的是对原图片进行长宽的压缩,以减少图片的内存占用,使其能够在应用上正常显示,同时保证在加载和显示过程中不会出现内存溢出的情况。
BitmapFactory是一个创建Bitmap对象的工具类,使用它可以利用不同来源的数据生成Bitamp对象,在创建过的过程中还可以对需要生成的对象进行不同的配置和控制,BitmapFactory的类声明如下:
CreatesBitmapobjectsfromvarioussources,includingfiles,streams,andbyte-arrays.
由于在加载图片前,是无法提前预知图片大小的,所以在实际加载前必须根据图片的大小和当前进程的内存情况来决定是否需要对图片进行压缩,如果加载原图片所需的内存空间已经超过了进程打算提供或可以提供的内存大小,就必须考虑压缩图片。
2.1.1确定原图片长宽
简单来说,压缩图片就是对原图的长宽按照一定的比例进行缩小,所以首先要确定原图的长宽信息。为了获得图片的长宽信息,利用BitmapFactory.decodeResource(Resourcesres,intid,Optionsopts)接口,其声明如下:
/** *Synonymforopeningthegivenresourceandcalling *{@link#decodeResourceStream}. * *@paramresTheresourcesobjectcontainingtheimagedata *@paramidTheresourceidoftheimagedata *@paramoptsnull-ok;Optionsthatcontroldownsamplingandwhetherthe *imageshouldbecompletelydecoded,orjustissizereturned. *@returnThedecodedbitmap,ornulliftheimagedatacouldnotbe *decoded,or,ifoptsisnon-null,ifoptsrequestedonlythe *sizebereturned(inopts.outWidthandopts.outHeight) *@throwsIllegalArgumentExceptionif{@linkBitmapFactory.Options#inPreferredConfig} *is{@linkandroid.graphics.Bitmap.Config#HARDWARE} *and{@linkBitmapFactory.Options#inMutable}isset,ifthespecifiedcolorspace *isnot{@linkColorSpace.Model#RGBRGB},orifthespecifiedcolorspace'stransfer *functionisnotan{@linkColorSpace.Rgb.TransferParametersICCparametriccurve} */ publicstaticBitmapdecodeResource(Resourcesres,intid,Optionsopts){
通过这个函数声明,可以看到通过这个接口可以得到图片的长宽信息,同时由于返回null并不申请内存空间,避免了不必要的内存申请。
为了得到图片的长宽信息,必须传递一个Options参数,其中的inJustDecodeBounds设置为true,其声明如下:
/**
*Ifsettotrue,thedecoderwillreturnnull(nobitmap),but
*theout...
fieldswillstillbeset,allowingthecallerto
*querythebitmapwithouthavingtoallocatethememoryforitspixels.
*/
publicbooleaninJustDecodeBounds;
下面给出得到图片长宽信息的示例代码:
BitmapFactory.Optionsoptions=newBitmapFactory.Options(); //指定在解析图片文件时,仅仅解析边缘信息而不创建bitmap对象。 options.inJustDecodeBounds=true; //R.drawable.test是使用的2560x1920的测试图片资源文件。 BitmapFactory.decodeResource(getResources(),R.drawable.test,options); intwidth=options.outWidth; intheight=options.outHeight; Log.i(TAG,"width:"+width+",height:"+height);
在实际测试中,得到的长宽信息如下:
01-0504:06:23.0222983629836IAndroid_Test:width:2560,height:1920
2.1.2确定目标压缩比例
得知原图片的长宽信息后,为了能够进行后续的压缩操作,必须要先确定目标压缩比例。所谓压缩比例就是指要对原始的长宽进行的裁剪比例,如果如果原图片是2560x1920,采取的压缩比例是4,进行压缩后的图片是640x480,最终大小是原图片的1/16。
压缩比例在BitmapFactory.Options中对应的属性是inSampleSize,其声明如下:
/** *Ifsettoavalue>1,requeststhedecodertosubsampletheoriginal *image,returningasmallerimagetosavememory.Thesamplesizeis *thenumberofpixelsineitherdimensionthatcorrespondtoasingle *pixelinthedecodedbitmap.Forexample,inSampleSize==4returns *animagethatis1/4thewidth/heightoftheoriginal,and1/16the *numberofpixels.Anyvalue<=1istreatedthesameas1.Note:the *decoderusesafinalvaluebasedonpowersof2,anyothervaluewill *beroundeddowntothenearestpowerof2. */ publicintinSampleSize;
需要特别注意的是,inSampleSize只能是2的幂,如果传入的值不满足条件,解码器会选择一个和传入值最节俭的2的幂;如果传入的值小于1,解码器会直接使用1。
要确定最终的压缩比例,首先要确定目标大小,即压缩后的目标图片的长宽信息,根据原始长宽和目标长宽来选择一个最合适的压缩比例。下面给出示例代码:
/** *@paramoriginWidththewidthoftheoriginbitmap *@paramoriginHeighttheheightoftheoriginbitmap *@paramdesWidththemaxwidthofthedesiredbitmap *@paramdesHeightthemaxheightofthedesiredbitmap *@returntheoptimalsamplesizetomakesurethesizeofbitmapisnotmorethanthedesired. */ publicstaticintcalculateSampleSize(intoriginWidth,intoriginHeight,intdesWidth,intdesHeight){ intsampleSize=1; intwidth=originWidth; intheight=originHeight; while((width/sampleSize)>desWidth&&(height/sampleSize)>desHeight){ sampleSize*=2; } returnsampleSize; }
需要注意的是这里的desWidth和desHeight是目标图片的最大长宽值,而不是最终的大小,因为通过这个方法确定的压缩比例会保证最终的图片长宽不大于目标值。
在实际测试中,把原图片大小设置为2560x1920,把目标图片大小设置为100x100:
intsampleSize=BitmapCompressor.calculateSampleSize(2560,1920,100,100); Log.i(TAG,"sampleSize:"+sampleSize);
测试结果如下:
01-0504:42:07.752 8835 8835IAndroid_Test:sampleSize:32
最终得到的压缩比例是32,如果使用这个比例去压缩2560x1920的图片,最终得到80x60的图片。
2.1.3压缩图片
在前面两部分,分别确定了原图片的长宽信息和目标压缩比例,其实确定原图片的长宽也是为了得到压缩比例,既然已经得到的压缩比较,就可以进行实际的压缩操作了,只需要把得到的inSampleSize通过Options传递给BitmapFactory.decodeResource(Resourcesres,intid,Optionsopts)即可。
下面是示例代码:
publicstaticBitmapcompressBitmapResource(Resourcesres,intresId,intinSampleSize){ BitmapFactory.Optionsoptions=newBitmapFactory.Options(); options.inJustDecodeBounds=false; options.inSampleSize=inSampleSize; returnBitmapFactory.decodeResource(res,resId,options); }
2.2图片局部显示
图片压缩会在一定程度上影响图片质量和显示效果,在某些场景下并不可取,例如地图显示时要求必须是高质量图片,这时就不能进行压缩处理,在这种场景下其实并不要求要一次显示图片的所有部分,可以考虑一次只加载和显示图片的特定部分,即***局部显示***。
要实现局部显示的效果,可以使用BitmapRegionDecoder来实现,它就是用来对图片的特定部分进行显示的,尤其是在原图片特别大而无法一次全部加载到内存的场景下,其声明如下:
/** *BitmapRegionDecodercanbeusedtodecodearectangleregionfromanimage. *BitmapRegionDecoderisparticularlyusefulwhenanoriginalimageislargeand *youonlyneedpartsoftheimage. * *TocreateaBitmapRegionDecoder,callnewInstance(...). *GivenaBitmapRegionDecoder,userscancalldecodeRegion()repeatedly *togetadecodedBitmapofthespecifiedregion. * */ publicfinalclassBitmapRegionDecoder{...}
这里也说明了如果使用BitmapRegionDecoder进行局部显示:首先通过newInstance()创建实例,再利用decodeRegion()对指定区域的图片内存创建Bitmap对象,进而在显示控件中显示。
通
过BitmapRegionDecoder.newInstance()创建解析器实例,其函数声明如下:
/** *CreateaBitmapRegionDecoderfromaninputstream. *Thestream'spositionwillbewhereeveritwasaftertheencodeddata *wasread. *CurrentlyonlytheJPEGandPNGformatsaresupported. * *@paramisTheinputstreamthatholdstherawdatatobedecodedintoa *BitmapRegionDecoder. *@paramisShareableIfthisistrue,thentheBitmapRegionDecodermaykeepa *shallowreferencetotheinput.Ifthisisfalse, *thentheBitmapRegionDecoderwillexplicitlymakeacopyofthe *inputdata,andkeepthat.Evenifsharingisallowed, *theimplementationmaystilldecidetomakeadeep *copyoftheinputdata.Ifanimageisprogressivelyencoded, *allowingsharingmaydegradethedecodingspeed. *@returnBitmapRegionDecoder,ornulliftheimagedatacouldnotbedecoded. *@throwsIOExceptioniftheimageformatisnotsupportedorcannotbedecoded. * *Priorto{@linkandroid.os.Build.VERSION_CODES#KITKAT}, *if{@linkInputStream#markSupportedis.markSupported()}returnstrue, * is.mark(1024)
wouldbecalled.Asof *{@linkandroid.os.Build.VERSION_CODES#KITKAT},thisisnolongerthecase. */ publicstaticBitmapRegionDecodernewInstance(InputStreamis, booleanisShareable)throwsIOException{...}
需要注意的是,这只是BitmapRegionDecoder其中一个newInstance函数,除此之外还有其他的实现形式,读者有兴趣可以自己查阅。
在创建得到BitmapRegionDecoder实例后,可以调用decodeRegion方法来创建局部Bitmap对象,其函数声明如下:
/** *Decodesarectangleregionintheimagespecifiedbyrect. * *@paramrectTherectanglethatspecifiedtheregiontobedecode. *@paramoptionsnull-ok;Optionsthatcontroldownsampling. *inPurgeableisnotsupported. *@returnThedecodedbitmap,ornulliftheimagedatacouldnotbe *decoded. *@throwsIllegalArgumentExceptionif{@linkBitmapFactory.Options#inPreferredConfig} *is{@linkandroid.graphics.Bitmap.Config#HARDWARE} *and{@linkBitmapFactory.Options#inMutable}isset,ifthespecifiedcolorspace *isnot{@linkColorSpace.Model#RGBRGB},orifthespecifiedcolorspace'stransfer *functionisnotan{@linkColorSpace.Rgb.TransferParametersICCparametriccurve} */ publicBitmapdecodeRegion(Rectrect,BitmapFactory.Optionsoptions){...}
由于这部分比较简单,下面直接给出相关示例代码:
//解析得到原图的长宽值,方便后面进行局部显示时指定需要显示的区域。 BitmapFactory.Optionsoptions=newBitmapFactory.Options(); options.inJustDecodeBounds=true; BitmapFactory.decodeResource(getResources(),R.drawable.test,options); intwidth=options.outWidth; intheight=options.outHeight; try{ //创建局部解析器 InputStreaminputStream=getResources().openRawResource(R.drawable.test); BitmapRegionDecoderdecoder=BitmapRegionDecoder.newInstance(inputStream,false); //指定需要显示的矩形区域,这里要显示的原图的左上1/4区域。 Rectrect=newRect(0,0,width/2,height/2); //创建位图配置,这里使用RGB_565,每个像素占2字节。 BitmapFactory.OptionsregionOptions=newBitmapFactory.Options(); regionOptions.inPreferredConfig=Bitmap.Config.RGB_565; //创建得到指定区域的Bitmap对象并进行显示。 BitmapregionBitmap=decoder.decodeRegion(rect,regionOptions); ImageViewimageView=(ImageView)findViewById(R.id.main_image); imageView.setImageBitmap(regionBitmap); }catch(Exceptione){ e.printStackTrace(); }
从测试结果看,确实只显示了原图的左上1/4区域的图片内容,这里不再贴出结果。
3.加载多图片
有时需要在应用中同时显示多张图片,例如使用ListView,GridView和ViewPager时,可能会需要在每一项都显示一个图片,这时情况就会变得复杂些,因为可以通过滑动改变控件的可见项,如果每增加一个可见项就加载一个图片,同时不可见项的图片继续在内存中,随着不断的增加,就会导致内存溢出。
为了避免这种情况的内存溢出问题,就需要对不可见项对应的图片资源进行回收,即当前项被滑出屏幕的显示区域时考虑回收相关的图片,这时回收策略对整个应用的性能有较大影响。
- 立即回收:在当前项被滑出屏幕时立即回收图片资源,但如果被滑出的项很快又被滑入屏幕,就需要重新加载图片,这无疑会导致性能的下降。
- 延迟回收:在当前项被滑出屏幕时不立即回收,而是根据一定的延迟策略进行回收,这时对延迟策略有较高要求,如果延迟时间太短就退回到立即回收状况,如果延迟时间较长就可能导致一段时间内,内存中存在大量的图片,进而引发内存溢出。通过上面的分析,针对加载多图的情况,必须要采取延迟回收,而Android提供了一中基于LRU,即最近最少使用策略的内存缓存技术:LruCache,其基本思想是,以强引用的方式保存外界对象,当缓存空间达到一定限制后,再把最近最少使用的对象释放回收,保证使用的缓存空间始终在一个合理范围内。
其声明如下:
/** *Acachethatholdsstrongreferencestoalimitednumberofvalues.Eachtime *avalueisaccessed,itismovedtotheheadofaqueue.Whenavalueis *addedtoafullcache,thevalueattheendofthatqueueisevictedandmay *becomeeligibleforgarbagecollection. */ publicclassLruCache{...}
从声明中,可以了解到其实现LRU的方式:内部维护一个有序队列,每当其中的一个对象被访问就被移动到队首,这样就保证了队列中的对象是根据最近的使用时间从近到远排列的,即队首的对象是最近使用的,队尾的对象是最久之前使用的。正是基于这个规则,如果缓存达到限制后,直接把队尾对象释放即可。
在实际使用中,为了创建LruCache对象,首先要确定该缓存能够使用的内存大小,这是效率的决定性因素。如果缓存内存太小,无法真正发挥缓存的效果,仍然需要频繁的加载和回收资源;如果缓存内存太大,可能导致内存溢出的发生。在确定缓存大小的时候,要结合以下几个因素:
- 进程可以使用的内存情况
- 资源的大小和需要一次在界面上显示的资源数量
- 资源的访问频率
下面给出一个简单的示例:
//获得进程可以使用的最大内存量 intmaxMemory=(int)Runtime.getRuntime().maxMemory(); mCache=newLruCache(maxMemory/4){ @Override protectedintsizeOf(Stringkey,Bitmapvalue){ returnvalue.getByteCount(); } };
在示例中简单地把缓存大小设定为进程可以使用的内存的1/4,当然在实际项目中,要考虑的因素会更多。需要注意的是,在创建LruCache对象的时候需要重写sizeOf方法,它用来返回每个对象的大小,是用来决定当前缓存实际大小并判断是否达到了内存限制。
在创建了LruCache对象后,如果需要使用资源,首先到缓存中去取,如果成功取到就直接使用,否则加载资源并放入缓存中,以方便下次使用。为了加载资源的行为不会影响应用性能,需要在子线程中去进行,可以利用AsyncTask来实现。
下面是示例代码:
publicBitmapget(Stringkey){ Bitmapbitmap=mCache.get(key); if(bitmap!=null){ returnbitmap; }else{ newBitmapAsyncTask().execute(key); returnnull; } } privateclassBitmapAsyncTaskextendsAsyncTask{ @Override protectedBitmapdoInBackground(String...url){ Bitmapbitmap=getBitmapFromUrl(url[0]); if(bitmap!=null){ mCache.put(url[0],bitmap); } returnbitmap; } privateBitmapgetBitmapFromUrl(Stringurl){ Bitmapbitmap=null; //在这里要利用给定的url信息从网络获取bitmap信息. returnbitmap; } }
示例中,在无法从缓存中获取资源的时候,会根据url信息加载网络资源,当前并没有给出完整的代码,有兴趣的同学可以自己去完善。
4.总结
本文主要针对不同的图片加载场景提出了不同的加载策略,以保证在加载和显示过程中既然能满足基本的显示需求,又不会导致内存溢出,具体包括针对单个图片的压缩显示,局部显示和针对多图的内存缓存技术,如若有表述不清甚至错误的地方,请及时提出,大家一起学习。
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。