APP性能-内存优化-实际分析

前言

项目开发过程中,因为一些不好的编码习惯导致App运行过程中出现内存泄漏,APP卡顿;甚至内存溢出(OOM),APP强行退出,这对用户体验来说是非常不好的。

1.内存泄漏(Memory Leak)

内存泄漏出现的原因,是因为一些对象没有被使用,但是在GC Roots是不可达的,那么GC无法正常回收。

内存泄漏会产生以下问题。

1.1 导致App卡顿, ANR

内存泄漏会使得可用内存越来越少,我们知道,可用内存少的时候,系统会触发GC回收,我们知道频繁的触发GC会导致UI渲染卡顿,影响用户体验。Android有16ms的阕值
这里写图片描述

甚至可能会导致出现ANR,我们看下ANR触发的条件。

  • 在5秒内没有响应输入事件(如按键或屏幕触摸事件)
  • BroadcastReceiver在10秒内尚未完成。

1.2 导致OOM发生

内存泄漏,会导致没有内存不能正常回收。所以APP会向系统请求更多可内存,但是我们知道系统会给APP一个上限值,一旦APP申请的内存超过了上限值,就会导致OOM错误。

1.3 内存泄漏很难复现

内存泄漏在QA/测试中很难被发现,所以一旦出现问题,也很难去复现。

1.4 内存泄漏出现的可能原因

  • 单例模式,使用了activity的context,但是单例模式的静态属性使得它的生命周期要长于activity,所以activity退出后,因为单例仍然会持有activity的引用,所以会导致activity无法被回收
Instance(Context context){
    this.context = context.getApplicationContext();//正确写法
}
  • 静态变量(静态常量不会,是因为静态常量是放在常量区单独保存的),同样是因为它的生命周期要长于activity,所以activity退出之后,会导致这个变量无法被回收,而导致内存泄漏

  • 非静态内部类,因为其会默认持有它的外部类的引用,当内部类的生命周期长于外部类的生命周期,就会导致内存泄漏,最典型的的例子就是handler(可以去熟悉下handler的机制),还有像Thread和AsycnTask都存在可能。

  • 未取消的广播和回调,同样会导致内存泄漏,原因还是因为内部类会默认持有外部类的引用,而它的生命周期大于它持有的外部类,同上。

  • Timer和TimerTask,属性动画等。没有正常被cancel掉的情况下,因为持有activity的引用,所以会导致内存泄漏,同上

  • 集合和队列中的对象没有被清除,因为集合或者队列会持有item的引用,所以如果集合还在使用,但是对象没有在使用,就会导致内存泄漏

  • IO流,File,Sqlite,cursor,因为使用了缓冲,如果没有及时close,就会导致缓冲对象没有被使用,但无法得到释放。

  • 关于WebView的内存泄露,因为WebView在加载网页后会长期占用内存而不能被释放,因此我们在Activity销毁后要调用它的destory()方法来销毁它以释放内存。
    Webview下面的Callback持有Activity引用,造成Webview内存无法释放,即使是调用了Webview.destory()等方法都无法解决问题(Android5.1之后)。

@Override
protected void onDestroy() {
    super.onDestroy();
    // 先从父控件中移除WebView
    mWebViewContainer.removeView(mWebView);
    mWebView.stopLoading();
    mWebView.getSettings().setJavaScriptEnabled(false);
    mWebView.clearHistory();
    mWebView.removeAllViews();
    mWebView.destroy();
}
  • 还有其他的例子,有机会再列举

其实内存泄漏问题还是很容易出现的,所以写代码的时候要多想想。

1.4 鉴定工具

1.4.1 Leak Canary

Leak Canary是Square开源的一款检测内存泄漏的工具,不管是开发者还是测试人员都可以通过这款工具上报内存泄漏问题,从而得到解决。
这里写图片描述
上图的错误就是不正确使用静态变量导致内存泄漏

1.4.2 Android Monitor

备注:Android3.0使用了Android Profiler代替了Android Monitor.
Android Studio提供了这款工具,操作步骤

  • 编译运行App
  • 找到可疑的Activity(我们怀疑会出现内存泄漏的界面)
  • 返回到上一个界面(按返回键)
  • 触发GC,并导出Java堆,见下图
    这里写图片描述
  • 打开保存的.hprof文件,如下图
    这里写图片描述
    现在让我们分析下:选中的LeakActivity是已经退出的界面,但是我们可以可以到第二列的totalCount是2,说明有两个地方发生了泄漏,然后看下面的引用树(Referennce Tree),找找为什么会发生内存泄漏,相信到了这一步,基本上已经可以确定问题了。

