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

1.前言

上一篇文章關(guān)于Android性能優(yōu)化--啟動優(yōu)化探討了啟動優(yōu)化相關(guān)的知識點(diǎn),在本篇將介紹內(nèi)存優(yōu)化的相關(guān)優(yōu)化扁远。主要大綱參照如下

memory.jpg

2.常見問題

常見的Android內(nèi)存相關(guān)問題屏箍,通沉埽可以分為以下三種才避,內(nèi)存抖動、內(nèi)存泄露嗤形、內(nèi)存溢出。

  • 內(nèi)存抖動:在短時(shí)間內(nèi)有大量的對象被創(chuàng)建或者被回收的現(xiàn)象弧圆,主要是循環(huán)中大量創(chuàng)建赋兵、回收對象。當(dāng)系統(tǒng)內(nèi)存不足搔预,不斷GC內(nèi)存的時(shí)候霹期,也有可能出現(xiàn)內(nèi)存抖動情況。
  • 內(nèi)存泄露:當(dāng)一個(gè)對象不在使用了拯田,本應(yīng)該被垃圾回收器回收历造。但是這個(gè)對象由于被其他正在使用的對象所持有,造成無法被回收的現(xiàn)象船庇。
  • 內(nèi)存溢出:Android系統(tǒng)給每個(gè)應(yīng)用分配的內(nèi)存也有一個(gè)閥值吭产,也就是Heap Size。當(dāng)應(yīng)用占用的內(nèi)存加上我們申請的內(nèi)存資源超過了系統(tǒng)分配的最大內(nèi)存時(shí)就會拋出的Out Of Memory異常鸭轮。
    上述三者之間的是一個(gè)遞進(jìn)關(guān)系臣淤,內(nèi)存抖動<內(nèi)存泄露<內(nèi)存溢出。對于一般應(yīng)用主要是處理內(nèi)存抖動和內(nèi)存泄露兩點(diǎn)窃爷,處理好這兩點(diǎn)就會大大降低內(nèi)存溢出的可能性邑蒋。

3.內(nèi)存管理

3.1 Java內(nèi)存管理

JVM的內(nèi)存回收對于大多數(shù)開發(fā)者來說接觸的并不是很多。因?yàn)镴VM本身是有一套內(nèi)存回收的機(jī)制吞鸭,對于開發(fā)者更多的是申請對象直接調(diào)用即可寺董,其內(nèi)部并不是很在意。下面主要通過內(nèi)存存儲和回收這兩塊介紹刻剥。

  • 存儲:JVM將可以存儲內(nèi)存的空間大概分為棧遮咖、本地方法棧、程序計(jì)數(shù)器造虏、堆御吞、方法區(qū)等模塊麦箍。
  1. 棧:主要是針對方法使用的空間,當(dāng)JVM在執(zhí)行方法時(shí)陶珠,會在此區(qū)域中創(chuàng)建一個(gè)棧幀來存放方法的各種信息挟裂,比如返回值,局部變量表和各種對象引用等揍诽。
  2. 本地方法棧:專門提供給Native方法用的诀蓉。
  3. 程序計(jì)數(shù)器:記錄當(dāng)前執(zhí)行的位置。
  4. 堆:幾乎所有對象暑脆、數(shù)組等都是在此分配內(nèi)存的渠啤,在JVM內(nèi)存中占的比例也是很大的,也是GC回收的主要陣地添吗,平時(shí)我們說的新生代沥曹、老年代、永久代也是指這片區(qū)域碟联。
  5. 方法區(qū):存放類似類定義妓美、常量、編譯后的代碼鲤孵、靜態(tài)變量等壶栋。
  • 回收:針對上述各個(gè)模塊的內(nèi)存回收,通常所說的GC主要是對堆空間的回收裤纹,一般比較常用的方法為:標(biāo)記-清除算法委刘、復(fù)制算法、分代收集算法等其它方法和其變形鹰椒。
3.2 Android內(nèi)存管理

