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

1 概述

本篇博客是Android App性能優(yōu)化專(zhuān)題的第一篇文章,該專(zhuān)題會(huì)在渲染辆飘、計(jì)算啦辐、內(nèi)存谓传、電量方面進(jìn)行講解Android App的性能優(yōu)化。

為了更加高效的優(yōu)化App性能芹关,Android Studio提供了一個(gè)Android Monitor工具续挟,它位于Android Studio主窗口的下方,Android Monitor提供了實(shí)時(shí)記錄和觀察App以下信息的工具:

  • 系統(tǒng)或用戶定義的Log信息
  • Memory侥衬,CPU和GPU使用率
  • Network流量(僅限硬件設(shè)備)

2 預(yù)備知識(shí)

2.1 Android App的內(nèi)存結(jié)構(gòu)

Random Access Memory(RAM)在任何軟件開(kāi)發(fā)環(huán)境中都是一個(gè)很寶貴的資源诗祸。這一點(diǎn)在物理內(nèi)存通常很有限的移動(dòng)操作系統(tǒng)上,顯得尤為突出轴总。系統(tǒng)會(huì)在RAM上為App進(jìn)程分配固定的內(nèi)存空間直颅,然后該App進(jìn)程就會(huì)運(yùn)行在該內(nèi)存空間上。該內(nèi)存空間會(huì)被分成Stack內(nèi)存空間和Heap內(nèi)存空間怀樟,其中Stack內(nèi)存空間里存放對(duì)象的引用功偿,Heap內(nèi)存空間里存放對(duì)象數(shù)據(jù)。

在Android的高級(jí)系統(tǒng)版本里面針對(duì)Heap內(nèi)存空間有一個(gè)3級(jí)的Generational Heap Memory的模型往堡,它包括Young Generation械荷,Old Generation,Permanent Generation三個(gè)區(qū)域虑灰。最新分配的對(duì)象會(huì)存放在Young Generation區(qū)域吨瞎,當(dāng)這個(gè)對(duì)象在這個(gè)區(qū)域停留的時(shí)間超過(guò)某個(gè)值的時(shí)候,會(huì)被移動(dòng)到Old Generation瘩缆,最后到Permanent Generation區(qū)域关拒。整個(gè)結(jié)構(gòu)如下圖所示:



這3個(gè)區(qū)域都有固定的大小,隨著新的對(duì)象陸續(xù)被分配到此區(qū)域庸娱,當(dāng)這些對(duì)象總的大小快達(dá)到該區(qū)域的大小時(shí),會(huì)觸發(fā)GC的操作谐算,以便騰出空間來(lái)存放其他新的對(duì)象熟尉,如下圖所示:



最近剛分配的對(duì)象會(huì)放在Young Generation區(qū)域,這個(gè)區(qū)域的GC操作速度也是比Old Generation區(qū)域的GC操作速度更快的洲脂,如下圖所示:

通常情況下斤儿,當(dāng)GC線程運(yùn)行時(shí),其他線程會(huì)暫停工作(包括UI線程)恐锦,直到GC完成往果,如下圖所示:



雖然單個(gè)的GC操作并不會(huì)占用太多時(shí)間,但是頻繁的GC操作有可能會(huì)影響到幀率一铅,導(dǎo)致卡頓陕贮。

2.2 GC root and Dominator tree

Java中有以下幾種GC root:

  • references on the stack
  • Java Native Interface (JNI) native objects and memory
  • static variables and functions
  • threads and objects that can be referenced
  • classes loaded by the bootstrap loader
  • finalizers and unfinalized objects
  • busy monitor objects

如果從GC Root到達(dá)Y的的所有path都經(jīng)過(guò)X,那么我們稱(chēng)X dominates Y潘飘,或者X是Y的Dominator tree肮之。當(dāng)優(yōu)化內(nèi)存時(shí)掉缺,可以通過(guò)釋放一個(gè)dominator對(duì)象來(lái)釋放其所有下級(jí)對(duì)象。 例如戈擒,在下圖中眶明,如果要?jiǎng)h除對(duì)象B,那么也會(huì)釋放其所主導(dǎo)的對(duì)象所使用的內(nèi)存筐高,即對(duì)象C搜囱,D,E和F柑土,實(shí)際上犬辰,如果對(duì)象C,D冰单, E和F被標(biāo)記為刪除幌缝,但對(duì)象B仍然指向它們,這可能是它們未被釋放的原因诫欠。


3 Memory Monitor

