成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

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

cheng10 / 2314人閱讀

摘要:導(dǎo)語智能手機(jī)發(fā)展到今天已經(jīng)有十幾個(gè)年頭,手機(jī)的軟硬件都已經(jīng)發(fā)生了翻天覆地的變化,特別是陣營,從一開始的一兩百到今天動(dòng)輒,內(nèi)存。恰好最近做了內(nèi)存優(yōu)化相關(guān)的工作,這里也對(duì)內(nèi)存優(yōu)化相關(guān)的知識(shí)做下總結(jié)。

導(dǎo)語

智能手機(jī)發(fā)展到今天已經(jīng)有十幾個(gè)年頭,手機(jī)的軟硬件都已經(jīng)發(fā)生了翻天覆地的變化,特別是Android陣營,從一開始的一兩百M(fèi)到今天動(dòng)輒4G,6G內(nèi)存。然而大部分的開發(fā)者觀看下自己的異常上報(bào)系統(tǒng),還是會(huì)發(fā)現(xiàn)各種內(nèi)存問題仍然層出不窮,各種OOM為crash率貢獻(xiàn)不少。Android開發(fā)發(fā)展到今天也是已經(jīng)比較成熟,各種新框架,新技術(shù)也是層出不窮,而內(nèi)存優(yōu)化一直都是Android開發(fā)過程一個(gè)不可避免的話題。 恰好最近做了內(nèi)存優(yōu)化相關(guān)的工作,這里也對(duì)Android內(nèi)存優(yōu)化相關(guān)的知識(shí)做下總結(jié)。

在開始文章之前推薦下公司同事翻譯整理版本《Android性能優(yōu)化典范 - 第6季》,因?yàn)槠邢捱@里我對(duì)一些內(nèi)容只做簡單總結(jié),同時(shí)如果有不正確內(nèi)容也麻煩幫忙指正。

本文將會(huì)對(duì)Android內(nèi)存優(yōu)化相關(guān)的知識(shí)進(jìn)行總結(jié)以及最后案例分析(一二部分是理論知識(shí)總結(jié),你也可以直接跳到第三部分看案例):

一、 Android內(nèi)存分配回收機(jī)制
二 、Android常見內(nèi)存問題和對(duì)應(yīng)檢測,解決方式。
三、 JOOX內(nèi)存優(yōu)化案例
四 、總結(jié)

工欲善其事必先利其器,想要優(yōu)化App的內(nèi)存占用,那么還是需要先了解Android系統(tǒng)的內(nèi)存分配和回收機(jī)制。

一 ,Android內(nèi)存分配回收機(jī)制

參考Android 操作系統(tǒng)的內(nèi)存回收機(jī)制[1],這里簡單做下總結(jié):

從宏觀角度上來看Android系統(tǒng)可以分為三個(gè)層次
1. Application Framework,
2. Dalvik 虛擬機(jī)
3. Linux內(nèi)核。

這三個(gè)層次都有各自內(nèi)存相關(guān)工作:

1. Application Framework

Anroid基于進(jìn)程中運(yùn)行的組件及其狀態(tài)規(guī)定了默認(rèn)的五個(gè)回收優(yōu)先級(jí):

Empty process(空進(jìn)程)

Background process(后臺(tái)進(jìn)程)

Service process(服務(wù)進(jìn)程)

Visible process(可見進(jìn)程)

Foreground process(前臺(tái)進(jìn)程)

系統(tǒng)需要進(jìn)行內(nèi)存回收時(shí)最先回收空進(jìn)程,然后是后臺(tái)進(jìn)程,以此類推最后才會(huì)回收前臺(tái)進(jìn)程(一般情況下前臺(tái)進(jìn)程就是與用戶交互的進(jìn)程了,如果連前臺(tái)進(jìn)程都需要回收那么此時(shí)系統(tǒng)幾乎不可用了)。

由此也衍生了很多進(jìn)程?;畹姆椒ǎㄌ岣邇?yōu)先級(jí),互相喚醒,native?;畹鹊龋霈F(xiàn)了國內(nèi)各種全家桶,甚至各種殺不死的進(jìn)程。

Android中由ActivityManagerService 集中管理所有進(jìn)程的內(nèi)存資源分配。

2. Linux內(nèi)核

參考QCon大會(huì)上阿里巴巴的Android內(nèi)存優(yōu)化分享[2],這里最簡單的理解就是ActivityManagerService會(huì)對(duì)所有進(jìn)程進(jìn)行評(píng)分(存放在變量adj中),然后再講這個(gè)評(píng)分更新到內(nèi)核,由內(nèi)核去完成真正的內(nèi)存回收(lowmemorykiller, Oom_killer)。這里只是大概的流程,中間過程還是很復(fù)雜的,有興趣的同學(xué)可以一起研究,代碼在系統(tǒng)源碼ActivityManagerService.java中。

