添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

Android开发过程中总会在意想不到的的地方出现一些OOM的错误,类似于下面的代码:

1
2
3
4
5
Caused by: java.lang.OutOfMemoryError
at java.io.ByteArrayOutputStream.expand(ByteArrayOutputStream.java:91)
at java.io.ByteArrayOutputStream.write(ByteArrayOutputStream.java:201)
at com.fanli.android.base.general.util.FileUtil.InputStreamToString(FileUtil.java:400)
at com.fanli.android.base.general.util.FileUtil.readStringFromInputStream(FileUtil.java:210)

1. 一些简单的背景知识

  • 进程的地址空间
    在32位操作系统中,进程的地址空间为0到4GB,示意图如下:

    这里主要说明一下Stack和Heap:
    Stack空间:(进栈和出栈)由操作系统控制,其中主要存储 函数地址、函数参数、局部变量 等等。
    所以Stack空间不需要很大,一般为几MB大小。
    Heap空间:使用由程序员控制,程序员可以使用malloc、new、free、delete等函数调用来操作这片地址空间。
    Heap为程序完成各种复杂任务提供内存空间,所以空间比较大,一般为几百MB到几GB。
    正是因为Heap空间由程序员管理,所以容易内存出现使用不当而导致严重问题。

  • Android的进程一般分为Native进程和Java进程.

  • native进程:采用C/C++实现,不包含dalvik实例的linux进程**,/system/bin/目录下面的程序文件运行后都是以native进程形式存在的。比如/system/bin/surfaceflinger、/system/bin/rild、procrank等就是native进程。
  • java进程:实例化了dalvik虚拟机实例的linux进程,进程的入口main函数为java函数。 dalvik虚拟机实例的宿主进程是fork()系统调用创建的linux进程,所以每一个Android上的java进程实际上就是一个linux进程,只是进程中多了一个dalvik虚拟机实例。
  • 进程中的对应的Heap空间内存(堆内存)也同样分为两种:

  • C/C++申请的内存空间在Native Heap中
  • Java申请的内存空间在dalvik heap中
    其中Android系统针对每一个具体Java进程的dalvik heap都会设定一个具体的阀值, 当Java进程申请的空间超出这个阀值时就会抛出OOM异常 ,也就是说 OOM仅与Java进程相关,且即使在RAM充足的情况下,也是可能发生OOM的
    具体的dalvik heap内存阀值与Android机型ROM在编译时的设置有关,具体的大小可以执行下面的代码查看:
    1
    adb shell getprop | grep dalvik.vm.heap*
  • Android对应的GC操作
    当虚拟机内存使用接近上限时,Android系统将自动触发Java的GC(Garbage Collection)操作,Android中GC的具体机制可以参考 这篇文章

    Android不是用GC会自动回收资源么,为什么app的那些不用的资源不回收呢?
    Android的GC是按照特定的算法回收程序不用的内存资源,避免app的内存申请越积越多,但GC一般回收的资源是那些无主的对象内存或者软饮用的资源,或者更软引用的引用资源。但是如果发生内存泄露,例如某个Activity一直被静态变量引用无法释放。当重新进入该Activity时又会new出一个新的实例。对应的内存实例越来越多,泄露的内存又无法通过gc进行释放,最后内存耗尽导致OOM。

    2. 常用的避免内存泄露的套路
    由于绝大多数发生OOM的情况都是由内存泄露引起的,针对内存泄露常用的一些方案,总结如下:
    常见的内存泄露:
    a. 非静态内部类内存泄露
    在Activity中创建非静态内部类,非静态内部类会持有Activity的隐式引用,若内部类生命周期长于Activity,会导致Activity实例无法被回收。(屏幕旋转后会重新创建Activity实例,如果内部类持有引用,将会导致旋转前的实例无法被回收)。
    解决方案:如果一定要使用内部类,就改用static内部类,在内部类中通过WeakReference的方式引用外界资源。

    正确的代码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    static class ImageDownloadTask extends AsyncTask<String, Void, Bitmap> {

    private String url;
    private WeakReference<PhotoAdapter> photoAdapter;

    public ImageDownloadTask(PhotoAdapter photoAdapter) {
    this.photoAdapter = new WeakReference<PhotoAdapter>(photoAdapter);
    }

    @Override
    protected Bitmap doInBackground(String... params) {
    //在后台开始下载图片
    url = params[0];
    Bitmap bitmap = photoAdapter.get().loadBitmap(url);
    if (bitmap != null) {
    //把下载好的图片放入LruCache中
    String key = MD5Tools.decodeString(url);
    photoAdapter.get().put(key, bitmap);
    }
    return bitmap;
    }

    @Override
    protected void onPostExecute(Bitmap bitmap) {
    super.onPostExecute(bitmap);
    //把下载好的图片显示出来
    ImageView mImageView = (ImageView) photoAdapter.get().mGridView.get().findViewWithTag(MD5Tools.decodeString(url));
    if (mImageView != null && bitmap != null) {
    mImageView.setImageBitmap(bitmap);
    photoAdapter.get().mDownloadTaskList.remove(this);//把下载好的任务移除
    }
    }
    }

    b. 匿名内部类内存泄漏
    跟非静态内部类一样,匿名内部类也会持有外部类的隐式引用,比较常见的情况有,耗时Handler,耗时Thread,都会造成内存泄漏,解决方式也是static+WeakReference,下面给出正确写法。
    Handler的正确写法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    private static class MyHandler extends Handler {

    private final WeakReference<Context> context;

    private MyHandler(Context context) {
    this.context = new WeakReference<Context>(context);
    }

    @Override
    public void handleMessage(Message msg) {
    switch (msg.what) {

    }
    }
    }

    private final MyHandler mHandler = new MyHandler(this);

    private static final Runnable sRunnable = new Runnable() {
    @Override
    public void run() {

    }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_home);
    // 发送一个10分钟后执行的一个消息
    mHandler.postDelayed(sRunnable, 600000);
    }

    Thread的正确写法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    private static class MyThread extends Thread {

    @Override
    public void run() {
    while (true) {
    // TODO 耗时任务
    }
    }
    }

    new MyThread().start();

    c. Context持有导致内存泄漏
    Activity Context被传递到其他实例中,这可能导致自身被引用而发生泄漏。
    对于大部分非必须使用Activity Context的情况(创建Dialog的Context必须是Activity Context),应该使用Application Context。

    d. 记得注销监听器和广播
    注册监听器的时候会add Listener,不要忘记在不需要的时候remove掉Listener

    e. 谨慎使用单例中不合理的持有
    单例中的对象生命周期与应用一致,注意不要在单例中进行不必要的外界引用持有。如果一定要引用外部变量,需要在外部变量生命周期结束的时候接触引用(赋为null)

    f. 一定要记得关闭无用连接
    在onDestory中关闭Cursor,I/O,数据库,网络的连接用完记得关闭

    g. 集合中的内存泄漏
    我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。所以要在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。

    3. 常用的内存优化套路
    当APP使用的内存较大时为了避免因为达到使用上限的阀值,也可以进行这方面的一些优化:

  • 使用轻量的数据结构
    使用ArrayMap/SparseArray来代替HashMap,ArrayMap/SparseArray是专门为移动设备设计的高效的数据结构。
    HashMap的缺点:
    (1)就算没有数据,也需要分配默认16个元素的数组(2)一旦数据量达到Hashmap限定容量的75%,就将按两倍扩容
    ArrayMap/SparseArray针对HashMap的主要优点是单个Entry占用内存低且每次增加元素是size++
    ArrayMap/SparseArray缺陷:使用二分查找法的效率低于HashMap对应的效率
    更加详细的原因可以参考 这篇文章
  • 不要使用Enum
    Android中使用枚举类型将会显著增加dex的体积,并且会产生额外的内存占用
    更加详细的原因可以参考 这篇文章
  • 不要使用String进行字符串拼接
    严格的讲,String拼接只能归结到内存抖动中,因为产生的String副本能够被GC,不会造成内存泄露。
    频繁的字符串拼接,使用StringBuffer或者StringBuilder代替String,可以在一定程度上避免OOM和内存抖动。
    更加详细的介绍可以参考 这篇文章
  • 资源文件需要选择合适的文件夹进行存放
    hdpi/xhdpi/xxhdpi等等不同dpi的文件夹下的图片在不同的设备上会经过scale的处理。例如我们只在hdpi的目录下放置了一张100100的图片,那么根据换算关系,xxhdpi的手机去引用那张图片就会被拉伸到200200。需要注意到在这种情况下,内存占用是会显著提高的。对于不希望被拉伸的图片,需要放到assets或者nodpi的目录下
  • 谨慎使用static对象
    static对象的生命周期过长,应该谨慎使用
  • 恰当的使用Bitmap
    1. 及时的销毁
      在用完Bitmap时,要及时的bitmap.recycle( )掉。
      注意,recycle( )并不能确定立即就会将Bitmap释放掉,但是会给虚拟机一个暗示:“该图片可以释放了”。
    2. 设置采样率
      有时候,我们要显示的区域很小,没有必要将整个图片都加载出来,而只需要记载一个缩小过的图片,这时候可以设置一定的采样率,那么就可以大大减小占用的内存。如下面的代码:
      1
      2
      3
      4
      5
      6
      7
      private ImageView preview;    
      BitmapFactory.Options options = newBitmapFactory.Options();
      // 图片宽高都为原来的二分之一,即图片为原来的四分之一
      options.inSampleSize = 2;

      Bitmap bitmap = BitmapFactory.decodeStream(cr.openInputStream(uri),
      null, options); preview.setImageBitmap(bitmap);

      4. 内存溢出时一些可能的脑洞套路

    3. 虚拟机栈和本地方法栈溢出
      如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
      如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
      这里需要注意当栈的大小越大可分配的线程数就越少。

    4. 运行时常量池溢出
      异常信息:java.lang.OutOfMemoryError:PermGen space
      如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。该方法的作用是:如果池中已经包含一个等于此String的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量。

    5. 方法区溢出
      方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。
      异常信息:java.lang.OutOfMemoryError:PermGen space
      方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在经常动态生成大量Class的应用中,要特别注意这点。

  •