Android 系統(tǒng)主要是在Art和Dalvik虛擬機(jī)中的托管環(huán)境中跟蹤每個(gè)內(nèi)存分配锡移,當(dāng)發(fā)現(xiàn)有可回收的對象,進(jìn)行內(nèi)存回收漆际∠海回收有兩個(gè)目標(biāo):在程序中查找將來無法訪問的數(shù)據(jù)對象,并回收那些對象使用的資源 奸汇。

  • 進(jìn)程間的內(nèi)存管理:Android對于進(jìn)程間的內(nèi)存管理主要是通過內(nèi)核交換守護(hù)程序和onTrimMemory()進(jìn)程殺死來管理施符。
  1. 內(nèi)核交換守護(hù)程序(kswapd):RAM中存在一個(gè)區(qū)域空間zRAM。當(dāng)設(shè)備上的可用內(nèi)存不足時(shí)擂找,守護(hù)程序?qū)⒆優(yōu)榛顒訝顟B(tài)戳吝。kswapd可以將緩存的私有臟頁和匿名臟頁移動到zRAM,并在其中進(jìn)行壓縮贯涎。
  2. onTrimMemory:系統(tǒng)用于 onTrimMemory()通知應(yīng)用程序內(nèi)存即將用盡听哭,并應(yīng)減少其分配。如果這還不夠,內(nèi)核將開始?xì)⑺肋M(jìn)程以釋放內(nèi)存陆盘。它使用低內(nèi)存殺手(LMK)來執(zhí)行此操作普筹。PS:LMK 這就會涉及到應(yīng)用保活等相關(guān)隘马。
  • 應(yīng)用內(nèi)存管理:Android應(yīng)用內(nèi)內(nèi)存管理太防,主要是從Java層和Native 層優(yōu)化。本文主要介紹如何從Java層進(jìn)行內(nèi)存管理優(yōu)化酸员,具體細(xì)節(jié)可以下面會一一介紹蜒车。

4.常見場景及解決方案

4.1 內(nèi)存抖動

由于短時(shí)間內(nèi)有大量對象進(jìn)出Young Generiation區(qū)導(dǎo)致的,它伴隨著頻繁的GC幔嗦。

  1. 盡量避免在循環(huán)體內(nèi)創(chuàng)建對象醇王,應(yīng)該把對象創(chuàng)建移到循環(huán)體外。
  2. 注意自定義View的onDraw()方法會被頻繁調(diào)用崭添,所以在這里面不應(yīng)該頻繁的創(chuàng)建對象。
    如下面一部分代碼就對應(yīng)著內(nèi)存抖動
    Handler handler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            for (int i =0;i<100;i++){
                String string[] = new String[10000];
            }
            handler.sendEmptyMessageDelayed(1,30);

        }
    };

通過Profile 查看其內(nèi)存圖

memory_shake.jpg

可以看到其內(nèi)存圖基本上是一個(gè)鋸齒狀叛氨,是因?yàn)檫@時(shí)候一直在創(chuàng)建對象和回收對象所致呼渣。

4.2 內(nèi)存泄露

業(yè)內(nèi)一般對內(nèi)存泄露的原因總結(jié)為長生命周期對象引用短生命周期對象,導(dǎo)致短生命周期對象無法及時(shí)回收所致寞埠。

  1. 單例引起的內(nèi)存泄漏
public static  FacebookAnalysis getInstance(Context context){
        if (facebookAnalysis == null){
            synchronized (FacebookAnalysis.class){
                facebookAnalysis = new FacebookAnalysis(getAppEventsLoggerInstance(context));
            }
        }
        return facebookAnalysis;
    }

上面是一個(gè)常見的單例模式屁置,如果參數(shù)引用Activity的Context,而單例模式的生命周期長于Activity仁连。這里單例模式引用Activity的實(shí)例蓝角,當(dāng)Activity被銷毀,Activity無法被回收饭冬,造成內(nèi)存泄露使鹅。
如果這里引用的Application的Context,將無任何影響昌抠。因?yàn)锳pplication的生命周期與單例模式同樣長患朱。

  1. 靜態(tài)集合添加對象,在使用完之后未及時(shí)釋放炊苫。
        for (int i = 0; i < 10; i++) {
            Object obj = new Object();
            list.add(obj);
            obj = null;
        }

此時(shí)list是一個(gè)靜態(tài)的集合裁厅,obj單個(gè)對象,當(dāng)list集合使用完畢侨艾,應(yīng)當(dāng)及時(shí)清除該集合执虹,避免obj被靜態(tài)對象引用。

  1. 匿名內(nèi)部類&非靜態(tài)內(nèi)部類
    Android 中常見的是對ListView中各個(gè)元素設(shè)置點(diǎn)擊事件唠梨,如果此時(shí)采用匿名內(nèi)部類袋励,會存在內(nèi)存泄露的風(fēng)險(xiǎn)。常規(guī)做法是將該點(diǎn)擊事件用接口和setTag()的方式往外傳遞。
    同樣Handler在使用過程中也會出現(xiàn)內(nèi)存泄露的風(fēng)險(xiǎn)插龄,一般則是采用弱引用的方式處理愿棋,或者在Activity 的onDestroy方法中移除該Handler的所有消息handler.removeCallbacksAndMessages(null)
  2. 線程泄露
    在主線程Activity中出現(xiàn)如下代碼:
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        mIntent = (icicle == null) ? getIntent() : null;
        new Thread(new Runnable() {
            @Override
            public void run() {
                doSomeThing();
            }
        }).start();
}

