分类:ASCE1885的Android学习研究 1190人阅读 评论(0) 收藏 举报

做Androidapp开发的同学应该都听说过或者用过nostra13的Android-Universal-Image-Loader开源库,它在图片异步加载、缓存和显示等方面提供了强大灵活的框架。之前介绍过的android-smart-image-view开源库跟它比起来,真是小巫见大巫了,不仅在功能上,而且在代码量上都差别很大。当然我们在选取开源库的时候并不是功能越强大越好,一切都要看具体需求,只选取能够满足需求的就行,LessIsMore。

Android-Universal-Image-Loader可以到https://github.com/nostra13/Android-Universal-Image-Loader上面获取,至于它的使用方法,作者写了3篇博文进行了详细的介绍

(http://www.intexsoft.com/blog/item/68-universal-image-loader-part-1.html),本文就不再赘述了。首先,让我们先瞄一下Image-Loader库的整体包结构:


可以看到,包结构基本上也是根据功能来命名的,即图片异步加载、缓存和显示以及一些工具类。这篇文章我们先来分析下图片的内存缓存实现原理,内存缓存在介绍smart-image-view开源库时已经接触过,只不过当时的实现很简单,Bitmap以软引用的方式直接放到CurrentHashMap中,没有任何过期删除策略,也没有限制缓存大小等等。Image-Loader库将这些都考虑在内了,是一个较完整的内存缓存实现,使用者可以根据需要选择已经实现的策略,也可以定制自己项目中需要的策略。ImageLoader库大量使用了面向接口设计,面向接口设计方式专注于对象所提供的服务或模块的职责,而不是它们的实现。它明确地将规范视图从实现视图中剥离出来。内存缓存实现代码在memory和memory.impl这两个包中,前者就是规范视图,后者是实现视图。memory包中有MemoryCacheAware接口和BaseMemoryCache和LimitedMemoryCache两个抽象类,加上memory.impl包中的WeakMemoryCache类,类图如下所示:


MemoryCacheAware接口以泛型方式定义了实现缓存所需的基础规约,包括写缓存、读缓存、删除缓存和遍历缓存等,如下所示:

  1. /**
  2. *Interfaceformemorycache
  3. *
  4. *@authorSergeyTarasevich(nostra13[at]gmail[dot]com)
  5. *@since1.0.0
  6. */
  7. publicinterfaceMemoryCacheAware<K,V>{
  8. /**
  9. *Putsvalueintocachebykey
  10. *
  11. *@return<b>true</b>-ifvaluewasputintocachesuccessfully,<b>false</b>-ifvaluewas<b>not</b>putinto
  12. *cache
  13. */
  14. booleanput(Kkey,Vvalue);
  15. /**Returnsvaluebykey.Ifthereisnovalueforkeythennullwillbereturned.*/
  16. Vget(Kkey);
  17. /**Removesitembykey*/
  18. voidremove(Kkey);
  19. /**Returnsallkeysofcache*/
  20. Collection<K>keys();
  21. /**Removeallitemsfromcache*/
  22. voidclear();
  23. }

接口定义好,一般都会提供一个接口的基础实现类,这个类需要实现接口中的方法,并提供子类可以公用的操作,由smart-image-view这篇文章我们知道,在Android中图片最终表现为Bitmap的实例,为了Bitmap及时释放,一般内存缓存中不会直接存放Bitmap的强引用,而是使用弱引用SoftReference,但在Java中,还存在另外两种引用类型,即WeakReference和PhantomReference,考虑到通用性,在内存缓存的抽象基类BaseMemoryCache中将使用上面三种引用的父类Reference。内存缓存依然使用哈希表来实现。代码如下:

  1. /**Storesnotstrongreferencestoobjects*/
  2. privatefinalMap<K,Reference<V>>softMap=Collections.synchronizedMap(newHashMap<K,Reference<V>>());

回顾我们在smart-image-view库中的哈希表定义:

  1. privateConcurrentHashMap<String,SoftReference<Bitmap>>memoryCache=newConcurrentHashMap<String,SoftReference<Bitmap>>();;

可以发现最大的区别是一个使用Collections.synchronizedMap,一个使用ConcurrentHashMap,两者都是实现线程安全的HashMap,区别在哪里呢?感兴趣的可以自己看JDK源码,或者百度之,这里只给出结论:ConcurrentHashMap的读写性能要比Collections.synchronizedMap高,尽量使用前者。下面就看下BaseMemoryCache的代码吧:

  1. /**
  2. *Basememorycache.Implementscommonfunctionalityformemorycache.Providesobjectreferences(
  3. *{@linkplainReferencenotstrong})storing.
  4. *
  5. *@authorSergeyTarasevich(nostra13[at]gmail[dot]com)
  6. *@since1.0.0
  7. */
  8. publicabstractclassBaseMemoryCache<K,V>implementsMemoryCacheAware<K,V>{
  9. /**Storesnotstrongreferencestoobjects*/
  10. privatefinalMap<K,Reference<V>>softMap=Collections.synchronizedMap(newHashMap<K,Reference<V>>());
  11. @Override
  12. publicVget(Kkey){
  13. Vresult=null;
  14. Reference<V>reference=softMap.get(key);
  15. if(reference!=null){
  16. result=reference.get();
  17. }
  18. returnresult;
  19. }
  20. @Override
  21. publicbooleanput(Kkey,Vvalue){
  22. softMap.put(key,createReference(value));
  23. returntrue;
  24. }
  25. @Override
  26. publicvoidremove(Kkey){
  27. softMap.remove(key);
  28. }
  29. @Override
  30. publicCollection<K>keys(){
  31. synchronized(softMap){
  32. returnnewHashSet<K>(softMap.keySet());
  33. }
  34. }
  35. @Override
  36. publicvoidclear(){
  37. softMap.clear();
  38. }
  39. /**Creates{@linkplainReferencenotstrong}referenceofvalue*/
  40. protectedabstractReference<V>createReference(Vvalue);
  41. }

基本上就是HashMap的操作,由于具体Reference的实现需要看到底用的哪种引用,因此,这里将createReference定义成抽象函数,让BaseMemoryCache的子类去定制实现。

BaseMemoryCache的一个最简单的子类实现是WeakMemoryCache类,它仅仅是实现了createReference抽象方法,返回Bitmap对象的弱引用:

  1. publicclassWeakMemoryCacheextendsBaseMemoryCache<String,Bitmap>{
  2. @Override
  3. protectedReference<Bitmap>createReference(Bitmapvalue){
  4. returnnewWeakReference<Bitmap>(value);
  5. }
  6. }

BaseMemoryCache的另一个子类是LimitedMemoryCache,它也是抽象类,定义了实现内存缓存限制策略的公共操作。既然要限制缓存大小,那么首先需要定义默认的缓存最大值,同时需要维护一个表示当前缓存已占用大小的变量,考虑到多线程问题,这个变量值得增减必须保证是原子的,因此,该变量类型选用AtomicInteger。如下所示:

  1. privatestaticfinalintMAX_NORMAL_CACHE_SIZE_IN_MB=16;
  2. privatestaticfinalintMAX_NORMAL_CACHE_SIZE=MAX_NORMAL_CACHE_SIZE_IN_MB*1024*1024;
  3. privatefinalintsizeLimit;
  4. privatefinalAtomicIntegercacheSize;

同时,为了计算已添加到缓存中的数据大小,需要新增一个指向强引用的数据结构来进行记录,而不是使用BaseMemoryCache中的softMap哈希表,因为softMap中存放的是Reference类,里面的数据可能会被GC回收,用来统计已占用大小时不准确。指向数据强引用的数据结构选用LinkedList,定义如下,同样采用线程安全版本。

  1. /**
  2. *Containsstrongreferencestostoredobjects.Eachnextobjectisaddedlast.Ifhardcachesizewillexceed
  3. *limitthenfirstobjectisdeleted(butitcontinueexistat{@link#softMap}andcanbecollectedbyGCatany
  4. *time)
  5. */
  6. privatefinalList<V>hardCache=Collections.synchronizedList(newLinkedList<V>());

往LimitedMemoryCache中添加数据时,首先要计算出当前这个数据的大小(getSize()),然后加上已占用缓存大小后,跟缓存最大值相比较,超过缓存最大值时,就需要根据缓存清理策略(removeNext())删除以前的一些数据,直到添加数据后,总占用大小在最大值之内为止。上面提到的getSize()函数和removeNext函数需要子类定制,所以定义成了抽象函数。这是典型的模板方法模式的应用,模板方法模式定义一个操作中算法的步骤,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。下面的put函数中实现的就是算法的步骤,而getSize()和removeNext()函数则是子类可重定义的特定步骤。相关代码如下所示:

  1. @Override
  2. publicbooleanput(Kkey,Vvalue){
  3. booleanputSuccessfully=false;
  4. //Trytoaddvaluetohardcache
  5. intvalueSize=getSize(value);
  6. intsizeLimit=getSizeLimit();
  7. intcurCacheSize=cacheSize.get();
  8. if(valueSize<sizeLimit){
  9. while(curCacheSize+valueSize>sizeLimit){
  10. VremovedValue=removeNext();
  11. if(hardCache.remove(removedValue)){
  12. curCacheSize=cacheSize.addAndGet(-getSize(removedValue));
  13. }
  14. }
  15. hardCache.add(value);
  16. cacheSize.addAndGet(valueSize);
  17. putSuccessfully=true;
  18. }
  19. //Addvaluetosoftcache
  20. super.put(key,value);
  21. returnputSuccessfully;
  22. }
  23. protectedabstractintgetSize(Vvalue);
  24. protectedabstractVremoveNext();

接下来就介绍LimitedMemoryCache的几个子类的具体实现,它们分别是FIFOLimitedMemoryCache、LargestLimitedMemoryCache、LRULimitedMemoryCache和UsingFreqLimitedMemoryCache。类图如下:


【FIFOLimitedMemoryCache类】

FIFO算法意思是在缓存超过最大值时,缓存清理策略是将最老的数据清理掉,因此使用队列这样的数据结构就可以很好的实现,在Java中,LinkedList类可以实现这样的功能。

  1. privatefinalList<Bitmap>queue=Collections.synchronizedList(newLinkedList<Bitmap>());

而父类LimitedMemoryCache中也定义了类似的数据结构hardCache专门用于计算缓存的数据大小,只不过它是私有的,子类不能使用。但这样就会造成在FIFOLimitedMemoryCache类中存在两个类似的数据结构,内存占用会变大,事实上,FIFOLimitedMemoryCache中的queue完全可以直接使用父类的hardCache,作者之所以没有直接使用,应该是考虑到代码整体类层次结构清晰的问题,毕竟hardCache专门是用于计算缓存大小的,子类实现方式很多种,很多子类并不需要直接使用hardCache,所以单独一个FIFOLimitedMemoryCache以空间来换取代码结构的清晰也是可以理解的。写缓存和清缓存代码如下:

  1. @Override
  2. publicbooleanput(Stringkey,Bitmapvalue){
  3. if(super.put(key,value)){
  4. queue.add(value);
  5. returntrue;
  6. }else{
  7. returnfalse;
  8. }
  9. }
  10. @Override
  11. publicvoidremove(Stringkey){
  12. Bitmapvalue=super.get(key);
  13. if(value!=null){
  14. queue.remove(value);
  15. }
  16. super.remove(key);
  17. }
  18. @Override
  19. publicvoidclear(){
  20. queue.clear();
  21. super.clear();
  22. }

前面说到的父类总共有3个抽象函数,需要子类予以实现,代码如下:

  1. @Override
  2. protectedintgetSize(Bitmapvalue){
  3. returnvalue.getRowBytes()*value.getHeight();
  4. }
  5. @Override
  6. protectedBitmapremoveNext(){
  7. returnqueue.remove(0);
  8. }
  9. @Override
  10. protectedReference<Bitmap>createReference(Bitmapvalue){
  11. returnnewWeakReference<Bitmap>(value);
  12. }

getSize函数中可以看到计算Bitmap占用字节大小的典型方法。removeNext函数中移除队列queue头部的数据,这个数据是最先进入队列的数据。而Bitmap的Reference使用的是弱引用,它和软引用的区别是,弱引用对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域过程中,只要发现只具有弱引用的对象,那么不管当前内存空间是否足够,都会回收弱引用对象占用的内存。这样可以更好的防止出现OutOfMemoryError错误。

【LargestLimitedMemoryCache类】

当缓存超出最大值限制时,清理策略是将占用空间最大的数据清理掉。因此,需要一个维护Bitmap对象到它占用大小的映射,这里使用的还是HashMap:

  1. /**
  2. *Containsstrongreferencestostoredobjects(keys)andlastobjectusagedate(inmilliseconds).Ifhardcache
  3. *sizewillexceedlimitthenobjectwiththeleastfrequentlyusageisdeleted(butitcontinueexistat
  4. *{@link#softMap}andcanbecollectedbyGCatanytime)
  5. */
  6. privatefinalMap<Bitmap,Integer>valueSizes=Collections.synchronizedMap(newHashMap<Bitmap,Integer>());

同理,关键代码还是在父类几个抽象函数的实现,其中getSize()和createReference()函数实现和FIFOLimitedMemoryCache类一样,removeNext函数实现如下:

  1. @Override
  2. protectedBitmapremoveNext(){
  3. IntegermaxSize=null;
  4. BitmaplargestValue=null;
  5. Set<Entry<Bitmap,Integer>>entries=valueSizes.entrySet();
  6. synchronized(valueSizes){
  7. for(Entry<Bitmap,Integer>entry:entries){
  8. if(largestValue==null){
  9. largestValue=entry.getKey();
  10. maxSize=entry.getValue();
  11. }else{
  12. Integersize=entry.getValue();
  13. if(size>maxSize){
  14. maxSize=size;
  15. largestValue=entry.getKey();
  16. }
  17. }
  18. }
  19. }
  20. valueSizes.remove(largestValue);
  21. returnlargestValue;
  22. }

基本原理是遍历valueSizes这个哈希表,比较并得到占用空间最大的Bitmap对象,然后删除它即可。这里要注意的一点是遍历时要加入synchronized同步机制。

【LRULimitedMemoryCache类】

LRU即LeastRecentlyUsed,缓存清理策略是将最近最少使用的Bitmap对象删除掉。按Java中刚好有这样一个数据结构可以实现这个策略,它就是LinkedHashMap类。

  1. privatestaticfinalintINITIAL_CAPACITY=10;
  2. privatestaticfinalfloatLOAD_FACTOR=1.1f;
  3. /**CacheprovidingLeast-Recently-Usedlogic*/
  4. privatefinalMap<String,Bitmap>lruCache=Collections.synchronizedMap(newLinkedHashMap<String,Bitmap>(INITIAL_CAPACITY,LOAD_FACTOR,true));

LinkedHashMap是HashMap的子类,注意它的构造函数第三个参数accessOrder,它定义了LinkedHashMap的排序模式,accessOrder为true时,表示LinkedHashMap中数据排序按照访问的顺序,当为false时,表示数据排序按照数据插入的顺序。而我们要实现LRU,就需要把accessOrder设置为true,同时,在读缓存时不能像FIFOLimitedMemoryCache类和LargestLimitedMemoryCache类一样使用父类BaseMemoryCache的get方法,而是应该重写该方法如下所示:

  1. @Override
  2. publicBitmapget(Stringkey){
  3. lruCache.get(key);//call"get"forLRUlogic
  4. returnsuper.get(key);
  5. }

当accessOrder设置为true时,LinkedHashMap就维护了记录的访问顺序,这时使用Iterator遍历LinkedHashMap时,先得到的记录肯定就是最近最不常使用的那个记录了,LRU算法水到渠成:

  1. @Override
  2. protectedBitmapremoveNext(){
  3. BitmapmostLongUsedValue=null;
  4. synchronized(lruCache){
  5. Iterator<Entry<String,Bitmap>>it=lruCache.entrySet().iterator();
  6. if(it.hasNext()){
  7. Entry<String,Bitmap>entry=it.next();
  8. mostLongUsedValue=entry.getValue();
  9. it.remove();
  10. }
  11. }
  12. returnmostLongUsedValue;
  13. }


【UsingFreqLimitedMemoryCache类】

和LargestLimitedMemoryCache类实现类似,只不过一个是将占用空间最大的记录剔除,一个是将访问次数最少的记录剔除,所用数据结构自然类似:

  1. /**
  2. *Containsstrongreferencestostoredobjects(keys)andlastobjectusagedate(inmilliseconds).Ifhardcache
  3. *sizewillexceedlimitthenobjectwiththeleastfrequentlyusageisdeleted(butitcontinueexistat
  4. *{@link#softMap}andcanbecollectedbyGCatanytime)
  5. */
  6. privatefinalMap<Bitmap,Integer>usingCounts=Collections.synchronizedMap(newHashMap<Bitmap,Integer>());

因为要记录访问次数,所以需要重写父类的get方法,每访问一次,就增加计数值:

  1. @Override
  2. publicBitmapget(Stringkey){
  3. Bitmapvalue=super.get(key);
  4. //IncrementusagecountforvalueifvalueiscontainedinhardCahe
  5. if(value!=null){
  6. IntegerusageCount=usingCounts.get(value);
  7. if(usageCount!=null){
  8. usingCounts.put(value,usageCount+1);
  9. }
  10. }
  11. returnvalue;
  12. }

removeNext函数实现原理是遍历usingCounts哈希表中的记录,比较它们的访问次数,并选取访问次数最少的一个删除之,代码如下所示,同LargestLimitedMemoryCache类,要注意使用synchronized关键字同步保护。

  1. @Override
  2. protectedBitmapremoveNext(){
  3. IntegerminUsageCount=null;
  4. BitmapleastUsedValue=null;
  5. Set<Entry<Bitmap,Integer>>entries=usingCounts.entrySet();
  6. synchronized(usingCounts){
  7. for(Entry<Bitmap,Integer>entry:entries){
  8. if(leastUsedValue==null){
  9. leastUsedValue=entry.getKey();
  10. minUsageCount=entry.getValue();
  11. }else{
  12. IntegerlastValueUsage=entry.getValue();
  13. if(lastValueUsage<minUsageCount){
  14. minUsageCount=lastValueUsage;
  15. leastUsedValue=entry.getKey();
  16. }
  17. }
  18. }
  19. }
  20. usingCounts.remove(leastUsedValue);
  21. returnleastUsedValue;
  22. }

最后,在memory.impl包中还有几个类是直接实现MemoryCacheAware接口的,我们先来看下他们的类结构图:


从类图可以看到,FuzzyKeyMemoryCache类和LimitedAgeMemoryCache类都实现了MemoryCacheAware接口,同时聚合了MemoryCacheAware类型的对象。熟悉设计模式的同学应该能够一眼看出这个就是装饰者模式的应用。装饰者(Decorator)模式用于动态地给一个对象添加一些额外的职责,就增加功能而言,Decorator模式相比生成子类更为灵活。在Decorator模式的实现中,为了能够实现和原来使用被装饰对象的代码的无缝结合,装饰者类需要实现与被装饰对象相同的接口,同时在装饰者类中转调被装饰对象的功能,在转调的前后添加新的功能。就我们的代码来说,被装饰对象就是实现了MemoryCacheAware接口的类对象,装饰者对象就是FuzzyKeyMemoryCache类和LimitedAgeMemoryCache类的对象。更详细的关于Decorator模式的了解查阅设计模式的书籍。

【FuzzyKeyMemoryCache类】

在之前实现内存缓存的映射时,是一个key对应一个value,而这个装饰者类提供的额外功能是:允许多个key对应同一个value,后面插入的key-value对会覆盖以前存在的key-value对。这个类主要用于Image-Loader库内部使用。在FuzzyKeyMemoryCache类的实现中,使用Comparator类实现多个key是否相等的判断,核心代码在put函数中:

  1. /**
  2. *Decoratorfor{@linkMemoryCacheAware}.Providesspecialfeatureforcache:somedifferentkeysareconsideredas
  3. *equals(using{@linkComparatorcomparator}).Andwhenyoutrytoputsomevalueintocachebykeysoentrieswith
  4. *"equals"keyswillberemovedfromcachebefore.<br/>
  5. *<b>NOTE:</b>Usedforinternalneeds.Normallyyoudon'tneedtousethisclass.
  6. *
  7. *@authorSergeyTarasevich(nostra13[at]gmail[dot]com)
  8. *@since1.0.0
  9. */
  10. publicclassFuzzyKeyMemoryCache<K,V>implementsMemoryCacheAware<K,V>{
  11. privatefinalMemoryCacheAware<K,V>cache;
  12. privatefinalComparator<K>keyComparator;
  13. publicFuzzyKeyMemoryCache(MemoryCacheAware<K,V>cache,Comparator<K>keyComparator){
  14. this.cache=cache;
  15. this.keyComparator=keyComparator;
  16. }
  17. @Override
  18. publicbooleanput(Kkey,Vvalue){
  19. //Searchequalkeyandremovethisentry
  20. synchronized(cache){
  21. KkeyToRemove=null;
  22. for(KcacheKey:cache.keys()){
  23. if(keyComparator.compare(key,cacheKey)==0){
  24. keyToRemove=cacheKey;
  25. break;
  26. }
  27. }
  28. if(keyToRemove!=null){
  29. cache.remove(keyToRemove);
  30. }
  31. }
  32. returncache.put(key,value);
  33. }
  34. @Override
  35. publicVget(Kkey){
  36. returncache.get(key);
  37. }
  38. @Override
  39. publicvoidremove(Kkey){
  40. cache.remove(key);
  41. }
  42. @Override
  43. publicvoidclear(){
  44. cache.clear();
  45. }
  46. @Override
  47. publicCollection<K>keys(){
  48. returncache.keys();
  49. }
  50. }

【LimitedAgeMemoryCache类】

在前面介绍过的MemoryCacheAware接口实现类中,有时可能需要添加这样一个额外的功能:当缓存的对象存在超过一定时间时,将其清理掉,LimitedAgeMemoryCache这个装饰者类就是实现这样一个功能。首先,实现一个缓存对象key到已存活时间的映射表,在获取缓存对象时,判断该对象是否超过最大存活时间,如果是则将其移除。代码如下所示:

  1. /**
  2. *Decoratorfor{@linkMemoryCacheAware}.Providesspecialfeatureforcache:ifsomecachedobjectageexceedsdefined
  3. *valuethenthisobjectwillberemovedfromcache.
  4. *
  5. *@authorSergeyTarasevich(nostra13[at]gmail[dot]com)
  6. *@seeMemoryCacheAware
  7. *@since1.3.1
  8. */
  9. publicclassLimitedAgeMemoryCache<K,V>implementsMemoryCacheAware<K,V>{
  10. privatefinalMemoryCacheAware<K,V>cache;
  11. privatefinallongmaxAge;
  12. privatefinalMap<K,Long>loadingDates=Collections.synchronizedMap(newHashMap<K,Long>());
  13. /**
  14. *@paramcacheWrappedmemorycache
  15. *@parammaxAgeMaxobjectage<b>(inseconds)</b>.Ifobjectagewillexceedthisvaluethenit'llberemovedfrom
  16. *cacheonnexttreatment(andthereforebereloaded).
  17. */
  18. publicLimitedAgeMemoryCache(MemoryCacheAware<K,V>cache,longmaxAge){
  19. this.cache=cache;
  20. this.maxAge=maxAge*1000;//tomilliseconds
  21. }
  22. @Override
  23. publicbooleanput(Kkey,Vvalue){
  24. booleanputSuccesfully=cache.put(key,value);
  25. if(putSuccesfully){
  26. loadingDates.put(key,System.currentTimeMillis());
  27. }
  28. returnputSuccesfully;
  29. }
  30. @Override
  31. publicVget(Kkey){
  32. LongloadingDate=loadingDates.get(key);
  33. if(loadingDate!=null&&System.currentTimeMillis()-loadingDate>maxAge){
  34. cache.remove(key);
  35. loadingDates.remove(key);
  36. }
  37. returncache.get(key);
  38. }
  39. @Override
  40. publicvoidremove(Kkey){
  41. cache.remove(key);
  42. loadingDates.remove(key);
  43. }
  44. @Override
  45. publicCollection<K>keys(){
  46. returncache.keys();
  47. }
  48. @Override
  49. publicvoidclear(){
  50. cache.clear();
  51. loadingDates.clear();
  52. }
  53. }

——欢迎转载,请注明出处http://blog.csdn.net/asce1885,未经本人同意请勿用于商业用途,谢谢——

更多相关文章

  1. Android:LayoutInflater的使用
  2. Android:解析JSON的三种方式
  3. Android(安卓)ListView列表视图的使用方法
  4. Android中EditText控件的几种使用方法
  5. Android(安卓)WebView安全研究
  6. Android(安卓)让你的SeekBar 也支持长按事件
  7. Android(安卓)对话框 (三)自定义对话框
  8. Android(安卓)在activity之间传递对象
  9. Android(安卓)利用HttpURLConnection对象和Internet交互

随机推荐

  1. 一文让你彻底掌握 TS 枚举
  2. 我去,你写的 switch 语句也太老土了吧
  3. 从 SQL 到 MongoDB,这一篇就够了
  4. 冬天的作为:企业如何逆境增长
  5. java获取文件路径
  6. 9种设计模式在 Spring 中的运用,记住!
  7. 内部开源的未来
  8. 终于和 null say 拜拜了,我超开心
  9. 对不起,这局我要赢!!!
  10. 想要成为一名优秀的Java程序员,你需要这8