浅谈android内存优化

680 阅读8分钟

写在开头

android优化系列:
浅谈androidUI布局优化

作为一个android开发来说,内存管理真的是一个烦的不能烦的问题,隔三差五的就听到产品和测试咆哮,app怎么这么卡,哎,内存泄露了。喂喂喂,怎么闪退了,我的天,内存溢出了!当然,我说的比较夸张,不过不可否认,的确有这种问题。

这里写图片描述

本文就自己学到的知识,从三个个方面总结。

  • 内存管理和一些基础概念知识
  • 内存泄露乃至oom优化解决
  • 内存优化的一些良好习惯

内存管理和一些基础概念知识

android内存的管理

内存管理本质其实就是内存的管理和回收。程序运行时的内存分配有三种策略,分别是静态的,栈式的,和堆式的,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、堆区和栈区。

静态存储区(方法区):内存在程序编译的时候就已经分配好,这块内存在程序整个运行期间都存在。它主要存放静态数据、全局static数据和常量。

栈区:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

堆区:亦称动态内存分配。程序在运行的时候用malloc或new申请任意大小的内存,程序员自己负责在适当的时候用free或delete释放内存(Java则依赖垃圾回收器)。如果未合理管理内存,比如不及时回收无效空间就会造成内存泄露,严重的话可能导致使用内存超过系统分配内存,即内存溢出OOM,导致程序卡顿甚至直接退出。

大白话总结一波:

  • 局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。因为它们属于方法中的变量,生命周期随方法而结束。
  • 成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体),因为它们属于类,类对象终究是要被new出来使用的。
  • 我们所说的内存泄露,只针对堆内存,他们存放的就是引用指向的对象实体。

android内存回收机制(gc)

进程优先级:Foreground进程、Visible进程、Service进程、Background进程、Empty进程; 如果用户按Home键返回桌面,那么该app成为Background进程;如果按Back返回,则成为Empty进程

ActivityManagerService直接管理所有进程的内存资源分配。所有进程要申请或释放内存都需要通过ActivityManagerService对象。

垃圾回收不定期执行。当内存不够时就会遍历heap空间,把垃圾对象删除。

堆内存越大,则GC的时间更长

您可以通过这行代码得到你引用的内存大小

    ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);  
    int normalHeap = activityManager.getMemoryClass();  

内存泄露及内存溢出(oom)

先说一下本宝宝使用的内存检测工具,LeakCanary。(Square出品,必属精品。没有使用的就赶紧入坑吧)

我理解的内存泄露其实就是在一个对象要回收时,但是发现这个对象被别人所持有不能释放,此时就会造成内存泄露。当系统所能给你的内存不足以应对你此次要申请的内存时就会内存溢出。注意:OOM并不代表内存不足,只要申请的heap超过Dalvik VM HeapGrowthLimit时,即使内存充足也会溢出。

注意:在开发中我们要做的其实就是尽可能的减少内存泄露,以此来减少内存溢出的发生。

常见的内存泄露问题及解决

  • 单例(主要原因还是因为一般情况下单例都是全局的,有时候会引用一些实际生命周期比较短的变量,导致其无法释放)
  • 非静态内部类创建静态实例造成的内存泄漏
  • Handler内存泄露
  • 匿名内部类(匿名内部类会引用外部类,导致无法释放,比如各种回调)
  • 资源使用完未关闭(BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap)
    等等!

单例

// 使用了单例模式 
//单例的Context 使用Application的Context,单例的生命周期和应用的一样长,这样基本可以防止单例引起来的内存泄露内存泄漏。
public class AppManager { 
    private static AppManager instance;  
    private Context context; private AppManager(Context context) { 
        this.context = context; 
     }  
    public static AppManager getInstance(Context context) {  
         if (instance != null) {  
             instance = new AppManager(context); 
          }  
          return instance;  
          }  
      }

非静态内部类创建静态实例造成的内存泄漏

public class MainActivity extends AppCompatActivity { 
    private static TestResource mResource = null;  

    @Override protected void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        setContentView(R.layout.activity_main);  
        if(mResource == null){  
            mResource = new TestResource(); 
         }  
         //...  
         }  
class TestResource { //... } }

非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,从而导致Activity的内存资源不能被正常回收。

解决方法:将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,就使用Application的Context。

Handler造成的内存泄漏

比如:

    public class MainActivity extends AppCompatActivity {  
        private final Handler handler = new Handler() {   
            @Override public void handleMessage(Message msg) {  
            //  
            ... } 
             };  
             @Override protected void onCreate(Bundle savedInstanceState) {  
                   super.onCreate(savedInstanceState);  
                   setContentView(R.layout.activity_main); 
                    new Thread(new Runnable() {  
                    @Override public void run() { 
                     // ...  
                     handler.sendEmptyMessage(0x123);  
                  } 
            });  
        }

当MainActivity结束时,未处理的消息持有handler的引用,而handler又持有它所属的外部类也就是MainActivity的引用。这条引用关系会一直保持直到消息得到处理,这样阻止了MainActivity被垃圾回收器回收,从而造成了内存泄漏。

解决方法:

将Handler类独立出来或者使用静态内部类,这样便可以避免内存泄漏。

private static class MyHandler extends Handler {
        @Override
        public void handleMessage(Message message) {
            super.handleMessage(message);
        }
    }

private static class MyRunnable implements Runnable {
        @Override
        public void run() {

            while (true) ;
        }
    }

资源未关闭

1)比如在Activity中register了一个BraodcastReceiver,但在Activity结束后没有unregister该BraodcastReceiver。
2)资源性对象比如Cursor,Stream、File文件等往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于 java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。
3)对于资源性对象在不使用的时候,应该调用它的close()函数将其关闭掉,然后再设置为null。在我们的程序退出时一定要确保我们的资源性对象已经关闭。
4)Bitmap对象不在使用时调用recycle()释放内存。2.3以后的bitmap应该是不需要手动recycle了,内存已经在java层了。

内存优化的一些良好习惯

避免在 Android 里面使用 Enum

Enum 是 Java 中包含固定常量的数据类型,当需要知道预先定制的几个值,这几个值表示一些数据类,我们都可以使用 Enum。我们一般用 Enum 做一些编译时检查,以避免传入不合法的参数。
    但 Enum 的每个对象都是 Object,在 Android 官网上就早已明确指出应该在 Android 开发中避免使用 Enum,因为与静态常量想必,它对内存的占用是要大很多的。
    因此在实际开发中,我更加倾向于接口变量,因为接口会自动把成员变量设置为 static 和 final 的,这一点可以防止某些情况下错误地添加新的常量,这也使得代码看起来更加简单和清晰。

减少 Bitmap 对象的内存占用

Bitmap是一个极容易消耗内存的大胖子,减小创建出来的Bitmap的内存占用是很重要的,通常来说有下面2个措施:
inSampleSize:缩放比例,在把图片载入内存之前,我们需要先计算出一个合适的缩放比例,避免不必要的大图载入。
decode format:解码格式,选择 ARGB_8888 / RBG_565 / ARGB_4444 / ALPHA_8,存在很大差异。

对于图片的处理压缩,我写过一个这个博客。有兴趣的可以看看。
blog.csdn.net/say_from_we…

尽量的采用 StringBuilder

是尽量的采用 StringBuilder / StringBuffer 来替换我们频繁的字符串拼接。

避免使用浮点数

通常的经验是,在Android设备中,浮点数会比整型慢两倍

写在最后

我只是对自己接触的进行了下基本的总结,有错误还请指出。也希望大家推荐更更多的学习资料出来,共同学习。