Android 内存原理详解以及优化(二)

上一篇讲了内存原理,如果还没看可以先看上一篇:Android 内存原理详解以及优化(一)
这一篇我总结一下我们经常遇到的内存优化问题:
1.内存抖动
自定义view的ondraw是会被频繁调用的,那在这个方法里面就不能频繁的new object 对象频繁的创建和回收就会内存抖动,凡是频繁调用的方法,都禁止类似这样的操作。
2.内存泄露
内存泄露的本质是某个对象已经不需要再用了,但是它却没有被系统所回收,一直在内存中占用着空间,而导致它无法被回收的原因大多是由于它被一个生命周期更长的对象所引用,用什么引用?引用链(在上一篇,gc内存的垃圾回收机制 使用的是GCRoot 可达性分析有解释)
举个例子,如果一个 Activity 被一个单例对象所引用,那么当退出这个 Activity 时,由于单例的对象依然存在(单例对象的生命周期跟整个 App 的生命周期一致),而单例对象又持有 Activity 的引用,这就导致了此 Activity 无法被回收,从而造成内存泄漏。

知道了内存泄漏的根本原因,再分析为什么会出现内存泄漏就很简单了,下面就针对一些常见的内存泄漏进行分析。
单例造成的内存泄漏

刚才已经分析过了,假设有一个单例是这样的
public class SingleTon {

private static SingleTon singleTon;

private Context context;

private SingleTon(Context context) {
    this.context = context;
}

public static SingleTon getInstance(Context context) {
    if (singleTon == null) {
        synchronized (SingleTon.class) {
            if (singleTon == null) {
                singleTon = new SingleTon(context);
            }
        }
    }
    return singleTon;
}

}

这是单例模式饿汉式的双重校验锁的写法,这里的 singleTon 持有 Context 对象,如果 Activity 中调用 getInstance 方法并传入 this 时,singleTon 就持有了此 Activity 的引用,当退出 Activity 时,Activity 就无法回收,造成内存泄漏,所以应该修改它的构造方法
private SingleTon(Context context) {
this.context = context.getApplicationContext();
}

通过 getApplicationContext 来获取 Application 的 Context,让它被单例持有,这样退出 Activity 时,Activity 对象就能正常被回收了,而 Application 的 Context 的生命周期和单例的生命周期是一致的,所有再整个 App 运行过程中都不会造成内存泄漏。
非静态内部类造成的内存泄漏

我们知道,非静态内部类会持有外部类的引用,如果这个非静态的内部类的生命周期比它的外部类的生命周期长,那么当销毁外部类的时候,它无法被回收,就会造成内存泄漏。
外部类中持有非静态内部类的静态对象

假设 Activity 的代码是这样的
public class MainActivity extends AppCompatActivity {

private static Test test;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    if (test == null) {
        test = new Test();
    }

}

private class Test {

}

}

这个其实和单例的原理是一样的,由于静态对象 test 的生命周期和整个应用的生命周期一致,而非静态内部类 Test 持有外部类 MainActivity 的引用,导致 MainActivity 退出的时候不能被回收,从而造成内存泄漏,解决的方法也很简单,把 test 改成非静态,这样 test 的生命周期和 MainActivity 是一样的了,就避免了内存泄漏。或者也可以把 Test 改成静态内部类,让 test 不持有 MainActivity 的引用,不过一般没有这种操作。

Handler 或 Runnable 作为非静态内部类

handler 和 runnable 都有定时器的功能,当它们作为非静态内部类的时候,同样会持有外部类的引用,如果它们的内部有延迟操作,在延迟操作还没有发生的时候,销毁了外部类,那么外部类对象无法回收,从而造成内存泄漏,假设 Activity 的代码如下
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            
        }
    }, 10 * 1000);
}

}

上面的代码中,Handler 和 Runnable 作为匿名内部类,都会持有 MainActivity 的引用,而它们内部有一个 10 秒钟的定时器,如果在打开 MainActivity 的 10 秒内关闭了 MainActivity,那么由于 Handler 和 Runnable 的生命周期比 MainActivity 长,会导致 MainActivity 无法被回收,从而造成内存泄漏。

那么应该如何避免内存泄漏呢?这里的一般套路就是把 Handler 和 Runnable 定义为静态内部类,这样它们就不再持有 MainActivity 的引用了,从而避免了内存泄漏
public class MainActivity extends AppCompatActivity {

private Handler handler;

private Runnable runnable;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    handler = new TestHandler();
    runnable = new TestRunnable();
    handler.postDelayed(runnable, 10 * 1000);
}

