摘要:Android中内存泄漏的的分析。
Android的内存基础知识
Android系统在安装、加载一个apk文件时,会在系统内存中划出一部分作为该apk的运行内存。
这个运行内存的大小,目前随着Android设备的进化,也已经适量增大。从早期默认的90M左右到现在200M、300M。当你在设定属性android:largeheap = "true"时,内存大小基本还会翻倍。如果想要得到具体可用内存,可在代码中获取具体数值:
Runtime runtimeMemory=Runtime.getRuntime();
long maxMemory=runtimeMemory.maxMemory()/(1024*1024);
在apk可用内存增大的情况下,你仍然需要注意合理的分配内存,使用内存。虽然在大内存的情况下,可能将一些内存使用的隐患隐藏起来,没有造成apk崩溃等,但如果apk中存在内存使用不善的情况,如内存泄漏,仍会影响apk的运行效率,严重的情况下,apk会发生内存溢出,导致崩溃。
如果将apk可用内存比喻成一只水桶,apk运行时占用的内存比喻成桶里的水。那么现在这个桶变大变高了,正常情况下桶里的水是不会满溢的,Android与Java一样,会隐式的进行GC垃圾对象回收(你可以显示调用GC方法回收)。但当apk中存在内存泄漏的情况下,每一次泄漏导致无法GC回收,桶里的水位就会慢慢增长,直至满溢,造成内存溢出。
内存溢出是日常代码编写中,因不易发现,会导致很多线上问题的产生。代码编写的规范与良好习惯,是避免这个问题的主要办法。
Android中常见的内存泄漏情景
内存泄漏的产生过程:apk运行时,操作系统为apk中的各种变量以及对象实例分配内存。假设程序运行后,产生了两个对象:长生命周期对象A,短生命周期对象B,其中A持有了B的引用。在B生命周期结束后,理论上系统GC应该要释放B占用的内存,但因为引用被A持有,导致内存无法释放,这就造成了内存泄漏。
Android中我们常见的泄漏场景,列出代表性的如下:
1、静态变量持有短生命周期的对象引用
示例如下:object是一个静态变量,在onCreate中将当前activity的引用赋值给了object。因为static变量赋值后,会将引用保存在整个app的方法区。生命周期是与整个app是一致的,会一直持有这个引用,导致当前activity即使onDestroy后,引用也无法被GC释放,造成内存泄漏。
public class StaticViewTestActivity extends AppCompatActivity {
private static Object object;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leak_test);
object = this;
}
}
解法:(1)尽量不要将生命周期短暂的对象赋值给static变量,谨慎使用 (2)如果业务有这个需求,以上述代码为例,在使用完object后,在onDestroy请将object置为null。
谨慎使用静态变量也要分清何时使用,不能总是担心引发问题而不用。
静态相关延伸-1
静态方法是否会造成内存泄漏呢?看下面这段代码:
public class StaticMethodTestActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leak_test);
test(this);
}
private static void test(Activity activity){
Object object = activity;
}
}
先给出结论:这是不会造成内存泄漏的。原因在于java、android中,调用方法时(无论是静态方法还是非静态方法),JVM的虚机栈都会为这个方法创建一个方法栈帧。你可以理解为:一个线程中,有多个方法时,会产生多个栈帧,存于这个线程的栈帧队列中。当方法被调用完毕后,该栈帧被弹出销毁。方法中的局部变量等,也都将被释放,因此不会存在内存泄漏情况。
静态相关延伸-2
当一个类中,同时存在静态变量、静态方法、普通方法时,关系如下:
如图中所述:一个类中静态变量、静态方法的生命周期与该类的实例化对象是没有关系的。
public static void main(String[] args){
A a = new A();
a.d();
A.c();
A.b = 1;
}
执行以上这段代码后,对象a的实例将会存在内存中的堆中,静态方法c与静态变量b将会存在内存的方法区。
静态相关延伸-3
还有哪些常见场景是静态变量持有短生命周期的引用,会引发泄漏?
(1)单例模式,持有短生命周期的context
public class Test {
private static Test INSTANCE;
private Context context;
private Test(Context context){
this.context = context;
}
public static Test getInstance(Context context) {
if (INSTANCE == null) {
synchronized (Test.class) {
if (INSTANCE == null) {
INSTANCE = new Test(context);
}
}
}
return INSTANCE;
}
}
单例模式下getInstance中如果传入的Context对象引用是activity的引用,因为单例模式内部INSTANCE是静态对象,没有赋值为null前,都会长存于内存中,context作为该对象的属性,也不会释放,引发内存泄漏。
解法:如果单例中必要传入Context对象,使用Application的Context对象,因为这个是与整个app生命周期同步的。
(2) 静态集合类引发的内存泄漏
public class Test {
private static List<Object> ls;
void operationList(){
ls = new ArrayList<>();
for(int i=0;i<100;i++){
Object o = new Object();
ls.add(o);
o = null;
}
}
}
以上代码虽然在循环中,每次在集合ls添加Object对象o后,都将o对象置为null,但集合ls仍持有o的引用,不会释放。
Object o = new Object(); 这个语句细分为三个阶段。
Object o:声明引用变量o,并在内存中分配空间;
new Object():创建Object对象,并在内存的堆中分配空间存放它;
= :等号,是个指向,将引用变量o指向创建好的Object对象。
以此分析,上面的代码在循环中,只是把引用变量本身的指向置为null,在这之前,已经把引用存入到了静态集合中,所以不会释放。
解法:不使用这种写法。
2、非静态内部类持有短生命周期的对象引用
内部类概述:
内部类包含静态内部类和非静态内部类。
非静态内部类包含匿名内部类以及内部类(有类名)。
Java与Android中不存在顶层的静态类,所有的静态类都是指静态内部类。
内部类有以下几种场景:
public class Test {
//局部变量
private int val = 1;
//一个成员内部类
class Inner{
public void testInInner(){
System.out.println("这是一个成员内部类的方法");
System.out.println("可以直接引用外部类Test的变量,val=" + val);
System.out.println("可以直接引用外部类Test的变量,该写法是在内部类中有同名变量时使用,val=" + Test.this.val);
}
}
public void test1(){
//匿名内部类
//此处有匿名内部类, new Runnable
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("这是一个匿名内部类的方法");
System.out.println("可以直接引用外部类Test的变量,val=" + val);
System.out.println("可以直接引用外部类Test的变量,该写法是在内部类中有同名变量时使用,val=" + Test.this.val);
}
}).start();
}
public void test2(){
class MethodInner{
public void testInMethodInner(){
System.out.println("这是一个方法内部类的方法");
System.out.println("可以直接引用外部类Test的变量,val=" + val);
System.out.println("可以直接引用外部类Test的变量,该写法是在内部类中有同名变量时使用,val=" + Test.this.val);
}
}
}
static class StaticInner{
public void testInStatic(){
System.out.println("这是一个成员内部类的方法");
System.out.println("与外部类无关,不可以直接引用外部类Test的非静态变量val");
}
}
}
(1)非静态内部类为何容易造成内存泄漏
主要原因在于:非静态内部类隐性的持有外部类的引用,当内部类中进行耗时等操作时,外部类的引用会被一直持有,无法被释放。
将上面的Test类做编译操作(javac),能看到同级目录下生成了5个字节码文件:Test$1.class 、Test$1MethodInner.class 、Test$Inner.class、Test$StaticInner.class、Test.class;
以Test$1.class为例,字节码文件中默认生成的构造函数中,参数是外部类的对象引用。除静态内部类外,其他的内部类也是类似的构造,因此说非静态内部类隐性的持有外部类的引用。
class Test$Inner {
Test$Inner(Test var1) {
this.this$0 = var1;
}
public void testInInner() {
System.out.println("这是一个成员内部类的方法");
System.out.println("可以直接引用外部类Test的变量,val=" + Test.access$000(this.this$0));
System.out.println("可以直接引用外部类Test的变量,该写法是在内部类中有同名变量时使用,val=" + Test.access$000(this.this$0));
}
}
其中access$000是编译器默认给外部类生成的方法。
(2)内部类的泄漏,有哪些场景
- 如上面举例的new Runnable(){...},如果再run中有耗时操作,在耗时操作未结束前,就退出页面,因持有外部类引用并不释放,就会造成内存泄漏。
public class ThreadTestActivity extends AppCompatActivity {
private static final String TAG = "ThreadTestActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leak_test);
Log.d(TAG, "onCreate-this:"+this.toString());
testThread();
}
private void testThread(){
new Thread(new Runnable() {
@Override
public void run() {
Log.d(TAG, "testThread-this:"+this.toString());
SystemClock.sleep(10*1000);
}
}).start();
}
}
解法:改写方法,如必要这么写,定义静态内部类实现Runnable接口。
- 如定时器 TimeTask
new Timer().schedule(new TimerTask() {
@Override
public void run() {
while (true);
}
},3 * 1000);
解法:在相对位置,对定时器做cancel,或者改为静态内部类实现。
- 如Handler。下方例子:匿名内部类new Handler(){...}持有外部类Test的引用。handlerOp方法执行后,主线程的消息队列在60s内都会持有handler的引用。handler又持有了外部类Test的引用,导致Test对象无法回收,造成内存泄漏。
public class Test {
Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
public void handlerOp(){
handler.postDelayed(new Runnable() {
@Override
public void run() {
// doSomeThing
}
},60 * 1000);
}
}
解法:静态内部类实现,或者handler改为弱引用,或者在相对位置对handler做remove,语句(Handler.removeCallbacksAndMessages(null);)
- 其他场景,如AsyncTask等,类似处理。
3、资源对象未释放
资源对象未释放,也是出现内存泄漏的一个常见场景。
(1)如文件未关闭,导致分配给该文件引用的缓冲未及时释放。如果多次未关闭,文件句柄太多没有被关闭(Could not read input channel file descriptors from parcel)
(2)如数据库操作的游标 Cursor未关闭,频繁操作后会导致(android.database.CursorWindowAllocationException: Cursor window allocation of 2048 kb failed)
如下示例,频繁操作而out不做处理,可能就会导致句柄过多,内存泄漏直至溢出。
public class ResourceTestActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leak_test);
test();
}
private void test(){
String filename = "app.txt";
File file = new File(getExternalCacheDir(),filename);
try{
file.createNewFile();
FileOutputStream out = new FileOutputStream(file);
} catch (FileNotFoundException e){
} catch (IOException e){
}
}
}
解法:针对以上等情况,在正常流程或异常流程中,对资源关闭做好处理,如finally中做好关闭操作。
总结的说,内存泄漏本质就是本该被回收的内存,没有被回收,多关注生命周期。
内存泄漏不经意间就会被你写出,时时轻拂拭,莫使惹尘埃。