主目錄見:Android高級進階知識(這是總目錄索引)
[written by 無心追求]
Activity內(nèi)部類泄漏
- Activity如果存在內(nèi)部類,無論是匿名內(nèi)部類,或者是聲明的內(nèi)部類,都有可能造成Activity內(nèi)存泄漏,因為內(nèi)部類默認(rèn)是直接持有這個activity的引用,如果內(nèi)部類的生命周期比activity的生命周期要長,那么在activity銷毀的時候內(nèi)部類仍然存在并且持有activity的引用,那么activity自然無法被gc,造成內(nèi)存泄漏
Activity內(nèi)部Handler
class MyHandler extends Handler {
MyHandler() {
}
@Override
public void handleMessage(Message msg) {
// to do your job
}
}
MyHandler myHandler = new MyHandler();
如上,在Activity內(nèi)部如果聲明一個這樣的Handler,那么myHandler就默認(rèn)持有Activity引用,假設(shè)Activity退出了,但是可能這時候才有myHandler的任務(wù)post,那么Activity是無法被回收的,可以采用以下方式解決:
static class MyHandler extends Handler {
WeakReference<Activity> mActivityReference;
MyHandler(Activity activity) {
mActivityReference = new WeakReference<Activity>(activity);
}
@Override
public void handleMessage(Message msg) {
final Activity activity = mActivityReference.get();
if (activity != null) {
if (msg.what == 1 && isJumpToHomePage) {
Intent intent = new Intent(activity, HomePageActivity.class);
// intent.putExtra("themeType", themeType);
// LogUtil.d("themeType == " + themeType);
activity.startActivity(intent);
activity.finish();
}
}
}
}
這里面是把MyHandler是一個內(nèi)部靜態(tài)類,靜態(tài)類在java虛擬機加載的時候就是獨立加載到內(nèi)存中的,不會依賴于任何其他類,而且這里面是把activity以弱引用的方式傳到MyHandler中,即便是靜態(tài)MyHandler類對象一直存在,但是由于它持有的是activity弱引用,在gc回收的時候activity對象是可以被回收的,另外注意一點,對于Handler的使用如果有sendEmptyMessageDelayed()來延遲任務(wù)執(zhí)行的話最好在Activity的onDestroy里面把Handler的任務(wù)都移除(removeCallbacks(null)),activity在退出后,就是應(yīng)該在onDestroy方法里面把一些任務(wù)取消掉,做一些清理的操作
Activity內(nèi)部線程
- 在Activity里面有時候為了實現(xiàn)異步操作會單獨開一個線程來執(zhí)行任務(wù),或者是異步的網(wǎng)絡(luò)請求也是單獨開線程來執(zhí)行的,那么就會存在一個問題,如果內(nèi)部線程的生命周期比Activity的生命周期要長,那么內(nèi)部線程任然默認(rèn)持有Activity的引用,導(dǎo)致Activity對象無法被回收,但是當(dāng)這個線程執(zhí)行完了之后,Activity對象就能被成功的回收了,這會造成一個崩潰風(fēng)險,可能在線程里面有調(diào)用到一些Activity的內(nèi)部對象,但是在Activity退出后這些對象有可能有些已經(jīng)被回收了,就變成null了,這時候要是不進行null的判斷就會報空指針異常,如果這個線程是一直跑的,那就會造成Activity對象一直不會被回收了,因此,在activity退出后一定要做相關(guān)的清理操作,中斷線程,取消網(wǎng)絡(luò)請求等等
Activity內(nèi)部類回調(diào)監(jiān)聽
- 在編碼中常常會定義各種接口回調(diào),類似有點擊時間監(jiān)聽OnClickListener,這些回調(diào)監(jiān)聽有時候就定義在Activity內(nèi)部,或者直接用Activity對象去實現(xiàn)這個接口,到時候設(shè)置監(jiān)聽的時候直接調(diào)用setListener(innerListener)或者setListener(this),innerListener是Activity內(nèi)部定義的,this就是Activity對象,那么問題來了,回調(diào)監(jiān)聽并不一定馬上返回,只有在觸發(fā)條件滿足的時候才會回調(diào),這個時間是無法確定的,因此在Activity退出的時候應(yīng)該顯示的把回調(diào)監(jiān)聽都移除掉setListener(null),既釋放了回調(diào)監(jiān)聽對象占用的內(nèi)存,也避免回調(diào)監(jiān)聽繼續(xù)持有activity引用;對與內(nèi)部類還有一種解決方式,和內(nèi)部Handler相似,定義成static內(nèi)部類,然后把Activity對象的弱引用傳遞進去,這樣也就萬無一失,舉個項目中遇到的實際場景:
private static class RecorderTimeListener implements TimeCallback {
WeakReference<ChatActivity> target;
RecorderTimeListener(ChatActivity activity) {
target = new WeakReference<>(activity);
}
@Override
public void onCountDown(final int time) {
if (target == null || target.get() == null) {
return;
}
final ChatActivity activity = target.get();
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
activity.volumeView.setResetTime(time);
}
});
}
@Override
public void onMaxTime() {
if (target == null || target.get() == null) {
return;
}
final ChatActivity activity = target.get();
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
activity.isMaxTime = true;
activity.stopRecord();
}
});
}
}
private class StartRecorderListener implements StartCallback {
@Override
public boolean onWait() {
cancelRecord();
return true;
}
@Override
public void onStarted() {
if (playerManager.isPlaying()) {
playerManager.stop();
}
recordWaveView.setVisibility(View.VISIBLE);
animation = (AnimationDrawable) recordWaveView.getBackground();
animation.start();
volumeView.showMoveCancelView();
volumeDialog.show();
viewHandler.postDelayed(volumeRunnable, 100);
}
@Override
public void onFailed(int errorCode) {
if (errorCode == RecorderManager.ERROR_START_FAIL) {
showHintDialog(R.string.chat_permission_dialog_title, R.string.chat_permission_dialog_message);
}
}
}
private void startRecord() {
SystemDateUtil.init(this);
LogUtil.i(ChatKey.TAG_INFO, "--------------------------錄音開始--------------------------");
final long startSendTime = SystemDateUtil.getCurrentDate().getTime();
sliceSender = dialogMsgService.createSliceSender(
AccountUtil.getCurrentFamilyChatDialogId(),
AccountUtil.getCurrentImAccountId(), new DialogMsgService.OnSendVoiceMsgListener() {
@Override
public void onSuccess() {
LogUtil.d(TAG, "錄音上傳成功");
sendBigData(sliceSender.getGroupId(), ChatMsgBeh.MsgType.EMOJI,
SystemDateUtil.getCurrentDate().getTime() - startSendTime, SendMsgEvent.CODE_SEND_SUCCESS);
}
@Override
public void onFailure() {
sendBigData(sliceSender.getGroupId(), ChatMsgBeh.MsgType.EMOJI,
SystemDateUtil.getCurrentDate().getTime() - startSendTime, SendMsgEvent.CODE_SEND_FAILURE);
LogUtil.d(TAG, "錄音上傳失敗");
}
});
RecorderManager.getInstance(this).startRecorder(sliceSender, new StartRecorderListener(), new RecorderTimeListener(this));
LogUtil.i(ChatKey.TAG_INFO, "groupId:" + sliceSender.getGroupId());
}
如上StartRecorderListener是內(nèi)部類,RecorderTimeListener是靜態(tài)內(nèi)部類并傳入Activity弱引用,如果把StartRecorderListener的實現(xiàn)改成RecorderTimeListener的實現(xiàn),那么Activity內(nèi)存泄漏就不存在了
動畫導(dǎo)致內(nèi)存泄漏
- 進入Activity界面后如果有一些和控件綁定在一起的屬性動畫在運行,退出的時候要記得cancel掉這些動畫
自定義控件ImageButton中:
public void start(float startAngle, float endAngle) {
setStop(false);
final AnimatorSet as = new AnimatorSet();
final ObjectAnimator oa = ObjectAnimator.ofFloat(this, "progress",
startAngle, endAngle);
oa.setDuration(duration);
oa.setInterpolator(new DecelerateInterpolator(1.1f));
oa.setRepeatCount(count);
// oa.setRepeatMode(ObjectAnimator.INFINITE);
oa.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animator) {
if (stop && as.isRunning()) {
as.cancel();
// oa.removeAllListeners();
} else {
float p = (float) animator.getAnimatedValue();
setProgress(p);
}
}
});
as.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
}
});
as.play(oa);
as.start();
}
public void cancel() {
setStop(true);
}
public void setStop(boolean stop) {
this.stop = stop;
if (stop) {
setProgress(0.0f);
}
}
如上如果不cancel掉屬性動畫就會一直運行并且一直去執(zhí)行控件的onDraw方法,那么ImageButton持有了Activity對象,而屬性動畫ObjectAnimator持有了ImageButton,ObjectAnimator一直在運行,那么Activity對象也就不能被釋放了
- 屬性動畫的對象盡量不要用static修飾,static修飾和,這個對象一旦被創(chuàng)建那么就一直存在了,屬性動畫一旦start之后,那么就一直運行,這時候就算退出activity的時候cancel掉動畫也仍然會持有activity引用,就像下面這個例子:
private static ValueAnimator valueAnimator;
private void startValueAnimator() {
int displayTime2Show = displayTime - 1;
if (displayTime2Show > 1) {
valueAnimator = ValueAnimator.ofInt(displayTime2Show, 1);
valueAnimator.setDuration(displayTime2Show * 1000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
tvStartPageTime.setText(animation.getAnimatedValue().toString());
}
});
valueAnimator.start();
}
}
protected void onPause() {
if (valueAnimator != null && valueAnimator.isRunning()) {
valueAnimator.cancel();
valueAnimator = null;
}
super.onPause();
}
即便是在activity退出后cancel掉動畫,activity依然無法被釋放,為什么?因為valueAnimator是靜態(tài)的,而且添加了動畫屬性改變的監(jiān)聽addUpdateListener,在監(jiān)聽回調(diào)里面有tvStartPageTime(TextView)控件,默認(rèn)持有Activity對象,因此即便Activity退出,動畫cancel掉也無法釋放持有的引用,修改方法有兩種,一種是把valueAnimator的static修飾去掉,另一中國是:
protected void onPause() {
valueAnimator.removeAllUpdateListeners();
if (valueAnimator != null && valueAnimator.isRunning()) {
valueAnimator.cancel();
valueAnimator = null;
}
super.onPause();
}
加一句監(jiān)聽器的移除代碼removeAllUpdateListeners()
傳Context參數(shù)的時候使用Activity對象造成內(nèi)存泄漏
- 在android中常常會用到Context環(huán)境變量,Activity繼承了Context,所以在傳入Context的時候常常直接在Activity中傳入this即Activity本對象,這是比較不好的習(xí)慣,在沒有規(guī)定一定要傳Activity對象的時候盡量采用全局的Context對象,即ApplicationContext來作為參數(shù)傳遞進去,因為ApplicationContext只要app在運行那么它就一直存在,因此即便有一個對象長期引用它,生命周期也不會比ApplicationContext長,所以不會造成ApplicationContext的內(nèi)存泄漏,因為ApplicationContext只要App在運行就不允許被回收
- 在Android程序中要慎用單例,如果單例需要傳Context對象,那么就需要謹(jǐn)慎了因為在單例中如果把Context保存起來,那么這個單例一旦被創(chuàng)建,就一直存在了,如果傳入的是Activity對象,那將一直持有Activity對象引用導(dǎo)致內(nèi)存泄漏,解決版本是傳入ApplicationContext對象,或者在Activity退出的時候銷毀這個單例對象,單例在什么時候時候使用,如果一個對象并不會被頻繁的調(diào)用,那就沒必要用單例,對于可能會被頻繁調(diào)用的對象方法可以采用單例,這樣做可以避免反復(fù)創(chuàng)建對象和gc對象造成的內(nèi)存抖動;對于需要保存的全局變量也可以用單例封裝起來;單例只要創(chuàng)建了就一直有存在引用,所以是不會被gc的
- 使用靜態(tài)變量來保存Activity對象,這是一個非常不好的編碼習(xí)慣,static修飾的代碼片段,變量或者類是在app加載的時候就已經(jīng)加載到內(nèi)存中了,所以和單例有點相似,static變量也會一直持有Activity對象直到APP被殺死或者顯示的把static變量置空
在Android5.0以上的WebView泄漏
- 如果Activity引用了WebView控件來加載一個網(wǎng)頁或者加載一個本地的網(wǎng)頁,在退出activity之后即便你調(diào)用了webView.destroy()方法,也無法釋放webview對于activity持有的引用,原因和解決方案可參考Android5.1的WebView內(nèi)存泄漏,如這篇文章所分析的解決方案確實有效,親測可用!
子線程中不當(dāng)?shù)氖褂肔ooper.prepare()和Looper.loop()方法造成內(nèi)存泄漏
- Looper.loop()是一個無限循環(huán)的方法,它是反復(fù)的去MessageQueue里面去取出Message并分發(fā)給對應(yīng)的Handler去執(zhí)行,如果在子線程中調(diào)用了Looper.prepare()和Looper.loop()方法,Looper.loop()會導(dǎo)致這個線程一直不死,一直堵在這里,因此線程就無法結(jié)束運行,在Looper.prepare()和Looper.loop()之間的所有對象都沒辦法被釋放,解決方案就是在不用的時候及時的把Looper給quit掉
EditText使用setTransformationMethod導(dǎo)致的內(nèi)存泄漏
- 這個問題只有在4.0的android系統(tǒng)上才會存在,在5.0以上的系統(tǒng)已經(jīng)不存在了,應(yīng)該是屬于Android的一個缺陷
[圖片上傳失敗...(image-73f0a7-1511154115320)]
問題的根源應(yīng)該就是這:
loginPasswdEt.setTransformationMethod(PasswordTransformationMethod.getInstance());
loginPasswdEt.setTransformationMethod(HideReturnsTransformationMethod.getInstance());
而PasswordTransformationMethod和HideReturnsTransformationMethod分別都是一個單例:
private static PasswordTransformationMethod sInstance;
private static HideReturnsTransformationMethod sInstance;
PasswordTransformationMethod
public CharSequence getTransformation(CharSequence source, View view) {
if (source instanceof Spannable) {
Spannable sp = (Spannable) source;
/*
* Remove any references to other views that may still be
* attached. This will happen when you flip the screen
* while a password field is showing; there will still
* be references to the old EditText in the text.
*/
ViewReference[] vr = sp.getSpans(0, sp.length(),
ViewReference.class);
for (int i = 0; i < vr.length; i++) {
sp.removeSpan(vr[i]);
}
removeVisibleSpans(sp);
sp.setSpan(new ViewReference(view), 0, 0,
Spannable.SPAN_POINT_POINT);
}
return new PasswordCharSequence(source);
}
private static class ViewReference extends WeakReference<View>
implements NoCopySpan {
public ViewReference(View v) {
super(v);
}
}
上面是5.0系統(tǒng)的源碼,里面已經(jīng)用ViewReference來包裝view設(shè)置到Spannable中了,所以是把view的弱引用傳進去了,因此可以被gc回收,而在4.0android系統(tǒng)上,很可能就不是這么做的,所以4.0系統(tǒng)上面就是View對象被PasswordTransformationMethod和HideReturnsTransformationMethod單例長期持有,而View又持有Activity對象,所以針對4.0系統(tǒng)我們只需要釋放這兩個單例對象即可:
private void releaseMemoryLeak() {
int sdk = Build.VERSION.SDK_INT;
if (sdk >= Build.VERSION_CODES.LOLLIPOP) {
return;
}
try {
Field field1 = PasswordTransformationMethod.class.getDeclaredField("sInstance");
if (field1 != null) {
field1.setAccessible(true);
field1.set(PasswordTransformationMethod.class, null);
}
Field field2 = HideReturnsTransformationMethod.class.getDeclaredField("sInstance");
if (field2 != null) {
field2.setAccessible(true);
field2.set(HideReturnsTransformationMethod.class, null);
}
} catch (NoSuchFieldException e) {
SyncLogUtil.e(e);
} catch (IllegalAccessException e) {
SyncLogUtil.e(e);
}
}
加上上述代碼后驗證發(fā)現(xiàn)內(nèi)存不再泄漏,搞定。
控件的BackGround導(dǎo)致的內(nèi)存泄漏(4.0android系統(tǒng)已經(jīng)解決)
- 有時候為了避免圖片反復(fù)的加載,就把第一次加載后的Bitmap或者Drawable用靜態(tài)變量保存起來,但是要是把這種靜態(tài)修飾的圖片對象設(shè)置成控件的背景,那就呵呵了
private static Drawable sBackground;
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
TextView label = new TextView(this);
label.setText("Leaks are bad");
if (sBackground == null) {
sBackground = getDrawable(R.drawable.large_bitmap);
}
label.setBackgroundDrawable(sBackground);
setContentView(label);
}
因為在View的setBackgroundDrawable方法里面有一句:
public void setBackgroundDrawable(Drawable background) {
......省略很多代碼
background.setCallback(this);
mBackground = background;
}
Drawable對象把View對象作為回調(diào)保存起來了,不過在4.0系統(tǒng)以后引入回調(diào)來保存View對象了,所以已經(jīng)不會造成內(nèi)存泄漏問題了:
public final void setCallback(Callback cb) {
mCallback = new WeakReference<Callback>(cb);
}
這里依然要舉例子出來是想說明不恰當(dāng)?shù)氖褂胹tatic來修飾變量很有可能導(dǎo)致對象無法被回收