如何在Android開發中讓你的代碼更有效率

最近看了Google IO 2012年的一個視頻,名字叫做Doing More With Less: Being a Good Android Citizen,主要是講如何用少少的幾句代碼來改善Android App的性能。在這個視頻裏面,演講者以一個圖片app爲例講解如何應用Android中現有的東西來改善app性能問題。這個圖片app的代碼在這裏。ppt在這裏。現在我將視頻裏面的內容記錄如下:

使用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<Long, Bitmap>{
    //設定最大的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()<Long, Boolean>來存儲哪些照片需要被收藏。不過這裏使用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();
}

 

使用RenderScript

如果這個時候我想讓這個圖片app擁有一些簡單的濾鏡,大家可能一下就想到使用NDK來實現相應的圖片處理工作。其實Android提供RenderScript也能完成這樣的任務,而且優點不少呢:

  1. RenderScript能充分利用用戶設備的硬件資源。比如設備的CPU是雙核的,那麼RenderScript便會相應地開闢兩個worker線程來進行圖片處理工作
  2. RenderScript面向所有架構,包括ARM、X86、MIPS等等。其中原理是首先RenderScript在第一階段編譯成中間代碼並打包到apk文件裏面,接着在apk被安裝到設備上面之後,中間代碼會被再次編譯成與設備平臺相關的native代碼
  3. RenderScript會自動生成相應的JNI 膠水代碼

以上這三點相對於NDK來說,節省了開發者許多精力,讓開發者更專注於效果代碼的實現。這個示例相應的RenderScript代碼在這裏

這個視頻是專門介紹RenderScript的一些用法,也是來自於Google IO。

善用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性能更上一個臺階。本文涉及的所有代碼可以在這裏找到。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章