Android 性能 - 內存優化

一、解決所有的內存泄漏

內存泄漏概念:

不再使用的對象沒有被回收,就是內存泄露。

1. 單利泄漏

主要原因還是因為一般情況下單例都是全局的,有時候會引用一些實際生命周期比較短的變量,導致其無法釋放。

例如 :

activity 的 content 賦值到單利對象里面的成員量變量

code:

privatestaticvolatileClassXX?instance;

privateContext?context;

privateClassXX(Context?context){

this.context?=?context;

}

publicstaticClassXXgetInstance(Context?context){

if(instance?==null)?{

synchronized(instance)?{

if(instance?==null)?{

instance?=newClassXX(context);

}

}

}

returninstance;

}

如果這個Context

是Activity的Context,當你的Activity finish();之后Activity這個對象的內存還是在堆中,沒有釋放。

因為單利對象持有Activity的引用,jvm認為你這個對象還是在使用中,不敢去 回收掉你的Activity。那單例什么時候被回收?

那就只有等到整個進程被回收了,單例才會被回收。

進程殺死(回收):

Process.killProcess(Process.myPid())

用戶手動卡片式摧毀 (親測可行)

解決方法:

傳入和單例一樣生命周期的對象,如context.getApplication();

不將context保存在單例的成員變量里面。

2. Handler AsyncTask 等內部類的內存泄漏

主要原因是內部類默認持有外部類的引用

大家應該很喜歡吧Handler寫成一個內部類譬如:

privateHandler?mMainActivityHandler?=newHandler(){

@Override

publicvoidhandleMessage(Message?msg){

super.handleMessage(msg);

}

};

其實包括我也很喜歡,而且一個Activity對應一個Handler,每一個Handler負責更新本Activity的 UI,一對一關系,分工明確。好用到爆炸。

然而 java 內部類是默認持有一個外部類的引用,因為 jvm 在把.java 源文件編譯成 .class 字節碼的時候,會在默認的構造函數加入外部類的引用。所以我們在內部類中也能訪問外部類的引用。

然后問題就發生了,當前Handler持有當前Activity的引用,Handler不釋放,Activity也別想釋放了。MMP

(為什么Handler有時候會不會被釋放?)

解決方法:

構造函數傳入Activity并用WeakReferencemActivity;弱引用保存下來。GC的時候會不計入Handler對Activity的引用,可以被回收。

Activity OnDestroy的時候 ,把所有的相關請求終止,并且把消息隊列清空removeCallbacksAndMessages(null);防止有數據回調到 UI 層。(當然如果不這么做,Activity照樣被回收,但是Handler不及時回收而已)

(什么叫 強引用 軟引用 弱引用 虛引用 ,以及 Handler 的消息驅動模型是怎么樣子的,這里就不展開講,本文著重內存泄漏)

當然AsyncTask和其它對象內部類也是有這種問題,解決方法同上。

3. 資源使用完未關閉

主要是:

廣播(BraodcastReceiver)動態注冊之后要反注冊,推薦在onStart onStop對應的生命周期執行。

服務(Service)Start之后 記得Stop。啟動服務時機看需求。一般不建議在Application啟動(啟動Service耗時基本要100ms+)。

io Cursor流要記得close,一定要在finally去close,防止拋異常沒執行close,那就泄漏了。

Bitmap內存大戶,要記得回收recycle一下,當然 90% 的場景Glide已經幫我們處理的。

4.檢測內存泄漏的工具

當然有時候不能完全在寫代碼的時候規避掉所有的內存泄漏,就要用一些工具檢測一下:

LeakCanary

Android Studio profile

MAT

選自己喜歡的工具,去研究一下。(網上很多教程)

二、圖片壓縮

1. bitmap 壓縮

大家都知道bitmap占用內存很大,用完之后要recycle一下。

不知道大家有沒有用過,圖片加載出來內存就爆掉了(OOM)情況,本寶寶就遇到過了(心中一千萬頭草擬嗎奔騰而過)。

