為了能夠簡單迅速的發(fā)現(xiàn)內(nèi)存泄漏,Square公司基于MAT開源了LeakCanary。使用LeakCanary,在內(nèi)存泄漏后,通過分析引用鏈可以分析內(nèi)存泄漏的原因,LeakCanary用于檢測Activity、Fragment的內(nèi)存泄漏。
下面通過一些實際案例來進(jìn)行分析。
1、類編譯后結(jié)構(gòu)
非靜態(tài)成員類的每個實例都隱含著與外層類的一個外層類實例(屬性名為this$0)。在非靜態(tài)成員類實例方法內(nèi)部,可以調(diào)用外層類的方法,或者利用修飾過的this構(gòu)造獲得外層類實例的引用。
當(dāng)一個類文件編譯之后有很多類名字中有$符, 比如Test.class, Test$1.class, Test$2.class, Test$MyTest.class
$后面跟數(shù)字的類就是匿名類編譯出來的結(jié)果。Test$MyTest.class則是內(nèi)部類MyTest編譯后得到的。
在非靜態(tài)內(nèi)部類中,我們可以任意使用OuterClass.this來獲取外部類實例。
package com.sdcuike.java.nestclass;
public class OuterClass {
private static class StaticInnerClass {
}
private class NoStaticInnerClass {
}
編譯后,生成的字節(jié)碼文件:
Compiled from "OuterClass.java"
class com.sdcuike.java.nestclass.OuterClass$NoStaticInnerClass {
final com.sdcuike.java.nestclass.OuterClass this$0;
}
Compiled from "OuterClass.java"
class com.sdcuike.java.nestclass.OuterClass$StaticInnerClass {
}
1、數(shù)據(jù)回調(diào)匿名內(nèi)部類泄漏

整個引用鏈分析,匿名內(nèi)部類持有外部類引用,外部類通過持有surfaceView間接引用了Activity。匿名內(nèi)部類里處理數(shù)據(jù)回調(diào),activity退出了,但是數(shù)據(jù)回調(diào)可能還沒停止,從而最終導(dǎo)致Activity泄漏。
public class HXPlaybackTransModel {
/** surfaceview句柄 */
private SurfaceView devSurfaceView = null;
public void setParams(){
//1、修改前代碼
ffmepgPlay.setHikTransDataCallback(new FFMpegAsyncPlayer.GA_SystemTransDataCallback() {
@Override
public void onTransStreamDataCallback(int datatype, byte[] pdata, int datalen) {
···
}
});
//2 修改后代碼,匿名顳部類改為靜態(tài)內(nèi)部類
ffmepgPlay.setHikTransDataCallback(transDataCallback);
}
//3 匿名內(nèi)部類改為靜態(tài)內(nèi)部類
private TransDataCallback transDataCallback = new TransDataCallback(this);
private static class TransDataCallback implements FFMpegAsyncPlayer.GA_SystemTransDataCallback{
private WeakReference<HXPlaybackTransModel> playModelRef;
public TransDataCallback(HXPlaybackTransModel playmodel) {
this.playModelRef = new WeakReference<>(playmodel);
}
@Override
public void onTransStreamDataCallback(int datatype, byte[] pdata, int datalen) {
HXPlaybackTransModel playModel = playModelRef.get();
if (playModel != null){
//處理具體業(yè)務(wù)邏輯
}
}
}
}
修復(fù)前后如上所示。
2、SurfaceHolder泄漏

SurfaceView$4.this$0
public class SurfaceView extends MockView {
//SurfaceHolder是SurfaceView的匿名內(nèi)部類,所以SurfaceView$4即是指SurfaceHolder。而.this$0即是指其外部類,即SurfaceView。
private SurfaceHolder mSurfaceHolder = new SurfaceHolder() {
@Override
public boolean isCreating() {
return false;
}
@Override
public void addCallback(Callback callback) {
}
···
}
}
解決方法:
// 1、修改前
public class FFMpegAsyncPlayer {
private SurfaceHolder surfaceHolder;
···
public void play(String videoPath, Surface surface, SurfaceHolder sfHolder){
this.surfaceHolder = sfHolder;
if (ffmpegPlayer != 0){
play(ffmpegPlayer,videoPath,surface);
}
}
}
···
//2 、修改后
public class FFMpegAsyncPlayer {
···
public void play(String videoPath){
//1、當(dāng)前方案已不需要surfaceHolder,調(diào)用play方法時,sfHolder可以不傳。
if (ffmpegPlayer != 0){
play(ffmpegPlayer,videoPath,null);
}
}
}
FFMpegAsyncPlayer中會有耗時處理,及處理后的回調(diào),所以會導(dǎo)致持有surfaceHolder,導(dǎo)致最終Activity無法回收。所以對于部分View可能比其所在Acitivity生存時間長的問題要引起注意。可以在子線程操作刷新的View如SurfaceView等幾個特例。
實現(xiàn)方案調(diào)整,原有方案會持有surfaceHolder,并傳給native層進(jìn)行解碼后視頻畫面渲染。新方案中渲染使用了另一個播放庫,已不再需要傳入surfaceHolder。所以解決方案,是調(diào)用這個方法的時候,不需傳入surfaceHolder,F(xiàn)FMpegAsyncPlayer已不需持有其引用。
3、Rxjava Consumer(匿名內(nèi)部類)導(dǎo)致的泄漏

SurfaceView$4就是SurfaceHolder。

每個view都有一個上下文Context,所以SurfaceView的mContext(在繼承的View中)最終引用到了其所在的Activity。
CustomSurfaceView繼承自SurfaceView。
這個CameraPlaybackCompatPresenter$9匿名內(nèi)部類究竟在哪里。

點開LeakCanary,看到是一個Rxjava 的Consumer。
具體是哪個,通過debug打斷點調(diào)試分析找到。

主要原因是其他Rxjava的觀察者都通過CompositeDisposable,但dspPlayTimeRefresh并沒有加入到其中,導(dǎo)致Activity destroy的時候沒有取消其訂閱。
CameraPlaybackCompatPresenter{
/**rxjava取消訂閱*/
private CompositeDisposable mCompositeDisposable = new CompositeDisposable();
//1、clearMessage在Activity 銷毀時調(diào)用
public void clearMessage(){
stopRefreshPlayOsdTime(); // 2、增加的解決方法,取消訂閱
mCompositeDisposable.clear();
}
private void stopRefreshPlayOsdTime(){
if (dspPlayTimeRefresh != null) {
dspPlayTimeRefresh.dispose();
dspPlayTimeRefresh = null;
}
}
}
總結(jié)
使用LeakCanary進(jìn)行內(nèi)存泄漏分析并不麻煩,將引用鏈分析清楚,內(nèi)存泄漏原因自然很快查到。
主要排查思路
1、查看類引用依賴關(guān)系
2、引用解除可以在引用鏈上一個合適節(jié)點解除,解決方案并不唯一。
android常見內(nèi)存泄漏原因:
1、Handler引起的內(nèi)存泄漏。即使用Handler(非靜態(tài)內(nèi)部類)持有外部類(Activity)引用,消息處理不合適導(dǎo)致Activity泄漏。
2、單例模式引起的內(nèi)存泄漏。例如單例持有Activity上下文導(dǎo)致泄漏。
3、非靜態(tài)內(nèi)部類創(chuàng)建靜態(tài)實例引起的內(nèi)存泄漏
4、非靜態(tài)匿名內(nèi)部類引起的內(nèi)存泄漏
5、注冊/反注冊未成對使用引起的內(nèi)存泄漏。廣播接收、EventBus等
6、資源對象沒有關(guān)閉引起的內(nèi)存泄漏
7、集合對象沒有及時清理引起的內(nèi)存泄漏
參考
https://blog.csdn.net/doctor_who2004/article/details/102329237
常見android內(nèi)存泄漏問題
https://www.cnblogs.com/andashu/p/6440944.html