如何在Android開發中讓你的代碼更有效率
如何在Android開發中讓你的代碼更有效率
最近看了一個視頻,名字叫做Doing More With Less: Being a Good Android Citizen,主要是講如何用少少的幾句代碼來改善Android App的效能。在這個視頻裡面,演講者以一個圖片app為例講解如何應用Android中現有的東西來改善app效能問題。
這個圖片app的代碼:https://github.com/penkzhou/iogallery。ppt:http://greenrobot.qiniudn.com/103.pdf。
現在我將視頻裡面的內容記錄如下:
使用LruCache避免OOM
首先我們的圖片app是用來展示手機裡面儲存的圖片。當app裡面需要展示大量的圖片的時候,我們需要將這些圖片從disk載入到記憶體當中。如果我們來回地滑動activity,系統會重複許多disk I/O;而且在一個activity裡面同時載入多張圖片將會佔用大量記憶體,造成系統記憶體緊張,進而影響使用者體驗。
如何使用LruCache
這個時候我們可以引入LruCache,將我們最常用的圖片緩衝到記憶體裡面,這樣可以避免大量重複的disk I/O,還可以讓app載入圖片時記憶體佔用不超過設定值。具體代碼如下:
//這裡假設通過picture的id(Long)來作為key擷取對應的bitmappublic class PictureCache extends LruCache{//設定最大的byte值,也就是說整個緩衝所能佔用的最大記憶體public PictureCache(int maxByteSizes){super(maxByteSizes);}// 計算每次添加bitmap的時候,給緩衝所添加的數字,預設就是數量,//這裡因為添加的是bitmap,所以每次添加都是計算bitmap對應的位元組數protected int sizeOf(Long key,Bitmap value){return value.getByteCount();}public void put(Long key, Bitmap value);public Bitmap get(Long key);}
如何確定Cache大小
一般我們是通過ActivityManager.getMemorySize()來確定Cache的大小。ActivityManager.getMemorySize()表明了在系統正常啟動並執行前提下一個App所佔記憶體的極限。所以我們可以使用它來作為Cache大小的一個衡量,比方如下的代碼中,我們使用它的一半來作為Cache的大小:
final ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);final int memoryClassBytes = am.getMemoryClass() * 1024 * 1024;PictureCache mCache = new PictureCache(memoryClassBytes / 2);
app跑到後台去了
當我們用這個圖片app瀏覽完圖片之後呢,我們回到Android 主介面,開始玩遊戲。大家都知道,遊戲很耗記憶體。可能我們在玩的過程中,直接用完了剩下可用的所有記憶體都還不夠,那怎麼辦呢?Android會在這個時候kill一些背景app擷取相應的記憶體。
這裡需要先說一命令。我們如何擷取一個app的記憶體佔用?很簡單,使用 “adb shell procrank”命令列。這個命令列顯示所有系統啟動並執行進程所佔記憶體大小,一般包括Vss、Rss、Pss、Uss,其中Uss對我們來說最重要,Uss表示如果這個進程被系統幹掉了,那麼系統可以從這個進程上面獲得多少的可用記憶體。
好了回到之前的情景。Android會kill掉一些背景程式來供給遊戲所需的記憶體。假設Android因為記憶體緊張kill掉了圖片app,我在玩了一會遊戲之後又開啟了圖片app。這個時候圖片app又要重新布局,重新載入圖片,整個體驗對使用者來說非常不好。有什麼辦法讓Android在kill應用之前通知app一聲,好讓app有所準備?
這個時候我們就要用到ComponentCallbacks2,詳情看文檔。app在系統處於不同的記憶體環境時會有相應的callback,我們只需在activity裡面重寫這個onTrimMemory方法即可,具體範例程式碼如下:
public void onTrimMemory(int level) {super.onTrimMemory(level);if (level >= TRIM_MEMORY_MODERATE) { // 60// 這個app已經進入後台有一段時間了,基本上表示使用者接下來//不會重新開啟這個app,我們可以清掉所有緩衝所佔記憶體Log.v(TAG, evicting entire thumbnail cache);mCache.evictAll();} else if (level >= TRIM_MEMORY_BACKGROUND) { // 40// 表示app剛進入後台,我們可以縮減一部分緩衝所佔記憶體// 來保證其他前台app的記憶體需要Log.v(TAG, evicting oldest half of thumbnail cache);mCache.trimToSize(mCache.size() / 2);}}
這個callback只是建議,不一定會被系統調用。系統在記憶體緊張的時候可能會直接kill掉app而不去調用這個callback。但是如果這些callback可以調用的話,這將大大地提升我們app的使用者體驗。
善用Android內建容器
現在我們需要為這個app添加一些新特性,我們要讓這個app可以進行收藏操作(為圖片添加一個是否被收藏的屬性即可)。我們會一次性收藏多張圖片,那麼我們可以使用GridView的多選模式。按照常理來說,我們可以使用HashMap()來儲存哪些照片需要被收藏。不過這裡使用HashMap有點大材小用,效率不高。我們可以使用SparseBooleanArray等Android特有的容器來代替HashMap,節省系統開銷(主要是autoboxing帶來的開銷)
SQLite中讀寫操作最佳化
當我們獲得了要收藏的圖片資訊(儲存在SparseBooleanArray中)之後,我們需要講這些資料儲存在SQLite當中,範例程式碼如下:
SQLiteDatabase db;long[] mPhotoIds;ContentValues values = new ContentValues();for (long photoId : mPhotoIds) {values.put(COLUMN_ID, photoId);values.put(COLUMN_FAVORITE, favorite);db.insert(TABLE_FAVORITE, null, values);}
上面的代碼中,每次insert都有開銷。這個時候我們可以考慮使用transaction。代碼如下:
SQLiteDatabase db;long[] mPhotoIds;ContentValues values = new ContentValues();db.beginTransaction();try{for (long photoId : mPhotoIds) {values.put(COLUMN_ID, photoId);values.put(COLUMN_FAVORITE, favorite);db.insert(TABLE_FAVORITE, null, values);}db.setTransactionSuccessful();}finally{db.endTransaction();}
但是使用這個transaction的時候,db會被鎖住,而碰到更重要的操作只能等待。碰到這種情況怎麼辦?使用db.yieldIfContendedSafely,這個方法表示我現在執行我的多次資料庫操作,如果碰到其他的資料庫操作,我先讓別的操作完 再執行我的操作。具體代碼如下:
SQLiteDatabase db;long[] mPhotoIds;ContentValues values = new ContentValues();db.beginTransaction();try{for (long photoId : mPhotoIds) {values.put(COLUMN_ID, photoId);values.put(COLUMN_FAVORITE, favorite);db.insert(TABLE_FAVORITE, null, values);db.yieldIfContendedSafely();}db.setTransactionSuccessful();}finally{db.endTransaction();}
善用Broadcast
如果我們需要對使用者新增的照片預設添加濾鏡效果,怎麼實現?很好辦啦,因為Android在API14的時候添加了一個新的Broadcast Intent,叫做”android.hardware.action.NEW_PICTURE”,就是在系統新增了照片時,我們截獲這個intent就可以了。然後我們就在相應的BroadcastReceiver裡面進行處理,相關代碼如下:
public void onReceive(Context context, Intent intent) {if (isAutoApplyEnabled) {// 執行相應的濾鏡操作final Intent serviceIntent = new Intent(context, EffectService.class);serviceIntent.setData(intent.getData());context.startService(serviceIntent);} else {Log.d(TAG, Processed no-op broadcast!);}}
上面的isAutoApplyEnabled變數表示系統是否開啟自動對新增照片進行濾鏡的操作。那如果關閉預設濾鏡功能,即將isAutoApplyEnabled的值設為false呢?結果就是每次新增照片都會有intent過來,只是在onReceive方法裡面沒有做操作。這樣造成的結果就是每次intent都會被系統傳遞,只是走了不同的分支,系統照樣消耗資源。
因此比較好的方案就是系統中isAutoApplyEnabled變數的值在變化的時候,我們需要相應地對BroadcastReceiver進行開閉操作。具體需要用到PackageManage.setComponentEnabledSetting()方法。
最後的幾句話
本文以一個圖片app為背景講述了Android開發中官方推薦的小tip。瞭解了這些可以讓你的app效能更上一個台階。