首先一張圖片從網絡獲下來,從InputStream轉成Bitmap,這個bitmap占了多少內存怎么計算?

獻上代碼:

Bitmap.getAllocationByteCount();

其實就是 ByteCount = 長* 寬 * 4(假設這里每一個像素點是是RGB888) 那就是 4 個字節。也有一個像素點 RGB565 占 3 個字節,當然占更多字節的 RGB888 更加高清無碼。起初版本Glide使用 RGB565,目前Glide4.XX 的默認都是 RGB888,當然自己可以配置一下。

為了解決這個問題一般都是通過下面代碼:

BitmapFactory.Options?options?=newBitmapFactory.Options();

options.inJustDecodeBounds?=true;

//?通過這個bitmap獲取圖片的寬和高?

Bitmap?bitmap?=?BitmapFactory.decodeFile("/sdcard/MTXX/3.jpg",?options);

floatrealWidth?=?options.outWidth;

floatrealHeight?=?options.outHeight;

//計算出scale

options.inSampleSize?=?scale;

options.inJustDecodeBounds?=false;

//?注意這次要把options.inJustDecodeBounds?設為?false,這次圖片是要讀取出來的。

bitmap?=?BitmapFactory.decodeFile("/sdcard/MTXX/3.jpg",?options);

先獲取他的圖片大小,根據自己需要的大小計算出縮放比例。(圖片大小都是放在圖片的頭部,這時候不會去加載整張圖片)

進行縮放,得出符合自己的控件尺寸的大小。

(當然還有些非法的圖片頭部是獲取不出 長* 寬。這時候記得搞個默認的縮放率,防止 OOM)

有時候為了優化內存,還不如壓縮一張圖片 所節約的內存來的更快。

譬如 一張 1080 * 1920 圖片再乘以 4 等于 7.9 M。

我壓縮到 一張縮略圖 200*200 等于 156KB。瞬間節約了7M 空間。區別真的太大了,頓時內心 一句 MMP 。

三、解決內存抖動

1.String VS StringBuffer VS StringBuilder

大家應該對著三個類都非常熟悉。那就先看代碼:

longtime=?System.currentTimeMillis();

Strings=?new?String("JAVA");

for(inti?=0;i<10000;?i++)?{

s=s+"VERSION";

}

Log.d("TestString","Time?consumption:"+(System.currentTimeMillis()?-time));

time=?System.currentTimeMillis();

StringBuilder?s1?=?new?StringBuilder("JAVA");

for(inti?=0;i<10000;?i++)?{

s1.append("VERSION");

}

Log.d("TestString","Time?consumption:"+(System.currentTimeMillis()?-time));

D/TestString:?Time?consumption:3786

D/TestString:?Time?consumption:2

很明顯使用StringBuilder去拼接字符,效率大大快于用加號,我們帶著問題來找原因。

那我們看一下用 + 號去拼接的字節碼:

使用+號去拼接字符,jvm 會創建一個臨時的StringBuilder

25 new #24

然后把上次的結果集,通過構造函數傳入,

29invokespecial#25>?//調用構造函數,這串符號引用類似?jni?中反調?java的類查找寫法

32aload_3//將局變量表Slot?3的元素入棧

再拼接本次需要拼接的字符。然后存到局部變量表中,等待下次循環操作。

44?astore_3

然后跳轉編號17 去繼續循環。這時候又重新創建了一個StringBulider去拼接。真是啃爹啊。。。

48 goto 17 (-31)

那我們看一下用 StringBuilder 去拼接的字節碼:

這個很明顯new StringBulider字節碼在循環體外面,所以并沒有循環新建對象。

總結:

通過上面的例子,String的拼接通過一個for循環創建了 10000 個StringBulider,而且用完就拋棄。特別浪費,在內存吃緊的情況下,很容易引起gc,導致App卡頓。

也許有同學要問 一個StringBuilder的空對象才占堆內存多大?我們來算一算

一個對象 = 對象頭 + 成員屬性

對象頭 =MardWord+Klass= ?12個字節 (數組除外)