3. Dalvik虛擬機(jī)

Android進(jìn)程的內(nèi)存管理分析[3],對(duì)Android中進(jìn)程內(nèi)存的管理做了分析。

Android中有Native Heap和Dalvik Heap。Android的Native Heap言理論上可分配的空間取決了硬件RAM,而對(duì)于每個(gè)進(jìn)程的Dalvik Heap都是有大小限制的,具體策略可以看看android dalvik heap 淺析[4]。

Android App為什么會(huì)OOM呢?其實(shí)就是申請(qǐng)的內(nèi)存超過了Dalvik Heap的最大值。這里也誕生了一些比較”黑科技”的內(nèi)存優(yōu)化方案,比如將耗內(nèi)存的操作放到Native層,或者使用分進(jìn)程的方式突破每個(gè)進(jìn)程的Dalvik Heap內(nèi)存限制。

Android Dalvik Heap與原生Java一樣,將堆的內(nèi)存空間分為三個(gè)區(qū)域,Young Generation,Old Generation, Permanent Generation。

最近分配的對(duì)象會(huì)存放在Young Generation區(qū)域,當(dāng)這個(gè)對(duì)象在這個(gè)區(qū)域停留的時(shí)間達(dá)到一定程度,它會(huì)被移動(dòng)到Old Generation,最后累積一定時(shí)間再移動(dòng)到Permanent Generation區(qū)域。系統(tǒng)會(huì)根據(jù)內(nèi)存中不同的內(nèi)存數(shù)據(jù)類型分別執(zhí)行不同的gc操作。

GC發(fā)生的時(shí)候,所有的線程都是會(huì)被暫停的。執(zhí)行GC所占用的時(shí)間和它發(fā)生在哪一個(gè)Generation也有關(guān)系,Young Generation中的每次GC操作時(shí)間是最短的,Old Generation其次,Permanent Generation最長。

GC時(shí)會(huì)導(dǎo)致線程暫停,導(dǎo)致卡頓,Google在新版本的Android中優(yōu)化了這個(gè)問題, 在ART中對(duì)GC過程做了優(yōu)化揭秘 ART 細(xì)節(jié) —— Garbage collection[5],據(jù)說內(nèi)存分配的效率提高了10倍,GC的效率提高了2-3倍(可見原來效率有多低),不過主要還是優(yōu)化中斷和阻塞的時(shí)間,頻繁的GC還是會(huì)導(dǎo)致卡頓。

上面就是Android系統(tǒng)內(nèi)存分配和回收相關(guān)知識(shí),回過頭來看,現(xiàn)在各種手機(jī)廠商鼓吹人工智能手機(jī),號(hào)稱18個(gè)月不卡頓,越用越快,其實(shí)很大一部分Android系統(tǒng)的內(nèi)存優(yōu)化有關(guān),無非就是利用一些比較成熟的基于統(tǒng)計(jì),機(jī)器學(xué)習(xí)的算法定時(shí)清理數(shù)據(jù),清理內(nèi)存,甚至提前加載數(shù)據(jù)到內(nèi)存。

二 ,Android常見內(nèi)存問題和對(duì)應(yīng)檢測,解決方式 1. 內(nèi)存泄露

不止Android程序員,內(nèi)存泄露應(yīng)該是大部分程序員都遇到過的問題,可以說大部分的內(nèi)存問題都是內(nèi)存泄露導(dǎo)致的,Android里也有一些很常見的內(nèi)存泄露問題[6],這里簡單羅列下:

單例(主要原因還是因?yàn)橐话闱闆r下單例都是全局的,有時(shí)候會(huì)引用一些實(shí)際生命周期比較短的變量,導(dǎo)致其無法釋放)

靜態(tài)變量(同樣也是因?yàn)樯芷诒容^長)

Handler內(nèi)存泄露[7]

匿名內(nèi)部類(匿名內(nèi)部類會(huì)引用外部類,導(dǎo)致無法釋放,比如各種回調(diào))

資源使用完未關(guān)閉(BraodcastReceiver,ContentObserver,F(xiàn)ile,Cursor,Stream,Bitmap)

