Android開發筆記——常見BUG類型之記憶體泄露與安全執行緒

來源:互聯網
上載者:User

標籤:

本文內容來源於最近一次內部分享的總結,沒來得及詳細整理,見諒。

本次分享主要對記憶體泄露安全執行緒這兩個問題進行一些說明,內部代碼掃描發現的BUG大致分為四類:1)null 指標;2)除0;3)記憶體、資源流失;4)安全執行緒。第一、二個問題屬於編碼考慮不周,第三、四個問題則需要更深入的分析。

  1. 記憶體泄露
  2. 安全執行緒
一、記憶體泄露 1、很抱歉,”XXX”已停止運行。OOM?

怎樣才能讓app報OOM呢?最簡單的辦法如下:

Bitmap bt1 = BitmapFactory.decodeResource(this.getResources(), R.drawable.image);Bitmap bt2 = BitmapFactory.decodeResource(this.getResources(), R.drawable.image);Bitmap btn = ...
 2、查看記憶體佔用
  • 命令列:adb shell dumpsys meminfo packageName

  • 通過Android Studio的Memory Monitor查看記憶體中Dalvik Heap的即時變化

 3、發生記憶體泄露的條件

首先,每個app有最大記憶體限制。

ActivityManager activityManager = (ActivityManager) context.getSystemServiceContext.ACTIVITY_SERVICE);activityManager.getMemoryClass();
 
getMemoryClass()取到的是最大記憶體資源。Android中的堆記憶體分為Native Heap和Dalvik Heap。C/C++申請的記憶體空間在Native Heap中,Java申請的記憶體空間則在Dalvik Heap中。對於head堆的大小限制,可以查看/system/build.prop檔案:
 
dalvik.vm.heapstartsize=8mdalvik.vm.heapgrowthlimit=96mdalvik.vm.heapsize=256m

注意:

heapsize參數表示單個進程heap可用的最大記憶體,但如果存在以下參數”dalvik.vm.headgrowthlimit =96m”表示單個進程heap記憶體被限定在96m,即程式運行過程實際只能使用96m記憶體。

如果申請的記憶體資源超過上述限制,系統就會拋出OOM錯誤。

  4、常見避免OOM的措施

以下主要從四個方面總結常見的措施:1)減小對象的記憶體佔用;2)記憶體對象的重複利用;3)避免對象的記憶體泄露;4)記憶體使用量策略最佳化。

4.1 減小對象的記憶體佔用

  • 使用ArrayMap/SparseArray而不是HashMap等傳統資料結構。
    • 參考連結:Android記憶體最佳化(使用SparseArray和ArrayMap代替HashMap)
  • 在Android中避免使用枚舉。
  • 減小Bitmap對象的記憶體佔用。inSampleSize和decode format。

4.2 記憶體對象的重複利用

  • ListView/GridView等出現大量重複子組件的視圖裡面對ConvertView的複用
  • 使用LRU機制緩衝Bitmap
  • 避免在onDraw方法裡面執行對象的建立
  • 使用StringBuilder來替代頻繁的”+”

4.3 避免對象的記憶體泄露

4.1和4.2都是比較常規的措施,4.3需要重點關注。

1)Activity泄露

導致Activity泄露的原因較多,下面列舉一些比較常見的。從原理上主要分為兩類:i)靜態對象;ii)this$0

  • Activity被static變數引用。這段代碼來自於我們的Crash上傳
    private static Map<ComponentName, ExceptionHandler> configMap =                         new HashMap<ComponentName, ExceptionHandler>();public static void setActivity(final Activity activity, boolean send2Server) {    Log.d(TAG, "bind exception handler : " + activity.getComponentName().getClassName());    //上下文初始化    SDKContext.init(activity.getApplication());    init(activity.getApplication());    ExceptionHandler exceptionHandler = new ExceptionHandler(                        activity, send2Server, Thread.getDefaultUncaughtExceptionHandler());    configMap.put(activity.getComponentName(), exceptionHandler);    Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);}

下面是通過MAT分析一個Activity泄露的:
 

  • 內部類引用導致Activity的泄漏
    最典型的情境是Handler導致的Activity泄漏,如果Handler中有延遲的任務或者是等待執行的任務隊列過長,都有可能因為Handler繼續執行而導致Activity發生泄漏。此時的參考關聯性鏈是Looper -> MessageQueue -> Message -> Handler -> Activity。為瞭解決這個問題,可以在UI退出之前,執行remove Handler訊息佇列中的訊息與runnable對象。或者是使用Static + WeakReference的方式來達到斷開Handler與Activity之間存在參考關聯性的目的。
    可參考連結:線程通訊

2)考慮使用Application Context而不是Activity Context

