Android性能優化-內存泄漏(上)

爲什麼要做性能優化?

  1. 手機性能越來越好,不用糾結這些細微的性能?

    • Android每一個應用都是運行的獨立的Dalivk虛擬機,根據不同的手機分配的可用內存可能只有(32M、64M等),所謂的4GB、6GB運行內存其實對於我們的應用不是可以任意索取

    • 優秀的算法與效率低下的算法之間的運行效率要遠遠超過計算機硬件的的發展,雖然手機單核、雙核到4核、8核的發展,但性能優化任然不可忽略

  2. 手機應用一般使用的週期比較短,用完就關了。不像服務器應用要長年累月運行,似乎影響不大?

    • 現在一般的用戶都不會重啓手機,可能一個月都不會重啓。像微信這樣的APP,每天都在使用。如果一旦發生內存泄漏,那麼可能一點一點的累積,程序就會出現OOM。
  3. 等應用出現卡頓、發燙等,再來關注性能優化?

    • 似乎是沒錯的。現在一般我們也都是等出現問題了再來找原因。但是學好性能優化的目的不僅僅如此,我們在編碼階段就應該從源頭來杜絕一些坑,這樣的成本比後期再來尋找原因要少得多

    所以爲了我們的應用的健壯性、有良好的用戶體驗。性能優化技術,需要我們用心去研究和應用。

什麼是內存泄漏?

JVM內存管理


Java採用GC進行內存管理。深入的JVM內存管理知識,推薦《深入理解Java虛擬機》。關於內存泄漏我們要知道,JVM內存分配的幾種策略。

  1. 靜態的

    靜態的存儲區,內存在程序編譯的時候就已經分配好了,這塊內存在程序整個運行期間都一直存在,它主要存放靜態數據、全局的static數據和一些常量。

  2. 棧式的

    在執行方法時,方法一些內部變量的存儲都可以放在棧上面創建,方法執行結束的時候這些存儲單元就會自動被註釋掉。棧 內存包括分配的運算速度很快,因爲內在在處理器裏面。當然容量有限,並且棧式一塊連續的內存區域,大小是由操作系統決定的,他先進後 出,進出完成不會產生碎片,運行效率高且穩定

  3. 堆式的

    也叫動態內存 。我們通常使用new 來申請分配一個內存。這裏也是我們討論內存泄漏優化的關鍵存儲區。GC會根據內存的使用情況,對堆內存裏的垃圾內存進行回收。堆內存是一塊不連續的內存區域,如果頻繁地new/remove會造成大量的內存碎片,GC頻繁的回收,導致內存抖動,這也會消耗我們應用的性能

我們知道可以調用 System.gc();進行內存回收,但是GC不一定會執行。面對GC的機制,我們是否無能爲力?其實我們可以通過聲明一些引用標記來讓GC更好對內存進行回收。

類型 回收時機 生命週期
StrongReference (強引用) 任何時候GC是不能回收他的,哪怕內存不足時,系統會直接拋出異常OutOfMemoryError,也不會去回收 進程終止
SoftReference (軟引用) 當內存足夠時不會回收這種引用類型的對象,只有當內存不夠用時纔會回收 內存不足,進行GC的時候
WeakReference (弱引用) GC一運行就會把給回收了 GC後終止
PhantomReference (虛引用) 如果一個對象與虛引用關聯,則跟沒有引用與之關聯一樣,在任何時候都可能被垃圾回收器回收 任何時候都有可能

開發時,爲了防止內存溢出,處理一些比較佔用內存並且生命週期長的對象時,可以儘量使用軟引用和弱引用。

Tip

成員變量全部存儲在堆中(包括基本數據類型,引用及引用的對象實體),因爲他們屬於類,類對象最終還是要被new出來的

局部變量的基本數據類型和引用存在棧中,應用的對象實體存儲在堆中。因爲它們屬於方法當中的變量,生命週期會隨着方法一起結束

內存泄漏的定義

當一個對象已經不需要使用了,本該被回收時,而有另外一個正在使用的對象持有它的引用,從而導致了對象不能被GC回收。這種導致了本該被回收的對象不能被回收而停留在堆內存中,就產生了內存泄漏

內存泄漏與內存溢出的區別

  • 內存泄漏(Memory Leak)
    進程中某些對象已經沒有使用的價值了,但是他們卻還可以直接或間接地被引用到GC Root導致無法回收。當內存泄漏過多的時候,再加上應用本身佔用的內存,日積月累最終就會導致內存溢出OOM

  • 內存溢出(OOM)
    當 應用的heap資源超過了Dalvik虛擬機分配的內存就會內存溢出

內存泄漏帶來的影響

  • 應用卡頓
    泄漏的內存影響了GC的內存分配,過多的內存泄漏會影響應用的執行效率

  • 應用異常(OOM)
    過多的內存泄漏,最終會導致 Dalvik分配的內存,出現OOM

Android開發常見的內存泄漏

單例造成的內存泄漏

  1. 錯誤示例
  2. 當調用getInstance時,如果傳入的context是Activity的context。只要這個單例沒有被釋放,那麼這個
    Activity也不會被釋放一直到進程退出纔會釋放。

    public class CommUtil {
        private static CommUtil instance;
        private Context context;
        private CommUtil(Context context){
        this.context = context;
        }
    
        public static CommUtil getInstance(Context mcontext){
        if(instance == null){
            instance = new CommUtil(mcontext);
        }
        return instance;
        }
  3. 解決方案

    能使用Application的Context就不要使用Activity的Content,Application的生命週期伴隨着整個進程的週期

    非靜態內部類創建靜態實例造成的內存泄漏

  4. 錯誤示例

 private static TestResource mResource = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if(mManager == null){
            mManager = new TestResource();
        }

    }
    class TestResource {

    }
  1. 解決方案