對(duì)Android內(nèi)存泄露業(yè)界已經(jīng)有很多優(yōu)秀的組件其中LeakCanary最為知名(Square出品,Square可謂Android開源界中的業(yè)界良心,開源的項(xiàng)目包括okhttp, retrofit,otto, picasso, Android開發(fā)大神Jake Wharton就在Square),其原理是監(jiān)控每個(gè)activity,在activity ondestory后,在后臺(tái)線程檢測引用,然后過一段時(shí)間進(jìn)行g(shù)c,gc后如果引用還在,那么dump出內(nèi)存堆棧,并解析進(jìn)行可視化顯示。使用LeakCanary可以快速地檢測出Android中的內(nèi)存泄露。

正常情況下,解決大部分內(nèi)存泄露問題后,App穩(wěn)定性應(yīng)該會(huì)有很大提升,但是有時(shí)候App本身就是有一些比較耗內(nèi)存的功能,比如直播,視頻播放,音樂播放,那么我們還有什么能做的可以降低內(nèi)存使用,減少OOM呢?

2. 圖片分辨率相關(guān)

分辨率適配問題。很多情況下圖片所占的內(nèi)存在整個(gè)App內(nèi)存占用中會(huì)占大部分。我們知道可以通過將圖片放到hdpi/xhdpi/xxhdpi等不同文件夾進(jìn)行適配,通過xml android:background設(shè)置背景圖片,或者通過BitmapFactory.decodeResource()方法,圖片實(shí)際上默認(rèn)情況下是會(huì)進(jìn)行縮放的。在Java層實(shí)際調(diào)用的函數(shù)都是或者通過BitmapFactory里的decodeResourceStream函數(shù)

public static Bitmap decodeResourceStream(Resources res, TypedValue value,
        InputStream is, Rect pad, Options opts) {

    if (opts == null) {
        opts = new Options();
    }

    if (opts.inDensity == 0 && value != null) {
        final int density = value.density;
        if (density == TypedValue.DENSITY_DEFAULT) {
            opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
        } else if (density != TypedValue.DENSITY_NONE) {
            opts.inDensity = density;
        }
    }

    if (opts.inTargetDensity == 0 && res != null) {
        opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
    }

    return decodeStream(is, pad, opts);
}

decodeResource在解析時(shí)會(huì)對(duì)Bitmap根據(jù)當(dāng)前設(shè)備屏幕像素密度densityDpi的值進(jìn)行縮放適配操作,使得解析出來的Bitmap與當(dāng)前設(shè)備的分辨率匹配,達(dá)到一個(gè)最佳的顯示效果,并且Bitmap的大小將比原始的大,可以參考下騰訊Bugly的詳細(xì)分析Android 開發(fā)繞不過的坑:你的 Bitmap 究竟占多大內(nèi)存?。

關(guān)于Density、分辨率、-hdpi等res目錄之間的關(guān)系:

舉個(gè)例子,對(duì)于一張1280×720的圖片,如果放在xhdpi,那么xhdpi的設(shè)備拿到的大小還是1280×720而xxhpi的設(shè)備拿到的可能是1920×1080,這兩種情況在內(nèi)存里的大小分別為:3.68M和8.29M,相差4.61M,在移動(dòng)設(shè)備來說這幾M的差距還是很大的。

盡管現(xiàn)在已經(jīng)有比較先進(jìn)的圖片加載組件類似Glide,F(xiàn)acebook Freso, 或者老牌Universal-Image-Loader,但是有時(shí)就是需要手動(dòng)拿到一個(gè)bitmap或者drawable,特別是在一些可能會(huì)頻繁調(diào)用的場景(比如ListView的getView),怎樣盡可能對(duì)bitmap進(jìn)行復(fù)用呢?這里首先需要明確的是對(duì)同樣的圖片,要 盡可能復(fù)用,我們可以簡單自己用WeakReference做一個(gè)bitmap緩存池,也可以用類似圖片加載庫寫一個(gè)通用的bitmap緩存池,可以參考GlideBitmapPool[8]的實(shí)現(xiàn)。

我們也來看看系統(tǒng)是怎么做的,對(duì)于類似在xml里面直接通過android:background或者android:src設(shè)置的背景圖片,以ImageView為例,最終會(huì)調(diào)用Resource.java里的loadDrawable:

Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException {

    // Next, check preloaded drawables. These may contain unresolved theme
    // attributes.
    final ConstantState cs;
    if (isColorDrawable) {
        cs = sPreloadedColorDrawables.get(key);
    } else {
        cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
    }

    Drawable dr;
    if (cs != null) {
        dr = cs.newDrawable(this);
    } else if (isColorDrawable) {
        dr = new ColorDrawable(value.data);
    } else {
        dr = loadDrawableForCookie(value, id, null);
    }

    ...

    return dr;
}  