NOTE:关于怎么分析hprof请查看hprof
NOTE:内存泄漏部分摘自memory-leak-patterns

1.4.3 使用MAT检测内存泄漏

2. OOM

如果出现OOM,APP离卸载也不远了,所以必须引起我们的高度重视。

2.1 内存泄漏,达到阕值会导致OOM

2.2 bitmap的使用

  • 尽量使用图片加载框架,比如:glide, picasso, uil。
  • 根据显示控件,使用对应大小的图片,可以使用七牛
  • 比如使用android shape,.9.png 图替代。
  • 解码格式也会影响bitmap占用内存大小,ARGB_8888,RBG_565,ARGB_4444, ALPHA_8
  • 及时recycle,包括:Activity退出,ListView不可见区域等。

2.3 监听内存使用状态

这样我们可以根据不同状态处理不同的动作。

import android.content.ComponentCallbacks2;
// Other import statements ...

public class MainActivity extends AppCompatActivity
    implements ComponentCallbacks2 {

    // Other activity code ...

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that was raised.
     */
    public void onTrimMemory(int level) {

        // Determine which lifecycle or system event was raised.
        switch (level) {

            case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:

                /*
                   Release any UI objects that currently hold memory.

                   The user interface has moved to the background.
                */

                break;

            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:

                /*
                   Release any memory that your app doesn't need to run.

                   The device is running low on memory while the app is running.
                   The event raised indicates the severity of the memory-related event.
                   If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system will
                   begin killing background processes.
                */

                break;

            case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
            case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
            case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:

                /*
                   Release as much memory as the process can.

                   The app is on the LRU list and the system is running low on memory.
                   The event raised indicates where the app sits within the LRU list.
                   If the event is TRIM_MEMORY_COMPLETE, the process will be one of
                   the first to be terminated.
                */

                break;

            default:
                /*
                  Release any non-critical data structures.

                  The app received an unrecognized memory level value
                  from the system. Treat this as a generic low-memory message.
                */
                break;
        }
    }
}

2.4 更多可见:优化OOM

3.防止内存抖动(Memory Churn)

内存抖动是因为在短时间内大量的对象被创建又马上被释放。瞬间产生大量的对象会严重占用Young Generation的内存区域,当达到阀值,剩余空间不够的时候,会触发GC从而导致刚产生的对象又很快被回收。即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加Heap的压力,从而触发更多其他类型的GC。这个操作有可能会影响到帧率,并使得用户感知到性能问题。回顾下上面提到频发触发GC会导致APP卡顿
这里写图片描述

3.1 使用memory monitor检测

这里写图片描述
上图中的,内存频繁涨跌,基本就可以判定是出现了内存抖动,针对性找到代码解决。看下面的这个解决例子
这里写图片描述

3.2 也可以使用Allocation Tracker

关于内存抖动,可以看下官方的memory churn

4.避免在onDraw方法分配内存

在onDraw方法中分配内存,会导致APP卡顿,严重影响体验。

5.其他建议

5.1 检查你应该使用多少内存

public void doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check to see whether the device is in a low memory state.
    ActivityManager.MemoryInfo memoryInfo = getAvailableMemory();

    if (!memoryInfo.lowMemory) {//内存不紧张
        // Do memory intensive work ...
    }
}

// Get a MemoryInfo object for the device's current memory status.
private ActivityManager.MemoryInfo getAvailableMemory() {
    ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
    ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
    activityManager.getMemoryInfo(memoryInfo);
    return memoryInfo;
}

5.2 使用更多内存高效的代码构造

一些Android功能,Java类和代码结构往往比其他功能使用更多的内存。 可以在代码中选择更有效的替代方案来最小化APP使用的内存量.

5.3 有节制地使用Service

内存管理最大的错误之一就是让Service一直运行。在后台使用service时,除非它需要被触发并执行一个任务,否则其他时候Service都应该是停止状态。另外需要注意Service工作完毕之后需要被停止,以免造成内存泄漏。

