1. 程式人生 > >android效能優化——記憶體洩漏

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

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

什麼是記憶體洩漏?

通俗一點就是記憶體沒有在GC掌控之內;當一個物件已經不需要再使用了,本該被回收,而有另外一個正在使用的物件持有它的引用從而就導致物件不能回收,這就導致了本該被回收的物件不能被回收而停留在堆記憶體中,就產生了記憶體洩漏。

android系統會給正在執行的程式一個獨立的程序,並分配一定的記憶體空間,對於每個程式來說它的記憶體空間是固定的,所以當每個物件不再使用時,應將其所開闢佔用用的記憶體釋放掉,如果發生記憶體洩漏並大量佔用記憶體,當所耗用的記憶體超過系統所分配的記憶體時就會導致記憶體溢位,從而導致程式閃退。

當定義一些成員變數或者例項化一些物件的時候,就會在系統分配的記憶體中去佔用一些記憶體,涉及到棧、堆、靜態方法區幾種記憶體分配策略;

棧:

在執行函式(方法)時,函式一些內部變數的儲存都可以放在棧上面建立,函式執行結束的時候這些儲存單元就會自動被釋放掉,棧記憶體包括分配的運算速度很快,因為內建在處理器裡面的,當然容量有限;

堆:

也叫做動態記憶體分配,有時候可以用malloc或者new來申請分配一個記憶體,在C/C++可能需要自己負責釋放,java裡面可以依賴GC機制,會將已不在使用或沒有其他物件引用的物件回收掉;

靜態方法區:

記憶體在程式編譯的時候就已經分配好,這塊的記憶體在程式整個執行期間都一直存在,它主要存放靜態資料、全域性的static資料和一些常量;

所以討論記憶體洩漏,主要討論堆記憶體,它存放的就是引用指向的物件實體;但是也要主要靜態方法區,大量靜態的使用可能不會造成記憶體洩漏,但是會導致記憶體一直會被佔用而無法釋放;所以在開發過程中根據需要可以使用一些java提供的StrongReference(強引用)、SoftReference(軟引用)、WeakReference(弱引用)、PhatomReference(虛引用);

StrongReference(強引用):從不回收;生命週期:JVM停止的時候才會停止;

SoftReference(軟引用):當記憶體不足的時候就會進行回收,SoftReference<String>結合ReferenceQueue構造有效期短;生命週期:記憶體不足時終止;

WeakReference(弱引用):在垃圾回收的時候就會進行回收;生命週期:GC後終止

PhatomReference(虛引用):在垃圾回收的時候;使用:合ReferenceQueue來跟蹤物件唄垃圾回收期回收的活動; 生命週期:GC後終止

所以在專案開發過程中需要養成良好的編碼習慣,同時儘量瞭解一些容易引發記憶體洩漏的場景,及一些檢測記憶體洩漏的工具和方法。

先去看一些容易引發記憶體洩漏的場景:

場景一:單例使用造成的記憶體洩漏

在專案開發過程中或多或少會使用到單例,例如:

public class CommUtil {
    private static CommUtil instance;
    private Context context;
    private CommUtil(Context context){
        this.context = context;
    }

    public static CommUtil getInstance(Context context){
        if(instance == null){
            instance = new CommUtil(context);
        }
        return instance;
    }
}
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //建立例項
        CommUtil commUtil = CommUtil.getInstance(this);
    }
}

這是很常見的單例寫法及例項化使用,其實自己沒有學習效能優化之前就是這樣寫的,學習之後才發這是一種錯誤的寫法,很容易就會出現記憶體洩漏;在例項化CommUtil時傳入的this代表的是當前的MainActivity,當MainActivity銷燬後,CommUtil中仍然持有MainActivity的引用,這樣就造成了MainActivity的洩漏;所以在例項化CommUtil的時,傳入的上下文可以使用getApplicationContext方式獲取,這樣就使得CommonUtil生命週期是跟Application程序一致;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //建立例項
        CommUtil commUtil = CommUtil.getInstance(getApplicationContext());
    }
}

場景二:執行緒使用導致的記憶體洩漏

專案開發過程中遇到耗時操作時,都會在子線中進行操作;

public class MainActivity extends AppCompatActivity {
    int a=10;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //模擬載入資料
        loadData();
    }

    private void loadData() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                        int b=a;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

這是一個模擬資料載入,當離開MainActivity後,如果new Thread()中持有MainActivity中成員變數的引用且子執行緒中的邏輯並沒執行完畢,這個時候就會造成記憶體洩漏,new Thread()其實是一個匿名內部類,這是由匿名內部類與MainActivity生命週期不一致,並持有MainActivity內的引用造成的記憶體洩漏;可以在loadData()方法上加static;

public class MainActivity extends AppCompatActivity {
    int a=10;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //模擬載入資料
        loadData();
    }

    private static void loadData() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                        int b=a;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

但是會發現,loadData();加了static後,int b=a;會報錯,如果loadData中沒有引用成員變數直接加static就可以了,如果有引用相應的成員變數可以採用下面的方式:

