性能优化实践(四)-内存优化思考

一、应用层

对应用层来说,最主要的内存问题还是内存泄漏问题。

Java中的内存分配

静态储存区:编译时就分配好,在程序整个运行期间都存在。它主要存放静态数据和常量;
栈区:当方法执行时,会在栈区内存中创建方法体内部的局部变量,方法结束后自动释放内存;
堆区:通常存放 new 出来的对象。由 Java 垃圾回收器回收。

Java中的内存泄漏

root可达,但对象本身已无用。造成问题的原因是:生命周期长的对象持有了生命周期短对象的引用,即使生命周期短的对象完成了任务,但是由于还继续被持有引用,造成无法被回收。

四种引用类型的介绍

强引用(StrongReference):JVM 宁可抛出 OOM ,也不会让 GC 回收具有强引用的对象;
软引用(SoftReference):只有在内存空间不足时,才会被回的对象;
弱引用(WeakReference):在 GC 时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存;
虚引用(PhantomReference):任何时候都可以被GC回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为GC回收Object的标志。

常见的内存泄漏的场景包含如下几类:

1 静态造成的泄漏

静态对象的生命周期与应用程序一样长,持有对象的引用基本都是泄漏,因为一般对象的生命周期都不会有它长。

举例:

  • 单例造成的内存泄露;
  • 静态集合保存对象;
  • 全局静态大对象。

解决方案:

  • 如果需要持有对象引用,将该对象的引用方式改为弱引用;
  • 如果需要持有Context,使用ApplicationContext;
  • 大对象本身尽量别做成静态的

2 非静态内部类和匿名类造成的泄漏

在Java中,非静态内部类 和 匿名类 都会潜在的引用它们所属的外部类,但是,静态内部类却不会。如果这个非静态内部类实例做了一些耗时的操作,就会造成外围对象不会被回收,从而导致内存泄漏。

举例:

这部分常见的有Thread、Handler、AsyncTask作为Activity内部类使用时,当然也包括自定义的内部类。

解决方案:

  • 将内部类变成静态内部类;
  • 如果有强引用Activity中的属性,则将该属性的引用方式改为弱引用
  • 在业务允许的情况下,当Activity执行onDestory时,结束这些耗时任务;

3 Activity Context 的不正确使用

在Android应用程序中通常可以使用两种Context对象:Activity和Application。当类或方法需要Context对象的时候常见的做法是使用第一个作为Context参数。这样就意味着View对象对整个Activity保持引用,因此也就保持对Activty的所有的引用。

假设一个场景,当应用程序有个比较大的Bitmap类型的图片,每次旋转是都重新加载图片所用的时间较多。为了提高屏幕旋转是Activity的创建速度,最简单的方法时将这个Bitmap对象使用Static修饰。 当一个Drawable绑定在View上,实际上这个View对象就会成为这份Drawable的一个Callback成员变量。而静态变量的生命周期要长于Activity。导致了当旋转屏幕时,Activity无法被回收,而造成内存泄露。

解决方案:

  • 使用ApplicationContext代替ActivityContext,因为ApplicationContext会随着应用程序的存在而存在,而不依赖于activity的生命周期;
  • 对Context的引用不要超过它本身的生命周期,慎重的对Context使用“static”关键字。Context里如果有线程,一定要在onDestroy()里及时停掉。

下图可以参考下什么场景下选择ApplicationContext,建议是能用ApplicationContext的就不用ActivityContext:

性能优化实践(四)-内存优化思考_第1张图片

4 代码习惯

Cursor,Stream没有close,Bitmap没有recyle,注册监听没有取消注册,集合对象不使用之后,内部对象没有及时清理掉, Adapter没有使用convertView等等。

解决方案:按规范来。

5 大View造成的泄漏

这里所谓的大View比如:WebView,当我们不要使用WebView对象时,应该调用它的destory()函数来销毁它,并释放其占用的内存,否则其占用的内存长期也不能被回收,从而造成内存泄露。

解决方案:

  • 为webView开启另外一个进程,通过AIDL与主线程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。

VideoView也同样如此,业务条件允许,尽量全局使用一个VideoView。

如果这些你都熟练于心的话,那在日常开发中就会避免掉不少主流的泄漏问题。未雨绸缪,防范于未然是非常重要的,毕竟内存泄漏很大程度上就是一个量变引起质变的问题。

6 谨慎使用三方库

当然,避免归避免,内存泄漏还是有可能会发生的。发生之后如何分析?

常用的内存泄漏排查流程:

1)adb shell dumpsys meminfo packageName

先通过dumpsys ,宏观了解下当前进程的内存具体分配情况,看看dalvik heap、native heap等等内存占用情况,大致了解下泄漏发送在java层还是native层,或者graphic跟视图、SurfaceView、图片相关的内存占用情况。

2)LeakCanary

用LeakCanary扫一遍,可以非常高效地先检查出Activity/Fragment的内存泄漏。触发的点是:Activity/Fragment执行onDestroy回调,它的原理大概是:

性能优化实践(四)-内存优化思考_第2张图片

当然,它无法覆盖所有问题,最底层的activity因为很难调用onDestroy,所以检测不到,这里倒是可以配合dumpsys meminfo的Objects部分通过back退出app,activitys 是否为0来帮助看最底层的activity是否有泄漏。另外,Service的内存泄漏也无法检测。

