Android性能优化(二)- 丝般顺滑地加载大量图片
2015-03-31 11:43
441 查看
一、SoftReference
备注:其实SoftReference已经弃用了,具体原因下面有讲到,但是为何还要讲它,下面也有原因,哈哈!首先什么是softreference呢?直译就是软引用,看样子就是说,这是一个非强势的引用,当别人强硬起来时候,它就不行了,嘿嘿,确实是这样的!
软引用(SoftReference):
如果一个对象它只具有软引用,当内存足够的时候,GC是不会去回收它的,但是当内存不足时,就会回收这些对象所占用的内存啦。只要GC没有回收它,那么它就可以被程序使用。因此,软应用可以用来实现内存敏感的高速缓存。说得简单一点就是,当涉及会占用大量内存的图片操作时,比如加载相册,如果将其声明为软引用,那在报OOM之前,GC就去释放内存,所以就不会发生OOM的惨剧。
既然有软引用,自然就该有”硬引用”,好吧,其实没有硬,是强应用(StrongReference):结合软引用这个软柿子,这个就十分好理解了。如果一个对象具有强引用,那么GC是不会去回收它的,即便当内存不足时,JVM宁可抛出OOM的异常使程序crash,也不会靠回收强引用对象的内存来解决内存不足的问题。
软引用的实现:
private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>(); public void addBitmapToCache(String path) { // 强引用的Bitmap对象 Bitmap bitmap = BitmapFactory.decodeFile(path); // 软引用的Bitmap对象 SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap); // 添加该对象到Map中使其缓存 imageCache.put(path, softBitmap); } public Bitmap getBitmapByPath(String path) { // 从缓存中取软引用的Bitmap对象 SoftReference<Bitmap> softBitmap = imageCache.get(path); // 判断是否存在软引用 if (softBitmap == null) { return null; } // 取出Bitmap对象,如果由于内存不足Bitmap被回收,将取得空 Bitmap bitmap = softBitmap.get(); if(bitmap==null){ return null; } return bitmap; }
看到这里觉得SoftReference真的还不赖,因为会经常遇到加载大量强引用的对象啊,如果只是String那还好,但是套图啥的,额,这种东西吃内存的祖宗,OOM不来才怪,但是SoftReference确实可以有效避免OOM,万事大吉了!
BUT!!!….
二、Lrucache
Google从Android 2.3+开始宣布说,他们要从此版本开始,让GC更加频繁地去回收具有软引用对象的内存,好吧。。。动不动就被GC回收了,那我们的对象岂不就会经常丢失?对的,这样的话,SoftReference虽然不会造成OOM,但是我们的数据就会丢失,就会变的十分不可靠了(你看一组套图,结果关键部分丢了。。。++!)可是,那怎么办呢?
果然哇,挖掘机技术还是Google强,Google提出了一个叫做LruCache的东西,这又是个什么鬼?
所谓LRU,即为 Least recently used,近期最少使用策略,唉,翻译成汉语果然就不高大上了…当然,其实很熟悉啦,操作系统还是学过的,嘿嘿。
这个LruCache要取代之前的SoftReference还是有两下子的。首先,它会把最近最常使用的对象的强引用放在LinkedHashMap里面,(因为软引用不可靠啊,既然最近常使用,一定要保证它们,嗯!),还有那些不常使用的对象兄弟们呢?就只能在内存到达警戒值得时候,被强行踢出去了。。。
LruCache出场之前,首先要知道风险啊,不然一不小心以强引用的方式存了太多的对象,那就OOM了,所以呢,首先要获得JVM能够给我分配多少内存,Runtime().getRuntime().maxmemory(),哎呀,这下我就放心了,先留个1/8给以强引用方式来存储对象,这样,这些危险的东西被锁在盒子里就不会OOM了~
但是咱也不能太小气了,如果一毛不拔的,只留给一丁点儿内存,那就还是要不断加载到内存,释放内存,加载到内存,释放内存。。。好了,这个问题,=嘛,究竟分多少内存合适呢?!好吧,咱自己心里清楚就行了,JVM给每个程序分配了32MB(有的机器不一样),结合每个对象可能占据的内存,大概能够知道该给分个多大的窝了。。。
好了,LruCache的具体实现:
private LruCache<String, Bitmap> mMemoryCache; @Override protected void onCreate(Bundle savedInstanceState) { // 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。 // LruCache通过构造函数传入缓存值,以KB为单位。 int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // 使用最大可用内存值的1/8作为缓存的大小。 int cacheSize = maxMemory / 8; mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { // 必须重写此方法来衡量每张图片的大小,默认返回图片数量。 return bitmap.getByteCount() / 1024; } }; } public void addBitmapToMemoryCache(String key, Bitmap bitmap) { if (getBitmapFromMemCache(key) == null) { mMemoryCache.put(key, bitmap); } } public Bitmap getBitmapFromMemCache(String key) { return mMemoryCache.get(key); }
假设一个全屏幕的GridView使用4张800*480的图片来填充,这时大约会占用1.5MB的内存(800 * 480 * 4 / 1024 / 1024 MB),所以这个设置的LruCache可以用来缓存2.5页,所以,分配的大小判断就是这么来的。
这里可以简单看一下,分配了1/8的内存,即4MB的缓存空间,根据需要将对象存进去,存取就是键值对啦,键的话就是url,而值就是这个Bitmap的对象。
但是问题来了,我分配的这1/8的内存满了怎么办?
Cache保存一个强引用来限制内容数量,每当Item被访问的时候,此Item就会移动到队列的头部。当cache已满的时候加入新的item时,在队列尾部的item会被回收。
Soga!!原来如此~这样看来,我们貌似并没有必要去考虑这4MB内存的心酸,他自己知道怎么做,嗯!我们在实现的时候,只要首先去这块缓存中查找,如果有,那就太好了,它在LruCache中的位置也会被提升,但是如果没有的话,那我们只能去加载了。。。然后再把它添加到缓存里面。到这里貌似可以结束了。。
BUT!!!。。。
三、结合SoftReference和LruCache的二级缓存结构
这里废话这么多,终究说的是LruCache,即为缓存,既然缓存嘛,之前还听过一个东西叫做多级缓存的,不知道有木有关联呢。。。。难道LruCache中木有,我就只能屁颠屁颠地跑去加载么。。。。既然SoftReference不可靠,那我不依赖它,但是如果作为一个辅助还是可以的嘛,至少比直接去加载资源到内存,不停地加载,释放,加载,释放要好嘛,当SoftReference实在也没有了,那我再去加载资源到内存也行啊!对啊,我貌似可以把LruCache作为一级缓存,SoftReference作为二级缓存呀!好像挺高大上的,其实就是个补丁,哈哈!这里借助用了一下SoftReferenceManager(其实就是个map嘛)
试试嘛~具体实现:
import android.graphics.Bitmap; import android.support.v4.util.LruCache; public class LruCacheManager { private static LruCacheManager lruCacheManager; private SoftReferenceCacheManager softReferenceCacheManager; private LruCache<String, Bitmap> lruCache; // 配置一级缓存设置LruCache private LruCacheManager() { // (Runtime.getRuntime().maxMemory()运行时系统所分配的内存资源 lruCache = new LruCache<String, Bitmap>((int) (Runtime.getRuntime() .maxMemory() / 8)) { @Override protected int sizeOf(String key, Bitmap value) { if (value != null) { return value.getRowBytes() * value.getHeight(); } return 0; } // 当内存资源不足时 进行调用 @Override protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) { if (evicted) { // 放到软引用当中 softReferenceCacheManager.put(key, oldValue); } } }; // 初始化softRefrenceCacheManager softReferenceCacheManager = SoftReferenceCacheManager .getSoftReferenceCacheManager(); } // 初始化LruCachaManager操作 public static LruCacheManager getLruCacheManager() { if (lruCacheManager == null) { lruCacheManager = new LruCacheManager(); } return lruCacheManager; } // 将图片添加到lrucache中 public void putBitmap(String url, Bitmap bm) { lruCache.put(url, bm); } // 从lrucache中取出图片 public Bitmap getBitmap(String url) { Bitmap bm = null; bm = lruCache.get(url); if (bm == null) { // 软引用当中取 bm = softReferenceCacheManager.get(url); // 将从软引用中取出的图片添加到lrucache中,并将软引用中的删除掉 if (bm != null) { lruCache.put(url, bm); softReferenceCacheManager.remove(url); } } return bm; }
改到这里,貌似异步加载吃内存的图片已经可以飞起了,OOM神马的,就不提了!到这里可以结束了嘛~~
BUT!!!…
四、DiskLruCache
从始至终,都是在内存里面啊,无论是软引用还是LruCache,万一内存吃紧,又来了个优先级超级高的phoneCall,好了,全shi了,啥都没了,从零开始。。。但是为了有效避免这种情况,DiskCache又出场了!
DiskCache和LruCache用法是相似的:
private DiskLruCache mDiskLruCache; private final Object mDiskCacheLock = new Object(); private boolean mDiskCacheStarting = true; private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB private static final String DISK_CACHE_SUBDIR = "thumbnails"; @Override protected void onCreate(Bundle savedInstanceState) { ... // Initialize memory cache ... // Initialize disk cache on background thread File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR); new InitDiskCacheTask().execute(cacheDir); ... } class InitDiskCacheTask extends AsyncTask<File, Void, Void> { @Override protected Void doInBackground(File... params) { synchronized (mDiskCacheLock) { File cacheDir = params[0]; //初始化DiskLruCache对象 mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE); mDiskCacheStarting = false; // Finished initialization mDiskCacheLock.notifyAll(); // Wake any waiting threads } return null; } } //在需要获取图片的时候,先去DiskCache中找,实在没有再去加载到内存 class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { ... // Decode image in background. @Override protected Bitmap doInBackground(Integer... params) { final String imageKey = String.valueOf(params[0]); // Check disk cache in background thread Bitmap bitmap = getBitmapFromDiskCache(imageKey); if (bitmap == null) { // Not found in disk cache // Process as normal final Bitmap bitmap = decodeSampledBitmapFromResource( getResources(), params[0], 100, 100)); } // Add final bitmap to caches:源码是有缺漏的,其实应该是先去内存中//找该图,比如LruCache,找不到再去DiskCache中,都没有才去加载,并且要//把它加进这两个缓存数据结构 addBitmapToCache(imageKey, bitmap); return bitmap; } ... } public void addBitmapToCache(String key, Bitmap bitmap) { // Add to memory cache as before if (getBitmapFromMemCache(key) == null) { mMemoryCache.put(key, bitmap); } // Also add to disk cache synchronized (mDiskCacheLock) { if (mDiskLruCache != null && mDiskLruCache.get(key) == null) { mDiskLruCache.put(key, bitmap); } } } public Bitmap getBitmapFromDiskCache(String key) { synchronized (mDiskCacheLock) { // Wait while disk cache is started from background thread while (mDiskCacheStarting) { try { mDiskCacheLock.wait(); } catch (InterruptedException e) {} } if (mDiskLruCache != null) { return mDiskLruCache.get(key); } } return null; } // Creates a unique subdirectory of the designated app cache directory. Tries to use external // but if not mounted, falls back on internal storage. public static File getDiskCacheDir(Context context, String uniqueName) { // Check if media is mounted or storage is built-in, if so, try and use external cache dir // otherwise use internal cache dir final String cachePath = Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() : context.getCacheDir().getPath(); return new File(cachePath + File.separator + uniqueName); }
好吧,这个google的源码,是有缺漏的,其实应该是先去内存中找该图,比如LruCache,找不到再去DiskCache中,都没有才去加载,并且要把它加进这两个缓存数据结构,以便下次的使用。
还有个问题,为啥这里加了锁(synchronized ),这个因为在DiskCache初始化以及置值的时候,可能是一个耗时操作,在其还没有完成的时候,程序就可能去找图片了,这可不行,所以就在该过程执行过程中,将Diskcache锁上,这样就让这个请求乖乖等着咯,不过对于人类使用来说,还是非常快的,等不了多少毫秒!
五、结合SoftReference、LruCache,DiskLruCache的三级缓存结构
但是,是不是可以再结合呢,所以呢。。。结合DiskCache,我们可以建立一个三级缓存啦!类似于Lrucache和SoftReference的二级缓存的逻辑,我们再在此基础上加上DiskCache的第三级缓存,即过程便是:
首先去LruCache一级缓存中查找该图,如果没有,就去保存有SoftReference的map二级缓存中找,如果还没有,那就去DiskCache的三级缓存中找,最后实在没有了就去加载或者下载咯。具体用法,其实是一致的,就是在最初的加载或者下载时,需要将该对象同时加到LruCache、SoftReference的Map结构以及DiskCache中。
六、ContentProvider
到这里,基本上已经可以完结了,但是google又来个挑逗的note:Note: A ContentProvider might be a more appropriate place to store cached images if they are accessed more frequently, for example in an image gallery application.
好吧,问题还没有结束,继续挖掘。。。
怎么用ContentProvider还Cache我的图片呢?
好吧,关于如果使用ContentProvider以及其原理,下篇接着讲。
BUT!!!…
七、Lrucache源码分析
额,太罗嗦了,还是不说了,附上LruCache的源码,哈哈!packageandroid.util; importjava.util.LinkedHashMap; importjava.util.Map; /** * A cache that holds strong references to a limited number of values. Each time * a value is accessed, it is moved to the head of a queue. When a value is * added to a full cache, the value at the end of that queue is evicted and may * become eligible for garbage collection. * Cache保存一个强引用来限制内容数量,每当Item被访问的时候,此Item就会移动到队列的头部。 * 当cache已满的时候加入新的item时,在队列尾部的item会被回收。 * <p>If your cached values hold resources that need to be explicitly released, * override {@link #entryRemoved}. * 如果你cache的某个值需要明确释放,重写entryRemoved() * <p>If a cache miss should be computed on demand for the corresponding keys, * override {@link #create}. This simplifies the calling code, allowing it to * assume a value will always be returned, even when there's a cache miss. * 如果key相对应的item丢掉啦,重写create().这简化了调用代码,即使丢失了也总会返回。 * <p>By default, the cache size is measured in the number of entries. Override * {@link #sizeOf} to size the cache in different units. For example, this cache * is limited to 4MiB of bitmaps: 默认cache大小是测量的item的数量,重写sizeof计算不同item的 * 大小。 * <pre> {@code * int cacheSize = 4 * 1024 * 1024; // 4MiB * LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) { * protected int sizeOf(String key, Bitmap value) { * return value.getByteCount(); * } * }}</pre> * * <p>This class is thread-safe. Perform multiple cache operations atomically by * synchronizing on the cache: <pre> {@code * synchronized (cache) { * if (cache.get(key) == null) { * cache.put(key, value); * } * }}</pre> * * <p>This class does not allow null to be used as a key or value. A return * value of null from {@link #get}, {@link #put} or {@link #remove} is * unambiguous: the key was not in the cache. * 不允许key或者value为null * 当get(),put(),remove()返回值为null时,key相应的项不在cache中 */ publicclass LruCache<K, V> { privatefinal LinkedHashMap<K, V> map; /** Size of this cache in units. Not necessarily the number of elements. */ privateint size; //已经存储的大小 privateint maxSize; //规定的最大存储空间 privateint putCount; //put的次数 privateint createCount; //create的次数 privateint evictionCount; //回收的次数 privateint hitCount; //命中的次数 privateint missCount; //丢失的次数 /** * @param maxSize for caches that do not override {@link #sizeOf}, this is * the maximum number of entries in the cache. For all other caches, * this is the maximum sum of the sizes of the entries in this cache. */ publicLruCache(intmaxSize) { if(maxSize <= 0) { thrownew IllegalArgumentException("maxSize <= 0"); } this.maxSize = maxSize; this.map = newLinkedHashMap<K, V>(0, 0.75f, true); } /** * Returns the value for {@code key} if it exists in the cache or can be * created by {@code #create}. If a value was returned, it is moved to the * head of the queue. This returns null if a value is not cached and cannot * be created. 通过key返回相应的item,或者创建返回相应的item。相应的item会移动到队列的头部, * 如果item的value没有被cache或者不能被创建,则返回null。 */ publicfinal V get(K key) { if(key == null) { thrownew NullPointerException("key == null"); } V mapValue; synchronized(this) { mapValue = map.get(key); if(mapValue != null) { hitCount++; //命中 returnmapValue; } missCount++; //丢失 } /* * Attempt to create a value. This may take a long time, and the map * may be different when create() returns. If a conflicting value was * added to the map while create() was working, we leave that value in * the map and release the created value. * 如果丢失了就试图创建一个item */ V createdValue = create(key); if(createdValue == null) { returnnull; } synchronized(this) { createCount++;//创建++ mapValue = map.put(key, createdValue); if(mapValue != null) { // There was a conflict so undo that last put //如果前面存在oldValue,那么撤销put() map.put(key, mapValue); } else{ size += safeSizeOf(key, createdValue); } } if(mapValue != null) { entryRemoved(false, key, createdValue, mapValue); returnmapValue; } else{ trimToSize(maxSize); returncreatedValue; } } /** * Caches {@code value} for {@code key}. The value is moved to the head of * the queue. * * @return the previous value mapped by {@code key}. */ publicfinal V put(K key, V value) { if(key == null|| value == null) { thrownew NullPointerException("key == null || value == null"); } V previous; synchronized(this) { putCount++; size += safeSizeOf(key, value); previous = map.put(key, value); if(previous != null) { //返回的先前的value值 size -= safeSizeOf(key, previous); } } if(previous != null) { entryRemoved(false, key, previous, value); } trimToSize(maxSize); returnprevious; } /** * @param maxSize the maximum size of the cache before returning. May be -1 * to evict even 0-sized elements. * 清空cache空间 */ privatevoid trimToSize(int maxSize) { while(true) { K key; V value; synchronized(this) { if(size < 0|| (map.isEmpty() && size != 0)) { thrownew IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!"); } if(size <= maxSize) { break; } Map.Entry<K, V> toEvict = map.eldest(); if(toEvict == null) { break; } key = toEvict.getKey(); value = toEvict.getValue(); map.remove(key); size -= safeSizeOf(key, value); evictionCount++; } entryRemoved(true, key, value, null); } } /** * Removes the entry for {@code key} if it exists. * 删除key相应的cache项,返回相应的value * @return the previous value mapped by {@code key}. */ publicfinal V remove(K key) { if(key == null) { thrownew NullPointerException("key == null"); } V previous; synchronized(this) { previous = map.remove(key); if(previous != null) { size -= safeSizeOf(key, previous); } } if(previous != null) { entryRemoved(false, key, previous, null); } returnprevious; } /** * Called for entries that have been evicted or removed. This method is * invoked when a value is evicted to make space, removed by a call to * {@link #remove}, or replaced by a call to {@link #put}. The default * implementation does nothing. * 当item被回收或者删掉时调用。改方法当value被回收释放存储空间时被remove调用, * 或者替换item值时put调用,默认实现什么都没做。 * <p>The method is called without synchronization: other threads may * access the cache while this method is executing. * * @param evicted true if the entry is being removed to make space, false * if the removal was caused by a {@link #put} or {@link #remove}. * true---为释放空间被删除;false---put或remove导致 * @param newValue the new value for {@code key}, if it exists. If non-null, * this removal was caused by a {@link #put}. Otherwise it was caused by * an eviction or a {@link #remove}. */ protectedvoid entryRemoved(boolean evicted, K key, V oldValue, V newValue) {} /** * Called after a cache miss to compute a value for the corresponding key. * Returns the computed value or null if no value can be computed. The * default implementation returns null. * 当某Item丢失时会调用到,返回计算的相应的value或者null * <p>The method is called without synchronization: other threads may * access the cache while this method is executing. * * <p>If a value for {@code key} exists in the cache when this method * returns, the created value will be released with {@link #entryRemoved} * and discarded. This can occur when multiple threads request the same key * at the same time (causing multiple values to be created), or when one * thread calls {@link #put} while another is creating a value for the same * key. */ protectedV create(K key) { returnnull; } privateint safeSizeOf(K key, V value) { intresult = sizeOf(key, value); if(result < 0) { thrownew IllegalStateException("Negative size: "+ key + "="+ value); } returnresult; } /** * Returns the size of the entry for {@code key} and {@code value} in * user-defined units. The default implementation returns 1 so that size * is the number of entries and max size is the maximum number of entries. * 返回用户定义的item的大小,默认返回1代表item的数量,最大size就是最大item值 * <p>An entry's size must not change while it is in the cache. */ protectedint sizeOf(K key, V value) { return1; } /** * Clear the cache, calling {@link #entryRemoved} on each removed entry. * 清空cacke */ publicfinal void evictAll() { trimToSize(-1); // -1 will evict 0-sized elements } /** * For caches that do not override {@link #sizeOf}, this returns the number * of entries in the cache. For all other caches, this returns the sum of * the sizes of the entries in this cache. */ publicsynchronized final int size() { returnsize; } /** * For caches that do not override {@link #sizeOf}, this returns the maximum * number of entries in the cache. For all other caches, this returns the * maximum sum of the sizes of the entries in this cache. */ publicsynchronized final int maxSize() { returnmaxSize; } /** * Returns the number of times {@link #get} returned a value that was * already present in the cache. */ publicsynchronized final int hitCount() { returnhitCount; } /** * Returns the number of times {@link #get} returned null or required a new * value to be created. */ publicsynchronized final int missCount() { returnmissCount; } /** * Returns the number of times {@link #create(Object)} returned a value. */ publicsynchronized final int createCount() { returncreateCount; } /** * Returns the number of times {@link #put} was called. */ publicsynchronized final int putCount() { returnputCount; } /** * Returns the number of values that have been evicted. * 返回被回收的数量 */ publicsynchronized final int evictionCount() { returnevictionCount; } /** * Returns a copy of the current contents of the cache, ordered from least * recently accessed to most recently accessed. 返回当前cache的副本,从最近最少访问到最多访问 */ publicsynchronized final Map<K, V> snapshot() { returnnew LinkedHashMap<K, V>(map); } @Overridepublic synchronized final String toString() { intaccesses = hitCount + missCount; inthitPercent = accesses != 0? (100* hitCount / accesses) : 0; returnString.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]", maxSize, hitCount, missCount, hitPercent); } }
相关文章推荐
- 【Android性能优化】Android图片加载方案--Bitmap的内存管理和优化方案
- Android进阶:ListView性能优化异步加载图片 使滑动效果流畅
- Android滑动列表加载大量图片时候的优化
- Android进阶:ListView性能优化异步加载图片 使滑动效果流畅
- android中ListView性能优化异步加载图片 使滑动效果流畅
- Android Listview 加载图片优化--本地加载大量图片,解决滑动卡顿现象(滑动停止加载图片)
- Android Listview 加载图片优化--本地加载大量图片,解决滑动卡顿现象(滑动停止加载图片)
- Android性能优化之实现双缓存的图片异步加载工具(LruCache+SoftReference) - 拿来即用
- Android性能优化之实现双缓存的图片异步加载工具(LruCache+SoftReference) - 拿来即用
- scrollView加载大量图片性能优化
- Android 性能优化(十)图片加载和大图片缓存机制OOM完美解决方案LruCache&DiskLruCache
- 弱引用的场景——解决android gridView加载大量图片的性能问题
- [置顶] 【Android性能优化】Android图片加载方案--Bitmap的内存管理和优化方案
- android——再谈加载大量图片性能问题
- Android Bitmap大量使用不产生OOM之“加载大图片资源优化”
- Android进阶:ListView性能优化异步加载图片 使滑动效果流畅
- Android ListView 性能优化-----(异步加载图片资源)
- 又优化了一下 Android ListView 异步加载图片
- Android之ListView异步加载网络图片(优化缓存机制) .
- listview异步图片加载之优化篇(android)