public class MainActivity extends AppCompatActivity {
    int a=10;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new LoadThread(new WeakReference<MainActivity>(this)).start();
    }
    private static class LoadThread extends Thread implement Runnable{
        private WeakReference<MainActivity> mainActivity;//設定軟引用儲存,當記憶體一發生GC的時候就會回收。

        public LoadThread(WeakReference<MainActivity> mainActivity) {
            this.mainActivity = mainActivity;
        }

        @Override
        public void run() {
            super.run();
            MainActivity main =  mainActivity.get();
            if(main==null||main.isFinishing()){
                return;
            }
            try {
                int b=main.a;
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

場景三:Handler造成的記憶體洩漏

Handler所造成的記憶體洩漏和執行緒造成的記憶體洩漏原因和解決方式都差不多;這裡直接說解決方式吧;

private static class LoadHander extends Handler{
        private WeakReference<MainActivity> mainActivity;//設定軟引用儲存,當記憶體一發生GC的時候就會回收。

        public LoadHander(WeakReference<MainActivity> mainActivity) {
            this.mainActivity = mainActivity;
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            MainActivity main =  mainActivity.get();
            if(main==null||main.isFinishing()){
                return;
            }
            int b=main.a;
        }
    }

場景四:一些檢視監聽所造成的洩漏

在獲取某個view的高度和寬度的時候,直接獲取是獲取不到的,可以採用view.post()方法,也可以採用監聽該view的檢視樹;

public class MainActivity extends AppCompatActivity {
    
    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
		
        final TextView tv= (TextView) findViewById(R.id.tv);
		
        tv.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
            @Override
            public void onWindowFocusChanged(boolean hasFocus) {
                //計算完後,一定要移除這個監聽
                tv.getViewTreeObserver().removeOnWindowFocusChangeListener(this);
            }
        });
    }
}

但是採用監聽view檢視樹的方式需要注意在操作完成後需要將該監聽移除調,在新增監聽的時候,該監聽是新增到了CopyOnWriteArrayList集合中,所以在不需要的時候就要從CopyOnWriteArrayList集合中移除。

當然了造成記憶體洩漏的場景不只上面這些,在專案開發中要養成效能優化的意識,養成良好的編碼習慣;還可以使用工具來檢測程式碼或者程式是否有記憶體洩漏。

Android Monitor工具

這是android studio自帶的一個工具;執行專案,在android studio下方找到Android Monitor,切換到Monitor


會看到Memory(反映記憶體情況)、CPU(CPU執行情況)、Network(網路耗用情況)、GPU(虛擬機器是沒有的,真機會有,反映ui渲染情況),Memory就是程式執行的記憶體情況,Free(剩下的記憶體),Allocated(已經使用的記憶體),在Memory右邊有些按鈕可以點選;


手動點選GC(多點選幾次),就會將沒有持有引用或空閒的記憶體回收掉;


多點選幾次GC,當記憶體耗用平穩後,點選形成快照,系統就會根據記憶體的情況形成一個.hporf檔案;




多次點選GC後生產的記憶體快照檔案中有兩個MainActivity,有一個被CommUtil持有沒有釋放就容易會造成洩漏。

Lint分析工具

Lint分析工具也是android studio自帶的工具,很方便,很好用,功能強大;

  1. 檢測資原始檔是否有沒有用到的資源。
  2. 檢測常見記憶體洩露
  3. 安全問題SDK版本安全問題
  4. 是否有費的程式碼沒有用到
  5. 程式碼的規範---甚至駝峰命名法也會檢測
  6. 自動生成的羅列出來
  7. 沒用的導包
  8. 可能的bug

上面這些是可以自動檢測的,Analyze--->Inspect Code


根據彈框選擇如下(第一次):


第一次好像和第二次的選擇好像不一樣;第二次選擇:


就會出現下面頁面:


可以根據提示對專案進行各方面的優化;

Memory Usage

Memory Usage只能大致粗略的知道程式有沒有記憶體洩漏等,不能定位到大致的造成洩漏的地方;


點選檢視生產的txt檔案,找到Objects,主要看Views和Activities,需要注意的是Views的數量並不代表就存在記憶體洩漏,還是要看Activities的數量,如果數量不對多半會是記憶體洩漏。


LeakCanary

LeakCanary是一個第三方的檢測記憶體洩漏,使用方便、簡單,可以定位到具體造成洩漏的地方,它是根據程式去進行檢測的,反饋的時間會有些遲;

github地址:https://github.com/square/leakcanary

首先引入依賴庫:

dependencies {
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
  releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
}

在專案的Application檔案中進行註冊;

public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    LeakCanary.install(this);
    // Normal app init code...
  }
}

執行應用程式,會在手機安裝相應的檢測程式;


操作應用程式,有造成記憶體洩漏的時候,LeakCanary就會發送訊息在手機通知欄,並顯示造成洩漏的地方;


使用起來還是比較簡單的,並且容易定位地方;當然了檢測記憶體洩漏的方法和工具不止上面這些,還有MAT、HeapTool(檢視堆資訊)、Allaction Tracking等方法,下面提供了一些檢測方法和工具使用的word文件地址:

https://pan.baidu.com/s/1r0V6bbSyR3aY7Qy8uqhB1Q