如何在Android開發中讓你的代碼更有效率
最近看瞭一個視頻,名字叫做Doing More With Less: Being a Good Android Citizen,主要是講如何用少少的幾句代碼來改善Android App的性能。在這個視頻裡面,演講者以一個圖片app為例講解如何應用Android中現有的東西來改善app性能問題。
這個圖片app的代碼:https://github.com/penkzhou/iogallery。ppt:https://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獲取對應的bitmap public 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性能更上一個臺階。