1. 程式人生 > >Android 效能優化——記憶體篇

Android 效能優化——記憶體篇

歡迎轉載,轉載請標明出處【Hoolay Team】:
http://www.cnblogs.com/hoolay/p/6278229.html
Author : Hoolay Android Team  Chiclaim

一、android官方一些記憶體方面的記憶體tips

1、避免建立不必要的物件。

  • 如儘量避免字串的加號拼接,可以使用StringBuilder來拼接。
  • 如果需要TextView設定多個字串片段,可以使用textView.append方法,不要直接用加號拼起來。

2、儘量使用for-each迴圈,對於ArrayList,請使用普通的for,如:

int len = list.size();
for (int i = 0; i < len; ++i) {
    //todo somgthing
}

3、使用系統庫函式。

  • 使用系統庫函式,並且還有彙編級別的優化,他們通常比帶有JIT的Java編譯出來的程式碼更高效 如:System.arraycopy()String.indexOf()等。
  • 如果系統函式能夠解決的,不要加入第三方庫,可能第三方庫使用起來比較簡單(jsoup,htmlparser等)。如解析HTML可以使用系統的XmlPullParser

二、使用 ArrayMap、SparseArray代替HashMap

ArrayMap 和 HashMap的在記憶體的使用上,更加高效。

ArrayMap實現上有兩個陣列,一個數組是儲存key hash,另一個數組儲存value,ArrayMap通過二分法(binary search)來進行查詢的。

HashMap通過一個數組來實現的,key hash作為陣列的索引,這樣就需要更大的記憶體來減少key hash的衝突,key hash就是陣列的索引,所以查詢效率很高。

使用建議:

  • 當資料量比較小的時候(小於1000),優先使用ArrayMap,否則使用HashMap。

  • map裡巢狀map。

使用方法:

  • ArrayMap和HashMap的使用方法都是一樣的,ArrayMap也實現了Map介面。

  • 另外,ArrayMap可以通過keyAt(index)方法來獲取第index位置的key,keyValue(int index)同理。但是HashMap是不可以的。

  arrayMap.keyAt(0);
  arrayMap.valueAt(0);

SparseArray和ArrayMap非常像,它們都是通過兩種緊密包裝的陣列,而不是一個大的雜湊雜湊,從而減少了整個記憶體的覆蓋區。但是查詢的速度就慢了。

只不過SparseArray和ArrayMap最大的區別是SparseArray的key是一個基本型別。

SparseArray的key是int型別,而不是Integer。像以前使用HashMap的時候,如果key是整形,必須是Integer。

Integer佔16個位元組,int只佔4個位元組,如果元素比較多,從而可以很好的減少記憶體的佔用。

除了SparseArray類還有如下類可供使用:

SparseBooleanMap <boolean,Object>
SparseIntMap <int,Object>
SparseLongMap <long,Object>

SparseArray和ArrayMap的使用建議 和 使用方法都是一樣的。

三、Thread與Thread Pool

在android開發中,一些耗時的操作都會放到後臺執行緒去執行,比如:網路、本地檔案、資料庫等。

    new Thread(new Runnable() {
        @Override
        public void run() {
            //do something...
        }
    }).start();

每個執行緒至少消耗64k的記憶體,如果你在某個時間點,迅速開啟了很多執行緒(比如載入列表圖片,然後使用者滑動列表),這個時候可能記憶體使用量就會飆升。

  • 會出現記憶體抖動(memory churn),因為短時間開啟了很多執行緒,完成任務後,這些執行緒都會被回收。記憶體表現為:低-高-低。甚至可能出現OOM。

  • 一個系統所能處理的執行緒數量是有限的,如果超多了最大承載量,效能會受到很大的影響。而且可能還會影響使用者的後續操作。

這時候Thread Pool執行緒池的作用就凸顯出來了。

Java為我們提供了操作執行緒池的api ThreadPoolExecutor ,ExecutorService是一個介面,相關的執行緒池的類都實現了該介面,如 ThreadPoolExecutor 。

建立一個執行緒池可以通過 ThreadPoolExecutor類來實現。

ThreadPoolExecutor pool = new ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue);//新建一個執行緒池
pool.execute(Runnable);//執行任務

下面是官方對ThreadPoolExecutor的引數說明:

