欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 美景 > Android 内存原理详解以及优化(二)

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

2024/10/24 9:20:16 来源:https://blog.csdn.net/u012553125/article/details/140000198  浏览:    关键词: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() {@Overridepublic 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 {@Overridepublic 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 {@Overridepublic 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 {@Overridepublic 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,且签名一致),从而减少资源的占用。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com