主目录见:Android高级进阶知识(这是总目录索引)
[written by 无心追求]
Activity内部类泄漏
- Activity如果存在内部类,无论是匿名内部类,或者是声明的内部类,都有可能造成Activity内存泄漏,因为内部类默认是直接持有这个activity的引用,如果内部类的生命周期比activity的生命周期要长,那么在activity销毁的时候内部类仍然存在并且持有activity的引用,那么activity自然无法被gc,造成内存泄漏
Activity内部Handler
class MyHandler extends Handler {
MyHandler() {
}
@Override
public void handleMessage(Message msg) {
// to do your job
}
}
MyHandler myHandler = new MyHandler();
如上,在Activity内部如果声明一个这样的Handler,那么myHandler就默认持有Activity引用,假设Activity退出了,但是可能这时候才有myHandler的任务post,那么Activity是无法被回收的,可以采用以下方式解决:
static class MyHandler extends Handler {
WeakReference mActivityReference;
MyHandler(Activity activity) {
mActivityReference = new WeakReference(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是一个内部静态类,静态类在java虚拟机加载的时候就是独立加载到内存中的,不会依赖于任何其他类,而且这里面是把activity以弱引用的方式传到MyHandler中,即便是静态MyHandler类对象一直存在,但是由于它持有的是activity弱引用,在gc回收的时候activity对象是可以被回收的,另外注意一点,对于Handler的使用如果有sendEmptyMessageDelayed()来延迟任务执行的话最好在Activity的onDestroy里面把Handler的任务都移除(removeCallbacks(null)),activity在退出后,就是应该在onDestroy方法里面把一些任务取消掉,做一些清理的操作
Activity内部线程
- 在Activity里面有时候为了实现异步操作会单独开一个线程来执行任务,或者是异步的网络请求也是单独开线程来执行的,那么就会存在一个问题,如果内部线程的生命周期比Activity的生命周期要长,那么内部线程任然默认持有Activity的引用,导致Activity对象无法被回收,但是当这个线程执行完了之后,Activity对象就能被成功的回收了,这会造成一个崩溃风险,可能在线程里面有调用到一些Activity的内部对象,但是在Activity退出后这些对象有可能有些已经被回收了,就变成null了,这时候要是不进行null的判断就会报空指针异常,如果这个线程是一直跑的,那就会造成Activity对象一直不会被回收了,因此,在activity退出后一定要做相关的清理操作,中断线程,取消网络请求等等
Activity内部类回调监听
- 在编码中常常会定义各种接口回调,类似有点击时间监听OnClickListener,这些回调监听有时候就定义在Activity内部,或者直接用Activity对象去实现这个接口,到时候设置监听的时候直接调用setListener(innerListener)或者setListener(this),innerListener是Activity内部定义的,this就是Activity对象,那么问题来了,回调监听并不一定马上返回,只有在触发条件满足的时候才会回调,这个时间是无法确定的,因此在Activity退出的时候应该显示的把回调监听都移除掉setListener(null),既释放了回调监听对象占用的内存,也避免回调监听继续持有activity引用;对与内部类还有一种解决方式,和内部Handler相似,定义成static内部类,然后把Activity对象的弱引用传递进去,这样也就万无一失,举个项目中遇到的实际场景:
private static class RecorderTimeListener implements TimeCallback {
WeakReference 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是内部类,RecorderTimeListener是静态内部类并传入Activity弱引用,如果把StartRecorderListener的实现改成RecorderTimeListener的实现,那么Activity内存泄漏就不存在了
动画导致内存泄漏
- 进入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掉属性动画就会一直运行并且一直去执行控件的onDraw方法,那么ImageButton持有了Activity对象,而属性动画ObjectAnimator持有了ImageButton,ObjectAnimator一直在运行,那么Activity对象也就不能被释放了
- 属性动画的对象尽量不要用static修饰,static修饰和,这个对象一旦被创建那么就一直存在了,属性动画一旦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是静态的,而且添加了动画属性改变的监听addUpdateListener,在监听回调里面有tvStartPageTime(TextView)控件,默认持有Activity对象,因此即便Activity退出,动画cancel掉也无法释放持有的引用,修改方法有两种,一种是把valueAnimator的static修饰去掉,另一中国是:
protected void onPause() {
valueAnimator.removeAllUpdateListeners();
if (valueAnimator != null && valueAnimator.isRunning()) {
valueAnimator.cancel();
valueAnimator = null;
}
super.onPause();
}
加一句监听器的移除代码removeAllUpdateListeners()
传Context参数的时候使用Activity对象造成内存泄漏
- 在android中常常会用到Context环境变量,Activity继承了Context,所以在传入Context的时候常常直接在Activity中传入this即Activity本对象,这是比较不好的习惯,在没有规定一定要传Activity对象的时候尽量采用全局的Context对象,即ApplicationContext来作为参数传递进去,因为ApplicationContext只要app在运行那么它就一直存在,因此即便有一个对象长期引用它,生命周期也不会比ApplicationContext长,所以不会造成ApplicationContext的内存泄漏,因为ApplicationContext只要App在运行就不允许被回收
- 在Android程序中要慎用单例,如果单例需要传Context对象,那么就需要谨慎了因为在单例中如果把Context保存起来,那么这个单例一旦被创建,就一直存在了,如果传入的是Activity对象,那将一直持有Activity对象引用导致内存泄漏,解决版本是传入ApplicationContext对象,或者在Activity退出的时候销毁这个单例对象,单例在什么时候时候使用,如果一个对象并不会被频繁的调用,那就没必要用单例,对于可能会被频繁调用的对象方法可以采用单例,这样做可以避免反复创建对象和gc对象造成的内存抖动;对于需要保存的全局变量也可以用单例封装起来;单例只要创建了就一直有存在引用,所以是不会被gc的
- 使用静态变量来保存Activity对象,这是一个非常不好的编码习惯,static修饰的代码片段,变量或者类是在app加载的时候就已经加载到内存中了,所以和单例有点相似,static变量也会一直持有Activity对象直到APP被杀死或者显示的把static变量置空
在Android5.0以上的WebView泄漏
- 如果Activity引用了WebView控件来加载一个网页或者加载一个本地的网页,在退出activity之后即便你调用了webView.destroy()方法,也无法释放webview对于activity持有的引用,原因和解决方案可参考Android5.1的WebView内存泄漏,如这篇文章所分析的解决方案确实有效,亲测可用!
子线程中不当的使用Looper.prepare()和Looper.loop()方法造成内存泄漏
- Looper.loop()是一个无限循环的方法,它是反复的去MessageQueue里面去取出Message并分发给对应的Handler去执行,如果在子线程中调用了Looper.prepare()和Looper.loop()方法,Looper.loop()会导致这个线程一直不死,一直堵在这里,因此线程就无法结束运行,在Looper.prepare()和Looper.loop()之间的所有对象都没办法被释放,解决方案就是在不用的时候及时的把Looper给quit掉
EditText使用setTransformationMethod导致的内存泄漏
- 这个问题只有在4.0的android系统上才会存在,在5.0以上的系统已经不存在了,应该是属于Android的一个缺陷
[图片上传失败...(image-2fd60f-1511154075108)]
问题的根源应该就是这:
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
implements NoCopySpan {
public ViewReference(View v) {
super(v);
}
}
上面是5.0系统的源码,里面已经用ViewReference来包装view设置到Spannable中了,所以是把view的弱引用传进去了,因此可以被gc回收,而在4.0android系统上,很可能就不是这么做的,所以4.0系统上面就是View对象被PasswordTransformationMethod和HideReturnsTransformationMethod单例长期持有,而View又持有Activity对象,所以针对4.0系统我们只需要释放这两个单例对象即可:
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);
}
}
加上上述代码后验证发现内存不再泄漏,搞定。
控件的BackGround导致的内存泄漏(4.0android系统已经解决)
- 有时候为了避免图片反复的加载,就把第一次加载后的Bitmap或者Drawable用静态变量保存起来,但是要是把这种静态修饰的图片对象设置成控件的背景,那就呵呵了
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对象作为回调保存起来了,不过在4.0系统以后引入回调来保存View对象了,所以已经不会造成内存泄漏问题了:
public final void setCallback(Callback cb) {
mCallback = new WeakReference(cb);
}
这里依然要举例子出来是想说明不恰当的使用static来修饰变量很有可能导致对象无法被回收[written by 无心追求]
使用android shell命令查看内存使用情况
使用adb shell dumpsys meminfo pkgname或者直接使用AndroidStudio里面的memory usage功能然后就会出现如下信息:
Applications Memory Usage (kB):
Uptime: 14237237 Realtime: 23790474
** MEMINFO in pid 8071 [com.xtc.watch] **
Pss Private Private Swapped Heap Heap Heap
Total Dirty Clean Dirty Size Alloc Free
------ ------ ------ ------ ------ ------ ------
Native Heap 0 0 0 0 21924 8558 6405
Dalvik Heap 122472 122372 0 15672 143308 65400 77908
Dalvik Other 10361 10076 164 224
Stack 440 440 0 8
Other dev 4 0 4 0
.so mmap 6441 3452 2636 2048
.apk mmap 611 0 340 0
.ttf mmap 538 0 504 0
.dex mmap 8407 1640 2940 40
Other mmap 80 4 0 0
Unknown 10940 10936 0 148
TOTAL 160294 148920 6588 18140 165232 73958 84313
Objects
Views: 288 ViewRootImpl: 2
AppContexts: 11 Activities: 2
Assets: 5 AssetManagers: 5
Local Binders: 30 Proxy Binders: 38
Death Recipients: 3
OpenSSL Sockets: 1
SQL
MEMORY_USED: 138
PAGECACHE_OVERFLOW: 24 MALLOC_SIZE: 62
DATABASES
pgsz dbsz Lookaside(b) cache Dbname
4 20 306 25/47/12 /data/data/com.xtc.watch/databases/upload.db
- Native Heap是native层的内存堆栈,Dalvik Heap是java层的内存堆栈,如果这二者加起来的内存占用超过了应用最大内存限制就会报OOM异常,剩下的.so mmap是 C 库代码占用的内存,.jar mmap是Java 文件代码占用的内存 ,.apk mmap是apk代码占用的内存,.dex mmap是Dex 文件代码占用的内存
- Objects中的Activities表示当前内存中的activity对象的个数,启动一个activity就会生成一个activity对象,当退出activity的时候,activity对象就会被释放,所以反复的进出一个activity界面然后查看Activities的个数有没有保持不变,如果增加了,那么就说明这个activity对象没有被释放,也就是说可能存在内存泄漏,但是具体哪里泄漏了并不知道
DDMS查看内存使用情况
eclipse中有一个ddms工具,可以查看线程信息(Threads),内存使用情况(VM Heap),内存分配跟踪(Allocation Tracker),CUP使用情况(Sysinfo CUP load),内存使用饼状图(Sysinfo Memory usage),这里我们暂时用到VM Heap,选择要查看的app进程,点击左上角的show heap updates,选择VM Heap并点击Cause GC按钮,然后就出现下图:
[图片上传失败...(image-ad6bc7-1511154091691)]
观察data object的Total Size选项,这个是app的创建的java对象做占用的内存大小,Count是总内存的对象的个数,反复的进出一个activity,看data object的Total Size有没有明显的增加,正常情况下进入一个activity的时候会明显增加,退出一个activity会有明显的回落,总体是维持在一个比较稳定的水平如果反复进出activity,Total Size不断上升,那么可能就存在内存泄漏了,需要具体排查
MAT分析内存泄漏,用AndroidStudio的Monitors的Memory
- 多点击几下Initiate GC来回收一下可被释放的java对象,因为java的GC是定期有条件执行的,当内存中只存在很少的无用对象,这时候可能并不会触发GC,所以手动触发GC来保证开始检测内存的时候内存都是最干净的
- 点击Dump Java Heap,然后过一会儿就会出现一份数据分析文件,这时候的这份数据文件是刚开始的程序对象内存占用情况,接下去就针对一个activity反复操作进出等等各种反复操作,觉得差不多了,这时候就再次疯狂点击Initiate GC回收一下可是放的对象,点击Dump Java Heap,这时候生成的数据分析文件就是经过你疯狂操作后的内存占用情况了
[图片上传失败...(image-3a960f-1511154091691)]
生成的上述两个文件右键,点击Export to Standar .hprof导出到一个自己指定的目录文件夹 - 去官网上面下载MAT来打开这两个文件开始内存分析
[图片上传失败...(image-495cd9-1511154091691)]
上图中Problem Suspect部分是代表可能存在内存泄漏的地方,Remainder表示正常的部分,再继续往下看
[图片上传失败...(image-32efc5-1511154091691)]
[图片上传失败...(image-13261-1511154091691)]
上面就是对可能存在内存泄漏部分的代码的一个详细的信息,可以看到有些byte数组占用了大量的内存,keywords也是byte[],第二张图的DexCache可能占用的较多的内存,再点击[图片上传失败...(image-36bdaf-1511154091691)]红色部分就会出现一些对象的信息列表:
[图片上传失败...(image-765e0-1511154091691)]
可以输入正则表达式来筛选你想要的类,包名下所有的类,Objects是对象的个数,Shallow Heap是当前对象所占用的内存大小,不包括对象内包含的对象的大小,Retained Heap表示当前对象包括对象内的子对象一共占用的内存大小,所以Retained Heap会比Shallow Heap大得多,对于一些我们已知的对象在内存不泄露的情况下,该对象的个数是确定的,所以可以通过分析Objects的个数来确定对象是否存在内存泄漏,例如同一个Activity对象在反复进出该Activity5次之后Objects的值为5,那就有问题了,说明同一个Activity创建了5个对象,正常情况下应该是退出Activity后,对象会被回收的,所以Objects的值应该是0才对,而有些对象的Objects不为0并不代表一定存在内存泄漏,例如ConnectionService是一个常驻的Service,那么它是不会被GC的,而ConnectionService里面的对象可不会被回收,所以这些对象的Objects值不为0其实就是正常的了,至于Shallow Heap和Retained Heap,我觉得可以用来分析一些对象的内存占用,Shallow Heap一般情况下不会很大,当你发现Retained Heap非常大的时候,那就说明该对象里面的对象可能占用了大量的内存,可能存在问题;在用Objects找到可能存在内存泄漏的对象后,右键List Objects,然后有两个选项:with outgoing references(表示的是当前对象,引用了外部引用)和with incoming references(表示的是当前查看的对象,被外部引用),一般当前对象泄漏了就是对象还被外部对象持有引用,无法被释放,所以我们选择查看with incoming references[图片上传失败...(image-ef298b-1511154091691)],点击with incoming references后
[图片上传失败...(image-7f488-1511154091691)]
可以看到TaskExecutor对象被外部的两个对象所引用到了,并且可以看到引用的路径,右键TaskExecutor,选择Path to GC Roots或者或者Merge Shortest Paths to GC Roots选项,再选择exclude all phantom/weak/soft .ect references排除所有的弱引用,软引用对象寻找看有没有存在GC Roots,如果没有那就说明这个对象不存在内存泄漏,最终是会被GC回收,如果存在GC Roots
[图片上传失败...(image-70d856-1511154091691)]
在继续查看GC Roots的对象是什么,其实就是被哪一个对象所引用了,上图可以查看到被外部的sendTaskExecutor对象所引用了而sendTaskExecutor是在ConnectionService这个常驻Service中的,所以理论上是不应该被回收的,所以这里不算是内存泄漏,假如sendTaskExecutor是一个Activity里面的字段,而此时Activity已经退出了,那么这时候就属于内存泄漏了,因为Activity退出后,Activity资源包括里面的对象应该是被回收掉的,那就找到对应的代码去具体分析可能造成内存泄漏的问题所在了,这里明确一点,存在GC Roots的不一定就一定存在内存泄漏,GC是不会回收GC Root或者被GC Root所引用的对象的,java对象内存泄漏其实就是对象在不用的时候仍然被其他对象持有引用导致GC无法回收 - 比较反复操作前的内存信息和反复操作后的内存信息分析前后有哪些不同[图片上传失败...(image-2670ab-1511154091691)],点击红色部分后会出现下图
[图片上传失败...(image-54a276-1511154091691)]
可以看到对比后的Objects和Shallow Heap信息,看Objects表示前后两种内存信息对于同一个对象是否有个数上的增加,因为如果对象能被正常回收,那么开始操作前是0,操作后经过GC应该也是0,如果个数增加了,那就表示这个对象可能存在内存泄漏了,拖动到底部可看到总的比较情况
[图片上传失败...(image-2af6e7-1511154091691)]
上图可知,操作前的app的Objects总数比操作后的Objects少了1个,同时可以看到是HttpDns这个对象的问题,于是再切换到操作后的histogram去按照上述步骤查找GC Roots,再具体分析内存泄漏的问题 - MAT当中还有一个dominator tree视图,具体就不复述了,可以参考这篇大神文章MAT内存分析