可以看到實(shí)際上系統(tǒng)也是有一份全局的緩存,sPreloadedDrawables, 對(duì)于不同的drawable,如果圖片時(shí)一樣的,那么最終只會(huì)有一份bitmap(享元模式),存放于BitmapState中,獲取drawable時(shí),系統(tǒng)會(huì)從緩存中取出這個(gè)bitmap然后構(gòu)造drawable。而通過BitmapFactory.decodeResource()則每次都會(huì)重新解碼返回bitmap。所以其實(shí)我們可以通過context.getResources().getDrawable再從drawable里獲取bitmap,從而復(fù)用bitmap,然而這里也有一些坑,比如我們獲取到的這份bitmap,假如我們執(zhí)行了recycle之類的操作,但是假如在其他地方再使用它是那么就會(huì)有”Canvas: trying to use a recycled bitmap android.graphics.Bitmap”異常。

3. 圖片壓縮

BitmapFactory 在解碼圖片時(shí),可以帶一個(gè)Options,有一些比較有用的功能,比如:

inTargetDensity 表示要被畫出來時(shí)的目標(biāo)像素密度

inSampleSize 這個(gè)值是一個(gè)int,當(dāng)它小于1的時(shí)候,將會(huì)被當(dāng)做1處理,如果大于1,那么就會(huì)按照比例(1 / inSampleSize)縮小bitmap的寬和高、降低分辨率,大于1時(shí)這個(gè)值將會(huì)被處置為2的倍數(shù)。例如,width=100,height=100,inSampleSize=2,那么就會(huì)將bitmap處理為,width=50,height=50,寬高降為1 / 2,像素?cái)?shù)降為1 / 4

inJustDecodeBounds 字面意思就可以理解就是只解析圖片的邊界,有時(shí)如果只是為了獲取圖片的大小就可以用這個(gè),而不必直接加載整張圖片。

inPreferredConfig 默認(rèn)會(huì)使用ARGB_8888,在這個(gè)模式下一個(gè)像素點(diǎn)將會(huì)占用4個(gè)byte,而對(duì)一些沒有透明度要求或者圖片質(zhì)量要求不高的圖片,可以使用RGB_565,一個(gè)像素只會(huì)占用2個(gè)byte,一下可以省下50%內(nèi)存。

inPurgeableinInputShareable 這兩個(gè)需要一起使用,BitmapFactory.java的源碼里面有注釋,大致意思是表示在系統(tǒng)內(nèi)存不足時(shí)是否可以回收這個(gè)bitmap,有點(diǎn)類似軟引用,但是實(shí)際在5.0以后這兩個(gè)屬性已經(jīng)被忽略,因?yàn)橄到y(tǒng)認(rèn)為回收后再解碼實(shí)際會(huì)反而可能導(dǎo)致性能問題

inBitmap 官方推薦使用的參數(shù),表示重復(fù)利用圖片內(nèi)存,減少內(nèi)存分配,在4.4以前只有相同大小的圖片內(nèi)存區(qū)域可以復(fù)用,4.4以后只要原有的圖片比將要解碼的圖片大既可以復(fù)用了。

4. 緩存池大小

現(xiàn)在很多圖片加載組件都不僅僅是使用軟引用或者弱引用了,實(shí)際上類似Glide 默認(rèn)使用的事LruCache,因?yàn)檐浺?弱引用都比較難以控制,使用LruCache可以實(shí)現(xiàn)比較精細(xì)的控制,而默認(rèn)緩存池設(shè)置太大了會(huì)導(dǎo)致浪費(fèi)內(nèi)存,設(shè)置小了又會(huì)導(dǎo)致圖片經(jīng)常被回收,所以需要根據(jù)每個(gè)App的情況,以及設(shè)備的分辨率,內(nèi)存計(jì)算出一個(gè)比較合理的初始值,可以參考Glide的做法。

5. 內(nèi)存抖動(dòng)

什么是內(nèi)存抖動(dòng)呢?Android里內(nèi)存抖動(dòng)是指內(nèi)存頻繁地分配和回收,而頻繁的gc會(huì)導(dǎo)致卡頓,嚴(yán)重時(shí)還會(huì)導(dǎo)致OOM。

一個(gè)很經(jīng)典的案例是string拼接創(chuàng)建大量小的對(duì)象(比如在一些頻繁調(diào)用的地方打字符串拼接的log的時(shí)候), 見Android優(yōu)化之String篇[9]。

而內(nèi)存抖動(dòng)為什么會(huì)引起OOM呢?

