Android中的内存管理机制
分配机制
Android为每个进程分配内存的时候,采用了弹性的分配方式,也就是刚开始并不会一下分配很多内存给每个进程,而是给每一个进程分配一个“够用”的量。这个量是根据每一个设备实际的物理内存大小来决定的。随着应用的运行,可能会发现当前的内存可能不够使用了,这时候Android又会为每个进程分配一些额外的内存大小。但是这些额外的大小并不是随意的,也是有限度的,系统不可能为每一个App分配无限大小的内存。
Android系统的宗旨是最大限度的让更多的进程存活在内存中,因为这样的话,下一次用户再启动应用,不需要重新创建进程,只需要恢复已有的进程就可以了,减少了应用的启动时间,提高了用户体验。
回收机制
Android对内存的使用方式是“尽最大限度的使用”,这一点继承了Linux的优点。Android会在内存中保存尽可能多的数据,即使有些进程不再使用了,但是它的数据还被存储在内存中,所以Android现在不推荐显式的“退出”应用。因为这样,当用户下次再启动应用的时候,只需要恢复当前进程就可以了,不需要重新创建进程,这样就可以减少应用的启动时间。只有当Android系统发现内存不够使用,需要回收内存的时候,Android系统就会需要杀死其他进程,来回收足够的内存。但是Android也不是随便杀死一个进程,比如说一个正在与用户交互的进程,这种后果是可怕的。所以Android会有限清理那些已经不再使用的进程,以保证最小的副作用。
Android杀死进程有两个参考条件:
进程优先级:
Android为每一个进程分配了优先级的概念,优先级越低的进程,被杀死的概率就更大。Android中总共有5个进程优先级。具体含义这里不再给出。
- 前台进程:正常不会被杀死
- 可见进程:正常不会被杀死
- 服务进程:正常不会被杀死
- 后台进程:存放于一个LRU缓存列表中,先杀死处于列表尾部的进程
- 空进程:正常情况下,为了平衡系统整体性能,Android不保存这些进程
回收收益:
当Android系统开始杀死LRU缓存中的进程时,系统会判断每个进程杀死后带来的回收收益。因为Android总是倾向于杀死一个能回收更多内存的进程,从而可以杀死更少的进程,来获取更多的内存。杀死的进程越少,对用户体验的影响就越小。
官方推荐的App内存使用方式
- 当Service完成任务后,尽量停止它。因为有Service组件的进程,优先级最低也是服务进程,这会影响到系统的内存回收。IntentService可以很好地完成这个任务。
- 在UI不可见的时候,释放掉一些只有UI使用的资源。系统会根据onTrimMemory()回调方法的TRIM_MEMORY_UI_HIDDEN等级的事件,来通知App UI已经隐藏了。这和onStop()方法还是有很大区别的,因为onStop()方法只是当一个Activity完全不可见的时候就会调用,比如说用户打开了我们程序中的另一个Activity。因此,我们可以在onStop()方法中去释放一些Activity相关的资源,比如说取消网络连接或者注销广播接收器等,但是UI相关的资源等onTrimMemory(TRIM_MEMORY_UI_HIDDEN)这个回调之后才去释放,这样可以保证如果用户只是从我们程序的一个Activity回到了另外一个Activity,界面相关的资源都不需要重新加载,从而提升响应速度。
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
switch (level) {
case TRIM_MEMORY_UI_HIDDEN:
// 进行资源释放操作
break;
}
}
- 在系统内存紧张的时候,尽可能多的释放掉一些非重要资源。系统会根据onTrimMemory()回调方法来通知内存紧张的状态,App应该根据不同的内存紧张等级,来合理的释放资源,以保证系统能够回收更多内存。当系统回收到足够多的内存时,就不用杀死进程了。
- 检查自己最大可用的内存大小。这对一些缓存框架很有用,因为正常情况下,缓存框架的缓存池大小应当指定为最大内存的百分比,这样才能更好地适配更多的设备。通过getMemoryClass()和getLargeMemoryClass()来获取可用内存大小的信息。
- 避免滥用Bitmap导致的内存浪费。
根据当前设备的分辨率来压缩Bitmap是一个不错的选择,在使用完Bitmap后,记得要使用recycle()来释放掉Bitmap。使用软引用或者弱引用来引用一个Bitmap,使用LRU缓存来对Bitmap进行缓存。 - 使用针对内存优化过的数据容器。针对移动设备内存有限的问题,Android提供了一套针对内存优化过的数据容器,来替代JDK原生提供的数据容器。但是缺点就是,时间复杂度被提高了。比如SparseArray、SparseBooleanArray、LongSparseArray、
- 意识到内存的过度消耗。Enum类型占用的内存是常量的两倍多,所以避免使用enum,直接使用常量。
每一个Java的类(包括匿名内部类)都需要500Byte的代码。每一个类的实例都有12-16 Byte的额外内存消耗。注意类似于HashMap这种,内部还需要生成Class的数据容器,这会消耗更多内存。 - 抽象代码也会带来更多的内存消耗。如果你的“抽象”设计实际上并没有带来多大好处,那么就不要使用它。
- 使用nano protobufs 来序列化数据。Google设计的一个语言和平台中立打的序列化协议,比XML更快、更小、更简单。
- 避免使用依赖注入的框架。依赖注入的框架需要开启额外的服务,来扫描App中代码的Annotation,所以需要额外的系统资源。
- 使用ZIP对齐的APK。对APK做Zip对齐,会压缩其内部的资源,运行时会占用更少的内存。
- 合理使用多进程。
Android内存泄漏分析及优化
内存泄漏的根本原因
如上图所示,GC会选择一些它了解还存活的对象作为内存遍历的根节点(GC Roots),比方说thread stack中的变量,JNI中的全局变量,zygote中的对象(class loader加载)等,然后开始对heap进行遍历。到最后,部分没有直接或者间接引用到GC Roots的就是需要回收的垃圾,会被GC回收掉。如下图蓝色部分。
内存泄漏指的是进程中某些对象(垃圾对象)已经没有使用价值了,但是它们却可以直接或间接地引用到gc roots导致无法被GC回收。无用的对象占据着内存空间,使得实际可使用内存变小,形象地说法就是内存泄漏了。下面分析一些可能导致内存泄漏的情景。
Android中常见的内存泄漏原因
1.使用static变量引起的内存泄漏
因为static变量的生命周期是在类加载时开始 类卸载时结束,也就是说static变量是在程序进程死亡时才释放,如果在static变量中引用了Activity那么这个Activity由于被引用,便会随static变量的生命周期一样,一直无法被释放,造成内存泄漏。
一般解决办法:
想要避免context相关的内存泄漏,需要注意以下几点:
- 不要对activity的context长期引用(一个activity的引用的生存周期应该和activity的生命周期相同)
- 如果可以的话,尽量使用关于application的context来替代和activity相关的context
- 如果一个acitivity的非静态内部类的生命周期不受控制,那么避免使用它;正确的方法是使用一个静态的内部类,并且对它的外部类有一WeakReference,就像在ViewRootImpl中内部类W所做的那样,使用弱引用private final WeakReference<ViewRootImpl> mViewAncestor;。
下面的代码存在内存泄漏的问题,非静态内部类的静态实例导致内存泄漏。
/**
mDemo会获得并一直持有MemoryLeakActivity的引用。当MemoryLeakActivity销毁后重建,因为mDemo持有引用,无法被GC回收的,进程中会存在2个MemoryLeakActivity实例。所以,对于lauchMode不是singleInstance的Activity, 应该避免在activity里面实例化其非静态内部类的静态实例。
*/
public class MemoryLeakActivity extends AppCompatActivity{
private TextView view;
private static final String TAG = "MemoryLeakActivity";
static Demo mDemo;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
view = new TextView(MemoryLeakActivity.this);
view.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
view.setText("启动一个持有本对象的线程");
view.setTextSize(40);
view.setTextColor(Color.parseColor("#0000ff"));
setContentView(view);
mDemo = new Demo();
mDemo.run();
}
class Demo{
void run(){
Log.i(TAG, "run: ");
}
}
}
解决方法:将Demo改成静态内部类
因为普通的内部类对象隐含地保存了一个引用,指向创建它的外围类对象。然而,当内部类是static的时,就不是这样了。嵌套类意味着: 1. 嵌套类的对象,并不需要其外围类的对象。 2. 不能从嵌套类的对象中访问非静态的外围类对象。
2.线程引起的内存泄漏
下面的代码存在内存泄漏的问题,启动线程的匿名内部类会持有MemoryLeakActivity.this的引用。如果线程还没有结束,Activity已经销毁那就会造成内存泄漏。
public class MemoryLeakActivity extends AppCompatActivity{
private TextView view;
private static final String TAG = "MemoryLeakActivity";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
view = new TextView(MemoryLeakActivity.this);
view.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
view.setText("启动一个持有本对象的线程");
view.setTextSize(40);
view.setTextColor(Color.parseColor("#0000ff"));
setContentView(view);
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(8000000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
new Thread(runnable).start();
}
}
解决办法:
1.合理安排线程执行的时间,控制线程在Activity结束前结束。
2.将内部类改为静态内部类,并使用弱引用WeakReference来保存Activity实例 因为弱引用 只要GC发现了 就会回收它 ,因此可尽快回收。
将匿名内部类改成静态类,避免了Activity context的内存泄漏问题
/**
* 示例通过将线程类声明为私有的静态内部类避免了 Activity context 的内存泄漏问题,但
* 在配置发生改变后,线程仍然会执行。原因在于,DVM 虚拟机持有所有运行线程的引用,无论
* 这些线程是否被回收,都与 Activity 的生命周期无关。运行中的线程只会继续运行,直到
* Android 系统将整个应用进程杀死
*/
public class MemoryLeakActivity extends AppCompatActivity{
private TextView view;
private static final String TAG = "MemoryLeakActivity";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
view = new TextView(MemoryLeakActivity.this);
view.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
view.setText("启动一个持有本对象的线程");
view.setTextSize(40);
view.setTextColor(Color.parseColor("#0000ff"));
setContentView(view);
new MyThread().start();
}
private static class MyThread extends Thread{
@Override
public void run() {
try {
Thread.sleep(8000000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
在Activity生命周期的onDestory中结束线程运行
/**
* 除了我们需要实现销毁逻辑以保证线程不会发生内存泄漏。在退出当前
* Activity 前使用 onDestroy() 方法结束你的运行中线程。
*/
public class MemoryLeakActivity extends AppCompatActivity{
private TextView view;
private static final String TAG = "MemoryLeakActivity";
private static boolean mRunnale = false;
private MyThread mThread;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
view = new TextView(MemoryLeakActivity.this);
view.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
view.setText("启动一个持有本对象的线程");
view.setTextSize(40);
view.setTextColor(Color.parseColor("#0000ff"));
setContentView(view);
new MyThread().start();
}
@Override
protected void onDestroy() {
super.onDestroy();
mThread.closeThread();
}
private static class MyThread extends Thread{
@Override
public void run() {
mRunnale = true;
while (true){
//TODO
Log.i(TAG, "run: do something");
}
}
public void closeThread(){
mRunnale = false;
}
}
}
3.Handler的使用造成的内存泄漏
由于在Handler的使用中,handler会发送message对象到 MessageQueue中 然后 Looper会轮询MessageQueue 然后取出Message执行,但是如果一个Message长时间没被取出执行,那么由于 Message中有 Handler的引用,而 Handler 一般来说也是内部类对象,Message引用 Handler ,Handler引用 Activity 这样 使得 Activity无法回收。或者说Handler在Activity退出时依然还有消息需要处理,那么这个Activity就不会被回收。
解决办法:
依旧使用 静态内部类+弱引用的方式 可解决
例如下面的代码
public class MemoryLeakActivity extends AppCompatActivity{
private TextView view;
private static final String TAG = "MemoryLeakActivity";
private MyHandler mHandler;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
view = new TextView(MemoryLeakActivity.this);
view.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
view.setText("启动一个持有本对象的线程");
view.setTextSize(40);
view.setTextColor(Color.parseColor("#0000ff"));
setContentView(view);
mHandler = new MyHandler(this);
mHandler.sendEmptyMessage(0);
}
@Override
protected void onDestroy() {
super.onDestroy();
//第三步,在Activity退出的时候移除回调
mHandler.removeCallbacksAndMessages(null);
}
//第一步,将Handler改成静态内部类。
static class MyHandler extends Handler{
//第二步,将需要引用Activity的地方,改成弱引用。
private WeakReference<MemoryLeakActivity> mActivityRef;
public MyHandler(MemoryLeakActivity activity){
mActivityRef = new WeakReference<MemoryLeakActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
MemoryLeakActivity mla = mActivityRef == null ? null : mActivityRef.get();
if(mla == null || mla.isFinishing()){
return;
}
//TODO
mla.view.setText("do something");
}
}
}
4.资源未被及时关闭造成的内存泄漏
比如一些Cursor 没有及时close 会保存有Activity的引用,导致内存泄漏
解决办法:
在onDestory方法中及时 close即可
5.BitMap占用过多内存
bitmap的解析需要占用内存,但是内存只提供8M的空间给BitMap,如果图片过多,并且没有及时 recycle bitmap 那么就会造成内存溢出。
解决办法:
及时recycle 压缩图片之后加载图片
其中还有一些关于 集合对象没移除,注册的对象没反注册,代码压力的问题也可能产生内存泄漏,但是使用上述的几种解决办法一般都是可以解决的。