一、解決所有的內存泄漏
內存泄漏概念:
不再使用的對象沒有被回收,就是內存泄露。
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