Parameters:
    corePoolSize - the number of threads to keep in the pool, even if they are idle, unless allowCoreThreadTimeOut is set
    maximumPoolSize - the maximum number of threads to allow in the pool
    keepAliveTime - when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating.
    unit - the time unit for the keepAliveTime argument
    workQueue - the queue to use for holding tasks before they are executed. This queue will hold only the Runnable tasks submitted by the execute method.
  • corePoolSize 核心執行緒數,核心執行緒會一直存活,即使沒有任務需要處理。當執行緒數小於核心執行緒數時,即使現有的執行緒空閒,執行緒池也會優先建立新執行緒來處理任務,而不是直接交給現有的執行緒處理。核心執行緒在allowCoreThreadTimeout被設定為true時會超時退出,預設情況下不會退出。

  • maxPoolSize 執行緒池允許最大的執行緒數量。

  • keepAliveTime 當執行緒空閒時間達到keepAliveTime,該執行緒會退出,直到執行緒數量等於corePoolSize。如果allowCoreThreadTimeout設定為true,則所有執行緒均會退出直到執行緒數量為0。

  • allowCoreThreadTimeout 是否允許核心執行緒空閒keepAliveTime退出,預設值為false。

  • workQueue 任務佇列。pool.execute(runnable)提交的task都會放到workQueue。

下面來一個簡單的sample:

public class MyClass {

    private ThreadPoolExecutor pool ;

