Android 性能优化之内存优化

在移动操作系统上,通常物理内存有限,尽管 Android 的 Dalvik 虚拟机扮演了常规的垃圾回收的角色,但这并不意味着我们可以忽略 APP 的内存分配与释放,为了 GC 能够从 APP 中及时回收内存,我们在日常的开发中就需要时刻注意内存泄露,并在合适的时候来释放引用对象,对于大多数的 APP 来说,Dalvik 的 GC 会自动把离开活动线程的对象进行回收,接下来我们就来看看有关内存方面的具体优化思想,这里我们从两个方面来谈,在具体写代码的过程中我们需要注意的以及使用那些工具来检测出我们在写代码过程中所没有注意到的内存泄露

 

 

一、如何管理内存

 

1)Service 资源使用

      如果需要在后台使用 Service,当 Service 完成任务但停止失败的情况下,会引起内存泄露,我们在处理 Service 的时候尽量使用 IntentService 它会在处理完给它的任务以后自动关闭,当一个 Service 已经不需要时还必须保留它,这对 Android 应用内存管理来说是很糟糕的错误之一

2)onTrimMemory 优化

      onTrimMemory 主要作用就是指导应用程序在不同的情况下进行自身的内存释放,以避免系统被直接杀掉,提高应用程序的用户体验,接下来我们来看一下具体哪些组件可以实现 onTrimMemory 的回调

  • Application.onTrimMemory()
  • Activity.onTrimMemory()
  • Fragment.onTrimMemory()
  • Service.onTrimMemory()
  • ContentProvider.onTrimMemory()

App 生命周期的任何阶段,应该根据 onTrimMemory() 方法中的内存级别来进一步决定释放哪些内存

  • TRIM_MEMORY_UI_HIDDEN: 表示应用程序的所有 UI 界面被隐藏了,即用户点击了 Home 键或者 Back 键导致应用的UI 界面不可见.这时候应该释放一些资源.
    TRIM_MEMORY_UI_HIDDEN:这个等级比较常用,和下面六个的关系不是很强,所以单独说

下面三个等级是当我们的应用程序真正运行时的回调:

  • TRIM_MEMORY_RUNNING_MODERATE:    表示应用程序正常运行,并且不会被杀掉。但是目前手机的内存已经有点低了,系统可能会开始根据LRU缓存规则来去杀死进程了。
  • TRIM_MEMORY_RUNNING_LOW:    表示应用程序正常运行,并且不会被杀掉。但是目前手机的内存已经非常低了,我们应该去释放掉一些不必要的资源以提升系统的性能,同时这也会直接影响到我们应用程序的性能。
  • TRIM_MEMORY_RUNNING_CRITICAL:    表示应用程序仍然正常运行,但是系统已经根据LRU缓存规则杀掉了大部分缓存的进程了。这个时候我们应当尽可能地去释放任何不必要的资源,不然的话系统可能会继续杀掉所有缓存中的进程,并且开始杀掉一些本来应当保持运行的进程,比如说后台运行的服务。

当应用程序是缓存的,则会收到以下几种类型的回调:

  • TRIM_MEMORY_BACKGROUND:    表示手机目前内存已经很低了,系统准备开始根据 LRU 缓存来清理进程。这个时候我们的程序在 LRU 缓存列表的最近位置,是不太可能被清理掉的,但这时去释放掉一些比较容易恢复的资源能够让手机的内存变得比较充足,从而让我们的程序更长时间地保留在缓存当中,这样当用户返回我们的程序时会感觉非常顺畅,而不是经历了一次重新启动的过程。
  • TRIM_MEMORY_MODERATE:    表示手机目前内存已经很低了,并且我们的程序处于 LRU 缓存列表的中间位置,如果手机内存还得不到进一步释放的话,那么我们的程序就有被系统杀掉的风险了。
  • TRIM_MEMORY_COMPLETE:    表示手机目前内存已经很低了,并且我们的程序处于 LRU 缓存列表的最边缘位置,系统会最优先考虑杀掉我们的应用程序,在这个时候应当尽可能地把一切可以释放的东西都进行释放。

