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产生一个图片文件名称的方法。

它包含两个实现类

  1. HashCodeFileNameGenerator

  2. 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是操作记录描述性文件,内容如下

  1. DIRTY: 操作记录创建,如果DIRTY后面没有CLEAN或者REMOVE,那么这个图片会被删除。

  2. CLEAN: 记录成功创建和访问

  3. READ: 记录成功访问

  4. 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中如何实现磁盘命名和图片缓存算法”这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注恰卡编程网行业资讯频道!

发布于 2022-01-17 21:59:27
收藏
分享
海报
0 条评论
37
上一篇:做好Google排名的优化技巧有哪些 下一篇:提高网站在google谷歌排名的5个技巧分别是什么
目录

    推荐阅读

    0 条评论

    本站已关闭游客评论,请登录或者注册后再评论吧~

    忘记密码?

    图形验证码