主要原因還是有因?yàn)榇罅啃〉膶?duì)象頻繁創(chuàng)建,導(dǎo)致內(nèi)存碎片,從而當(dāng)需要分配內(nèi)存時(shí),雖然總體上還是有剩余內(nèi)存可分配,而由于這些內(nèi)存不連續(xù),導(dǎo)致無法分配,系統(tǒng)直接就返回OOM了。

比如我們坐地鐵的時(shí)候,假設(shè)你沒帶公交卡去坐地鐵,地鐵的售票機(jī)就只支持5元,10元,而哪怕你這個(gè)時(shí)候身上有1萬張1塊的都沒用(是不是覺得很反人類..)。當(dāng)然你可以去兌換5元,10元,而在Android系統(tǒng)里就沒那么幸運(yùn)了,系統(tǒng)會(huì)直接拒絕為你分配內(nèi)存,并扔一個(gè)OOM給你(有人說Android系統(tǒng)并不會(huì)對(duì)Heap中空閑內(nèi)存區(qū)域做碎片整理,待驗(yàn)證)。

其他

常用數(shù)據(jù)結(jié)構(gòu)優(yōu)化,ArrayMap及SparseArray是android的系統(tǒng)API,是專門為移動(dòng)設(shè)備而定制的。用于在一定情況下取代HashMap而達(dá)到節(jié)省內(nèi)存的目的,具體性能見HashMap,ArrayMap,SparseArray源碼分析及性能對(duì)比[10],對(duì)于key為int的HashMap盡量使用SparceArray替代,大概可以省30%的內(nèi)存,而對(duì)于其他類型,ArrayMap對(duì)內(nèi)存的節(jié)省實(shí)際并不明顯,10%左右,但是數(shù)據(jù)量在1000以上時(shí),查找速度可能會(huì)變慢。

枚舉,Android平臺(tái)上枚舉是比較爭議的,在較早的Android版本,使用枚舉會(huì)導(dǎo)致包過大,在個(gè)例子里面,使用枚舉甚至比直接使用int包的size大了10多倍 在stackoverflow上也有很多的討論, 大致意思是隨著虛擬機(jī)的優(yōu)化,目前枚舉變量在Android平臺(tái)性能問題已經(jīng)不大,而目前Android官方建議,使用枚舉變量還是需要謹(jǐn)慎,因?yàn)槊杜e變量可能比直接用int多使用2倍的內(nèi)存。

ListView復(fù)用,這個(gè)大家都知道,getView里盡量復(fù)用conertView,同時(shí)因?yàn)間etView會(huì)頻繁調(diào)用,要避免頻繁地生成對(duì)象

謹(jǐn)慎使用多進(jìn)程,現(xiàn)在很多App都不是單進(jìn)程,為了保活,或者提高穩(wěn)定性都會(huì)進(jìn)行一些進(jìn)程拆分,而實(shí)際上即使是空進(jìn)程也會(huì)占用內(nèi)存(1M左右),對(duì)于使用完的進(jìn)程,服務(wù)都要及時(shí)進(jìn)行回收。

盡量使用系統(tǒng)資源,系統(tǒng)組件,圖片甚至控件的id

減少view的層級(jí),對(duì)于可以 延遲初始化的頁面,使用viewstub

數(shù)據(jù)相關(guān):序列化數(shù)據(jù)使用protobuf可以比xml省30%內(nèi)存,慎用shareprefercnce,因?yàn)閷?duì)于同一個(gè)sp,會(huì)將整個(gè)xml文件載入內(nèi)存,有時(shí)候?yàn)榱俗x一個(gè)配置,就會(huì)將幾百k的數(shù)據(jù)讀進(jìn)內(nèi)存,數(shù)據(jù)庫字段盡量精簡,只讀取所需字段。

dex優(yōu)化,代碼優(yōu)化,謹(jǐn)慎使用外部庫, 有人覺得代碼多少于內(nèi)存沒有關(guān)系,實(shí)際會(huì)有那么點(diǎn)關(guān)系,現(xiàn)在稍微大一點(diǎn)的項(xiàng)目動(dòng)輒就是百萬行代碼以上,多dex也是常態(tài),不僅占用rom空間,實(shí)際上運(yùn)行的時(shí)候需要加載dex也是會(huì)占用內(nèi)存的(幾M),有時(shí)候?yàn)榱耸褂靡恍炖锏哪硞€(gè)功能函數(shù)就引入了整個(gè)龐大的庫,此時(shí)可以考慮抽取必要部分,開啟proguard優(yōu)化代碼,使用Facebook redex使用優(yōu)化dex(好像有不少坑)。