Android Monitor提供了Memory Monitor工具涵卵,以便更輕松地實(shí)時(shí)監(jiān)聽(tīng)App的性能和內(nèi)存使用情況,通過(guò)該工具可以:

  • 顯示空閑和已分配的Java內(nèi)存隨時(shí)間變化的圖表荒叼。
  • 隨著時(shí)間的推移顯示垃圾回收(GC)事件轿偎。
  • 啟動(dòng)GC事件。
  • 快速測(cè)試UI線程卡頓是否與頻繁GC事件有關(guān)被廓。
    當(dāng)GC線程運(yùn)行時(shí)坏晦,其他線程都會(huì)暫停(包括UI線程),直到GC完成嫁乘。頻繁GC操作有可能會(huì)影響到幀率昆婿,導(dǎo)致卡頓,特別是性能比較差的手機(jī)上蜓斧,尤為明顯仓蛆。
  • 快速測(cè)試app崩潰是否與內(nèi)存不足(內(nèi)存溢出或者內(nèi)存泄漏)有關(guān)。

Memory Monitor的工作流程
為了分析和優(yōu)化內(nèi)存使用挎春,典型的工作流程是運(yùn)行app并執(zhí)行以下操作:

  1. 使用Memory Monitor來(lái)分析是否由于不良GC事件模式導(dǎo)致的app性能問(wèn)題看疙。
  2. 如果在短時(shí)間內(nèi)產(chǎn)生頻繁的GC事件,就通過(guò)Dump Java Heap操作來(lái)查看當(dāng)前內(nèi)存快照直奋,繼而查找哪些類(lèi)型的對(duì)象有可能發(fā)生了內(nèi)存泄漏或者占用了太大內(nèi)存能庆。
  3. 最后通過(guò)Start allocation tracking操作來(lái)追蹤對(duì)象分配內(nèi)存時(shí)對(duì)應(yīng)的方法調(diào)用。

在Memory Monitor中執(zhí)行Dump Java Heap操作時(shí)脚线,會(huì)創(chuàng)建一個(gè)Android-specific Heap/CPU Profiling (HPROF)文件搁胆,HPROF文件中保存了app該時(shí)刻內(nèi)存中的GC root列表,文件創(chuàng)建完成后會(huì)自動(dòng)在HPROF Viewer中打開(kāi), HPROF Viewer使用


圖標(biāo)標(biāo)示GC root(深度為零)以及使用


圖標(biāo)標(biāo)示Dominator tree丰涉。

4 常見(jiàn)內(nèi)存性能問(wèn)題模擬及優(yōu)化

4.1 內(nèi)存抖動(dòng)現(xiàn)象模擬及優(yōu)化

內(nèi)存抖動(dòng)是因?yàn)樵诙虝r(shí)間內(nèi)大量的對(duì)象被創(chuàng)建又馬上被釋放導(dǎo)致的拓巧。因此下面的例子中我通過(guò)一個(gè)for循環(huán)來(lái)不斷的創(chuàng)建和釋放對(duì)象來(lái)模擬內(nèi)存抖動(dòng)的現(xiàn)象。
舉個(gè)例子:

public class TestLeakActivity1 extends AppCompatActivity implements View.OnClickListener {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Button click = new Button(this);
        click.setOnClickListener(this);
        click.setText("模擬內(nèi)存抖動(dòng)");
        setContentView(click);
    }

    @Override
    public void onClick(View v) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    Bitmap result1;
                    result1 = BitmapFactory.decodeResource(getResources(), R.drawable.noah_silliman);
                }
            }
        }).start();
    }
}

上面代碼很簡(jiǎn)單一死,當(dāng)點(diǎn)擊模擬內(nèi)存抖動(dòng)按鈕時(shí)肛度,通過(guò)Memory Monitor工具可以看到出現(xiàn)了非常明顯的內(nèi)存抖動(dòng)情況,如下圖所示:


當(dāng)內(nèi)存抖動(dòng)的峰值快達(dá)到Y(jié)oung Generation區(qū)域的容量時(shí)就會(huì)觸發(fā)GC操作投慈,因此為了觸發(fā)GC操作承耿,就在代碼中加載來(lái)一張非常大圖片(5184*3456),對(duì)應(yīng)的GC log如下:

