Android性能優(yōu)化-內(nèi)存泄漏(上)

本篇文章已授權(quán)微信公眾號 鴻洋 發(fā)布

為什么要做性能優(yōu)化?

  1. 手機性能越來越好,不用糾結(jié)這些細微的性能?
  • Android每一個應(yīng)用都是運行的獨立的Dalivk虛擬機,根據(jù)不同的手機分配的可用內(nèi)存可能只有(32M、64M等),所謂的4GB、6GB運行內(nèi)存其實對于我們的應(yīng)用不是可以任意索取

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

  1. 手機應(yīng)用一般使用的周期比較短,用完就關(guān)了。不像服務(wù)器應(yīng)用要長年累月運行,似乎影響不大?
  • 現(xiàn)在一般的用戶都不會重啟手機,可能一個月都不會重啟。像微信這樣的APP,每天都在使用。如果一旦發(fā)生內(nèi)存泄漏,那么可能一點一點的累積,程序就會出現(xiàn)OOM。
  1. 等應(yīng)用出現(xiàn)卡頓、發(fā)燙等,再來關(guān)注性能優(yōu)化?
  • 似乎是沒錯的。現(xiàn)在一般我們也都是等出現(xiàn)問題了再來找原因。但是學(xué)好性能優(yōu)化的目的不僅僅如此,我們在編碼階段就應(yīng)該從源頭來杜絕一些坑,這樣的成本比后期再來尋找原因要少得多

所以為了我們的應(yīng)用的健壯性、有良好的用戶體驗。性能優(yōu)化技術(shù),需要我們用心去研究和應(yīng)用。

什么是內(nèi)存泄漏?

JVM內(nèi)存管理

Java采用GC進行內(nèi)存管理。深入的JVM內(nèi)存管理知識,推薦《深入理解Java虛擬機》。關(guān)于內(nèi)存泄漏我們要知道,JVM內(nèi)存分配的幾種策略。

  1. 靜態(tài)的

    靜態(tài)的存儲區(qū),內(nèi)存在程序編譯的時候就已經(jīng)分配好了,這塊內(nèi)存在程序整個運行期間都一直存在,它主要存放靜態(tài)數(shù)據(jù)、全局的static數(shù)據(jù)和一些常量。

  2. 棧式的

    在執(zhí)行方法時,方法一些內(nèi)部變量的存儲都可以放在棧上面創(chuàng)建,方法執(zhí)行結(jié)束的時候這些存儲單元就會自動被注釋掉。棧 內(nèi)存包括分配的運算速度很快,因為內(nèi)在在處理器里面。當然容量有限,并且棧式一塊連續(xù)的內(nèi)存區(qū)域,大小是由操作系統(tǒng)決定的,他先進后 出,進出完成不會產(chǎn)生碎片,運行效率高且穩(wěn)定

  3. 堆式的

    也叫動態(tài)內(nèi)存 。我們通常使用new 來申請分配一個內(nèi)存。這里也是我們討論內(nèi)存泄漏優(yōu)化的關(guān)鍵存儲區(qū)。GC會根據(jù)內(nèi)存的使用情況,對堆內(nèi)存里的垃圾內(nèi)存進行回收。堆內(nèi)存是一塊不連續(xù)的內(nèi)存區(qū)域,如果頻繁地new/remove會造成大量的內(nèi)存碎片,GC頻繁的回收,導(dǎo)致內(nèi)存抖動,這也會消耗我們應(yīng)用的性能

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

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

開發(fā)時,為了防止內(nèi)存溢出,處理一些比較占用內(nèi)存并且生命周期長的對象時,可以盡量使用軟引用和弱引用。

Tip

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

局部變量的基本數(shù)據(jù)類型和引用存在棧中,應(yīng)用的對象實體存儲在堆中。因為它們屬于方法當中的變量,生命周期會隨著方法一起結(jié)束

內(nèi)存泄漏的定義

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

內(nèi)存泄漏與內(nèi)存溢出的區(qū)別

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

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

內(nèi)存泄漏帶來的影響

  • 應(yīng)用卡頓
    泄漏的內(nèi)存影響了GC的內(nèi)存分配,過多的內(nèi)存泄漏會影響應(yīng)用的執(zhí)行效率

  • 應(yīng)用異常(OOM)
    過多的內(nèi)存泄漏,最終會導(dǎo)致 Dalvik分配的內(nèi)存,出現(xiàn)OOM

Android開發(fā)常見的內(nèi)存泄漏

單例造成的內(nèi)存泄漏

  1. 錯誤示例

當調(diào)用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;
        }
  1. 解決方案

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

非靜態(tài)內(nèi)部類創(chuàng)建靜態(tài)實例造成的內(nèi)存泄漏

  1. 錯誤示例
 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. 解決方案

將非靜態(tài)內(nèi)部類修改為靜態(tài)內(nèi)部類。(靜態(tài)內(nèi)部類不會隱式持有外部類)

Handler造成的內(nèi)存泄漏

  1. 錯誤示例

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

 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. 解決方案

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

   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);
    }
}

線程造成的內(nèi)存泄漏

  1. 錯誤示例

異步任務(wù)和Runnable都是一個匿名內(nèi)部類,因此它們對當前Activity都有一個隱式引用。如果Activity在銷毀之前,任務(wù)還未完成, 那么將導(dǎo)致Activity的內(nèi)存資源無法回收,造成內(nèi)存泄漏

 
        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. 解決方案

使用 靜態(tài)內(nèi)部類,避免了Activity的內(nèi)存資源泄漏,當然在Activity銷毀時候也應(yīng)該取消相應(yīng)的任務(wù)AsyncTask::cancel(),避免任務(wù)在后臺執(zhí)行浪費資源

   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();

資源未關(guān)閉造成的內(nèi)存泄漏

  1. 錯誤示例

對于使用了BraodcastReceiver,ContentObserver,F(xiàn)ile,Cursor,Stream,Bitmap等資源的使用,應(yīng)該在Activity銷毀時及時關(guān)閉或者注銷,否則這些資源將不會被回收,造成內(nèi)存泄漏

  1. 解決方案

在Activity銷毀時及時關(guān)閉或者注銷

使用了靜態(tài)的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. 解決方案

    應(yīng)該及時將靜態(tài)的應(yīng)用 置為null,而且一般不建議將View及Activity設(shè)置為靜態(tài)

注冊了系統(tǒng)的服務(wù),但onDestory未注銷

  1. 錯誤示例
SensorManager sensorManager = getSystemService(SENSOR_SERVICE);
Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
sensorManager.registerListener(this,sensor,SensorManager.SENSOR_DELAY_FASTEST);
  1. 解決方案
  //不需要用的時候記得移除監(jiān)聽
       sensorManager.unregisterListener(listener);

不需要用的監(jiān)聽未移除會發(fā)生內(nèi)存泄露

  1. 錯誤示例
//add監(jiān)聽,放到集合里面
       tv.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
           @Override
           public void onWindowFocusChanged(boolean b) {
               //監(jiān)聽view的加載,view加載出來的時候,計算他的寬高等。
           }
       });
  1. 解決方案
  //計算完后,一定要移除這個監(jiān)聽
               tv.getViewTreeObserver().removeOnWindowFocusChangeListener(this);

Tip

tv.setOnClickListener();//監(jiān)聽執(zhí)行完回收對象,不用考慮內(nèi)存泄漏
tv.getViewTreeObserver().addOnWindowFocusChangeListene,add監(jiān)聽,放到集合里面,需要考慮內(nèi)存泄漏

下一篇將介紹如何進行內(nèi)存泄漏的分析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內(nèi)容