自己编写的一个Activity内存泄漏检测工具
看完LeakCanary的源码之后,有点想自己动手写一个内存泄漏的工具了,正所谓自己动手,丰衣足食。
真正做起来,其实会出现各种问题,最大的问题就是误判,经常由于GC没有及时把销毁的Activity回收而误判未内存泄漏,即便把检测时间延长也是如此:
(这是在两次gc后等待10秒仍然存活的activity,此时仅被虚引用指向)
最后的解决办法就是,尽可能多地调用GC,让误判率降低,这样导致的一个结果时,当频繁的活动销毁时会带来性能下降。
使用
配置
首先在Application中安装:
public class MyApplication extends Application{
@Override
public void onCreate() {
super.onCreate();
new LeakMonitor().install(this);
}
}
当发生内存泄漏时会弹出通知:
创建一个模拟内存泄漏的activity:
定位内存泄漏
打开目录:
/storage/emulated/0/Android/data/com.newsapp/files/Documents/
把文件导出到桌面:
用以下命令转换为标准的hprof文件:
hprof-conv C:\Users\mingC\Desktop\com.newsapp.main.LeakActivity@a4c55ea heapdump.hprof
用MAT工具打开heapdump.hprof,进入Histogram页面:
在regex中输入疑似内存泄漏的类名,回车:
可以看到LeakActivity虽然已经走完了生命周期,但是仍然存在它是一个实例,LeakActivity$1表示内部类Thread,接下来查看该对象的强引用链(exclude掉弱引用、软应用、虚引用):
最终我们定位到了引用着LeakActivity的是内部类Thread:
Java会将内部类表示为”外部类 0”。
工具架构
- LeakInfo 内存泄漏数据实体
- LeakMonitor 主要的类,用来开启检测线程,传递LeakInfo给LeakHandler进行处理
- LeakHandler 处理器接口,用户可以实现这个接口来处理LeakInfo
- DefaultLeakHandler 默认的处理器,弹出通知告知用户发生内存泄漏
工具实现
LeakInfo
定义了泄漏数据的结构:
/**
* 对象内存泄漏的信息
* @author mingC
* @date 2018/8/8
*/
public class LeakInfo {
/**
* 泄漏的对象类名
*/
public String objectName = "null";
/**
* 堆栈信息文件,可能为null
*/
public File heapdumpFile = new File("NoneFile");
@Override
public String toString() {
return "对象名:" + objectName + ";heapdump:" + (heapdumpFile == null ? "null" : heapdumpFile.getAbsolutePath());
}
}
LeakHandler
public interface LeakHandler {
/**
* 处理内存泄漏,此方法在子线程运行
* @param leakInfo 泄漏信息
*/
void handleLeak(Context context, @NonNull LeakInfo leakInfo);
}
默认的实现类,适配了8.0以上通知栏行为变更:
/**
* 默认的泄漏处理器
* @author mingC
* @date 2018/8/8
*/
public class DefaultLeakHandler implements LeakHandler{
private static final String CHANNEL_ID = "Leak";
private LeakInfo mLeakInfo;
private Context mContext;
private static AtomicInteger id = new AtomicInteger(0); //发布通知的id,避免后来的通知覆盖上面之前的通知
private String mContentText;
private String mTitleText;
@Override
public void handleLeak(Context context, LeakInfo leakInfo) {
//在通知栏告知用户
mContext = context;
mLeakInfo = leakInfo;
mTitleText = "疑似发生内存泄漏";
mContentText = "泄漏对象:" + mLeakInfo.objectName;
Log.d("DefaultLeakHandler", "mLeakInfo:" + mLeakInfo);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
showNotificationV26();
} else {
showNotification();
}
}
//Android 8.0以下
private void showNotification() {
Uri sound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
NotificationManager manager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
Notification notification = new NotificationCompat.Builder(mContext)
.setWhen(System.currentTimeMillis())
.setContentTitle(mTitleText)
.setContentText(mContentText)
.setSound(sound)
.setVibrate(new long[]{1000,2000,1000})
.setSmallIcon(R.mipmap.ic_launcher_round)
.build();
manager.notify(id.getAndAdd(1), notification);
}
//Android 8.0以上
@RequiresApi(api = Build.VERSION_CODES.O)
private void showNotificationV26() {
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "Leak",
NotificationManagerCompat.IMPORTANCE_HIGH);
channel.enableVibration(true);
NotificationManager manager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
manager.createNotificationChannel(channel);
Notification notification = new Notification.Builder(mContext, CHANNEL_ID)
.setWhen(System.currentTimeMillis())
.setContentTitle(mTitleText)
.setContentText(mContentText)
.setAutoCancel(false)
.setSmallIcon(R.mipmap.ic_launcher_round)
.build();
manager.notify(id.getAndAdd(1), notification);
}
}
LeakMonitor
主要流程:
- 通过Application注册活动生命周期的回调
- 当活动执行onDestory时,添加一个IdelHandler到主线程Looper,当主线程闲置时提交检测任务DetectorRunnable到mExecutor执行器中执行
- 创建待检测activity的虚引用mRef,循环若干秒一次gc,如果超过一段时间虚引用还没有添加到关联队列mQueue中,则怀疑发生内存泄漏
- 执行heapdump,交给LeakHandler处理
/**
* 1、内存泄漏检测工具:只能检测Activity的内存泄漏
* 2、GC的问题:GC是不保证一定立即调用,不保证清理效果,不保证什么时候会调用的
* 3、活动销毁之后,并不一定马上就没有强引用了,所以最好在主线程空闲的时候才检测
* 4、被检测出内存泄漏的对象还需要手动分析hprof文件,查看gc root上是否还有强引用链
* @author mingC
* @date 2018/8/8
*/
public class LeakMonitor implements Application.ActivityLifecycleCallbacks{
public static final String TAG = "LeakMonitor";
private Application mContext;
private File mDumpDictory;
private final ReferenceQueue<Object> mQueue;
private final Handler mainHandler;
//检测内存泄漏的线程执行器(单线程执行器)
private final ExecutorService mExecutor;
//内存泄漏的处理器
private final LeakHandler mLeakHandler;
public LeakMonitor() {
//使用默认的处理器
this(new DefaultLeakHandler());
}
public LeakMonitor(LeakHandler leakHandler) {
this.mLeakHandler = leakHandler;
mQueue = new ReferenceQueue<>();
mExecutor = Executors.newSingleThreadExecutor();
mainHandler = new Handler(Looper.getMainLooper());
}
/**
* 安装使用
* @param application 需要检测活动内存泄漏的应用
*/
public void install(final Application application) {
mContext = application;
mDumpDictory = application.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);
//注册生命周期监听
application.registerActivityLifecycleCallbacks(this);
}
/**
* 取消内存泄漏监测
*/
public void uninstall() {
mContext.unregisterActivityLifecycleCallbacks(this);
mExecutor.shutdownNow();
}
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}
@Override
public void onActivityStarted(Activity activity) {}
@Override
public void onActivityResumed(Activity activity) {}
@Override
public void onActivityPaused(Activity activity) {}
@Override
public void onActivityStopped(Activity activity) {}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
@Override
public void onActivityDestroyed(final Activity activity) {
//在主线程空闲时才执行
mainHandler.post(new Runnable() {
@Override
public void run() {
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
Log.d(TAG, "主线程idle");
mExecutor.execute(new DetectorRunnable(activity));
return false;
}
});
}
});
}
/**
* 检测弱引用指向的对象是否已经是线程不可达(GC回收了)
*/
class DetectorRunnable implements Runnable{
//使用虚引用
PhantomReference<Object> mRef;
//对象类名+地址
String mObjectName;
public DetectorRunnable(Object object) {
mObjectName = object.toString();
mRef = new PhantomReference<>(object, mQueue);
}
@Override
public void run() {
//等待gc的时间
long waitTime = 10000;
Log.d(TAG, Thread.currentThread().getId() + "开始内存泄漏检测");
//第一次判断
if (mRef.isEnqueued()) {
Log.d(TAG, mObjectName + "第一次判断:已被回收");
return;
}
//这里不等待的话会出现正常活动不回收的情况
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//两次gc,增加不使用对象被回收的概率
Runtime.getRuntime().gc();
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(waitTime / 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
Runtime.getRuntime().gc();
//第二次判断
if (mRef.isEnqueued()) {
Log.d(TAG, mObjectName + ",第" + (i+1) + "次判断:已被回收" );
return;
}
}
//发生内存泄漏
Log.d(TAG, mObjectName + "发生内存泄漏");
try {
//dump出的文件以objectName命名
String file = new File(mDumpDictory, mObjectName).getAbsolutePath();
Debug.dumpHprofData(file);
LeakInfo info = new LeakInfo();
info.objectName = mObjectName;
info.heapdumpFile = new File(file);
//回调处理
mLeakHandler.handleLeak(mContext, info);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
正常销毁的activity在第二次gc后被回收:
08-09 14:15:30.725 3231-3350/com.newsapp I/zygote: Explicit concurrent copying GC freed 880(163KB) AllocSpace objects, 0(0B) LOS objects, 61% free, 950KB/2MB, paused 932us total 12.050ms
08-09 14:15:32.734 3231-3350/com.newsapp I/zygote: Explicit concurrent copying GC freed 1229(116KB) AllocSpace objects, 0(0B) LOS objects, 63% free, 866KB/2MB, paused 592us total 5.558ms
08-09 14:15:34.736 3231-3350/com.newsapp D/LeakMonitor: com.newsapp.main.UnexportedActivity@624e33d,第2次判断:已被回收