LMK;內存抖動、內存泄漏與內存溢出
優化的結果:使得app流暢不卡
內存抖動
短時間內有大量對象創建與銷毀,它伴隨著頻繁的GC。
比較典型的就是字符串的拼接造成內存抖動。
比如:
String str = "";
for(int i=0; i< 10; i++) {
str += i;
}
+=操作會編譯成StringBuilder對象,然后調用StringBuilder的append方法進行拼接。
所以上述代碼會創建10個StringBuilder對象,每執行一次+操作都會新創建一個StringBuilder對象。
優化方法:避免使用+或者+=操作,使用StringBuilder來實現字符串的拼接
StringBuilder str = new StringBuilder();
for (int i = 0; i < 10; i++) {
str.append(i);
}
內存抖動實戰
一個不斷旋轉的試圖,優化前的代碼:
public class IOSStyleLoadingView1 extends View {
private final Context context;
private double radius;
private double insideRadius;
private float northwestXStart;
private float northwestYStart;
private float northXStart;
private float northYStart;
private float notheastXStart;
private float notheastYStart;
private float eastXStart;
private float eastYStart;
private float southeastXStart;
private float southeastYStart;
private float southXStart;
private float southYStart;
private float southwestXStart;
private float southwestYStart;
private float westXStart;
private float westYStart;
private float northwestXEnd;
private float northwestYEnd;
private float northXEnd;
private float northYEnd;
private float notheastXEnd;
private float notheastYEnd;
private float eastXEnd;
private float eastYEnd;
private float southeastXEnd;
private float southeastYEnd;
private float southXEnd;
private float southYEnd;
private float southwestXEnd;
private float southwestYEnd;
private float westXEnd;
private float westYEnd;
private int currentColor = 7;
String color[] = new String[]{
"#a5a5a5",
"#b7b7b7",
"#c0c0c0",
"#c9c9c9",
"#d2d2d2",
"#dbdbdb",
"#e4e4e4",
"#e4e4e4"
};
public IOSStyleLoadingView1(Context context) {
this(context,null,0);
}
public IOSStyleLoadingView1(Context context, AttributeSet attrs) {
this(context,null,0);
}
public IOSStyleLoadingView1(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
radius = UIKits.dip2Px(context, 9);
insideRadius = UIKits.dip2Px(context, 5);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setStrokeWidth(UIKits.dip2Px(context, 2));
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeCap(Paint.Cap.ROUND);
Path p0 = new Path();
paint.setColor(Color.parseColor(color[0]));//Color.parseColor方法會調用String的substring()方法,substring()方法會產生新的String對象
p0.moveTo(northwestXStart, northwestYStart);
p0.lineTo(northwestXEnd, northwestYEnd);
canvas.drawPath(p0, paint);
Path p1 = new Path();
paint.setColor(Color.parseColor(color[1]));
p1.moveTo(northXStart, northYStart);
p1.lineTo(northXEnd, northYEnd);
canvas.drawPath(p1, paint);
Path p2 = new Path();
paint.setColor(Color.parseColor(color[2]));
p2.moveTo(notheastXStart, notheastYStart);
p2.lineTo(notheastXEnd, notheastYEnd);
canvas.drawPath(p2, paint);
Path p3 = new Path();
paint.setColor(Color.parseColor(color[3]));
p3.moveTo(eastXStart, eastYStart);
p3.lineTo(eastXEnd, eastYEnd);
canvas.drawPath(p3, paint);
Path p4 = new Path();
paint.setColor(Color.parseColor(color[4]));
p4.moveTo(southeastXStart, southeastYStart);
p4.lineTo(southeastXEnd, southeastYEnd);
canvas.drawPath(p4, paint);
Path p5 = new Path();
paint.setColor(Color.parseColor(color[5]));
p5.moveTo(southXStart, southYStart);
p5.lineTo(southXEnd, southYEnd);
canvas.drawPath(p5, paint);
Path p6 = new Path();
paint.setColor(Color.parseColor(color[6]));
p6.moveTo(southwestXStart, southwestYStart);
p6.lineTo(southwestXEnd, southwestYEnd);
canvas.drawPath(p6, paint);
Path p7 = new Path();
paint.setColor(Color.parseColor(color[7]));
p7.moveTo(westXStart, westYStart);
p7.lineTo(westXEnd, westYEnd);
canvas.drawPath(p7, paint);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
double centerX = getMeasuredWidth() / 2;
double centerY = getMeasuredHeight() / 2;
double leg = radius * Math.cos(Math.PI / 4);
double insideLeg = insideRadius * Math.cos(Math.PI / 4);
northwestXStart = (float) (centerX - leg);
northwestYStart = (float) (centerY - leg);
northXStart = (float) centerX;
northYStart = (float) (centerY - radius);
notheastXStart = (float) (centerX + leg);
notheastYStart = (float) (centerY - leg);
eastXStart = (float) (centerX + radius);
eastYStart = (float) centerY;
southeastXStart = (float) (centerX + leg);
southeastYStart = (float) (centerY + leg);
southXStart = (float) centerX;
southYStart = (float) (centerY + radius);
southwestXStart = (float) (centerX - leg);
southwestYStart = (float) (centerY + leg);
westXStart = (float) (centerX - radius);
westYStart = (float) centerY;
northwestXEnd = (float) (centerX - insideLeg);
northwestYEnd = (float) (centerY - insideLeg);
northXEnd = (float) centerX;
northYEnd = (float) (centerY - insideRadius);
notheastXEnd = (float) (centerX + insideLeg);
notheastYEnd = (float) (centerY - insideLeg);
eastXEnd = (float) (centerX + insideRadius);
eastYEnd = (float) centerY;
southeastXEnd = (float) (centerX + insideLeg);
southeastYEnd = (float) (centerY + insideLeg);
southXEnd = (float) centerX;
southYEnd = (float) (centerY + insideRadius);
southwestXEnd = (float) (centerX - insideLeg);
southwestYEnd = (float) (centerY + insideLeg);
westXEnd = (float) (centerX - insideRadius);
westYEnd = (float) centerY;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
startAnimation();
}
private ValueAnimator valueAnimator;
public void startAnimation() {
valueAnimator = ValueAnimator.ofInt(7, 0);
valueAnimator.setDuration(400);
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if ((int) animation.getAnimatedValue() != currentColor) {
String b[] = new String[color.length];//移動后的數組
for (int c = 0, size = color.length - 1; c < size; c++) {
b[c + 1] = color[c];
}
b[0] = color[color.length - 1];
color = b;
invalidate();
currentColor = (int) animation.getAnimatedValue();
}
}
});
valueAnimator.start();
}
}
利用Android Studio內置的Profiler工具查看內存使用情況:
可以看到內存產生抖動,并且隨著程序的運行,內存一直在增加,選擇一段時間分析內存中的對象:
可以看到內存中的有大量的Path對象和String對象,一般來說是有異常的,分析代碼看看是否能避免這種情況。
可以在onDraw方法里進行優化,優化后的代碼:
/**
* 優化后的IOSStyleLoadingView
* onDraw方法里不創建Path和Paint對象,不調用Color.parseColor創建String對象
*
*/
public class IOSStyleLoadingView extends View {
private final Context context;
private double radius;
private double insideRadius;
private float northwestXStart;
private float northwestYStart;
private float northXStart;
private float northYStart;
private float notheastXStart;
private float notheastYStart;
private float eastXStart;
private float eastYStart;
private float southeastXStart;
private float southeastYStart;
private float southXStart;
private float southYStart;
private float southwestXStart;
private float southwestYStart;
private float westXStart;
private float westYStart;
private float northwestXEnd;
private float northwestYEnd;
private float northXEnd;
private float northYEnd;
private float notheastXEnd;
private float notheastYEnd;
private float eastXEnd;
private float eastYEnd;
private float southeastXEnd;
private float southeastYEnd;
private float southXEnd;
private float southYEnd;
private float southwestXEnd;
private float southwestYEnd;
private float westXEnd;
private float westYEnd;
private int currentColor = 7;
String color[] = new String[]{
"#a5a5a5",
"#b7b7b7",
"#c0c0c0",
"#c9c9c9",
"#d2d2d2",
"#dbdbdb",
"#e4e4e4",
"#e4e4e4"
};
int[] colors = new int[8];
public IOSStyleLoadingView(Context context) {
this(context, null, 0);
}
public IOSStyleLoadingView(Context context, AttributeSet attrs) {
this(context, null, 0);
}
/**
* 提前在構造方法里調用Color.parseColor解析好數據,而不是在onDraw方法里頻繁調用
*
*
* @param context
* @param attrs
* @param defStyleAttr
*/
public IOSStyleLoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
radius = UIKits.dip2Px(context, 9);
insideRadius = UIKits.dip2Px(context, 5);
for (int i = 0; i < color.length; i++) {
colors[i] = Color.parseColor(color[i]);
}
paint.setAntiAlias(true);
paint.setStrokeWidth(UIKits.dip2Px(context, 2));
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeCap(Paint.Cap.ROUND);
}
Path path = new Path();
Paint paint = new Paint();
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
path.reset();
paint.setColor(colors[(currentColor++) % 8]);
path.moveTo(northwestXStart, northwestYStart);
path.lineTo(northwestXEnd, northwestYEnd);
canvas.drawPath(path, paint);
path.reset();
paint.setColor(colors[(currentColor++) % 8]);
path.moveTo(northXStart, northYStart);
path.lineTo(northXEnd, northYEnd);
canvas.drawPath(path, paint);
path.reset();
paint.setColor(colors[(currentColor++) % 8]);
path.moveTo(notheastXStart, notheastYStart);
path.lineTo(notheastXEnd, notheastYEnd);
canvas.drawPath(path, paint);
path.reset();
paint.setColor(colors[(currentColor++) % 8]);
path.moveTo(eastXStart, eastYStart);
path.lineTo(eastXEnd, eastYEnd);
canvas.drawPath(path, paint);
path.reset();
paint.setColor(colors[(currentColor++) % 8]);
path.moveTo(southeastXStart, southeastYStart);
path.lineTo(southeastXEnd, southeastYEnd);
canvas.drawPath(path, paint);
path.reset();
paint.setColor(colors[(currentColor++) % 8]);
path.moveTo(southXStart, southYStart);
path.lineTo(southXEnd, southYEnd);
canvas.drawPath(path, paint);
path.reset();
paint.setColor(colors[(currentColor++) % 8]);
path.moveTo(southwestXStart, southwestYStart);
path.lineTo(southwestXEnd, southwestYEnd);
canvas.drawPath(path, paint);
path.reset();
paint.setColor(colors[(currentColor++) % 8]);
path.moveTo(westXStart, westYStart);
path.lineTo(westXEnd, westYEnd);
canvas.drawPath(path, paint);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
double centerX = getMeasuredWidth() / 2;
double centerY = getMeasuredHeight() / 2;
double leg = radius * Math.cos(Math.PI / 4);
double insideLeg = insideRadius * Math.cos(Math.PI / 4);
northwestXStart = (float) (centerX - leg);
northwestYStart = (float) (centerY - leg);
northXStart = (float) centerX;
northYStart = (float) (centerY - radius);
notheastXStart = (float) (centerX + leg);
notheastYStart = (float) (centerY - leg);
eastXStart = (float) (centerX + radius);
eastYStart = (float) centerY;
southeastXStart = (float) (centerX + leg);
southeastYStart = (float) (centerY + leg);
southXStart = (float) centerX;
southYStart = (float) (centerY + radius);
southwestXStart = (float) (centerX - leg);
southwestYStart = (float) (centerY + leg);
westXStart = (float) (centerX - radius);
westYStart = (float) centerY;
northwestXEnd = (float) (centerX - insideLeg);
northwestYEnd = (float) (centerY - insideLeg);
northXEnd = (float) centerX;
northYEnd = (float) (centerY - insideRadius);
notheastXEnd = (float) (centerX + insideLeg);
notheastYEnd = (float) (centerY - insideLeg);
eastXEnd = (float) (centerX + insideRadius);
eastYEnd = (float) centerY;
southeastXEnd = (float) (centerX + insideLeg);
southeastYEnd = (float) (centerY + insideLeg);
southXEnd = (float) centerX;
southYEnd = (float) (centerY + insideRadius);
southwestXEnd = (float) (centerX - insideLeg);
southwestYEnd = (float) (centerY + insideLeg);
westXEnd = (float) (centerX - insideRadius);
westYEnd = (float) centerY;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
startAnimation();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (valueAnimator != null) {
valueAnimator.removeAllUpdateListeners();//移除監聽器,否則會造成內存泄漏
valueAnimator.cancel();//取消動畫
}
}
private ValueAnimator valueAnimator;
public void startAnimation() {
valueAnimator = ValueAnimator.ofInt(7, 0);
valueAnimator.setDuration(400);
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if ((int) animation.getAnimatedValue() != currentColor) {
// String b[] = new String[color.length];//移動后的數組
// for (int c = 0, size = color.length - 1; c < size; c++) {
// b[c + 1] = color[c];
// }
// b[0] = color[color.length - 1];
// color = b;
invalidate();
currentColor = (int) animation.getAnimatedValue();
}
}
});
valueAnimator.start();
}
}
再次查看內存使用情況:
可以看到內存平穩,選擇一段時間分析內存中的對象:
可以看到內存中不會再有大量的對象。
預防內存抖動
1.避免在循環中創建對象;
2.避免在頻繁調用的方法中創建對象,如View的onDraw方法;
3.允許復用的情況下,使用對象池進行緩存,如:Handler的Message單鏈表(obtain);
內存抖動會造成的問題:卡頓和OOM
卡頓
內存抖動會引起頻繁的gc,gc是會STW的,會暫停用戶線程。
OOM
CMS垃圾回收器老年代是標記-清除算法:不會移動存活的對象,會產生內存碎片。
像上述圖中的內存,雖然有很多內存可用,但卻是不連續的,如果申請連續的10個字節(假設圖中一個空格代表一個字節)的內存就會產生OOM,因為沒有連續的10個字節的可用內存。
比如申請bitmap時就很可能產生OOM。
內存泄漏
程序中己動態分配的堆內存由于某種原因程序未釋放或無法釋放,造成系統內存的浪費。
長生命周期對象持有短生命周期對象強引用,從而導致短生命周期對象無法被回收!
注意這里是持有對象的強引用,如果是持有對象的軟引用或者是弱引用或者是虛引用則不會造成內存泄漏。
可達性分析法
通過一系列稱為“GC Roots”的對象作為起始點,從這些節點向下搜索,搜索所有的引用鏈,當一個對象到GC Roots沒有任何引用鏈(即GC Roots到對象不可達)時,則證明此對象是不可用的。
GC Roots
在Java語言中可作為GC Roots的對象包括下面幾種:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象。
- 方法區中類靜態屬性引用的對象。
- 方法區中常量引用的對象。
- 本地方法棧中JNI(即一般說的Native方法)引用的對象。
- JVM的內部引用(class對象、異常對象NullPointException、OutofMemoryError,系統類加載器)。
- 所有被同步鎖(synchronized關鍵)持有的對象。
- JVM內部的JMXBean、JVMTI中注冊的回調、本地代碼緩存等
- JVM實現中的“臨時性”對象,跨代引用的對象(在使用分代模型回收只回收部分代時)
也可以參考:
Garbage Collection Roots
Garbage Collection Roots
A garbage collection root is an object that is accessible from outside the heap. The following reasons make an object a GC root:
System Class
Class loaded by bootstrap/system class loader. For example, everything from the rt.jar like java.util.* .
JNI Local
Local variable in native code, such as user defined JNI code or JVM internal code.
JNI Global
Global variable in native code, such as user defined JNI code or JVM internal code.
Thread Block
Object referred to from a currently active thread block.
Thread
A started, but not stopped, thread.
Busy Monitor
Everything that has called wait() or notify() or that is synchronized. For example, by calling synchronized(Object) or by entering a synchronized method. Static method means class, non-static method means object.
Java Local
Local variable. For example, input parameters or locally created objects of methods that are still in the stack of a thread.
Native Stack
In or out parameters in native code, such as user defined JNI code or JVM internal code. This is often the case as many methods have native parts and the objects handled as method parameters become GC roots. For example, parameters used for file/network I/O methods or reflection.
Finalizable
An object which is in a queue awaiting its finalizer to be run.
Unfinalized
An object which has a finalize method, but has not been finalized and is not yet on the finalizer queue.
Unreachable
An object which is unreachable from any other root, but has been marked as a root by MAT to retain objects which otherwise would not be included in the analysis.
Java Stack Frame
A Java stack frame, holding local variables. Only generated when the dump is parsed with the preference set to treat Java stack frames as objects.
Unknown
An object of unknown root type. Some dumps, such as IBM Portable Heap Dump files, do not have root information. For these dumps the MAT parser marks objects which are have no inbound references or are unreachable from any other root as roots of this type. This ensures that MAT retains all the objects in the dump.
軟引用,弱引用
軟引用:定義一些還有用但并非必須的對象。對于軟引用關聯的對象,GC不會直接回收,而是在系統將要內存溢出之前才會觸發GC將這些對象進行回收。
弱引用:同樣定義非必須對象。被弱引用關聯的對象在GC執行時會被直接回收。
Handler的內存泄露場景
public class MemoryLeakActivity extends Activity {
private int i = 10;//成員屬性i
private Handler mHandler = new Handler();
/**
* 如果不傳外部類引用則不能訪問外部類的成員屬性i
*/
static class Runnable1 implements Runnable {
@Override
public void run() {
//但是靜態內部類不能訪問外部類的成員屬性i,怎么辦?將外部類引用傳遞給靜態內部類
//System.out.println("i=" + i);
}
}
/**
* 如果直接傳遞外部類引用則依然會造成內存泄露
*/
static class Runnable2 implements Runnable {
//但是不能直接將外部類引用傳遞進來,因為雖然可以訪問外部類的成員屬性,但是還是會造成內存泄露
MemoryLeakActivity activity;
public Runnable2(MemoryLeakActivity activity) {
this.activity = activity;
}
@Override
public void run() {
System.out.println(activity.i);
}
}
/**
* 正確解決方案:使用弱引用,即靜態內部類持有外部類的弱引用
*/
static class Runnable3 implements Runnable {
/**
* 注意這里應該使用WeakReference,而不是SoftReference,
* 雖然使用SoftReference也不會造成OOM,但是當我們退出Activity時是希望Activity盡快被回收的,
* 所以使用WeakReference更合適,因為被WeakReference關聯的對象在GC執行時會被直接回收,
* 而對于SoftReference關聯的對象,GC不會直接回收,而是在系統將要內存溢出之前才會觸發GC將這些對象進行回收。
*/
WeakReference<MemoryLeakActivity> activityWeakReference;
public Runnable3(MemoryLeakActivity activity) {
this.activityWeakReference = new WeakReference<MemoryLeakActivity>(activity);
}
@Override
public void run() {
if (activityWeakReference.get() != null) {
int i = activityWeakReference.get().i;
System.out.println("i=" + i);
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memoryleak);
/**
* Handler的內存泄露場景
*/
mHandler.postDelayed(new Runnable() {//這里創建了一個匿名內部類對象
@Override
public void run() {
//匿名內部類對象持有外部類的引用(即持有Activity的引用),所以可以直接訪問外部類的的成員屬性i
//所以會造成內存泄露
System.out.println("i=" + i);
}
}, 1_000);
//解決方案1:在onDestroy()方法里調用 mHandler.removeCallbacksAndMessages(null);將消息隊列里的Message全部移除
//解決方案2:使用靜態內部類,靜態內部類不會持有外部類的引用,而內部類和匿名內部類會持有外部類的引用
mHandler.postDelayed(new Runnable3(this), 10_000);
}
@Override
protected void onDestroy() {
super.onDestroy();
//解決方案1:在onDestroy()方法里調用 mHandler.removeCallbacksAndMessages(null);將消息隊列里的Message全部移除
mHandler.removeCallbacksAndMessages(null);
}
MAT檢測內存泄漏實戰
1.打開Android studio的profiler的Memory視圖
2.點擊按鈕進入SecondActivity,退出,再次點擊按鈕進入SecondActivity,退出。
3.點擊dump按鈕生成內存快照
dump完成后Android studio會開啟一個新的視圖顯示當前內存中的所有對象
點擊Arrange by package選項讓所有類按照包進行組織,方便找到自己寫的類,找到SecondActivity,可以看到內存中有2個SecondActivity對象仍然存活,但這能代表SecondActivity就發生內存泄漏嗎?是不能的,因為只有強引用指向SecondActivity對象時才代表發生內存泄露,如果是軟引用、弱引用、虛引用指向SecondActivity對象是不會發生內存泄漏的,但是Android studio的profiler無法判斷是強引用、軟引用、弱引用、虛引用的,需要借助MAT工具。
\4. 導出hprof文件,點擊導出按鈕將Android studio中的hprof文件導出到自己的文件夾,用于后續在MAT工具中分析。
5.導出的hprof文件是無法直接在MAT工具中分析的,如果直接在MAT中打開這個文件會報錯,需要進行轉換,轉換工具是sdk/platform-tools里面的hprof-conv.exe工具,執行命令進行轉換:
6.在MAT中打開轉換后的文件,打開后選擇Histogram視圖,輸入SecondActivity進行過濾,可以看到SecondActivity對象有2個,右鍵選擇SecondActivity對象,選擇merge shortest paths to gc roots -> exclude all phantom/weak/soft etc. references,排除掉虛引用/弱引用/軟引用,只保留強引用,因為只有強引用才會導致內存泄露。
7.分析強引用的引用鏈:
ValueAnimator的mUpdateListeners成員屬性持有IOSStyleLoadingView的內部類對象,該內部類對象持有外部類的引用this$0(即持有IOSStyleLoadingView的引用),而IOSStyleLoadingView是一個View,它的context屬性(IOSStyleLoadingView中定義的)和mContext屬性(View中定義的,View的mContext屬性默認持有View所在Activity的引用)持有SecondActivity的引用。
分析下來原因就是valueAnimator添加的監聽器沒有移除:
public void startAnimation() {
valueAnimator = ValueAnimator.ofInt(7, 0);
valueAnimator.setDuration(400);
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if ((int) animation.getAnimatedValue() != currentColor) {
// String b[] = new String[color.length];//移動后的數組
// for (int c = 0, size = color.length - 1; c < size; c++) {
// b[c + 1] = color[c];
// }
// b[0] = color[color.length - 1];
// color = b;
invalidate();
currentColor = (int) animation.getAnimatedValue();
}
}
});
valueAnimator.start();
}
解決方法:在onDetachedFromWindow中移除監聽器,并取消動畫
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (valueAnimator != null) {
valueAnimator.removeAllUpdateListeners();//移除監聽器,否則會造成內存泄漏
valueAnimator.cancel();//取消動畫
}
}
8.重新執行1-7步,可以看到內存中SecondActivity還是有2個
在這里插入圖片描述
這是否意味著仍然存在內存泄露?右鍵選擇SecondActivity對象,選擇merge shortest paths to gc roots -> exclude all phantom/weak/soft etc. references
可以看到已經沒有強引用的引用鏈了,說明不會發生內存泄露。
那么到底是誰引用著SecondActivity?接下來選擇merge shortest paths to gc roots -> exclude weak references:
說明只有FinalizerReference引用著SecondActivity,即SecondActivity已經即將被回收了。
9.如果有很多頁面,那是否要每個頁面都執行上述操作?這需要大量的操作,有沒有更好的辦法進行一次性檢測?有,可以進行內存快照的比對,如何操作?
首次進入主頁面時dump一次內存快照,然后不斷進出各個頁面,再次返回到主頁面時又dump一次內存快照,然后比對這兩次的hprof文件。
圖中顯示3.hprof比1.hprof的相應對象多了多少個。
內存泄漏問題常見場景
1. 集合類
當使用集合時,只有添加元素,沒有對應的刪除元素。如EventBus只有注冊沒有注銷!
2. 靜態成員/單例
作為GC ROOT,持有短生命周期對象的引用(如持有Activity對象的引用)導致短生命周期對象無法釋放。
/**
* 內存泄漏問題常見場景
* 靜態成員/單例:作為GC ROOT,持有短生命周期對象的引用(如持有Activity對象的引用)導致短生命周期對象無法釋放。
*/
public class SingletonManager {
//GC ROOT:靜態屬性所引用的對象,這里new SingletonManager()創建的SingletonManager對象就是屬于GC ROOT
private static final SingletonManager ourInstance = new SingletonManager();
private Context mContext;
public static SingletonManager getInstance() {
return ourInstance;
}
private SingletonManager() {
}
/**
* 解決辦法:mContext傳遞ApplicationContext,而不是傳遞Activity
* 否則靜態對象SingletonManager的成員屬性mContext會一直持有Activity對象,導致Activity內存泄露
*
* @param context
*/
public void init(Context context) {
mContext = context;
}
public class MemoryMainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memory_main);
SingletonManager.getInstance().init(getApplicationContext());
}
}
3. 未關閉/釋放資源
如FileOutputStream未close。
/**
* 未關閉/釋放資源導致的內存泄露
*/
private void write() {
FileOutputStream fos = null;
try {
fos = new FileOutputStream(new File(""));
fos.write(1);//fos.write(1);可能會出現異常,導致fos.close()不能執行,從而資源未關閉
fos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 正確寫法
*/
private void write1() {
FileOutputStream fos = null;
try {
fos = new FileOutputStream(new File(""));
fos.write(1);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fos != null){
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
4. 非靜態內部類
如Handler postDelayed一個匿名Runnable,退出Activity時消息沒處理完
5. 系統Bug
WebView、InputMethodManager等
/**
* 系統BUG:InputMethodManager導致的內存泄露。
*/
private void resolveInputMethodManagerMemoryLeak() {
// 系統BUG:InputMethodManager導致的內存泄露。
// InputMethodManager是一個static單例對象(所以InputMethodManager是GC root),InputMethodManager的mCurRootView、mNextServedView、mServedView持有DecorView的引用,
// DecorView持有Activity的引用,導致Activity不能被回收。
//解決辦法:將InputMethodManager的mCurRootView、mNextServedView、mServedView置為null
InputMethodManager im = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
try {
Field mCurRootView = InputMethodManager.class.getDeclaredField("mCurRootView");
mCurRootView.setAccessible(true);
mCurRootView.set(im, null);
Field mNextServedView = InputMethodManager.class.getDeclaredField("mNextServedView");
mNextServedView.setAccessible(true);
mNextServedView.set(im, null);
Field mServedView = InputMethodManager.class.getDeclaredField("mServedView");
mServedView.setAccessible(true);
mServedView.set(im, null);
} catch (Exception e) {
e.printStackTrace();
}
}