3)使用 BitMap 优化


    在 Android 应用里,最耗费内存的就是图片资源。而且在 Android 系统中,读取位图 Bitmap 时,分给虚拟机中的图片的堆栈大小只有 8 M,如果超出了,就会出现 OutOfMemory 异常。所以,对于图片的内存优化,是 Android 应用开发中比较重要的内容

 

3.1 要及时回收 Bitmap 的内存

      Bitmap 类有一个方法 recycle(),从方法名可以看出意思是回收。这里就有疑问了,Android 系统有自己的垃圾回收机制,可以不定期的回收掉不使用的内存空间,当然也包括 Bitmap 的空间。

      Bitmap 类的构造方法都是私有的,所以开发者不能直接 new 出一个 Bitmap 对象,只能通过 BitmapFactory 类的各种静态方法来实例化一个 Bitmap。仔细查看 BitmapFactory 的源代码可以看到,生成 Bitmap 对象最终都是通过 JNI 调用方式实现的。所以,加载 Bitmap 到内存里以后,是包含两部分内存区域的。简单的说,一部分是 Java 部分的,一部分是 C 部分的。这个 Bitmap 对象是由 Java 部分分配的,不用的时候系统就会自动回收了,但是那个对应的 C 可用的内存区域,虚拟机是不能直接回收的,这个只能调用底层的功能释放。所以需要调用 recycle() 方法来释放 C 部分的内存。从 Bitmap 类的源代码也可以看到,recycle() 方法里也的确是调用了 JNI 方法了的。

      那如果不调用 recycle(),是否就一定存在内存泄露呢?也不是的。Android 的每个应用都运行在独立的进程里,有着独立的内存,如果整个进程被应用本身或者系统杀死了,内存也就都被释放掉了,当然也包括  C 部分的内存。

       Android 对于进程的管理是非常复杂的。简单的说,Android 系统的进程分为几个级别,系统会在内存不足的情况下杀死一些低优先级的进程,以提供给其它进程充足的内存空间。在实际项目开发过程中,有的开发者会在退出程序的时候使用Process.killProcess(Process.myPid()) 的方式将自己的进程杀死,但是有的应用仅仅会使用调用 Activity.finish() 方法的方式关闭掉所有的 Activity。

      一般来说,如果能够获得 Bitmap 对象的引用,就需要及时的调用 Bitmap 的 recycle() 方法来释放 Bitmap 占用的内存空间,而不要等 Android 系统来进行释放。

// 先判断是否已经回收
if(bitmap != null && !bitmap.isRecycled()){ 
        // 回收并且置为null
        bitmap.recycle(); 
        bitmap = null; 
} 
System.gc();

     从上面的代码可以看到,bitmap.recycle() 方法用于回收该 Bitmap 所占用的内存,接着将 bitmap置空,最后使用System.gc() 调用一下系统的垃圾回收器进行回收,可以通知垃圾回收器尽快进行回收。这里需要注意的是,调用 System.gc() 并不能保证立即开始进行回收过程,而只是为了加快回收的到来

 

 

 

3.2 异常捕获

      因为 Bitmap 非常消耗内存,为了避免应用在分配 Bitmap 内存的时候出现 OutOfMemory 异常以后 Crash 掉,需要特别注意实例化 Bitmap 部分的代码。通常,在实例化 Bitmap 的代码中,一定要对 OutOfMemory 异常进行捕获。

Bitmap bitmap = null;
try {
    // 实例化Bitmap
    bitmap = BitmapFactory.decodeFile(path);
} catch (OutOfMemoryError e) {
   //
}
if (bitmap == null) {
    // 如果实例化失败 返回默认的Bitmap对象
    return defaultBitmapMap;
}