當(dāng)此時(shí)該Activity已經(jīng)銷毀均牢,但是子線程中doSomeThing方法未執(zhí)行完成糠雨,此時(shí)會造成內(nèi)存泄露。一般做法是當(dāng)Activity銷毀時(shí)取消該線程或者采用其他方式實(shí)現(xiàn)徘跪,總之原則是線程不持有Activity的上下文甘邀,如果持有,就應(yīng)及時(shí)取消垮庐。

  1. 數(shù)據(jù)庫游標(biāo)松邪,文件資源未及時(shí)關(guān)閉,廣播未反向注冊哨查,服務(wù)未解綁等行為逗抑。
  2. Bitmap加載泄露,Bitmap的加載在Android一直是比較吃內(nèi)存的,且容易出現(xiàn)內(nèi)存泄露相關(guān)問題寒亥,一般都是采用統(tǒng)一的圖片請求框架去處理圖片加載緩存邮府,這些框架都會從加載、壓縮溉奕、緩存等策略對其做優(yōu)化處理褂傀。同時(shí)Google 官方也是推薦使用統(tǒng)一庫處理位圖,具體可以在Glide官網(wǎng)查看加勤。關(guān)于圖片加載這塊其實(shí)是很容易出現(xiàn)內(nèi)存泄露的問題仙辟,在此暫時(shí)不作展開,后續(xù)會細(xì)說鳄梅。

5.常用工具

5.1 Memory Profiler

Memory ProfilerAndroid Profiler 中的一個(gè)組件叠国,可幫助您識別可能會導(dǎo)致應(yīng)用卡頓、凍結(jié)甚至崩潰的內(nèi)存泄漏和內(nèi)存抖動卫枝。它顯示一個(gè)應(yīng)用內(nèi)存使用量的實(shí)時(shí)圖表煎饼,可以捕獲堆轉(zhuǎn)儲、強(qiáng)制執(zhí)行垃圾回收以及跟蹤內(nèi)存分配校赤。從圖中現(xiàn)象

memory_shake.jpg

可以看出應(yīng)用此時(shí)存在內(nèi)存抖動的現(xiàn)象吆玖,此時(shí)抓取紅色部分,可以得到如下圖:
shake.jpg

-A區(qū)域?yàn)橥献岩蓜觿拥牟糠?br> -B區(qū)域?yàn)榕判虬l(fā)現(xiàn)存在一組對象耗內(nèi)存
-D選中C區(qū)域中任一對象马篮,即可看見具體類為MainActivity沾乘,且可以看到行數(shù),右擊Jump to Source即可以跳入具體代碼浑测。
5.2 Memory Analyzer
Memory Analyzer MAT
是一個(gè)功能豐富的 JAVA 堆轉(zhuǎn)儲文件分析工具翅阵,可以幫助開發(fā)者發(fā)現(xiàn)內(nèi)存漏洞和減少內(nèi)存消耗歪玲。根據(jù)上面分析下面這段代碼是存在內(nèi)存泄露的問題:

public class CallBackManager {

    public static ArrayList<CallBack> sCallBacks = new ArrayList<>();

    public static void addCallBack(CallBack callBack) {
        sCallBacks.add(callBack);
    }

    public static void removeCallBack(CallBack callBack) {
        sCallBacks.remove(callBack);
    }

}

public class MemoryLeakActivity extends AppCompatActivity implements CallBack{

    @Override
    protected void onCreate( Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memoryleak);
        ImageView imageView = findViewById(R.id.iv_memoryleak);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.splash);
        imageView.setImageBitmap(bitmap);
        CallBackManager.addCallBack(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
//        CallBackManager.removeCallBack(this);
    }

    @Override
    public void doWork() {
        // do work
    }
}

連續(xù)進(jìn)入退出MemoryLeakActivity,通過Android StudioProfile可以查看到當(dāng)時(shí)的內(nèi)存入下圖

menmory_leak.jpg

