性能優化——內存泄漏(3)代碼分析篇

內存泄漏系列文章:
性能優化——內存泄漏(1)入門篇
性能優化——內存泄漏(2)工具分析篇
性能優化——內存泄漏(3)代碼分析篇

一、簡述

在上一篇《性能優化——內存泄漏(2)工具分析篇》中,介紹瞭如何使用工具幫助我們檢查APP中是否存在內存泄漏、及如何定位到內存泄漏,但項目並不能完全依賴工具來檢查,畢竟定位內存泄漏比較麻煩,還不如在開發時就考慮到內存泄漏問題,儘可能減少內存泄漏,後續優化纔不會那麼痛苦。下面就來看看開發中,哪些代碼可能造成內存泄漏,及避免內存泄漏的對應解決方案。

二、代碼分析

1、靜態變量引起的內存泄露

1)錯誤示例

這個可以拿之前的Demo來說明,Demo代碼如下:

// 單例工具類
public class CommonUtil {
    private static CommonUtil mInstance;
    private Context mContext;
    public CommonUtil(Context context) {
        mContext = context;
    }
    public static CommonUtil getInstance(Context context) {
        if (mInstance == null) {
            synchronized (CommonUtil.class) {
                if (mInstance == null) {
                    mInstance = new CommonUtil(context);
                }
            }
        }
        return mInstance;
    }
    ...
}

// Activity中使用單例工具
public class MemoryLeakActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak);
        CommonUtil.getInstance(this);
    }
}

當調用getInstance()時,如果傳入的context是Activity,那麼只要這個單例沒有被釋放,則這個Activity也不會被釋放,直到進程退出後纔會釋放。

2)解決方案

不要傳入Activity,可以使用getApplicationContext()來代替。

2、非靜態內部類引起內存泄露(包括匿名內部類)

在Java中,非靜態的內部類和匿名內部類都會隱式地持有其外部類的引用。

1)錯誤示例

public class MemoryLeakActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak);
        loadData();
    }

    public void loadData() {
        new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10000);
                    System.out.println("模擬同步網絡數據完畢");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }
}

上面的代碼中,使用Thread匿名內部類開闢線程同步網絡數據,而這個內部類會隱式持有外部類的引用,當退出界面後,該內部類任務還在進行,導致該界面無法被GC回收,於是就會產生內存泄漏。

APP退出後,執行GC,獲取內存快照。可以看到MemoryLeakActivity的Total Count爲1,說明存在內存泄漏。

2)解決方案

靜態的內部類不會持有外部類的引用。

(1) 使用靜態方法

public static void loadData() {
    new Thread() {
        @Override
        public void run() {
            try {
                Thread.sleep(10000);
                System.out.println("模擬同步網絡數據完畢");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }.start();
}

(2) 使用靜態內部類

public void loadData() {
    new MyThread().start();
}

static class MyThread extends Thread {
    @Override
    public void run() {
        try {
            Thread.sleep(10000);
            System.out.println("模擬同步網絡數據完畢");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

3)拓展

但項目開發中,可能會存在一定要使用內部類去持有外部類的情況,比如數據同步完成後,需要修改界面上的文本信息,而靜態內部類無法直接持有外部類的引用,這又該怎麼解決呢?其實可以通過內部類的構造函數將外部類傳入,並使用弱引用保存(GC執行後釋放),並在內部類中做好判空即可。

static class MyThread extends Thread {
    Reference<Context> mReference;

    public MyThread(Context context) {
        mReference = new WeakReference<>(context);
    }

    @Override
    public void run() {
        try {
            Thread.sleep(10000);
            MemoryLeakActivity context = (MemoryLeakActivity) mReference.get();
            if (context != null) {
                context.mTv.setText("模擬同步網絡數據完畢");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

APP退出後,執行GC,獲取內存快照。可以看到MemoryLeakActivity的Total Count爲0,說明沒有內存泄漏。

3、不需要用的監聽未移除會發生內存泄露

1)錯誤示例

public class MemoryLeakActivity extends AppCompatActivity implements SensorEventListener {

    private SensorManager mSm;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak);

        mSm = (SensorManager) getSystemService(SENSOR_SERVICE);
        Sensor sensor = mSm.getDefaultSensor(Sensor.TYPE_ALL);
        mSm.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL);
    }
}

本例中使用getSystemService()獲取傳感器服務,並設置了監聽,但沒有在onDestroy()方法中將監聽移除,此類監聽沒有及時清除的話,必定造成內存泄漏。

2)解決方案

只需在onDestroy()中移除監聽即可。

@Override
protected void onDestroy() {
    super.onDestroy();
    mSm.unregisterListener(this);
}

4、資源未關閉引起的內存泄露情況

1)錯誤示例

比如:BroadCastReceiver、Cursor、Bitmap、IO流、自定義屬性。
當不需要使用的時候,要記得及時釋放資源。否則就會內存泄露。

2)解決方案

這裏以Cursor、IO流和自定義屬性爲例。

(1)Cursor或IO流

try {
    ...
    使用cursor/io讀取數據操作
    ...
} catch (Exception e) {
    e.printStackTrace();
} finally {
    if (cursor/io != null) {
        cursor/io.close();
        cursor/io = null;
    }
}

(2)自定義屬性

TypedArray a = theme.obtainStyledAttributes(attrs,
                com.android.internal.R.styleable.TextViewAppearance, defStyleAttr, defStyleRes);
TypedArray appearance = null;
int ap = a.getResourceId(
        com.android.internal.R.styleable.TextViewAppearance_textAppearance, -1);
a.recycle();// 不執行回收會造成內存泄漏
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章