上圖:

MardWord 字段大全(出自網上扣得):

這個MardWord怎么有這么多鎖狀態,這些鎖狀態又是什么?

這就要涉及到synchronized同步鎖的知識,這個不在本文討論范圍之內。

那么StringBulider的成員屬性有哪些?清單:

staticfinallongserialVersionUID?=4383685877147921099L;

char[]value;

intcount;

對象結構圖

計算下來:12+8+8+4+24 = 56 個字節 10000 個對象 那就是要 560KB 內存。不小吧。當然我們實際需求不可能一次搞這么多個對象,但是多個地方都用String

去玩的話,積少成多,到時候APP內存比別人的高出一大截。那就尷尬了..

四、盡量使用 “池”

我們常見的池有

線程池

Lrucache緩存池

okhttp里面的ConnectionPool(socket復用池)

okio SegmentPool(buffer復用池)

池的功能:

可以重復利用對象,并且減少內存開銷,內存抖動,cpu 開銷。

線程池

publicThreadPoolExecutor(intcorePoolSize,

intmaximumPoolSize,

longkeepAliveTime,

TimeUnit?unit,

BlockingQueue?workQueue,

ThreadFactory?threadFactory,

RejectedExecutionHandler?handler)

盡量使用線程池去跑任務,而不是動不動就先new Thread去跑,這樣子線程是得不到復用的。當任務量一大,使用線程池的效率會超乎你想象(具體自己看源碼),畢竟 開啟一個線程cpu內存都是有開銷的。

這里推薦Rxjava的第三方庫,一個將 裝飾者模式 玩到上天的 框架,切換線程方便,支持函數式編程 杜絕回調地獄 等等:

Observable.create(newAction1>()?{

@Override

publicvoidcall(Emitter?subscriber){}

},?Emitter.BackpressureMode.BUFFER)

.subscribeOn(Schedulers.io())//切換到?io?線程池

.subscribeOn(Schedulers.computation())//切換?到計算?線程池

.subscribeOn(Schedulers.immediate())//?使用當前線程

.observeOn(AndroidSchedulers.mainThread())//切換到?android?UI?主線程

.subscribe();

2. Lrucache 緩存池

Lrucache 緩存池:最近最少使用緩存池,底層原理是用 LinkHashMap 實現。

谷歌的Glide圖片加載庫,就是使用了Lrucache,和LruDiskCache對圖片進行緩存,進而提高用戶體驗。

3. ConnectionPool 緩存池

ConnectionPool 緩存池 :復用tcp socket套接字,進行網絡通訊,每一次HTTP請求結束后,并不結束鏈接,可復用于下次的請求。把網絡傳輸速度極致化。

一次http請求分:

tcp三次握手

數據傳輸

tcp四次分手

如果每一次請求都經歷整個流程,可能別人所有數據都加載完畢了,我還在握手中… 這就不能忍。

(當然http 1.1+才支持這個鏈接復用,具體詳細源碼 看OKhttp,本文不做詳細展開)

4. okio SegmentPool (buffer 復用池)

SegmentPool:同上。

總結:

對于一些需要 大量頻繁生成和回收的對象,建議使用池,如果沒有輪子,也是可以手動寫一個。

五、其他

常用數據結構優化

xml 層級 和 view

1.常用數據結構優化

內存大用戶 :HashMap(及其子類)

HashMap是一個典型的 空間換時間,時間復雜度趨近 o(1)

占用空間 是大于size / 0.75(負載因子),

/**

*?hashMap?put?部分源碼,

*?size?當前已存入數據數目

*?threshold?=?容量?*0.75

*/

if?(++size?>?threshold)

resize();

通俗點就是 存入100個數據,要占用 133 個數據內存(及以上),所在數據量較小,或者對速度沒有那么要求的時候可用 SparseArray(二叉樹實現) 代替。

2.xml 層級 和 view

xml 層級最好控制在 5 層以內。

view 的使用多用:

ViewStub

Include

merge

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

推薦閱讀更多精彩內容