三 案例

JOOX是IBG一個(gè)核心產(chǎn)品,2014年發(fā)布以來已經(jīng)成為5個(gè)國家和地區(qū)排名第一的音樂App。東南亞是JOOX的主要發(fā)行地區(qū),實(shí)際上這些地區(qū)還是有很多的低端機(jī)型,對(duì)App的進(jìn)行內(nèi)存優(yōu)化勢在必行。

上面介紹了Android系統(tǒng)內(nèi)存分配和回收機(jī)制,同時(shí)也列舉了常見的內(nèi)存問題,但是當(dāng)我們接到一個(gè)內(nèi)存優(yōu)化的任務(wù)時(shí),我們應(yīng)該從何開始?下面是一次內(nèi)存優(yōu)化的分享。

1. 首先是解決大部分內(nèi)存泄露。

不管目前App內(nèi)存占用怎樣,理論上不需要的東西最好回收,避免浪費(fèi)用戶內(nèi)存,減少OOM。實(shí)際上自JOOX接入LeakCanary后,每個(gè)版本都會(huì)做內(nèi)存泄露檢測,經(jīng)過幾個(gè)版本的迭代,JOOX已經(jīng)修復(fù)了幾十處內(nèi)存泄露。

2. 通過MAT查看內(nèi)存占用,優(yōu)化占用內(nèi)存較大的地方。

JOOX修復(fù)了一系列內(nèi)存泄露后,內(nèi)存占用還是居高不下,只能通過MAT查看到底是哪里占用了內(nèi)存。關(guān)于MAT的使用,網(wǎng)上教程無數(shù),簡單推薦兩篇MAT使用教程[11],MAT - Memory Analyzer Tool 使用進(jìn)階[12]。

點(diǎn)擊Android Studio這里可以dump當(dāng)前的內(nèi)存快照,因?yàn)橹苯油ㄟ^Android Sutdio dump出來的hprof文件與標(biāo)準(zhǔn)hprof文件有些差異,我們需要手動(dòng)進(jìn)行轉(zhuǎn)換,利用sdk目錄/platform-tools/hprof-conv.exe可以直接進(jìn)行轉(zhuǎn)換,用法:hprof-conv 原文件.hprof 新文件.hprof。只需要輸入原文件名還有目標(biāo)文件名就可以進(jìn)行轉(zhuǎn)換,轉(zhuǎn)換完就可以直接用MAT打開。

下面就是JOOX打開App,手動(dòng)進(jìn)行多次gc的hprof文件。

這里我們看的是Dominator Tree(即內(nèi)存里占用內(nèi)存最多的對(duì)象列表)。

Shallo Heap:對(duì)象本身占用內(nèi)存的大小,不包含其引用的對(duì)象內(nèi)存。

Retained Heap: Retained heap值的計(jì)算方式是將retained set中的所有對(duì)象大小疊加?;蛘哒f,由于X被釋放,導(dǎo)致其它所有被釋放對(duì)象(包括被遞歸釋放的)所占的heap大小。

第一眼看去 居然有3個(gè)8M的對(duì)象,加起來就是24M啊 這到底是什么鬼?

我們通過List objects->with incoming references查看(這里with incoming references表示查看誰引用了這個(gè)對(duì)象,with outgoing references表示這個(gè)對(duì)象引用了誰)

通過這個(gè)方式我們看到這三張圖分別是閃屏,App主背景,App抽屜背景。

這里其實(shí)有兩個(gè)問題:

這幾張圖原圖實(shí)際都是1280x720,而在1080p手機(jī)上實(shí)測這幾張圖都縮放到了1920x1080

閃屏頁面,其實(shí)這張圖在閃屏顯示過后應(yīng)該可以回收,但是因?yàn)闅v史原因(和JOOX的退出機(jī)制有關(guān)),這張圖被常駐在后臺(tái),導(dǎo)致無謂的內(nèi)存占用。

優(yōu)化方式:我們通過將這三張圖從xhdpi挪動(dòng)到xxhdpi(當(dāng)然這里需要看下圖片顯示效果有沒很大的影響),以及在閃屏顯示過后回收閃屏圖片。
優(yōu)化結(jié)果:

從原來的8.29x3=24.87M 到 3.68x2=7.36M 優(yōu)化了17M(有沒一種萬馬奔騰的感覺。??赡苡袝r(shí)費(fèi)大力氣優(yōu)化很多代碼也優(yōu)化不了幾百K,所以很多情況下內(nèi)存優(yōu)化時(shí)優(yōu)化圖片還是比較立竿見影的)。