    private MyClass(){
        //建立執行緒池
        pool = new ThreadPoolExecutor(4, 7, 60, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
    }

    public static void main(String[] args) {
        MyClass myClass = new MyClass();
        for (int i = 0; i < 10; i++) {
            //提交任務
            myClass.pool.execute(new MyRunnable(myClass));
        }
        myClass.pool.shutdown();
    }

    private String getCount() {
        return pool.getCorePoolSize()+"-"+pool.getActiveCount() + "-" + pool.getMaximumPoolSize();
    }

    private static class MyRunnable implements Runnable {
        MyClass myClass;

        MyRunnable(MyClass myClass) {
            this.myClass = myClass;
        }

        @Override
        public void run() {
            System.out.println("thread name:" + Thread.currentThread().getName() + " start " + myClass.getCount());
            try {
                //模擬耗時任務
                Thread.sleep(3000L);
                System.out.println("thread name:" + Thread.currentThread().getName() + " end " + myClass.getCount());

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

上面的程式碼很簡單:建立了一個corePoolSize為4,maxPoolSize為7的執行緒池。
然後往執行緒池裡提交10個任務,每個任務列印pool.getCorePoolSize()+"-"+pool.getActiveCount() + "-" + pool.getMaximumPoolSize(),即corePoolSize(核心執行緒數),activeCount(正在活動的執行緒總數)和maximumPoolSize(執行緒池允許的最大執行緒數)值。

測試結果如下:

thread name:pool-1-thread-1 start 4-2-7
thread name:pool-1-thread-2 start 4-4-7
thread name:pool-1-thread-3 start 4-4-7
thread name:pool-1-thread-4 start 4-4-7
thread name:pool-1-thread-1 end 4-4-7
thread name:pool-1-thread-2 end 4-3-7
thread name:pool-1-thread-4 end 4-4-7
thread name:pool-1-thread-1 start 4-4-7
thread name:pool-1-thread-4 start 4-4-7
thread name:pool-1-thread-2 start 4-4-7
thread name:pool-1-thread-3 end 4-4-7
thread name:pool-1-thread-3 start 4-4-7
thread name:pool-1-thread-2 end 4-4-7
thread name:pool-1-thread-4 end 4-4-7
thread name:pool-1-thread-3 end 4-4-7
thread name:pool-1-thread-4 start 4-4-7
thread name:pool-1-thread-1 end 4-3-7
thread name:pool-1-thread-2 start 4-4-7
thread name:pool-1-thread-4 end 4-2-7
thread name:pool-1-thread-2 end 4-2-7

Process finished with exit code 0

從測試結果來看,我們列印pool.getCorePoolSize()+"-"+pool.getActiveCount() + "-" + pool.getMaximumPoolSize()的值是正常的。但是隻建立了4個執行緒:

pool-1-thread-1
pool-1-thread-2
pool-1-thread-3
pool-1-thread-4

我們設定了執行緒池的最大數為7,我們提交了10個任務,但是為什麼只建立了corePoolSize=4個執行緒?

檢視官方文件可以找到答案:

  • 當通過execute(Runnable)提交一個新任務,並且小於corePoolSize正在執行的執行緒數,將會建立一個新的執行緒來處理這個任務,不管執行緒池裡有沒有空閒的執行緒。

  • If there are more than corePoolSize but less than maximumPoolSize threads running, a new thread will be created only if the queue is full.
    大於corePoolSize小於maximumPoolSize,workQueue佇列滿了,才會建立新的執行緒。

  • 如果corePoolSize和maximumPoolSize值設定成一樣的,相當於建立了一個固定數量的執行緒池。

  • 多數情況下,都是通過構造方法來設定corePoolSize和maximumPoolSize,但是也可以通過setCorePoolSize和setMaximumPoolSize來動態設定。

所以上面的例子,只建立了4個執行緒,因為雖然我們提交了10個任務,但是構建workQueue時候沒有傳入佇列大小,預設大小是Integer.MAX_VALUE,所以workQueue是不會滿的。所以最多就建立了4個執行緒。

據此,我把workQueue佇列容量改成4:

pool = new ThreadPoolExecutor(4, 7, 60, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(4));

測試結果:

thread name:pool-1-thread-1 start 4-2-7
thread name:pool-1-thread-2 start 4-2-7
thread name:pool-1-thread-3 start 4-3-7
thread name:pool-1-thread-4 start 4-4-7
thread name:pool-1-thread-5 start 4-6-7
thread name:pool-1-thread-6 start 4-6-7
thread name:pool-1-thread-1 end 4-6-7
thread name:pool-1-thread-2 end 4-6-7
thread name:pool-1-thread-2 start 4-5-7
thread name:pool-1-thread-1 start 4-6-7
thread name:pool-1-thread-3 end 4-6-7
thread name:pool-1-thread-3 start 4-6-7
thread name:pool-1-thread-4 end 4-6-7
thread name:pool-1-thread-5 end 4-6-7
thread name:pool-1-thread-4 start 4-6-7
thread name:pool-1-thread-6 end 4-5-7
thread name:pool-1-thread-1 end 4-4-7
thread name:pool-1-thread-2 end 4-4-7
thread name:pool-1-thread-3 end 4-2-7
thread name:pool-1-thread-4 end 4-1-7

Process finished with exit code 0

發現建立了6個執行緒,大於上一次的測試結果(上一次是建立了4個執行緒),可是我們設定的maximumPoolSize為7,按道理應該是建立7個執行緒才對呀,這是為什麼呢?

這需要了解下workQueue佇列的策略了。我們上面的列子使用的是 LinkedBlockingQueue。

下面來看看官方文件對 BlockingQueue的描述:

Any link BlockingQueue may be used to transfer and hold
submitted tasks.  The use of this queue interacts with pool sizing:
If fewer than corePoolSize threads are running, the Executor
always prefers adding a new thread
rather than queuing.

If corePoolSize or more threads are running, the Executor
always prefers queuing a request rather than adding a new
thread.

If a request cannot be queued, a new thread is created unless
this would exceed maximumPoolSize, in which case, the task will be
rejected.

主要意思就是:

  • 如果執行的執行緒少於 corePoolSize,Executor會建立新執行緒來執行任務,不會把任務放進queue。

  • 如果執行的執行緒等於或多於 corePoolSize,Executor將請求加入佇列,而不是建立新的執行緒。

  • 如果佇列已滿,無法將請求加入佇列,則建立新的執行緒,除非建立此執行緒超出 maximumPoolSize,在這種情況下,任務將被拒絕。

這樣就能解釋為什麼只建立6個執行緒了。

總共有10個task,核心執行緒corePoolSize=4,所以3個任務是不會放進queue的,直接建立3個新執行緒來處理task了,然後再執行execute(Runnable)的時候,就會大於等於corePoolSize,所以就會把接下來的4個任務放進queue(容量為4),然後就剩下3個task了,發現佇列已經滿了,建立3個執行緒來處理這剩下的3個task,所以總共只建立6個執行緒了。

maximumPoolSize=7,我就是想讓它建立7個執行緒,我們知道了上面的原理就很簡單了,可以把佇列的最大容量改成3或者新增11個任務就可以了:

new LinkedBlockingQueue<Runnable>(3)

for (int i = 0; i < 11; i++) {
    myClass.pool.execute(new MyRunnable(myClass));
}

效果如下:

thread name:pool-1-thread-1 start 0-1-7
thread name:pool-1-thread-2 start 0-2-7
thread name:pool-1-thread-3 start 1-4-7
thread name:pool-1-thread-4 start 4-5-7
thread name:pool-1-thread-5 start 4-7-7
thread name:pool-1-thread-6 start 4-7-7
thread name:pool-1-thread-7 start 4-7-7
.....

Java提供了3種策略的Queue

  • SynchronousQueue 直接傳送task(Direct handoffs)

  • LinkedBlockingQueue 無邊界佇列(Unbounded queues)

  • ArrayBlockingQueue 有邊界佇列(Bounded queues)
    更多詳細資訊可以檢視官方文件。

Java給我們提供了一些工廠方法來來建立執行緒池():

  • Executors.newFixedThreadPool(int threads);

  • Executors.newCachedThreadPool();

  • Executors.newSingleThreadExecutor();

  • Executors.newScheduledThreadPool(int threads);

這些方法都是通過構建ThreadPoolExecutor來實現的,具體的細節可以去看看文件,如果都不滿足你的需求,可以自己構造ThreadPoolExecutor。

四、IntentService與Service

一般我們在app裡的版本更新邏輯在Service裡起一個執行緒來檢測。

為了避免Service一直存在,減少記憶體消耗,檢測版本後,還需要手動stopSelf,略麻煩。

這時候用IntentService就比較合適了,預設就給你啟動了一個執行緒來執行耗時操作,完成自動關閉service。

Service和IntentService主要區別:

  • IntentService執行完會自動關閉(stopSelf),而Service不會。

  • IntentService會啟動一個執行緒來執行耗時操作,把耗時操作放到onHandleIntent(Intent intent)方法裡。而Service需要自己new Thread。

  • 如果呼叫startService(intent)多次,IntentService會執行多次onHandleIntent(Intent intent),且必須等本次的onHandleIntent(Intent intent)執行完,才會執行下一次onHandleIntent(Intent intent),說白了就是如果正在執行任務,會把後面啟動的命令放到佇列裡。而多次呼叫startService(intent),Service僅僅會多次呼叫onStartCommand方法。

五、避免常見的記憶體洩露

1、CountDownTimer、TimerTask、Handler導致的記憶體洩露

在專案中,我們常常可能要做活動倒計時的功能,我是用CountDownTimer來做的。如:

public static class TimeCounter extends CountDownTimer {
    public TimeCounter(long millisInFuture, long countDownInterval) {
        super(millisInFuture, countDownInterval);
    }

    @Override
    public void onFinish() {
        //倒計時結束
    }

    @Override
    public void onTick(long millisUntilFinished) {
        //每間隔固定時間執行一次
        //再次處理倒計時邏輯
    }
}

如下圖所示:

倒計時
因為要在TimeCounter內部要修改View的顯示,所以要把TextView傳遞進來,使用WeakReference來引用TextView避免記憶體洩露,如:


            
           

相關推薦

Android 效能優化——記憶體

歡迎轉載,轉載請標明出處【Hoolay Team】: http://www.cnblogs.com/hoolay/p/6278229.html Author : Hoolay Android Team  Chiclaim 一、android官方一

Android 效能優化記憶體

一、android官方一些記憶體方面的記憶體tips 1、避免建立不必要的物件。 如儘量避免字串的加號拼接,可以使用StringBuilder來拼接。 如果需要TextView設定多個字串片段,可以使用textView.append方法,不要直接用加號拼起

Android效能優化--記憶體

看到很多關於記憶體優化的部落格好文,也收藏了好多地址,但每次看時都需要在眾多收藏的地址裡尋找一番,不如寫到自己的部落格裡,方便隨時翻看。 記憶體優化也是android進階的必學內容。APP記憶體的使用,是評價一款應用效能高低的一個重要指標。 1.記憶體與記憶

android效能優化——記憶體洩漏

在專案初期階段或者業務邏輯很簡單的時候對於app效能之一塊沒有太多感覺,但是隨著專案版本的迭代和專案業務邏輯越來越大,越來越複雜的時候,就會逐漸感覺到app效能的重要性,所以在專案初期階段時,就要有app效能這一意識,也便於專案後期的版本迭代和業務擴充套件;這裡所提到的效能優化問題是:記憶體洩漏

Android效能優化——工具

Android效能優化是Android開發中經常遇見的一個問題,接下來將對Android效能優化方面的知識點做一個簡單的梳理和總結,將從工具和程式碼兩方面進行梳理。所謂工欲善其事必先利其器,本文首先來看一下Android效能優化有哪些得力的工具。 1、TraceView

使用 adb 獲取 Android 效能資料--記憶體

記憶體說明 VSS - Virtual Set Size 虛擬耗用記憶體(包含共享庫佔用的記憶體) RSS - Resident Set Size 實際使用實體記憶體(包含共享庫佔用的記憶體) PSS - Proportional Set Size 實際使用的實體記憶

Android 效能優化 ---- 記憶體優化

### 1、Android記憶體管理機制 #### 1.1 Java記憶體分配模型 先上一張JVM將記憶體劃分區域的圖 ![](https://img2020.cnblogs.com/blog/967362/202007/967362-20200717091650518-110380858.png) 程式

Android效能優化記憶體優化--記憶體洩漏

文章目錄 介紹 什麼是記憶體洩露 android中導致記憶體洩漏的主要幾種情況 1.單例模式 2.使用非靜態內部類 3.使用非同步事件處理機制Handler 4.使用靜態

[Android 效能優化系列]記憶體之基礎--Android如何管理記憶體

轉載請標明出處(http://blog.csdn.net/kifile),再次感謝 在接下來的一段時間裡,我會每天翻譯一部分關於效能提升的Android官方文件給大家 下面是本次的正文: ################ 隨機訪問儲存器(Ram) 不管在哪種軟體開發

Android效能優化記憶體

Google近期在Udacity上釋出了Android效能優化的線上課程,分別從渲染,運算與記憶體,電量幾個方面介紹瞭如何去優化效能,這些課程是Google之前在Youtube上釋出的Android效能優化典範專題課程的細化與補充。 下面是記憶體篇章的學習筆記,部分內容與前面的效能優化典範有重合,歡

【朝花夕拾】Android效能優化之(四)Apk打包

        APK,即Android Package,是將android程式和資源整合在一起,形成的一個.apk檔案。相信所有的Android程式設計師是在IDE的幫助下,完成打包輕而易舉,但對打包流程真正清楚的可能並不多。本章的內容比較簡單,也是非常基礎的內容,但是對理解android應用的結構卻有很大

【朝花夕拾】Android效能優化之(一)序言及JVM

序言 筆者從事Anroid開發有些年頭了,深知掌握Anroid效能優化方面的知識的必要性,這是一個程式設計師必須修煉的內功。在面試中,它是面試官的摯愛,在工作中,它是程式碼質量的攔路虎,其重要性可見一斑。在團隊中,效能優化的工作又往往由經驗豐富的老師傅來完成,可見要做好效能優化,絕不是一件容易的事情。    

【朝花夕拾】Android效能優化之(五)Android虛擬機器簡介

前言        Android虛擬機器的使用,使得android應用和Linux核心分離,這樣做使得android系統更穩定可靠,比如程式中即使包含惡意程式碼,也不會直接影響系統檔案;也提高了跨平臺相容性。在Android4.4以前的系統中,Android系統均採用Dalvik作為執行andorid程式的

【朝花夕拾】Android效能優化之(五)Android虛擬機器

前言        Android虛擬機器的使用,使得android應用和Linux核心分離,這樣做使得android系統更穩定可靠,比如程式中即使包含惡意程式碼,也不會直接影響系統檔案;也提高了跨平臺相容性。在Android4.4以前的系統中,Android系統均採用Da

android效能優化實戰前

本文地址:http://blog.csdn.net/iamws/article/details/51629160 前言:         最近因為某專案cpu,記憶體的使用率實在讓人不敢恭維;手機發燙,電量下降已經讓使用者無法忍受;頻繁快速迭代發版導致各種效能問題突出;由

[Android 效能優化系列]佈局之減少你的介面層級

轉載請標明出處(http://blog.csdn.net/kifile),再次感謝 在接下來的一段時間裡,我會每天翻譯一部分關於效能提升的Android官方文件給大家 效能優化之佈局篇: 題外話: 複雜的佈局,既會提高我們的設計難度,也會降低我們的程式碼效

Android 效能優化之String

Android 效能優化之 String篇 關於String相關知識都是老掉牙的東西了,但我們經常可能在不經意的String 字串拼接的情況下浪費記憶體,影響效能,也常常會成為觸發記憶體OOM的最後一步。 所以本文對String字串進行深度解析,有

Android 效能優化——管理應用的記憶體

請保持淡定,分析程式碼,記住:效能很重要。 隨機存取儲存器(RAM)在任何軟體開發環境中都是一個很寶貴的資源。這一點在實體記憶體通常很有限的移動作業系統上,顯得尤為突出。儘管 Android Runtime(ART)和 Dalvik 虛擬機器都扮演了常

Android 效能優化記憶體洩漏檢測以及記憶體優化(中)

Android 記憶體洩漏檢測   通過上篇部落格我們瞭解了 Android JVM/ART 記憶體的相關知識和洩漏的原因,再來歸類一下記憶體洩漏的源頭,這裡我們簡單將其歸為一下三類:自身編碼引起由專案開發人員自身的編碼造成;第三方程式碼引起這裡的第三

Android 效能優化記憶體檢測、卡頓優化、耗電優化、APK瘦身

導語 自2008年智慧時代開始,Android作業系統一路高歌,10年智慧機發展之路,如今 Android 9.0 代號P  都發布了,Android系統性能已經非常流暢了。但是,到了各大廠商手裡,改原始碼自定系統,使得Android原生系統變得魚龍混雜。另外,到了不同層次的