3)AS memory monitor / Heap Viewer

经过LeakCanary的大扫除,大部分问题已经被解决了,那么还剩下一部分问题如何处理呢?
如果你更喜欢视图:AS memory monitor ,尽量使用AS 3.0之后的profiler。
如果你更喜欢数据:Heap Viewer。
操作app,AS memory monitor 中视图曲线是否存在不断上升的趋势且不会在程序返回时明显回落。或者Heap Viewer中free值是不是逐渐减少,两种方式都提供手动GC按钮,GC之后看内存回调数值是不是期望的。对可疑点可以进行反复多次的操作,缩小怀疑范围。

4)MAT

dump hprof文件,一般我喜欢再转一下格式,用MAT打开分析,不转的话只能用AS分析,但是MAT功能更全面。一般会生成两份hprof,一份是正常情况的,一份是怀疑存在泄漏的,针对怀疑的点进行对比,是否有泄漏。

主要看:
with outgoing references: 查看它所引用的对象
with incoming references: 查看它被哪些对象引用

然后排除掉软弱虚引用,剩下的再去分析是否存在泄漏,如果没有泄漏,那再重复怀疑重复分析,具体分析MAT工具篇有详细说明。

泄漏的问题基本上就到这了,下面我再分析下应用程序其他的内存问题:

内存抖动:短时间内大量创建和回收对象,造成频繁GC,Art虚拟机在Dalvik的基础上针对GC做了优化,GC效率提高了2-3倍,且缩短了线程暂停时间,偶尔进行的GC暂停所有工作线程的时间几乎无感觉,但是如果频繁GC的话,那是有可能影响到用户体验的。

优化策略:

  • 避免不必的对象开销。
  • 对于就是频繁需要创建和使用对象的场景下,引入池的概念,或则设计模式中的享元模式都是可以考虑使用的。

内存瘦身
说白了就是减少不必要的内存开销,首当其冲的就是资源瘦身,也变相的是APK瘦身,能用代码实现的视图就尽量用代码,不得不用图片的,在保证一定清晰度要求情况下尽量减少图片大小,Linit检测去掉不必要的代码,三方库,用不到的代码也能去掉的尽量去掉(同时还减少了方法数)等等吧。其次缓存优化,很多时候为了提升效率,会把部分数据缓存在内存中,包括图片,不是调用很频繁的完全可以以文件的形式保存到硬盘上。另外能用生命周期短的对象就不用长的,这样能更快被回收。

二、Java Framework

这一层主要是从进程调度的角度来优化内存,详细内存之前《Android进程管理篇(三)-AMS进程调度》做过详细分析。

作为系统层,并不是内存越低越好,内存低往往体验也会差。大部分时候都是在用户体验和剩余内存上做博弈。这里我主要总结下低内存手机的内存优化思路:

1 lmk:调整合适的lmk水线,及时在内存低的时候杀掉部分进程腾出内存。减少cache进程缓存数,只要活着就会占内存,牺牲启动速度来换取内存。b-service级别进程在内存偏低时,降级为更低的cache级别,让lmk能杀到更多进程。为某些占用内存大的高日活应用增加白名单,启动时,默认清掉所有cache进程等等。

2 调整虚拟机能分配给进程的内存大小 dalvik.vm.heapgrowthlimit 和 dalvik.vm.heapsize

等等

三、Kernel层

这层主要做的优化大部分是提高系统剩余内存,也总结几个优化点吧:

1 可以针对文件页和匿名页做回收,在lru链表中,把不活跃链队尾的页尽量回收掉。提高swapiness值来倾向于回收更多匿名页,这样能增大Free RAM。

2 针对低内存手机,都会打开zram。Android 4.4 推出ZRAM内存压缩技术. zram是内核的一个模块,是内存管理的一种机制。可以将交换出去的页按照1:4的比例进行压缩存储,减小内存占用。压缩的区域依然是内存,并不是硬盘。利用这一特性,我们可以将不经常使用的进程占用内存尽可能的放在ZRAM,实现占用尽量少的内存,占用尽量多的缓存,节省内存;保证前台的速度,前台的应用或者关键应用不放在ZRAM中。

3 cat proc/meminfo 时候发现Mlocked值比较高,固化了部分文件页,目的是对于频繁使用的文件页,系统会把他们锁定到对应的page页中,减少io,方便使用,典型的空间换时间,比如google 的pinnerService就是这样,建议关闭。

4 配置MALLOC_SVELTE 来disable tcache,即jemalloc不采用tcache方法分配内存,这样的好处是占用内存少,但是分配内存速度会变慢。

另外,cat proc/meminfo

Kernel内存可使用内存 ≈ Slab + KernelStack + PageTables。

Slab: 236424 kB
SReclaimable: 66836 kB
SUnreclaim: 169588 kB

如果Slab 与 SUnreclaim 都非常高,且一段时间后也没有明显回落的话,可能存在内存泄漏。

可以通过page_owner工具抓出kernel的调用栈,用脚本捋出top问题,分析其调用栈来排查。

好了,先写这么多。一些不成熟的小建议,欢迎补充。

你可能感兴趣的:(性能优化实践(四)-内存优化思考)