同樣方式我們發(fā)現(xiàn)對(duì)于一些默認(rèn)圖,實(shí)際要求的顯示要求并不高(圖片相對(duì)簡單,同時(shí)大部分情況下圖片加載會(huì)成功),比如下面這張banner的背景圖:

優(yōu)化前1.6M左右,優(yōu)化后700K左右。

同時(shí)我們也發(fā)現(xiàn)了默認(rèn)圖片一個(gè)其他問題,因?yàn)闅v史原因,我們使用的圖片加載庫,設(shè)置默認(rèn)圖片的接口是需要一個(gè)bitmap,導(dǎo)致我們?cè)瓉韼缀趺總€(gè)adapter都用BitmapFactory decode了一個(gè)bitmap,對(duì)同一張默認(rèn)圖片,不但沒有復(fù)用,還保存了多份,不僅會(huì)造成內(nèi)存浪費(fèi),而且導(dǎo)致滑動(dòng)偶爾會(huì)卡頓。這里我們也對(duì)默認(rèn)圖片使用全局的bitmap緩存池,App全局只要使用同一張bitmap,都復(fù)用了同一份。

另外對(duì)于從MAT里看到的圖片,有時(shí)候因?yàn)榭床坏皆陧?xiàng)目里面對(duì)應(yīng)的ID,會(huì)比較難確認(rèn)到底是哪一張圖,這里stackoverflow上有一種方法,直接用原始數(shù)據(jù)通過GIM還原這張圖片。

這里其實(shí)也看到JOOX比較吃虧一個(gè)地方,JOOX不少地方都是使用比較復(fù)雜的圖片,同時(shí)有些地方還需要模糊,動(dòng)畫這些都是比較耗內(nèi)存的操作,Material Design出來后,很多App都遵循MD設(shè)計(jì)進(jìn)行改版,通常默認(rèn)背景,默認(rèn)圖片一般都是純色,不僅App看起來比較明亮輕快,實(shí)際上也省了很多的內(nèi)存,對(duì)此,JOOX后面對(duì)低端機(jī)型做了對(duì)應(yīng)的優(yōu)化。

3. 我們也對(duì)Bugly上的OOM進(jìn)行了分析,發(fā)現(xiàn)其實(shí)有些OOM是可以避免的。

下面這個(gè)crash就是上面提到的在LsitView的adapter里不停創(chuàng)建bitmap,這個(gè)地方是我們的首頁banner位,理論上App一打開就會(huì)緩存這張默認(rèn)背景圖片了,而實(shí)際在使用過一段時(shí)間后,才因?yàn)闉榱私獯a這張背景圖而OOM, 改為用全局緩存解決。

下面這個(gè)就是傳說中的內(nèi)存抖動(dòng)

實(shí)際代碼如下,因?yàn)榇騆og而進(jìn)行了字符串拼接,一旦這個(gè)函數(shù)被比較頻繁地調(diào)用,那么就很有可能會(huì)發(fā)生內(nèi)存抖動(dòng)。這里我們新版本已經(jīng)改為使用stringbuilder進(jìn)行優(yōu)化。

還有一些比較奇怪的情況,這里是我們掃描歌曲文件頭的時(shí)候發(fā)生的,有些文件頭居然有幾百M(fèi)大,導(dǎo)致一次申請(qǐng)了過大的內(nèi)存,直接OOM,這里暫時(shí)也無法修復(fù),直接catch住out of memory error。

4. 同時(shí)我們對(duì)一些邏輯代碼進(jìn)行調(diào)整,比如我們的App主頁的第三個(gè)tab(Live tab)進(jìn)行了數(shù)據(jù)延遲加載,和定時(shí)回收。

這里因?yàn)檫@個(gè)頁面除了有大圖還有輪播banner,實(shí)際強(qiáng)引用的圖片會(huì)有多張,如果這個(gè)時(shí)候切到其他頁面進(jìn)行聽歌等行為,這個(gè)頁面一直在后臺(tái)緩存,實(shí)際是很浪費(fèi)耗內(nèi)存的,同時(shí)為優(yōu)化體驗(yàn),我們又不能直接通過設(shè)置主頁的viewpager的緩存頁數(shù),因?yàn)檫@樣經(jīng)常都會(huì)回收,導(dǎo)致影響體驗(yàn),所以我們?cè)陧撁娌豢梢姾筮^一段時(shí)間,清理掉adapter數(shù)據(jù)(只是清空adapter里的數(shù)據(jù),實(shí)際從網(wǎng)絡(luò)加載回來的數(shù)據(jù)還在,這里只是為了去掉界面對(duì)圖片的引用),當(dāng)頁面再次顯示時(shí)再用已經(jīng)加載的數(shù)據(jù)顯示,即減少了很多情況下圖片的引用,也不影響體驗(yàn)。