可以看到此時(shí)內(nèi)存處于一個(gè)波動狀態(tài)掷匠,保存該文件滥崩,生成memory-20191212T110510.hprof文件,通過命令轉(zhuǎn)換

D:\>hprof-conv D:\Android\log\memory-20191212T110510.hprof D:\Android\log\2.hprof

此時(shí)通過MAT打開轉(zhuǎn)換后的文件如下:

hprof.jpg

選擇Histogram讹语,輸入正則表達(dá)".MemoryLeak. "可以搜索到具體包名钙皮,然后右鍵List objects->With incoming references然后選擇Path to GC Roots->With all references(此處可以選擇其他)。此時(shí)可以看到下面這張圖
leak.jpg
從圖中即可看出內(nèi)存泄露的位置顽决,即該Activity被對象sCallbacks引用短条。在代碼中添加方法

    protected void onDestroy() {
        super.onDestroy();
        CallBackManager.removeCallBack(this);
    }

再次抓取內(nèi)存信息,并未出現(xiàn)上述結(jié)果才菠。
5.3 LeakCanary
可以通過LeakCanary在開發(fā)階段檢測到引用的內(nèi)存情況茸时。LeakCanary 主要是通過監(jiān)聽Activity的onDestory,手動調(diào)用GC赋访,然后通過ReferenceQueue+WeakReference可都,來判斷Activity對象是否被回收,然后結(jié)合dump Heap的hpof文件蚓耽,通過Haha開源庫分析泄露的位置汹粤。具體使用可以參照leakcanary

6. 總結(jié)

關(guān)于內(nèi)存優(yōu)化知識點(diǎn)很多田晚,很細(xì)。但究其根本我認(rèn)為是監(jiān)控內(nèi)存泄露和優(yōu)化內(nèi)存泄漏,各大廠商都有提過相關(guān)的方案
美團(tuán)—Android線上OOM問題定位組件
微信 Android 終端內(nèi)存優(yōu)化實(shí)踐
這些都具備參考價(jià)值国葬。同時(shí)我們也可以采用一些Hook黑科技相關(guān)方法進(jìn)行部分內(nèi)存性能消耗較大的業(yè)務(wù)進(jìn)行監(jiān)控贤徒,及時(shí)告知開發(fā)人員。例如:可以通過Epic監(jiān)控項(xiàng)目中所有的setImageBitmap()方法汇四,此時(shí)就可以知道傳入的Bitmap是否有內(nèi)存相關(guān)風(fēng)險(xiǎn)接奈,一旦有風(fēng)險(xiǎn),立馬通知反饋通孽。
以上為此次Android內(nèi)存優(yōu)化的總結(jié)序宦,歡迎指正。
感謝:
https://developer.android.google.cn/topic/performance/memory-overview
https://time.geekbang.org/column/article/71610
https://coding.imooc.com/learn/list/308.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末背苦,一起剝皮案震驚了整個(gè)濱河市互捌,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌行剂,老刑警劉巖秕噪,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異厚宰,居然都是意外死亡腌巾,警方通過查閱死者的電腦和手機(jī)遂填,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來澈蝙,“玉大人吓坚,你說我怎么就攤上這事〉朴” “怎么了礁击?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長漏麦。 經(jīng)常有香客問我客税,道長,這世上最難降的妖魔是什么撕贞? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任更耻,我火速辦了婚禮,結(jié)果婚禮上捏膨,老公的妹妹穿的比我還像新娘秧均。我一直安慰自己,他們只是感情好号涯,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布目胡。 她就那樣靜靜地躺著,像睡著了一般链快。 火紅的嫁衣襯著肌膚如雪誉己。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天域蜗,我揣著相機(jī)與錄音巨双,去河邊找鬼。 笑死霉祸,一個(gè)胖子當(dāng)著我的面吹牛筑累,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播丝蹭,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼慢宗,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了奔穿?” 一聲冷哼從身側(cè)響起镜沽,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎贱田,沒想到半個(gè)月后淘邻,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡湘换,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年宾舅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了统阿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,001評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡筹我,死狀恐怖扶平,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蔬蕊,我是刑警寧澤结澄,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站岸夯,受9級特大地震影響麻献,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜猜扮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一勉吻、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧旅赢,春花似錦齿桃、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至僵控,卻和暖如春香到,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背报破。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工养渴, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人泛烙。 一個(gè)月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像翘紊,于是被迫代替她去往敵國和親蔽氨。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評論 2 355