08-22 10:53:51.579 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 14(608B) AllocSpace objects, 1(68MB) LOS objects, 39% free, 17MB/29MB, paused 492us total 52.970ms
08-22 10:53:51.988 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 14(608B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 370us total 36.902ms
08-22 10:53:52.329 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 14(608B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 365us total 36.754ms
08-22 10:53:52.664 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 14(608B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 305us total 32.072ms
08-22 10:53:52.952 11758-11758/com.cytmxk.test I/art: WaitForGcToComplete blocked for 8.791ms for cause Alloc
08-22 10:53:52.988 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 8(304B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 305us total 32.178ms
08-22 10:53:53.396 11758-11758/com.cytmxk.test I/art: WaitForGcToComplete blocked for 9.036ms for cause Alloc
08-22 10:53:53.444 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 8(304B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 493us total 43.976ms
08-22 10:53:53.809 11758-11758/com.cytmxk.test I/art: WaitForGcToComplete blocked for 11.791ms for cause Alloc
08-22 10:53:53.853 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 8(304B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 373us total 38.598ms
08-22 10:53:54.181 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 14(608B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 311us total 32.794ms
08-22 10:53:54.617 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 18(736B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 481us total 46.280ms

通過(guò)上面的log中的時(shí)間點(diǎn)證明了發(fā)生了頻繁的GC, 由于導(dǎo)致GC的原因是Alloc(可以參考調(diào)查 RAM 使用情況來(lái)理解GC Log)伪煤,因此可能會(huì)在不久的將來(lái)會(huì)發(fā)生OOM異常加袋;當(dāng)我點(diǎn)擊兩次按鈕時(shí),確實(shí)引發(fā)OOM異常抱既;頻繁GC操作有可能會(huì)影響到幀率职烧,導(dǎo)致卡頓。

Allocation Tracker功能(可以參考Allocation Tracker)對(duì)于識(shí)別和優(yōu)化內(nèi)存抖動(dòng)是非常有效的防泵,接下來(lái)就通過(guò)這個(gè)功能來(lái)定位上面發(fā)生內(nèi)存抖動(dòng)的位置:


點(diǎn)擊右下角紅色框中的按鈕蚀之,即開(kāi)始執(zhí)行Allocation Tracker,等一段時(shí)間捷泞,再點(diǎn)擊一下右下角紅色框中的按鈕就會(huì)停止Allocation Tracker足删,此時(shí)下面的波形圖中矩形區(qū)域就是Allocation Tracker執(zhí)行的周期,并且會(huì)生成和打開(kāi)一個(gè)alloc格式的文件锁右,通過(guò)上圖可知分配內(nèi)存最多的是Thread 22線程失受,打開(kāi)Thread 22線程的調(diào)用stack,定位到TestLeakActivity1的34行就是分配內(nèi)存的位置咏瑟,接下去的問(wèn)題修復(fù)也就顯得相對(duì)簡(jiǎn)單了拂到,盡量避免在for循環(huán)里面分配對(duì)象,嘗試把對(duì)象的創(chuàng)建移到循環(huán)體之外响蕴,對(duì)于那些無(wú)法避免需要?jiǎng)?chuàng)建對(duì)象的情況谆焊,我們可以考慮對(duì)象池模型,通過(guò)對(duì)象池來(lái)解決頻繁創(chuàng)建與銷(xiāo)毀的問(wèn)題浦夷,注意在對(duì)象池沒(méi)用時(shí)需要手動(dòng)釋放對(duì)象池中的對(duì)象。

4.2 內(nèi)存泄漏現(xiàn)象模擬及優(yōu)化

內(nèi)存泄漏是指不再用到的對(duì)象由于被錯(cuò)誤引用而無(wú)法被GC回收辜王,這樣就導(dǎo)致這個(gè)對(duì)象一直留在內(nèi)存當(dāng)中劈狐,占用了寶貴的內(nèi)存空間。顯然會(huì)導(dǎo)致每級(jí)Generation的內(nèi)存區(qū)域可用空間變小呐馆,GC就會(huì)更容易被觸發(fā)肥缔,從而引起性能問(wèn)題。

舉個(gè)例子:

public class TestLeakActivity2 extends AppCompatActivity implements View.OnClickListener {

    private Button testLeakBtn = null;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_leak2);
        testLeakBtn = (Button) findViewById(R.id.button_test_leak);
        testLeakBtn.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        Intent intent = new Intent(this, TestLeakActivity3.class);
        startActivity(intent);
    }
}

public class TestLeakActivity3 extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ImageView imageView = new ImageView(this);
        imageView.setImageResource(R.drawable.noah_silliman);
        setContentView(imageView);
        handler.sendEmptyMessageDelayed(0, 60000);
    }

    private Handler handler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };
}

上面的代碼很簡(jiǎn)單汹来,運(yùn)行App续膳,多次并且快速執(zhí)行操作(從TestLeakActivity2跳轉(zhuǎn)到TestLeakActivity3改艇,然后再回到TestLeakActivity2),接著利用Memory Monitor工具的Dump Java Heap功能(可以參考HPROF Viewer and Analyzer)列舉此時(shí)Heap中各種類(lèi)型對(duì)象的多少和大蟹夭怼:


點(diǎn)擊右上角的箭頭谒兄,可以解析出當(dāng)前泄漏的activity,然后選中instance窗口中的第一個(gè)泄漏的實(shí)例社付,下面的reference tree窗口就會(huì)立即顯示該實(shí)例對(duì)應(yīng)的reference tree承疲,可以看出:
1> TestLeakActivity3$1@316570880是TestLeakActivity3@315071952的Dominator tree。
2> TestLeakActivity3$1@316570880的類(lèi)型是Message中target的類(lèi)型鸥咖,即Handler類(lèi)型燕鸽。
3> 由于TestLeakActivity3$1@316570880通過(guò)this$0引用TestLeakActivity3@315071952,因此TestLeakActivity3$1是TestLeakActivity3的內(nèi)部類(lèi)啼辣。
4> target和handler是同一個(gè)實(shí)例(TestLeakActivity3$1@316570880)啊研,并且handler是TestLeakActivity3@315071952的一個(gè)屬性。
由上面的4條信息可以得出只要TestLeakActivity3中handler的生命周期在TestLeakActivity3生命周期之內(nèi)鸥拧,就可以避免TestLeakActivity3實(shí)例的泄漏党远,接下去的問(wèn)題修復(fù)也就顯得相對(duì)簡(jiǎn)單了,就不在贅敘了住涉。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末麸锉,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子舆声,更是在濱河造成了極大的恐慌花沉,老刑警劉巖,帶你破解...
    沈念sama閱讀 223,126評(píng)論 6 520
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件媳握,死亡現(xiàn)場(chǎng)離奇詭異碱屁,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)蛾找,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,421評(píng)論 3 400
  • 文/潘曉璐 我一進(jìn)店門(mén)娩脾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人打毛,你說(shuō)我怎么就攤上這事柿赊。” “怎么了幻枉?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,941評(píng)論 0 366
  • 文/不壞的土叔 我叫張陵碰声,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我熬甫,道長(zhǎng)胰挑,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,294評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮瞻颂,結(jié)果婚禮上豺谈,老公的妹妹穿的比我還像新娘。我一直安慰自己贡这,他們只是感情好茬末,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,295評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著藕坯,像睡著了一般团南。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上炼彪,一...
    開(kāi)封第一講書(shū)人閱讀 52,874評(píng)論 1 314
  • 那天吐根,我揣著相機(jī)與錄音,去河邊找鬼辐马。 笑死拷橘,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的喜爷。 我是一名探鬼主播冗疮,決...
    沈念sama閱讀 41,285評(píng)論 3 424
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼檩帐!你這毒婦竟也來(lái)了术幔?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 40,249評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤湃密,失蹤者是張志新(化名)和其女友劉穎诅挑,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體泛源,經(jīng)...
    沈念sama閱讀 46,760評(píng)論 1 321
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡拔妥,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,840評(píng)論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了达箍。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片没龙。...
    茶點(diǎn)故事閱讀 40,973評(píng)論 1 354
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖缎玫,靈堂內(nèi)的尸體忽然破棺而出硬纤,到底是詐尸還是另有隱情,我是刑警寧澤赃磨,帶...
    沈念sama閱讀 36,631評(píng)論 5 351
  • 正文 年R本政府宣布咬摇,位于F島的核電站,受9級(jí)特大地震影響煞躬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,315評(píng)論 3 336
  • 文/蒙蒙 一恩沛、第九天 我趴在偏房一處隱蔽的房頂上張望在扰。 院中可真熱鬧,春花似錦雷客、人聲如沸芒珠。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,797評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)皱卓。三九已至,卻和暖如春部逮,著一層夾襖步出監(jiān)牢的瞬間娜汁,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,926評(píng)論 1 275
  • 我被黑心中介騙來(lái)泰國(guó)打工兄朋, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留掐禁,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,431評(píng)論 3 379
  • 正文 我出身青樓颅和,卻偏偏與公主長(zhǎng)得像傅事,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子峡扩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,982評(píng)論 2 361

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