5. 最后我們也遇到一個(gè)比較奇葩的問題,在我們的Bugly上報(bào)上有這樣一條上報(bào)

我們?cè)趕tackoverflow上看到了相關(guān)的討論,大致意思是有些情況下比如息屏,或者一些省電模式下,頻繁地調(diào)System.gc()可能會(huì)因?yàn)閮?nèi)核狀態(tài)切換超時(shí)的異常。這個(gè)問題貌似沒有比較好的解決方法,只能是優(yōu)化內(nèi)存,盡量減少手動(dòng)調(diào)用System.gc()

優(yōu)化結(jié)果

我們通過啟動(dòng)App后,切換到我的音樂界面,停留1分鐘,多次gc后,獲取App內(nèi)存占用

優(yōu)化前:

優(yōu)化后:

多次試驗(yàn)結(jié)果都差不多,這里只截取了其中一次,有28M的優(yōu)化效果。
當(dāng)然不同的場景內(nèi)存占用不同,同時(shí)上面試驗(yàn)結(jié)果是通過多次手動(dòng)觸發(fā)gc穩(wěn)定后的結(jié)果。對(duì)于使用其他第三方工具不手動(dòng)gc的情況下,試驗(yàn)結(jié)果可能會(huì)差異比較大。

對(duì)于上面提到的JOOX里各種圖片背景等問題,我們做了動(dòng)態(tài)的優(yōu)化,對(duì)不同的機(jī)型進(jìn)行優(yōu)化,對(duì)特別低端的機(jī)型設(shè)置為純色背景等方式,最終優(yōu)化效果如下:

平均內(nèi)存降低41M。

本次總結(jié)主要還是從圖片方面下手,還有一點(diǎn)邏輯優(yōu)化,已經(jīng)基本達(dá)到優(yōu)化目標(biāo)。

四 總結(jié)

上面寫了很多,我們可以簡單總結(jié),目前Andorid內(nèi)存優(yōu)化還是比較重要一個(gè)話題,我們可以通過各種內(nèi)存泄露檢測組件,MAT查看內(nèi)存占用,Memory Monitor跟蹤整個(gè)App的內(nèi)存變化情況, Heap Viewer查看當(dāng)前內(nèi)存快照, Allocation Tracker追蹤內(nèi)存對(duì)象的來源,以及利用崩潰上報(bào)平臺(tái)從多個(gè)方面對(duì)App內(nèi)存進(jìn)行監(jiān)控和優(yōu)化。上面只是列舉了一些常見的情況,當(dāng)然每個(gè)App功能,邏輯,架構(gòu)也都不一樣,造成內(nèi)存問題也是不盡相同,掌握好工具的使用,發(fā)現(xiàn)問題所在,才能對(duì)癥下藥。

參考鏈接

1.Android 操作系統(tǒng)的內(nèi)存回收機(jī)制
https://www.ibm.com/developerworks/cn/opensource/os-cn-android-mmry-rcycl/

2.阿里巴巴的Android內(nèi)存優(yōu)化分享
http://www.infoq.com/cn/presentations/android-memory-optimization

3.Android進(jìn)程的內(nèi)存管理分析
http://blog.csdn.net/gemmem/article/details/8920039

4.android dalvik heap 淺析
http://blog.csdn.net/cqupt_chen/article/details/11068129

5.揭秘 ART 細(xì)節(jié) —— Garbage collection
http://www.cnblogs.com/jinkeep/p/3818180.html

6.Android性能優(yōu)化之常見的內(nèi)存泄漏
http://blog.csdn.net/u010687392/article/details/49909477

7.Android App 內(nèi)存泄露之Handler
http://blog.csdn.net/zhuanglonghai/article/details/38233069

8.GlideBitmapPool
https://github.com/amitshekhariitbhu/GlideBitmapPool

9.Android 性能優(yōu)化之String篇
http://blog.csdn.net/vfush/article/details/53038437

10.HashMap,ArrayMap,SparseArray源碼分析及性能對(duì)比
http://www.jianshu.com/p/7b9a1b386265

11.MAT使用教程
http://blog.csdn.net/itomge/article/details/48719527

12.MAT - Memory Analyzer Tool 使用進(jìn)階
http://www.lightskystreet.com/2015/09/01/mat_usage/

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/70566.html

相關(guān)文章

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<