Android 性能篇 - 內存優化
來自專欄 Android資料庫1 人贊了文章
內存優化是一個程序員的基本功。有時也要切合項目的實際需求來做選擇。
一、解決所有的內存泄漏
內存泄漏概念:
不再使用的對象沒有被回收,就是內存泄露。
1. 單利泄漏
主要原因還是因為一般情況下單例都是全局的,有時候會引用一些實際生命周期比較短的變數,導致其無法釋放。
例如 :
- activity 的 content 賦值到單利對象裡面的成員量變數
code:
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 等內部類的內存泄漏
主要原因是內部類默認持有外部類的引用
大家應該很喜歡吧 Handler
寫成一個內部類譬如:
private Handler mMainActivityHandler = new Handler(){@Overridepublic void handleMessage(Message msg) { super.handleMessage(msg); }};
其實包括我也很喜歡,而且一個
然而 java 內部類是默認持有一個外部類的引用,因為 jvm 在把.java 源文件編譯成 .class 位元組碼的時候,會在默認的構造函數加入外部類的引用。所以我們在內部類中也能訪問外部類的引用。然後問題就發生了,當前Activity
對應一個Handler
,每一個Handler
負責更新本Activity
的 UI,一對一關係,分工明確。好用到爆炸。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 壓縮
大家都知道
不知道大家有沒有用過,圖片載入出來內存就爆掉了(OOM)情況,本寶寶就遇到過了(心中一千萬頭草擬嗎奔騰而過)。首先一張圖片從網路獲下來,從bitmap
佔用內存很大,用完之後要recycle
一下。InputStream
轉成Bitmap
,這個bitmap
佔了多少內存怎麼計算?獻上代碼:Bitmap.getAllocationByteCount();
其實就是 ByteCount = 長* 寬 * 4(假設這裡每一個像素點是是RGB888) 那就是 4 個位元組。也有一個像素點 RGB565 占 3 個位元組,當然占更多位元組的 RGB888 更加高清無碼。起初版本Glide
使用 RGB565,目前Glide
4.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;//計算出scaleoptions.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:3786D/TestString: Time consumption:2
很明顯使用
StringBuilder
去拼接字元,效率大大快於用加號,我們帶著問題來找原因。
那我們看一下用 + 號去拼接的位元組碼:
使用+號去拼接字元,jvm 會創建一個臨時的 StringBuilder
25 new #24 <java/lang/StringBuilder>
- 然後把上次的結果集,通過構造函數傳入,
29 invokespecial #25 <java/lang/StringBuilder.<init>> //調用構造函數,這串符號引用類似 jni 中反調 java的類查找寫法32 aload_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
的成員屬性有哪些?清單: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 開銷。
- 線程池
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>>() {@Overridepublic 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
推薦閱讀:
※快速排序演算法的優化思路總結
※【厚積薄發】關於LZMA和LZ4壓縮的疑惑解析
※document.write 的痛
※MySQL性能優化的最佳21條經驗