系统会倾向于保留有Service所在的进程,这使得进程的运行代价很高,因为系统没有办法把Service所占用的RAM空间腾出来让给其他组件,另外Service还不能被Paged out。这减少了系统能够存放到LRU缓存当中的进程数量,它会影响应用之间的切换效率,甚至会导致系统内存使用不稳定,从而无法继续保持住所有目前正在运行的service。

建议使用JobScheduler,而尽量避免使用持久性的Service。还有建议使用IntentService,它会在处理完交代给它的任务之后尽快结束自己。

更多可以看下:Manage Memory的”Use services sparingly”

5.4 使用优化的数据容器

Android API当中提供了一些优化过后的数据集合工具类,如SparseArray,SparseBooleanArray,以及LongSparseArray等,使用这些API可以让我们的程序更加高效

5.5 小心代码抽象

开发者经常把抽象作为好的编程实践,因为抽象能够提升代码的灵活性与可维护性。然而,抽象会导致一个显著的开销:面向抽象需要额外的代码(不会被执行到),同样会被咨映射到内存中,耗费了更多的时间以及内存空间。因此如果面向抽象对你的代码没有显著的收益,那你应该避免使用。

例如:使用枚举通常会比使用静态常量要消耗两倍以上的内存,在Android开发当中我们应当尽可能地不使用枚举。

5.6 使用nano protobufs进行序列化数据

Protocol buffers是Google为序列化数据设计的一种语言无关、平台无关、具有良好扩展性的数据描述语言,与XML类似,但是更加轻量、快速、简单。如果使用protobufs来实现数据的序列化及反序列化,建议在客户端使用nano protobufs,因为通常的protobufs会生成冗余代码,会导致可用内存减少,Apk体积变大,运行速度减慢

5.7 移除消耗内存的库

你的代码中的一些资源和库可以在不知道的情况下消除内存。 您的APK的总体尺寸(包括第三方库或嵌入式资源)可能会影响应用程序消耗多少内存。 您可以通过从代码中删除任何冗余的,不必要的或膨胀的组件,资源或库来提高应用程序的内存消耗。

5.8 减少APK大小

可以通过减少应用程序的总体大小来显着降低应用程序的内存使用量。 位图大小,资源,动画框架和第三方库都可以帮助你的APK的大小。 Android Studio和Android SDK提供多种工具来帮助您减少资源和外部依赖关系的大小。
更多:Reduce APK Size

5.9 使用Dagger 2进行依赖注入

如果您打算在应用程序中使用依赖注入框架,请考虑使用Dagger 2。 Dagger不使用反射来扫描应用程序的代码。 Dagger的编译时注解技术实现意味着它不需要不必要的运行时成本。而使用反射的其它依赖注入框架通常通过扫描代码来初始化过程。 此过程可能需要显着更多的CPU周期和RAM,并可能导致应用程序启动时明显的卡顿。
NOTE: 诸如此类的注入库,都尽量使用编译时生成,不要使用反射机制的,因为反射机制会影响运行时体验

5.10 谨慎使用第三方库

很多开源的library代码都不是为移动端而编写的,如果运用在移动设备上,并不一定适合。即使是针对Android而设计的library,也需要特别谨慎,特别是在你不知道引入的library具体做了什么事情的时候。例如,其中一个library使用的是nano protobufs, 而另外一个使用的是micro protobufs。这样一来,在你的应用里面就有2种protobuf的实现方式。这样类似的冲突还可能发生在输出日志,加载图片,缓存等等模块里面。另外不要为了1个或者2个功能而导入整个library,如果没有一个合适的库与你的需求相吻合,你应该考虑自己去实现,而不是导入一个大而全的解决方案。

6.其他

  • google搜索android memory performance
  • github 搜索android memory performance
  • 可以搜索下youtube

总结

  • 避免大量创建对象
  • 尽量不要手动调用GC
  • 注意内存泄漏,一般来说生命周期长的对象就要特别小心。
  • 熟悉使用内存分析工具
  • 了解Android的内存机制
  • 有需要,才加入Serivice,特性等。
  • 集成第三方library,要建议在你有把握,完全了解的情况下

参考

Manage Memory

Android Performance Patterns

胡凯优化内存

awesome-android-performance

这篇博客主要来自于:谷歌的performance建议,相关博客,自己汇总,后续有新的体会,在继续在这篇博客更新。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值