美文网首页
Android内存管理机制和内存泄漏分析及优化

Android内存管理机制和内存泄漏分析及优化

作者: 帝王鲨kingcp | 来源:发表于2017-09-04 19:15 被阅读0次

    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内存泄漏分析及优化

    内存泄漏的根本原因
    内存泄漏分析1.png

    如上图所示,GC会选择一些它了解还存活的对象作为内存遍历的根节点(GC Roots),比方说thread stack中的变量,JNI中的全局变量,zygote中的对象(class loader加载)等,然后开始对heap进行遍历。到最后,部分没有直接或者间接引用到GC Roots的就是需要回收的垃圾,会被GC回收掉。如下图蓝色部分。

    内存泄漏分析2.png

    内存泄漏指的是进程中某些对象(垃圾对象)已经没有使用价值了,但是它们却可以直接或间接地引用到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 压缩图片之后加载图片

    其中还有一些关于 集合对象没移除,注册的对象没反注册,代码压力的问题也可能产生内存泄漏,但是使用上述的几种解决办法一般都是可以解决的。

    相关文章

      网友评论

          本文标题:Android内存管理机制和内存泄漏分析及优化

          本文链接:https://www.haomeiwen.com/subject/vpfddxtx.html