將非靜態內部類修改爲靜態內部類。(靜態內部類不會隱式持有外部類)

Handler造成的內存泄漏

  1. 錯誤示例

mHandler是Handler的非靜態匿名內部類的實例,所以它持有外部類Activity的引用,我們知道消息隊列是在一個Looper線程中不斷輪詢處理消息,那麼當這個Activity退出時消息隊列中還有未處理的消息或者正在處理消息,而消息隊列中的Message持有mHandler實例的引用,mHandler又持有Activity的引用,所以導致該Activity的內存資源無法及時回收,引發內存泄漏。

 private MyHandler mHandler = new MyHandler(this);
    private TextView mTextView ;
    private static class MyHandler extends Handler {
        private WeakReference<Context> reference;
        public MyHandler(Context context) {
            reference = new WeakReference<>(context);
        }
        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = (MainActivity) reference.get();
            if(activity != null){
                activity.mTextView.setText("");
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView)findViewById(R.id.textview);
        loadData();
    }

    private void loadData() {

        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
  1. 解決方案

創建一個靜態Handler內部類,然後對Handler持有的對象使用弱引用,這樣在回收時也可以回收Handler持有的對象,這樣雖然避免了Activity泄漏,不過Looper線程的消息隊列中還是可能會有待處理的消息,所以我們在Activity的Destroy時或者Stop時應該移除消息隊列中的消息

   private MyHandler mHandler = new MyHandler(this);
    private TextView mTextView ;
    private static class MyHandler extends Handler {
        private WeakReference<Context> reference;
        public MyHandler(Context context) {
            reference = new WeakReference<>(context);
        }
        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = (MainActivity) reference.get();
            if(activity != null){
                activity.mTextView.setText("");
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView)findViewById(R.id.textview);
        loadData();
    }

    private void loadData() {
        //...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }
}

線程造成的內存泄漏

  1. 錯誤示例

異步任務和Runnable都是一個匿名內部類,因此它們對當前Activity都有一個隱式引用。如果Activity在銷燬之前,任務還未完成, 那麼將導致Activity的內存資源無法回收,造成內存泄漏

        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                SystemClock.sleep(10000);
                return null;
            }
        }.execute();


        new Thread(new Runnable() {
            @Override
            public void run() {
                SystemClock.sleep(10000);
            }
        }).start();
  1. 解決方案

使用 靜態內部類,避免了Activity的內存資源泄漏,當然在Activity銷燬時候也應該取消相應的任務AsyncTask::cancel(),避免任務在後臺執行浪費資源

   static class MyAsyncTask extends AsyncTask<Void, Void, Void> {
        private WeakReference<Context> weakReference;

        public MyAsyncTask(Context context) {
            weakReference = new WeakReference<>(context);
        }

        @Override
        protected Void doInBackground(Void... params) {
            SystemClock.sleep(10000);
            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            super.onPostExecute(aVoid);
            MainActivity activity = (MainActivity) weakReference.get();
            if (activity != null) {
                //...
            }
        }
    }
    static class MyRunnable implements Runnable{
        @Override
        public void run() {
            SystemClock.sleep(10000);
        }
    }
//——————
    new Thread(new MyRunnable()).start();
    new MyAsyncTask(this).execute();

資源未關閉造成的內存泄漏

  1. 錯誤示例

對於使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源的使用,應該在Activity銷燬時及時關閉或者註銷,否則這些資源將不會被回收,造成內存泄漏

  1. 解決方案

在Activity銷燬時及時關閉或者註銷

使用了靜態的Activity和View

  1. 錯誤示例
static view; 

    void setStaticView() { 
      view = findViewById(R.id.sv_button); 
    } 

    View svButton = findViewById(R.id.sv_button); 
    svButton.setOnClickListener(new View.OnClickListener() { 
      @Override public void onClick(View v) { 
        setStaticView(); 
        nextActivity(); 
      } 
    }); 


    static Activity activity; 

    void setStaticActivity() { 
      activity = this; 
    } 

    View saButton = findViewById(R.id.sa_button); 
    saButton.setOnClickListener(new View.OnClickListener() { 
      @Override public void onClick(View v) { 
        setStaticActivity(); 
        nextActivity(); 
      } 
    });
  1. 解決方案

    應該及時將靜態的應用 置爲null,而且一般不建議將View及Activity設置爲靜態

註冊了系統的服務,但onDestory未註銷

  1. 錯誤示例

    SensorManager sensorManager = getSystemService(SENSOR_SERVICE);
    Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
    sensorManager.registerListener(this,sensor,SensorManager.SENSOR_DELAY_FASTEST);
  2. 解決方案

    //不需要用的時候記得移除監聽
        sensorManager.unregisterListener(listener);

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

  1. 錯誤示例

    //add監聽,放到集合裏面
        tv.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
            @Override
            public void onWindowFocusChanged(boolean b) {
                //監聽view的加載,view加載出來的時候,計算他的寬高等。
            }
        });
  2. 解決方案

    //計算完後,一定要移除這個監聽
                tv.getViewTreeObserver().removeOnWindowFocusChangeListener(this);

Tip

tv.setOnClickListener();//監聽執行完回收對象,不用考慮內存泄漏
tv.getViewTreeObserver().addOnWindowFocusChangeListene,add監聽,放到集合裏面,需要考慮內存泄漏

轉自:http://www.jianshu.com/p/402225fce4b2



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