對於大部分非必須使用Activity Context的情況(Dialog的Context就必須是Activity Context),我們都可以考慮使用Application Context而不是Activity的Context,這樣可以避免不經意的Activity泄露。

3)注意臨時Bitmap對象的及時回收

雖然在大多數情況下,我們會對Bitmap增加緩衝機制,但是在某些時候,部分Bitmap是需要及時回收的。例如臨時建立的某個相對比較大的bitmap對象,在經過變換得到新的bitmap對象之後,應該儘快回收原始的bitmap,這樣能夠更快釋放原始bitmap所佔用的空間。

4)記憶體佔用監控
通過Runtime擷取maxMemory,而maxMemory-totalMemory即為剩餘可使用的dalvik記憶體。定期檢查這個值,達到80%就去釋放各種cache資源(bitmap的cache)。

/** * Returns the maximum number of bytes the heap can expand to. See {@link #totalMemory} for the * current number of bytes taken by the heap, and {@link #freeMemory} for the current number of * those bytes actually used by live objects. */int maxMemory = Runtime.getRuntime().maxMemory()); // 應用程式最大可用記憶體/** * Returns the number of bytes taken by the heap at its current size. The heap may expand or * contract over time, as the number of live objects increases or decreases. See * {@link #maxMemory} for the maximum heap size, and {@link #freeMemory} for an idea of how much * the heap could currently contract. */long totalMemory = Runtime.getRuntime().totalMemory()); // 應用程式已獲得記憶體/** * Returns the number of bytes currently available on the heap without expanding the heap. See * {@link #totalMemory} for the heap‘s current size. When these bytes are exhausted, the heap * may expand. See {@link #maxMemory} for that limit. */long freeMemory = Runtime.getRuntime().freeMemory()); // 應用程式已獲得記憶體中未使用記憶體

5)注意Cursor對象是否及時關閉

在程式中我們經常會進行查詢資料庫的操作,但時常會存在不小心使用Cursor之後沒有及時關閉的情況。這些Cursor的泄露,反覆多次出現的話會對記憶體管理產生很大的負面影響,我們需要謹記對Cursor對象的及時關閉。

4.4 記憶體使用量策略最佳化

  • 謹慎使用large heap
  • 綜合考慮裝置記憶體閾值與其他因素設計合適的緩衝大小
  • onLowMemory()/onTrimMemory(int)
  • 資源檔需要選擇合適的檔案夾進行存放
  • Try catch某些大記憶體配置的操作
  • 謹慎使用static對象
  • 最佳化布局層次,減少記憶體消耗
  • 謹慎使用多進程
  • 謹慎使用依賴注入架構
  • 使用ProGuard來剔除不需要的代碼
  • 謹慎使用第三方libraries
  • 考慮不同的實現方式來最佳化記憶體佔用
二、安全執行緒1、下面的方法是安全執行緒的嗎?
class MyCounter {    private static int counter = 0;    public static int getCount() {        return counter++;    }}

怎樣使上述方法安全執行緒?
 
2、Java中的安全執行緒

怎樣保持在多線程環境下的資料一致性,Java提供了多種方法實現:

  1. synchronized
  2. java.util.concurrent.atomic
  3. java.util.concurrent.locks
  4. thread safe collection(ConcurrentHashMap)
  5. volatile

2.1 synchronized