private static class TestHandler extends Handler {

}

private static class TestRunnable implements Runnable {
    @Override
    public void run() {
        Log.d(TAG, "run: ");
    }
}

private static final String TAG = "MainActivity";

}

还要在 onDestory 调用 handler 的 removeCallbacks 方法来移除 Message,因为如果在退出 Activity 关闭后,正好触发执行 run 方法,就也会造成message持有activity的引用,内存泄露
@Override
protected void onDestroy() {
super.onDestroy();
handler.removeCallbacks(runnable);
}

还有一种特殊情况,如果 Handler 或者 Runnable 中持有 Context 对象,那么即使使用静态内部类,还是会发生内存泄漏
public class MainActivity extends AppCompatActivity {

private Handler handler;

private Runnable runnable;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    handler = new TestHandler(this);
    runnable = new TestRunnable();
    handler.postDelayed(runnable, 10 * 1000);
}

private static class TestHandler extends Handler {
    private Context context;
    private TestHandler(Context context) {
        this.context = context;
    }
}

private static class TestRunnable implements Runnable {
    @Override
    public void run() {
        Log.d(TAG, "run: ");
    }
}

private static final String TAG = "MainActivity";

}

上面的代码,使用 leakcanary 工具会发现依然会发生内存泄漏,而且造成内存泄漏的原因和之前用非静态内部类是一样的,那么为什么会出现这种情况呢?

这是由于在 Handler 中持有 Context 对象,而这个 Context 对象是通过 TestHandler 的构造方法传入的,它是一个 MainActivity 对象,也就是说,虽然 TestHandler 作为静态内部类不会持有外部类 MainActivity 的引用,但是我们在调用它的构造方法时,自己传入了 MainActivity 的对象,从而 handler 对象持有了 MainActivity 的引用,handler 的生命周期比 MainActivity 的生命周期长,因此会造成内存泄漏,这种情况可以使用弱引用的方式来引用 Context 来避免内存泄漏,代码如下
public class MainActivity extends AppCompatActivity {

private Handler handler;

private Runnable runnable;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    handler = new TestHandler(new WeakReference<Context>(this));
    runnable = new TestRunnable();
    handler.postDelayed(runnable, 10 * 1000);
}

private static class TestHandler extends Handler {
    private Context context;
    private TestHandler(WeakReference<Context> weakContext) {
        context = weakContext.get();
    }
}

private static class TestRunnable implements Runnable {
    @Override
    public void run() {
        Log.d(TAG, "run: ");
    }
}

private static final String TAG = "MainActivity";

}

其他的内存泄漏情况

还有一些其他的会导致内存泄漏的情况,比如 BraodcastReceiver 未取消注册,InputStream 未关闭等,这类内存泄漏非常简单,只要在平时写代码时多多注意即可避免。
处理内存泄露当然不能没有 leakCanary这个工具,它的原理,我也分三篇来分析了,敢兴趣可以看看:leakcanary源码详解

3.内存溢出
3.1 对于大图片加载的时候,会内存溢出。这是我开发中遇到的,如果泛指的话,可以归结于为打对象分配内存 加载大图片bitmap内存溢出
3.2 内存泄露逐渐积累也会内存溢出。
上文中说了内存泄露的处理,在此不再赘述。
3.3 jni native 内存地址分配的,这个是听说,待验证。印象中是有的。

4.扩大内存的手段
4.1一个应用如果使用了largeHeap,会请求系统为Dalvik虚拟机分配更大的内存空间。使用起来也很方便,只需在manifest文件application节点加入android:largeHeap=“true” 默认128M ,设置后能分配的多大,取决于系统限制和设备硬件情况。注意不用用这个方法去解决oom,你走偏了,走正道。
4.2 应用开多进程,这样一个进程分配128M,多个能翻倍使用。
在 AndroidManifest.xml 中配置 android:process:

第一种:如 android:process = “:remote”,以 : 开始,后面的字符串是可以随意指定的。如果包名是 com.cah.androidtest,所以实际进程名是 
com.cah.androidtest:remote。这种设置形式表示该进程为当前应用的私有进程,其他应用的组件不可以和它跑在同一进程中

第二种:如 android:process = “com.cah.androidtest.remote”,以小写字母开头,表示运行在一个以这个名字命名的全局进程中,其他应用的组件
可以和它跑在同一进程中(使用 SharedUID,且签名一致),从而减少资源的占用。
  • 13
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值