一、解決所有的內存泄漏
1. 單利泄漏
主要原因還是因爲一般情況下單例都是全局的,有時候會引用一些實際生命週期比較短的變量,導致其無法釋放。
例如 :
activity 的 context賦值到單利對象裏面的成員量變量:
private static volatile ClassXX instance;
private Context context;
private ClassXX(Context context) {
this.context = context;
}
public static ClassXX getInstance(Context context) {
if (instance == null) {
synchronized (instance) {
if(instance == null) {
instance = new ClassXX(context);
}
}
}
return instance;
}
如果這個Context是 Activity 的 Context,當你的 Activity finish(); 之後Activity 這個對象的內存還是在堆中,沒有釋放。因爲單利對象持有Activity 的引用,jvm 認爲你這個對象還是在使用中,不敢去 回收掉你的 Activity。那單例什麼時候被回收?那就只有等到整個進程被回收了,單例纔會被回收。
進程殺死(回收):
- Process.killProcess(Process.myPid())
- 用戶手動卡片式摧毀 (親測可行)
解決方法:
- 傳入和單例一樣生命週期的對象,如context.getApplication();
- 不將 context保存在單例的成員變量裏面。
2. Handler AsyncTask 等內部類的內存泄漏
主要原因是內部類默認持有外部類的引用
private Handler mMainActivityHandler = new Handler(){
@Override
public void handleMessage(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 不及時回收而已)
3. 資源使用完未關閉
- 廣播(BraodcastReceiver)動態註冊之後要反註冊,推薦在onStart onStop 對應的生命週期執行。
- 服務(Service)Start 之後 記得 Stop。啓動服務時機看需求。一般不建議在 Application 啓動(啓動Service耗時基本要100ms+)。
- Bitmap 內存大戶,要記得回收 recycle 一下,當然 90% 的場景Glide 已經幫我們處理的。
4.檢測內存泄漏的工具
LeakCanary
Android Studio profile
MAT
圖片壓縮
大家都知道 bitmap 佔用內存很大,用完之後要 recycle 一下。
不知道大家有沒有用過,圖片加載出來內存就爆掉了(OOM)情況,本寶寶就遇到過了(心中一千萬頭草擬嗎奔騰而過)。
首先一張圖片從網絡獲下來,從 InputStream 轉成 Bitmap,這個 bitmap 佔了多少內存怎麼計算?
獻上代碼:
Bitmap.getAllocationByteCount();
其實就是 ByteCount = 長* 寬 * 4(假設這裏每一個像素點是是RGB888) 那就是 4 個字節。也有一個像素點 RGB565 佔 3 個字節,當然佔更多字節的 RGB888 更加高清無碼。起初版本 Glide 使用 RGB565,目前 Glide4.XX 的默認都是 RGB888,當然自己可以配置一下。
爲了解決這個問題一般都是通過下面代碼:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
// 通過這個bitmap獲取圖片的寬和高
Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/MTXX/3.jpg", options);
float realWidth = options.outWidth;
float realHeight = 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
大家應該對着三個類都非常熟悉。那就先看代碼:
long time = System.currentTimeMillis();
String s = new String("JAVA");
for(int i = 0 ;i<10000; i++) {
s = s+"VERSION";
}
Log.d("TestString","Time consumption:"+(System.currentTimeMillis() - time));
time = System.currentTimeMillis();
StringBuilder s1 = new StringBuilder("JAVA");
for(int i = 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 去拼接字符,效率大大快於用加號,我們帶着問題來找原因。
總結:
通過上面的例子,String 的拼接通過一個 for 循環創建了 10000 個 StringBulider,而且用完就拋棄。特別浪費,在內存吃緊的情況下,很容易引起 gc ,導致 App 卡頓。
也許有同學要問 一個 StringBuilder 的空對象才佔堆內存多大?我們來算一算
一個對象 = 對象頭 + 成員屬性
對象頭 = MardWord + Klass= 12個字節 (數組除外)
上圖:
MardWord 字段大全:
這個 MardWord 怎麼有這麼多鎖狀態,這些鎖狀態又是什麼?
這就要涉及到 synchronized 同步鎖的知識,這個不在本文討論範圍之內。
那麼 StringBulider 的成員屬性有哪些?清單:
static final long serialVersionUID = 4383685877147921099L;
char[] value;
int count;
對象結構圖
計算下來:12+8+8+4+24 = 56 個字節 10000 個對象 那就是要 560KB 內存。不小吧。當然我們實際需求不可能一次搞這麼多個對象,但是多個地方都用 String
去玩的話,積少成多,到時候 APP 內存比別人的高出一大截。那就尷尬了..
四、儘量使用 “池”
我們常見的池有
- 線程池 Lrucache
- 緩存池 okhttp 裏面的 ConnectionPool (socket 複用池)
- okio SegmentPool (buffer 複用池)
池的功能:
可以重複利用對象,並且減少內存開銷,內存抖動,cpu 開銷。
1. 線程池
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
儘量使用線程池去跑任務,而不是動不動就先 new Thread 去跑,這樣子線程是得不到複用的。當任務量一大,使用線程池的效率會超乎你想象(具體自己看源碼),畢竟 開啓一個線程 cpu 內存都是有開銷的。
這裏推薦 Rxjava 的第三方庫,一個將 裝飾者模式 玩到上天的 框架,切換線程方便,支持函數式編程 杜絕回調地獄 等等:
Observable.create(new Action1<Emitter<Integer>>() {
@Override
public void call(Emitter<Integer> 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