3.3 压缩图片

     如果图片像素过大,使用 BitmapFactory 类的方法实例化 Bitmap 的过程中,需要大于 8M 的内存空间,就必定会发生OutOfMemory 异常。这个时候该如何处理呢?如果有这种情况,则可以将图片缩小,以减少载入图片过程中的内存的使用,避免异常发生。

      使用 BitmapFactory.Options 设置 inSampleSize 就可以缩小图片。属性值 inSampleSize 表示缩略图大小为原始图片大小的几分之一。即如果这个值为 2,则取出的缩略图的宽和高都是原始图片的 1/2,图片的大小就为原始大小的 1/4。

如果知道图片的像素过大,就可以对其进行缩小。那么如何才知道图片过大呢?

      使用 BitmapFactory.Options 设置 inJustDecodeBounds 为 true 后,再使用 decodeFile() 等方法,并不会真正的分配空间,即解码出来的 Bitmap 为 null,但是可计算出原始图片的宽度和高度,即 options.outWidth 和 options.outHeight。通过这两个值,就可以知道图片是否过大了。

BitmapFactory.Options opts = new BitmapFactory.Options();
    // 设置inJustDecodeBounds为true
    opts.inJustDecodeBounds = true;
    // 使用decodeFile方法得到图片的宽和高
    BitmapFactory.decodeFile(path, opts);
    // 打印出图片的宽和高
    Log.d("example", opts.outWidth + "," + opts.outHeight);

      在实际项目中,可以利用上面的代码,先获取图片真实的宽度和高度,然后判断是否需要跑缩小。如果不需要缩小,设置inSampleSize 的值为 1。如果需要缩小,则动态计算并设置 inSampleSize 的值,对图片进行缩小。需要注意的是,在下次使用BitmapFactory 的 decodeFile() 等方法实例化 Bitmap 对象前,别忘记将 opts.inJustDecodeBound 设置回 false。否则获取的 bitmap 对象还是 null。

3.4 缓存通用的 Bitmap 对象

      有时候,可能需要在一个 Activity 里多次用到同一张图片。比如一个 Activity 会展示一些用户的头像列表,而如果用户没有设置头像的话,则会显示一个默认头像,而这个头像是位于应用程序本身的资源文件中的。

     如果有类似上面的场景,就可以对同一 Bitmap 进行缓存。如果不进行缓存,尽管看到的是同一张图片文件,但是使用BitmapFactory 类的方法来实例化出来的 Bitmap,是不同的 Bitmap 对象。缓存可以避免新建多个 Bitmap 对象,避免内存的浪费,如在项目中使用,建议使用成熟的图片加载框架,如 Glide、Picasso、Fraesco 等,这样能避免很多图片加载时出现的问题

4)使用优化的数据容器

    利用 Android FrameWork 里面优化过的容器类,例如 SparseArray、SparseBooleanArray、LongSparseArray,通常的HashMap 实现方式更加消耗内存,因为它需要一个额外的实现对象来记录 Mapping 的操作,另外 SparseArray 更加高效在于它们避免了对 key 与 value 的 auotbox 自动装箱,并且避免了自动装箱后的解箱

5)使用 ProGuard 来剔除不需要的代码

      ProGuard 能够通过移除不需要的代码,重命名类,和方法等方式对代码进行压缩,优化与混淆,使用 ProGuard 可以使你的代码更加紧凑

6)对最终的 APK 使用 zipalign

     在编写玩所有的代码,并通过编译系统生成 APK 之后,需要使用 zipalign 对 APK 进行重新校准,它会对齐升级包资源,Google Play 不接受没有 zipalign 的 APK

 

二、内存泄露和内存溢出优化

 

内存泄露:对象在内存 heap 堆中分配空间,当不再使用或没有引用指引的情况下,仍不能被 GC 正常回收的情况,多数出现在不合理的编码情况下,例如在 Activity 中注册了一个广播就收器,但是在页面关闭的时候未进行 unRegister,就会出现内存泄露现象,通常情况下,大量的内存泄露会导致 OOM

