ImageLoader中如何实现磁盘命名和图片缓存算法
ImageLoader中如何实现磁盘命名和图片缓存算法
这篇文章主要为大家展示了“ImageLoader中如何实现磁盘命名和图片缓存算法”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“ImageLoader中如何实现磁盘命名和图片缓存算法”这篇文章吧。
一. 前言
ImageLoader的图片缓存分成磁盘和内存两种,这里分析一下磁盘缓存以及图片文件名算法的实现
默认是不存储在磁盘上的,需要手动打开开关
如下
DisplayImageOptionsoptions=newDisplayImageOptions.Builder().cacheInMemory(true)//defaultfalse.cacheOnDisk(true)//defaultfalseimageLoader.displayImage("",imageView,options,null,null);
二. 磁盘文件命名
/***Generatesnamesforfilesatdiskcache**@authorSergeyTarasevich(nostra13[at]gmail[dot]com)*@since1.3.1*/publicinterfaceFileNameGenerator{/**GeneratesuniquefilenameforimagedefinedbyURI*/Stringgenerate(StringimageUri);}
接口是FileNameGenerator,此接口非常简单明了,只有一个根据图片uri产生一个图片文件名称的方法。
它包含两个实现类
HashCodeFileNameGenerator
Md5FileNameGenerator
接下来,分别看这两个类的实现
2.1 HashCodeFileNameGenerator
/***NamesimagefileasimageURI{@linkplainString#hashCode()hashcode}**@authorSergeyTarasevich(nostra13[at]gmail[dot]com)*@since1.3.1*/publicclassHashCodeFileNameGeneratorimplementsFileNameGenerator{@OverridepublicStringgenerate(StringimageUri){returnString.valueOf(imageUri.hashCode());}}
实现比较简单,根据uri的hashcode转化成String即可,默认就是Hashcode命名。
2.2 Md5FileNameGenerator
/***NamesimagefileasMD5hashofimageURI**@authorSergeyTarasevich(nostra13[at]gmail[dot]com)*@since1.4.0*/publicclassMd5FileNameGeneratorimplementsFileNameGenerator{privatestaticfinalStringHASH_ALGORITHM="MD5";privatestaticfinalintRADIX=10+26;//10digits+26letters@OverridepublicStringgenerate(StringimageUri){byte[]md5=getMD5(imageUri.getBytes());BigIntegerbi=newBigInteger(md5).abs();returnbi.toString(RADIX);}privatebyte[]getMD5(byte[]data){byte[]hash=null;try{MessageDigestdigest=MessageDigest.getInstance(HASH_ALGORITHM);digest.update(data);hash=digest.digest();}catch(NoSuchAlgorithmExceptione){L.e(e);}returnhash;}}
通过imageUri得到byte数组,然后通过MD5算法得到文件名
三. 磁盘目录选择
一般默认优先选择sdk/android/data/packageName/cache/uil-images卡,如果sdk目录创建失败,那么会选择/data/data/packageName目录
四. 图片缓存示例
其中-1557665659.0和1238391484.0两个就是图片存储文件
journal是操作记录描述性文件,内容如下
DIRTY: 操作记录创建,如果DIRTY后面没有CLEAN或者REMOVE,那么这个图片会被删除。
CLEAN: 记录成功创建和访问
READ: 记录成功访问
REMOVE: 记录删除
五. 磁盘缓存接口
磁盘缓存算法的接口是DiskCache,接口很简单明了。
publicinterfaceDiskCache{FilegetDirectory();Fileget(StringimageUri);booleansave(StringimageUri,InputStreamimageStream,IoUtils.CopyListenerlistener)throwsIOException;booleansave(StringimageUri,Bitmapbitmap)throwsIOException;booleanremove(StringimageUri);voidclose();voidclear();}
方法名 | 解释 |
---|---|
getDirectory() | 获取存储目录 |
get(String imageUri) | 根据imageUri获取图片文件 |
save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) | 保存图片 |
remove(String imageUri) | 删除图片缓存 |
close() | 关闭磁盘缓存,释放资源 |
clear() | 清理所有的磁盘缓存 |
5.1 实现类
下面详细看每个类的实现
六. LruDiskCache
publicclassLruDiskCacheimplementsDiskCache{protectedDiskLruCachecache;...protectedfinalFileNameGeneratorfileNameGenerator;...publicLruDiskCache(FilecacheDir,FilereserveCacheDir,FileNameGeneratorfileNameGenerator,longcacheMaxSize,intcacheMaxFileCount)throwsIOException{...initCache(cacheDir,reserveCacheDir,cacheMaxSize,cacheMaxFileCount);}privatevoidinitCache(FilecacheDir,FilereserveCacheDir,longcacheMaxSize,intcacheMaxFileCount)throwsIOException{try{cache=DiskLruCache.open(cacheDir,1,1,cacheMaxSize,cacheMaxFileCount);}catch(IOExceptione){...}}@OverridepublicFilegetDirectory(){returncache.getDirectory();}@OverridepublicFileget(StringimageUri){DiskLruCache.Snapshotsnapshot=null;try{snapshot=cache.get(getKey(imageUri));returnsnapshot==null?null:snapshot.getFile(0);}catch(IOExceptione){L.e(e);returnnull;}finally{if(snapshot!=null){snapshot.close();}}}@Overridepublicbooleansave(StringimageUri,InputStreamimageStream,IoUtils.CopyListenerlistener)throwsIOException{DiskLruCache.Editoreditor=cache.edit(getKey(imageUri));if(editor==null){returnfalse;}OutputStreamos=newBufferedOutputStream(editor.newOutputStream(0),bufferSize);booleancopied=false;try{copied=IoUtils.copyStream(imageStream,os,listener,bufferSize);}finally{IoUtils.closeSilently(os);if(copied){editor.commit();}else{editor.abort();}}returncopied;}...@Overridepublicbooleanremove(StringimageUri){try{returncache.remove(getKey(imageUri));}catch(IOExceptione){L.e(e);returnfalse;}}@Overridepublicvoidclose(){try{cache.close();}catch(IOExceptione){L.e(e);}cache=null;}@Overridepublicvoidclear(){try{cache.delete();}catch(IOExceptione){L.e(e);}try{initCache(cache.getDirectory(),reserveCacheDir,cache.getMaxSize(),cache.getMaxFileCount());}catch(IOExceptione){L.e(e);}}privateStringgetKey(StringimageUri){returnfileNameGenerator.generate(imageUri);}}
LruDiskCache有几个比较重要的属性,
protectedDiskLruCachecache;protectedfinalFileNameGeneratorfileNameGenerator;
FileNameGenerator就是上面说的文件命名生成器,包括hashcode和md5算法。我们思考下,为什么需要FileNameGenerator?
个人以为网络上面的uri可能是千奇百怪的,甚至包括特殊字符,那作为文件名显然不合适。所以,这个时候来一次hashcode,或者md5转换,获取文件名是最好的。
DiskLruCache,窃以为这个命名不是很好,因为跟LruDiskCache很类似(我第一眼就看成一个东西了!)
这个DiskLruCache很重要,它维护了磁盘图片文件缓存的操作记录,缓存和文件对应关系等。
而且如果你仔细看LruDiskCache的各个方法时会发现,基本都是调用cache的对应方法。
所以,我们主要接下来看DiskLruCache代码
finalclassDiskLruCacheimplementsCloseable{...privatefinalFiledirectory;privatefinalFilejournalFile;...privateWriterjournalWriter;privatefinalLinkedHashMap<String,Entry>lruEntries=newLinkedHashMap<String,Entry>(0,0.75f,true);...}
DiskLruCache包含了journalFile,文件里面具体的含义可以第四点的样例。包含了
LinkedHashMap<String,Entry>lruEntries
表示每个图片的缓存记录,String表示key, Entry表示图片的描述信息
privatefinalclassEntry{privatefinalStringkey;/**Lengthsofthisentry'sfiles.*/privatefinallong[]lengths;/**Trueifthisentryhaseverbeenpublished.*/privatebooleanreadable;/**Theongoingeditornullifthisentryisnotbeingedited.*/privateEditorcurrentEditor;/**Thesequencenumberofthemostrecentlycommittededittothisentry.*/privatelongsequenceNumber;publicFilegetCleanFile(inti){returnnewFile(directory,key+"."+i);}publicFilegetDirtyFile(inti){returnnewFile(directory,key+"."+i+".tmp");}}
我们以保存图片缓存为例,分析下LruDiskCache的工作流程,首先看LruDiskCache的save方法
publicbooleansave(StringimageUri,InputStreamimageStream,IoUtils.CopyListenerlistener)throwsIOException{DiskLruCache.Editoreditor=cache.edit(getKey(imageUri));if(editor==null){returnfalse;}OutputStreamos=newBufferedOutputStream(editor.newOutputStream(0),bufferSize);booleancopied=false;try{copied=IoUtils.copyStream(imageStream,os,listener,bufferSize);}finally{IoUtils.closeSilently(os);if(copied){editor.commit();}else{editor.abort();}}returncopied;}
6.1 getkey(imageUri)
首先根据imageUri生成文件名,也就是key,目前我们用的是hashCode
privateStringgetKey(StringimageUri){returnfileNameGenerator.generate(imageUri);}
6.2 cache.edit
privatesynchronizedEditoredit(Stringkey,longexpectedSequenceNumber)throwsIOException{checkNotClosed();validateKey(key);Entryentry=lruEntries.get(key);if(expectedSequenceNumber!=ANY_SEQUENCE_NUMBER&&(entry==null||entry.sequenceNumber!=expectedSequenceNumber)){returnnull;//Snapshotisstale.}if(entry==null){entry=newEntry(key);lruEntries.put(key,entry);}elseif(entry.currentEditor!=null){returnnull;//Anothereditisinprogress.}Editoreditor=newEditor(entry);entry.currentEditor=editor;//Flushthejournalbeforecreatingfilestopreventfileleaks.journalWriter.write(DIRTY+''+key+'\n');journalWriter.flush();returneditor;}
从lruEntries里面根据key获取到对应的图片Entry对象,如果没有就新建一个。
然后利用journalWriter写入一条DIRTY记录。
6.3 DiskLruCache 打开Dirty图片文件流
publicOutputStreamnewOutputStream(intindex)throwsIOException{synchronized(DiskLruCache.this){if(entry.currentEditor!=this){thrownewIllegalStateException();}if(!entry.readable){written[index]=true;}FiledirtyFile=entry.getDirtyFile(index);FileOutputStreamoutputStream;try{outputStream=newFileOutputStream(dirtyFile);}catch(FileNotFoundExceptione){//Attempttorecreatethecachedirectory.directory.mkdirs();try{outputStream=newFileOutputStream(dirtyFile);}catch(FileNotFoundExceptione2){//Weareunabletorecover.Silentlyeatthewrites.returnNULL_OUTPUT_STREAM;}}returnnewFaultHidingOutputStream(outputStream);}}
publicFilegetDirtyFile(inti){returnnewFile(directory,key+"."+i+".tmp");}
注意这里打开的是drity文件,就是正常的文件后面加上一个.tmp后缀。
6.4 copyStream把网络图片流写入Dirty文件
publicstaticbooleancopyStream(InputStreamis,OutputStreamos,CopyListenerlistener,intbufferSize)throwsIOException{intcurrent=0;inttotal=is.available();if(total<=0){total=DEFAULT_IMAGE_TOTAL_SIZE;}finalbyte[]bytes=newbyte[bufferSize];intcount;if(shouldStopLoading(listener,current,total))returnfalse;while((count=is.read(bytes,0,bufferSize))!=-1){os.write(bytes,0,count);current+=count;if(shouldStopLoading(listener,current,total))returnfalse;}os.flush();returntrue;}
privatestaticbooleanshouldStopLoading(CopyListenerlistener,intcurrent,inttotal){if(listener!=null){booleanshouldContinue=listener.onBytesCopied(current,total);if(!shouldContinue){if(100*current/total<CONTINUE_LOADING_PERCENTAGE){returntrue;//ifloadedmorethan75%thencontinueloadinganyway}}}returnfalse;}
很普通的文件流读写,有意思的是shouldStopLoading,它给了我们一个使用listener终止copy的时机。
publicstaticinterfaceCopyListener{/***@paramcurrentLoadedbytes*@paramtotalTotalbytesforloading*@return<b>true</b>-ifcopyingshouldbecontinued;<b>false</b>-ifcopyingshouldbeinterrupted*/booleanonBytesCopied(intcurrent,inttotal);}
6.5 关闭Dirty文件流
IoUtils.closeSilently(os);
6.6 写入图片文件
假设没有出错,completeEdit里面,会把dirty文件正式名称成图片缓存文件
dirty.renameTo(clean);
然后写入一条CLEAN或者REMOVE操作日志到journal文件中。
具体可以看代码
editor.commit();
publicvoidcommit()throwsIOException{if(hasErrors){completeEdit(this,false);remove(entry.key);//Thepreviousentryisstale.}else{completeEdit(this,true);}committed=true;}
privatesynchronizedvoidcompleteEdit(Editoreditor,booleansuccess)throwsIOException{...for(inti=0;i<valueCount;i++){Filedirty=entry.getDirtyFile(i);if(success){if(dirty.exists()){Fileclean=entry.getCleanFile(i);dirty.renameTo(clean);//保存dirty到正式图片文件longoldLength=entry.lengths[i];longnewLength=clean.length();entry.lengths[i]=newLength;size=size-oldLength+newLength;fileCount++;}}else{deleteIfExists(dirty);}}redundantOpCount++;entry.currentEditor=null;if(entry.readable|success){//写入CLEAN操作日志entry.readable=true;journalWriter.write(CLEAN+''+entry.key+entry.getLengths()+'\n');if(success){entry.sequenceNumber=nextSequenceNumber++;}}else{lruEntries.remove(entry.key);//操作失败,写入REMOVE操作日志journalWriter.write(REMOVE+''+entry.key+'\n');}journalWriter.flush();if(size>maxSize||fileCount>maxFileCount||journalRebuildRequired()){executorService.submit(cleanupCallable);}}
这样一次文件保存操作就完成了。
七. BaseDiskCache
BaseDiskCache是抽象类,实现了基本的图片文件存储,获取,删除等操作,并没有做什么限制。
如save和get, remove等操作
publicabstractclassBaseDiskCacheimplementsDiskCache{...protectedfinalFileNameGeneratorfileNameGenerator;...@OverridepublicFilegetDirectory(){returncacheDir;}@OverridepublicFileget(StringimageUri){returngetFile(imageUri);}@Overridepublicbooleansave(StringimageUri,InputStreamimageStream,IoUtils.CopyListenerlistener)throwsIOException{FileimageFile=getFile(imageUri);FiletmpFile=newFile(imageFile.getAbsolutePath()+TEMP_IMAGE_POSTFIX);booleanloaded=false;try{OutputStreamos=newBufferedOutputStream(newFileOutputStream(tmpFile),bufferSize);try{loaded=IoUtils.copyStream(imageStream,os,listener,bufferSize);}finally{IoUtils.closeSilently(os);}}finally{if(loaded&&!tmpFile.renameTo(imageFile)){loaded=false;}if(!loaded){tmpFile.delete();}}returnloaded;}@Overridepublicbooleanremove(StringimageUri){returngetFile(imageUri).delete();}@Overridepublicvoidclose(){//Nothingtodo}@Overridepublicvoidclear(){File[]files=cacheDir.listFiles();if(files!=null){for(Filef:files){f.delete();}}}protectedFilegetFile(StringimageUri){StringfileName=fileNameGenerator.generate(imageUri);Filedir=cacheDir;if(!cacheDir.exists()&&!cacheDir.mkdirs()){if(reserveCacheDir!=null&&(reserveCacheDir.exists()||reserveCacheDir.mkdirs())){dir=reserveCacheDir;}}returnnewFile(dir,fileName);}}
以save为例,首先会生成一个tmp文件,然后把网络图片文件流写入tmp文件。
OutputStreamos=newBufferedOutputStream(newFileOutputStream(tmpFile),loaded=IoUtils.copyStream(imageStream,os,listener,bufferSize);
然后把tmp文件重新名称成正式的文件
tmpFile.renameTo(imageFile)
八. UnlimitedDiskCache
和BaseDiskCache完全一样,并没有新的逻辑
九. LimitedAgeDiskCache
限制存储时间的文件存储管理,当我们尝试获取缓存文件的时候会去删除时间过长的文件,存储的空间没有限制。
我们以save和get为例
privatefinalMap<File,Long>loadingDates=Collections.synchronizedMap(newHashMap<File,Long>());
@Overridepublicbooleansave(StringimageUri,Bitmapbitmap)throwsIOException{booleansaved=super.save(imageUri,bitmap);rememberUsage(imageUri);returnsaved;}
privatevoidrememberUsage(StringimageUri){Filefile=getFile(imageUri);longcurrentTime=System.currentTimeMillis();file.setLastModified(currentTime);loadingDates.put(file,currentTime);}
save的时候,会调用rememberUsage方法,使用一个HashMap来存储缓存时间。
get
@OverridepublicFileget(StringimageUri){Filefile=super.get(imageUri);if(file!=null&&file.exists()){booleancached;LongloadingDate=loadingDates.get(file);if(loadingDate==null){cached=false;loadingDate=file.lastModified();}else{cached=true;}if(System.currentTimeMillis()-loadingDate>maxFileAge){file.delete();loadingDates.remove(file);}elseif(!cached){loadingDates.put(file,loadingDate);}}returnfile;}
get的时候会根据当前时间和缓存时间比较,如果大于maxFileAge,那么就删除它,从而实现了限制时间文件存储。
以上是“ImageLoader中如何实现磁盘命名和图片缓存算法”这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注恰卡编程网行业资讯频道!
推荐阅读
-
在Python中,将K添加到列元组列表中的最小元素
处理数据集涉及识别特定列中的最小值并通过添加常量值(K)来更新它。通过实施优化的解决方案,我们可以有效地执行此操作,这对于数据...
-
使用switch case语句编写的C程序,用于计算几何图形的面积
#includevoidmain(){intfig_code;floatside,base,length,...
-
如何使 C# 代码可重用?
要在C#中使代码可重用,请使用接口。接口定义属性、方法和事件,这些成员是接口的成员。接口只包含成员的声明。派生类负责定义成员。这通...
-
C# 中的覆盖和隐藏有什么区别?
方法隐藏在C#中也称为隐藏。父类的方法可供子类使用,无需在遮蔽中使用override关键字。子类有其自己版本的相同函数。在...
-
在Java中使用示例双倍longValue()函数
Java是一种强大的面向对象语言,可以对各种数据类型进行高度的控制和精确度。其中一种功能是doublelongValue(),...
-
如何在Java中定义JSON字段名称的命名约定?
TheFieldNamingPolicycanbeusedtodefineafewstandardnaming...
-
Servlet中的HttpSession接口
在JavaWeb开发领域,了解HttpSession接口是创建动态和响应式Web应用程序的关键。在本文中,我们将探讨...
-
使用while循环查找自然数之和的Java程序
自然数之和可以使用编程语言中的不同迭代语句来计算。迭代语句是执行一组特定代码行直到循环语句中的条件失败的语句。在本文中,我们将讨论...
-
我们可以将Java数组转换为列表吗?
我们可以使用Arrays.asList()方法轻松地将Java数组转换为List。语法publicstaticLi...
-
Java中如何在不使用任何外部库的情况下读取网页内容?
TheURLclassofthejava.netpackagerepresentsaUniformResour...