Profile
Profile是AS自带的功能,可分析CPU、内存、Energy,我只用过分析内存。Profile跟AS版本有关,不同版本小有差异,我这里用的是Android Studio Dolphin | 2021.3.1 Patch 1。
(图一)
如上图一红框,依次点击“+”,选择设备,选择进程,就可以看到该进程的CPU、Memory、ENERGY信息,点击Memory,看该应用的内存使用情况。(图二)
点击Memory后,即进入上图二,有Memory的标识,右边记录的某时刻内存占用情况。操作应用可以看到线条的起伏,这代表这内存的变化。如图中上升部分是我打开一个页面,下降部分是我关闭页面。上图中还有一个垃圾桶图标,因为java的内存回收不是实时触发的,我不知道什么时候触发,但内存紧张时更容易触发。这里的垃圾桶标识是手动触发,点击后就算内存不紧张,也必定触发内存回收机制。Profile Memory可以用来查看内存泄漏情况。当我们打开某页面,或者使用某功能时,会申请新的内存,对应的就是线条的上升,但是当我们退出该页面或者功能时,线条没有下降到原来的水平(或者跟原来接近的水平),就算手动触发内存回收也没有,这就代表已经发生了内存泄漏。如果我们重复打开该页面或功能,每次打开都申请新内存,线条上升,那没得说,这是严重的内存泄漏了,虽然我是初级段位,但我也知道这种情况瞒不掉的,不要指望测试返回发现,改进解决掉。
在一次dump后,profile会停止更新内存占用情况,表现为内存线条不在动了,点击图二右上角的图标,让Profile继续同步实时内存情况。
如何dump当前内存
选择上图二左边红框的‘Capture heap dump’,点击‘Record’,等一会儿dump成功,就会显示具体的内存使用情况。
(图三)
如图三展示的一次dump成功的内存,是按照类型分配的。我们也可以根据类型搜索,排除我们不想看的数据。用Profile抓内存溢出
在SecondActivity,申请了大内存,用上面的方法dump内存看。如下图四,
ArrayList<Object> list2 = new ArrayList<>();
onCreate(){
File f2 = new File(Environment.getExternalStorageDirectory() + "/Pictures/bing/a1p.png");
Bitmap bmp2 = BitmapFactory.decodeFile(f2.getAbsolutePath());
list2.add(bmp2);
}
(图四)
上图4中的bitmap,根据宽、高,就是我申请的内存。(图五)
图五延时的是,根据搜索关键字过滤的内存对象,可以看到第二个Activity:SecondActivity在内存中的信息。注意SecondActivity$2等,这是SencondActivity的匿名内部类对象。之后退出SecondActivity,手动触发垃圾回收(点击‘垃圾桶’图标),之后重写dump内存,然后新的内存信息中就看不到SecondActivity了,对应的图四中的bitmap的数组中也找不到我们申请的那个图片的bitmap了。
如何制造一个内存溢出
在SecondActivity中发送一个延时消息,延时久一点,注意要在SecondActivity中重新实现Handler的handlerMessage().
new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
Log.i("kobe", "receive msg");
}
}.sendEmptyMessageDelayed(0, 100000);
加入这段代码后,重复上面的步骤,手动回收内存,可以看到线条不会明显下降,dump内存发现,SecondActivity对象依然在内存中,bitmap也依旧在内存中。此时尽管SecondActivity依旧关闭,但SecondActivity对象和他持有的对象都无法回收,这就是发生了内存溢出。PS:上面延时了100秒,100秒后,再次触发内存回收,就能成功回收内存了。dump内存,将看不到SecondActivity对象和bitmap了。
(图六)
上图六就是发送延时消息后,关闭SecondActivity后,手动回收内存后,dump的应用内存,可以看到SecondActivity对象是在内存中的。我看们SecondActivity被持有的情况。MessageQueue持有Message,Message持有Handler,这里target=SecondActivity$1(原来我截图SecondActivity2$1,我写了两个SecondActivity页面,但我实在不想重新截图了)就是SecondActivity中的匿名内部类对象Handler,这个Handler是持有外部类SecondActivity的应用的,这就导致SecondActivity无法被内存回收。一般答这个问题时,都是说非静态内部类隐式持有外部类对象。看这段代码,
public class Test {
int i = 0;
Runnable r = new Runnable() {
@Override
public void run() {
i++;
}
};
static class StaticInner {
void aa() {
}
}
匿名内部类Runnable是不持有外部类对象的,但真的不持有吗,我们编译(javac xxx.java)成class文件看,如下,外部类Test是传入到匿名内部类的构造方法的,即是被持有的,这种就是隐式持有。
class Test$1 implements Runnable {
Test$1(Test var1) {
this.this$0 = var1;
}
public void run() {
}
}
我们再看另一个静态内部类,这种是不持有外部类对象。
class Test$StaticInner {
Test$StaticInner() {
}
}
上面的Handler是匿名内部类,是内部类的一种。Handler被Message持有,Message又被MessageQueue持有,这个MessageQueue是主线程的,肯定是无法被回收的;向下,Handler又隐式持有了外部类Activity,这就导致Activity的内存泄漏。
补充一下内部类的知识
内部类包括成员内部类,局部内部类,匿名内部类,静态内部类,前三者是非静态内部类。形式分别如下。
public class Test {
int i = 0;
/* 匿名内部类 */
Runnable r = new Runnable() {
@Override
public void run() {
i++;
}
};
/* 静态内部类 */
static class StaticInner {
void aa() {
}
}
/* 成员内部类 */
class Inner {
void aa() {
i++;
}
}
public void print() {
/* 局部内部类 */
class LocalInner {
void aa() {
i++;
}
}
/* 匿名内部类 */
Runnable r2 = new Runnable() {
@Override
public void run() {
}
};
}
}