OOM:OutOfMemoery,内存溢出是指 APP 向系统申请超过最大阈值的内存请求,系统不会在分配多余空间,就会造成 OOM,在 Android 平台下,多数情况是出现在图片处理不当时,造成内存泄露

1)静态变量导致的内存泄露

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private static Context sContext;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        sContext = this;
    }
}

      上面这种情形是一种最简单的内存泄露,相信大家都不会这么干,上面代码将导致 Activity 无法正常销毁,因为静态变量引用了它,上面的代码也可以改造一下,如下所示,sView 是一个静态变量,它内部持有了当前 Activity,所以 Activity 仍然无法释放,估计大家也明白

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private static View sView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        sView = new View(this);
    }
}

2)单例模式导致的内存泄露

public class CommUtils {

    private static CommUtils instance;
    private Context context;
    public CommUtils(Context context) {
        this.context = context;
    }

    public static CommUtils getInstance(Context context) {

        if (instance == null) {
            synchronized (CommUtils.class) {
                if (instance == null) {
                    instance = new CommUtils(context);
                }
            }
        }
        return instance;
    }
}

在 MainActivity 中的使用如下:

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        CommUtils commUtils = CommUtils.getInstance(this);
    }
}

      上面的单例是我们经常写的,但是是存在内存泄露风险的,这样写的问题就是,在 MainActivity 里使用 CommUtils 类,传入的context 是 MainActivity 的 Context,试想一下,当 MainActivity 声明周期结束,但 CommUtils 类里面还存在 MainActiviy 的引用(context),这样 MainActivity 占用的内存就一直不能回收,而 MainActivity 的对象也不会再被使用,那么我们如何来解决这个问题呢?在MainActivity中,可以用CommUtils.getInstance(getApplicationContext())或CommUtils.getInstance(getApplication()) 代替,因为 Application 的生命周期是贯穿整个程序的,所以 CommUtils 类持有它的引用,也不会造成内存泄露问题

3)非静态内部类导致的内存泄露

非静态内部类默认持有外部类的引用,使外部类不能被回收,具体解决方法如下:

 

  1. 去除隐式引用(通过静态内部类来去除隐式引用)
  2. 手动管理对象引用(修改静态内部类的构造方式,手动引入其外部类引用)
  3. 当内存不可用时,不执行不可控代码(Android 可以结合智能指针,WeakReference 包裹外部类实例)

4)资源未关闭导致的内存泄露

      对于使用了 BroadcastReceiver、ContentObserver、File、Cursor、Stream、Bitmap 等资源的代码,应该在 Activity 销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄露

5)AsyncTask、Handler 导致的内存泄露

基本也是由于非静态内部类持有外部类的应用,导致外部类在销毁时无法回收,造成内存泄露

6)WebView 导致的内存泄

 

  • 不在 xml 中定义 WebView 节点,而是需要的时候在 Activity 中创建,并且 Context 使用 getApplicationContext()
  • 在 Activity 销毁 WebView 的时候,先让 WebView 加载 null 内容,然后移除 WebView,再销毁 WebView
@Override
protected void onDestroy() {
    if( mWebView!=null) {

        // 如果先调用destroy()方法,则会命中if (isDestroyed()) return;这一行代码,需要先onDetachedFromWindow(),再
        // destory()
        ViewParent parent = mWebView.getParent();
        if (parent != null) {
            ((ViewGroup) parent).removeView(mWebView);
        }

        mWebView.stopLoading();
        // 退出时调用此方法,移除绑定的服务,否则某些特定系统会报错
        mWebView.getSettings().setJavaScriptEnabled(false);
        mWebView.clearHistory();
        mWebView.clearView();
        mWebView.removeAllViews();
        mWebView.destroy();
    }
    super.on Destroy();
}

关于内存优化的内容,今天就先介绍到这里,如有错误,请指正

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值