JVM保證被synchronized關鍵字修飾的程式碼片段在同一時間只能被一個線程訪問,內部通過對對象加鎖來實現的。當方法被synchronized修飾時,鎖加在對象上;當方法同時為static時,鎖加在類上。從效能的角度來講,一般不建議直接將鎖加在類上,這樣會使得類的所有對象的該方法均為synchronized的。

從之前掃描的問題來看,在編寫synchronized程式時主要有兩點需要注意:

  • synchronized需要建立基於對象或者類的鎖,所以不能在構造器或者變數上加鎖。
  • synchronized造成死結。

1) 鎖加在哪裡?

List<ResultPoint> currentPossible = possibleResultPoints;List<ResultPoint> currentLast = lastPossibleResultPoints;int frameLeft = frame.left;int frameTop = frame.top;if (currentPossible.isEmpty()) {    lastPossibleResultPoints = null;} else {    possibleResultPoints = new ArrayList<>(5);    lastPossibleResultPoints = currentPossible;    paint.setAlpha(CURRENT_POINT_OPACITY);    paint.setColor(resultPointColor);    synchronized (currentPossible) {        for (ResultPoint point : currentPossible) {            canvas.drawCircle(frameLeft                    + (int) (point.getX() * scaleX), frameTop                    + (int) (point.getY() * scaleY), POINT_SIZE,                    paint);        }    }}

上述方法中,possibleResultPoints的建立沒有採用同步措施,需要使用Collections.synchronizedXxx

List<MyType> list = Collections.synchronizedList(new ArrayList(<MyType>));...synchronized(list){    for(MyType m : list){        foo(m);        m.doSomething();    }}

一般比較推薦建立一個虛擬對象專門用於擷取鎖。
//dummy object variable for synchronizationprivate Object mutex=new Object();...//using synchronized block to read, increment and update count value synchronouslysynchronized (mutex) {        count++;}

PS:直接在方法上加synchronized可能DoS攻擊喔,舉個栗子:

public class MyObject {    // Locks on the object‘s monitor    public synchronized void doSomething() {     // ...    }}// 駭客的代碼MyObject myObject = new MyObject();synchronized (myObject) {    while (true) {        // Indefinitely delay myObject        Thread.sleep(Integer.MAX_VALUE);     }}

駭客的代碼擷取了MyObject對象的鎖,導致doSomething死結,從而引發Denial of Service。

public class MyObject {    //locks on the class object‘s monitor    public static synchronized void doSomething() {     // ...    }}// 駭客的代碼synchronized (MyObject.class) {    while (true) {        Thread.sleep(Integer.MAX_VALUE); // Indefinitely delay MyObject    }}

2) 死結。

public class ThreadDeadlock {    public static void main(String[] args) throws InterruptedException {        Object obj1 = new Object();        Object obj2 = new Object();        Object obj3 = new Object();        Thread t1 = new Thread(new SyncThread(obj1, obj2), "t1");        Thread t2 = new Thread(new SyncThread(obj2, obj3), "t2");        Thread t3 = new Thread(new SyncThread(obj3, obj1), "t3");        t1.start();        Thread.sleep(5000);        t2.start();        Thread.sleep(5000);        t3.start();    }}class SyncThread implements Runnable{    private Object obj1;    private Object obj2;    public SyncThread(Object o1, Object o2){        this.obj1=o1;        this.obj2=o2;    }    @Override    public void run() {        String name = Thread.currentThread().getName();        System.out.println(name + " acquiring lock on "+obj1);        synchronized (obj1) {         System.out.println(name + " acquired lock on "+obj1);         work();         System.out.println(name + " acquiring lock on "+obj2);         synchronized (obj2) {            System.out.println(name + " acquired lock on "+obj2);            work();        }         System.out.println(name + " released lock on "+obj2);        }        System.out.println(name + " released lock on "+obj1);        System.out.println(name + " finished execution.");    }    private void work() {        try {            Thread.sleep(30000);        } catch (InterruptedException e) {            e.printStackTrace();        }    }}

上述代碼會輸出什麼呢?

Android開發筆記——常見BUG類型之記憶體泄露與安全執行緒

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.