原文链接 让你不再害怕内存优化

之前已经写过一篇对于如何做性能优化的文章,当初针对内存这一专项再做精细化的探讨。对于安卓利用开发来说,内存到底会遇到什么样的问题,有什么办法能够用来测试和剖析,以及有什么样的策略能够去实际优化,明天就来好好聊聊这个话题。

缘起

古代计算机是基于冯*诺依曼架构的,计算机的软件是运行在内存之中的,过程(也即运行中的程序)会消耗肯定的内存,才可能失常执行。
在软件开发的中世纪,C和C++流行的时代,是由软件开发人员(下称猿)本人治理内存,也就是说猿本人申请内存,并解决申请不到内存的状况,并在应用实现后本人负责开释内存,这无疑会加大程序开发难度,产生一些难以调试的问题,如内存越界或者内存踩踏以及野指针。到了近现代,主动内存治理成为支流,研发人员不再用本人去手动治理内存了,只管用,可劲儿造,所有由GC(也即是Garbage Collector内存回收器)来善后。

这极大的解放了研发人员的双手,能够让他们把更多的精力放在接管产品经理的需要下面了,三天一小需要,一周一大需要,产品迭代速度相当快,业务倒退迅速,老板相当快乐啊,这干掉BAT不可企及,赶英超美就在今天,IPO触手可及。然而,事实是极其骨感的。

内存问题会引发什么问题

对于安卓 应用程序来说,内存优化很重要,因为Java VM自身就是比拟耗资源的,当利用简单到肯定水平的时候,就会呈现由内存使用不当造成的问题。如,测试同学反馈说利用越用越卡,常常crash,用户也反馈说利用越来越不好用了。老板把老猿叫进办公室一顿骂,而后老板让老猿尽快来解决一下问题。

老猿只得把需要放一边,花工夫看一看这些问题,而后说凭我多年教训来看,这怕是内存出了问题。

后面提过,古代编程语言个别都有GC,帮忙研发人员治理内存。但因为各种起因,还是会呈现内存相干的问题。

特地是对于安卓猿来说,实现利用的编程语言是Java(精确说是JVM,Java和Kotlin 以及像Scala都是基于JVM的编程语言),天生反对GC,导致很多人对内存治理知之甚少。当应用程序简单到肯定水平,当源码宏大到肯定的量级时,性能问题,特地是内存性能问题便随之而来。

具体可能是内存呈现问题的场景有:

  1. OOM导致的crash。OOM,也即OutOfMemoryError,可能产生在任何中央,当Heap中可用内存有余时,便可能会遇到此类crash
  2. 应用程序越用越慢,呈现黑屏或者白屏。
  3. UI操作呈现卡顿,不晦涩。造成UI卡,不晦涩的起因很多,当排除了其余起因时,就是内存问题了
  4. 应用程序莫名闪退

内存问题的具体类型及其起因

要想做好内存优化,则必须先弄懂内存问题的根本原因,而后再对内存问题进行归类,最初是通过技术手段来解决。

内存问题的根本原因

安卓应用程序是由Java构建的,而Java是反对GC的编程语言,所以安卓猿是不须要本人手动的去做内存治理的,只管不停的创建对象即可,Java虚拟机(JVM)会帮忙咱们治理内存,当有不必的对象时会主动被GC。

然而Java应用程序(当然也包含安卓)还是会遇到内存问题,次要是两类,一类是内存不合理应用,如内存应用过多,频繁创立大量对象,内存碎片等等;二是内存透露。很多人会把二者一概而论,网络上绝大多数文章一谈性能优化,一谈内存优化,必然说到内存透露,但其实并不谨严。内存透露的确是最常见的内存优化内容,也的确是内存应用不合理的最常见问题,但内存问题并不局限于内存透露。

内存应用不合理

次要分为三个方面:

  1. 节约内存,简略来了解就是用一个人住着一千平米的大平层
  2. 大量创立小对象,产生碎片,内存碎片会造成JVM中的内存管理效率变低,当前面申请大块内存的时候效率就变差,它须要把小对象(碎片)进行转移压缩,以腾出更大的空间给大的对象应用。简略了解,这个时候JVM的效率就会变差,你的应用程序性能变差,甚至可能引起卡顿。
  3. 频繁创建对象,特地是较大的对象,造成内存抖动,也即应用程序应用的内存忽多忽少,会频繁的触发GC,从而影响JVM的运行效率。

内存透露

JVM是反对主动GC的,也就是说JVM帮忙你治理内存,当有不再应用的对象时,会被JVM主动回收,此称之为GC(Garbage Collection)。但如果对象长期处于『应用』状态,并且超出了它本应该存的周期,无奈被及时GC,这就会造成透露。一般来说,这也没啥影响,然而如果透露的对象太多,或者透露的工夫够长,就会把零碎配额Java Heap空间耗尽,应用程序便会因没有内存创建对象而OOM,就会crash。即便没有crash,因为残余空间较少,会频繁触发GC,从而导致应用程序卡顿重大。

内存透露的根本原因是对象的生命周期错乱,对象存活了超过了其本该的生命周期,或者简言之,一个本该是较短的生命周期的对象被一个更长生命周期的对象所援用着,就会导致它本该生命周期完结时无奈被GC,便产生了透露。

这是要重点关注对象的生命周期,只有治理好了对象的生命周期,能力彻底的解决内存透露问题。

安卓利用中的生命周期

固定生命周期的对象

安卓应用程序外面,有一些是有固定生命周期的,或者说有显著生命周期,且不是由研发人猿本人管制的,如框架层管制的那一坨货色。

  • Activity
  • Fragment
  • View

特地是Activity,它也是内存透露的头等对象,90%的内存透露都是Activity对象。这货齐全由零碎框架管制,并且有显著的生命周期,而且还有重建实例的状况(波及状态复原时),所以它的生命周期其实相当短暂,并且它跟过程和主线程没有任何关系,Activity退出 了(走了onDestroy)过程仍还在,主线程也仍还在。而,又因为它是应用程序的第1级入口,应用程序所有的对象,以及GUI所有的货色,全副都由Activity间接或者间接持有,换句话说,Activity透露了,你整个应用程序的对象也基本上全透露了。

较长生命周期对象

这里所谓的长生命周期,是指它们的生命周期是与过程绑定的,除非过程退出,或者显著的执行一些退出,否则始终随过程而存在:

  • Looper,或者说音讯队列,这玩意儿除非被动quit,否则始终存在。主线程的Looper与过程同在,本人创立的Looper要手动退出才算终结。
  • 被static润饰的成员变量,这货色的生命周期是跟过程一样的
  • 单例,单例必须由static来润饰,所以与过程生命周期是一样的,过程在,则单例在
  • 线程池,或者一个长时间运行的thread,除非被动去shutdown
  • RxJava的Schedulers,这玩意跟looper一样,都是长时间运行的音讯队列,且与过程绑定的
  • 零碎框架,手机还在开机零碎框架就在运行,所以它的生命周期远远长于某一个应用程序
  • Application和ApplicationContext,这货色与过程生命周期是一样的,相当于单例了

业务逻辑中的生命周期

业务逻辑就纯属于应用程序的自身逻辑了,无奈一概而论,但一般来说,主页面的生命周期必定是长于某个子页面的。那么子页面在其退出后,实践上它的绝大多数对象应该要被回收。

如何发现内存问题

生存中不是缺少美,而是短少发现。

对于内存优化,第一步就是要通过各种测试伎俩发现问题。最现实的状况是建设一种监控伎俩,这样最能保住反动果实,以及十分及时的发现问题。

这里指的是一般性的粗略伎俩来发现你的利用有内存问题了,可能须要优化了。并且这些测试方法最好能做成定期监控,这样一旦内存性能有回撤时,能尽快发现。

『队长,咱们裸露了』

很多时候都是问题被动找上门来了。

后方有雷区

很可怜,你的应用程序中弹身亡(crash了),还是OOM。这是Java语言中的一个运行时的谬误,可能在创立任何对象时产生,但一般来说创立比拟大的对象时,这里的大是指对内存需要大,如图片,或者大块数组时,更容易产生。

当你的应用程序呈现了OOM的时候,就是一个特地显著的信号,通知你要器重内存优化了。

遇到终结者了,是lowmemorykiller

有时候,没有显著的谬误,然而利用却闪退了,特地是在后盾,或者跳到其余利用页面时。

这个会比拟荫蔽,通常会引发其余表象的问题。最显著的问题就是,当跳转到其余页面,再返回时,发现原来的页面状态不存在了,比方你的利用要拜访一个URL,跳转到了网页浏览器,但从浏览器返回时,要么你的利用不在了,要么你的利用的原先状态不在了。这其中的起因就是当你的利用不在前台了,就被零碎回收了,其中一个占大头的起因就是占用内存太多,被零碎的lmkd(lowmemorykiller)干掉了。

因为零碎要保障整个设施的失常运行,所以会把占用内存太多的先杀掉,以开释内存。

当你的利用频繁的遇到被lowmemory killer干掉时,也是一个显著的信号,要器重内存优化了。

读懂零碎GC日志

有些时候不像后面那样重大,然而查看logcat日志时,能发现大量的GC日志,就像这样的

259857:01-08 20:00:17.836 10083 26337 26347 I test.test: NativeAlloc concurrent copying GC freed 141174(6852KB) AllocSpace objects, 29(12MB) LOS objects, 49% free, 24MB/48MB, paused 180us total 308.126ms279178:01-08 20:00:19.618 10083 26337 26347 I test.test: Background young concurrent copying GC freed 469755(20MB) AllocSpace objects, 40(3608KB) LOS objects, 41% free, 28MB/48MB, paused 396us total 124.817ms

这是零碎在进行GC,通常来说这没有什么问题。但如果在短时间内,比方某个页面,点了某个按扭后大量呈现此类日志,也是一个显著的信号,通知你要器重内存优化了。

主动出击,以攻为守

作为一个优良的猿,不能坐着等问题上来,要能被动的去发明问题。每当实现一个需要后,或者写了一大坨代码当前,就须要被动的去查看一下内存方面是否有须要优化的中央。咱们能够通过如下测试方法,来看内存是否有问题,是否须要做优化。重点就是看应用程序在肯定工夫内,应用的内存是否始终在增长, 有没有抖动,并且在GC后,或者退出 后是否仍不回落。

meminfo

具命令是adb shell dumpsys meminfo <package>,这个命令还是比拟常见的,网上有很多材料能够用,能够看前面列举的参考文章中来具体理解它的具体用法以及各个字段的意义,这里就反复了。

  • dumpsys
  • dumpsys meminfo 的原理和利用
  • adb shell dumpsys meminfo 详解
  • dumpsys meminfo 含意

须要关注一下重点,就是,能够重点看Java Heap一栏的数据变动,这是Java层的占用内存状况。另外就是每次运行meminfo其实会对过程产生影响。所以,这个命令能够用来粗维度的监控,查看一些信息,做一些定性的剖析。

它最大的长处是不便,且只有是过程都能够查看,不必有源码。

Android Studio的Memory Profiler

在远古时代安卓SDK中会有DDMS,外面是一套调试工具,但当初都集成到Android Studio的Profiler外面了,通常会在下方的工具栏外面,如果 没有就到菜单View->Tools Window->Profiler把它调进去。而后抉择要调试的过程,默认它会把CPU,Network,Memory和功耗都显示,这里能够双击Memory那一坨,就会进入专门的内存页面。

它会以时间轴的形式来图形化的展现内存应用状况,十分的直观和不便。通过这个能够直观的看到两个问题,就是嫌疑内存透露以及内存抖动。

嫌疑内存透露就是看到曲线始终在增长,且通过显示GC,或者退出后,或者进行某我的项目操作后,仍不回落的,这就十分有可能有透露的存在,透露是超出了它本该的生命周期,比方某一操作完结了,退出 了某一页面,甚至退出利用了,内存仍没有回落,就可能有问题。

另外就是内存抖动,就是能看到内存曲线 有毛刺,短时间内忽上忽下的,这就是内存抖动。

leakcanary

这货也是十分风行的,专门用于检测内存透露的工具,它的性能较为弱小,除了能够监控以外,还能够给出具体的trace。具体应用能够参考官网的文档,并不难。

但它最大的问题在于,必须参加我的项目构建。如果你想钻研一下竞品的状况,就没有方法了。

如何调试内存问题

通过后面提到的伎俩,咱们能够发现内存有一些问题了,须要进行内存方面的优化了,但这还不够,还须要一些精细化的调试办法来具体定位问题,这样能力更好的去进行优化。

那么有哪些具体的调试办法呢?

Allocation tracer

这个是后面提到的Android Studio Profier外面的工具。用Profiler能够发现问题,但还须要进一步的深刻的剖析问题。这就须要Allocation tracer了。

具体做法就是,当你发现某一系列操作后内存始终增长,或者看到有抖动景象时,就能够抓取这段时间的Heap dump,而后详细分析,当初Android Studio都集成好了,只须要点几下,就能抓到,并把后果列出来,能够看到具体创立哪些对象,以及它们的援用关系是怎么的。

能够参考 以下资源来具体理解如何应用此工具:

  • The Android Profiler
  • Inspect your app's memory usage with Memory Profiler
  • Android 内存优化篇 - 应用profile 和 MAT 工具进行内存透露检测

MAT

这是专门用于Java heap内存剖析的工具,相当弱小。但不能间接应用。

须要先想方法抓取过程的heap dump,而后转换为Java规范的格局(因为安卓的Heap与Java SE的并不一样,安卓 SDK中有转换工具),而后再用MAT关上即可,它的性能要远弱小于后面的提到的Allocation tracer。所以,如果要深度的剖析和优化,还是要用MAT。

对于MAT的具体应用办法,能够参考以下资源:

  • 官网文档
  • 【Android 内存优化】应用 Memory Analyzer ( MAT ) 工具
  • Android 内存优化(1) - MAT 应用入门

leakcanary

除了能监控以外,它还能剖析具体的内存透露,并给出trace,所以当发现问题后,具体定位问题的时候,也能够应用此工具,还是相当弱小的。

它的应用相当简略,间接把它退出到dependencies,而后构建 就好了。

至于它的剖析后果也是相当直观的,会以Notification的形式告诉你,点开后有一个页面展现出援用关系链,而后判断是否是透露,即可。

具体能够参阅它的官网文档就能够了。

如何优化内存

内存优化,一大半在于测试,监控和调试剖析,约占70%,这部分是重头,因为只有找到具体的代码地位,才好去修复问题,并且修复后还要验证问题是否真的修复了。不能光在那里看代码,想当然的认为把几个外部类改为static,或者传递援用了ApplicationContext,就能优化了内存。

对于性能优化,当然也包含内存优化,必须用测试伎俩进行量化,以此来验证是否真有有改善。

本节内容,假如已通过后面提到的测试方法发现了内存问题,并通过调试伎俩定位到了具体位置。优化的伎俩也要针对 具体的问题来进行:

防止内存透露

内存优化的大头是要防止透露,所以重点来谈谈如何防止内存透露。

后面提到了,内存透露的根本原因是生命周期凌乱,较长生命周期的对象,甚至是超长生命周期的对象,持有了较短生命周期的对象,这肯定会导致透露。所以,要想真的解决内存透露问题,必须设计好对象的生命周期,这是基本解决之法。

要尽可能的,放大对象的生命周期

对象的生周期不应该超出它本该存在的范畴,并且应该尽可能的缩小对象的生命周期,这个可能在设计阶段思考到。但个别较难执行,代码简单了,很难控得住。

对于超过Activity生命周期的对象要及时清理

后面提到过的超长生命周期的货色,如Looper,如Frameworks,如单例,如RxJava的Schedulers,如线程池,这些货色的生命周期远长于Activity,所以,肯定要在对应的中央,及时革除对Activity的援用持有。

前面的参考 材料外面也有大量的实用倡议能够参考,这里就不反复了。防止内存透露应该要被总结成为编程标准,而后在团队外部推广,当然也能够设计一些源码动态检测工具,来强制执行。当然,再好的工具和标准也须要人来恪守,任何事件可能在编码阶段避免产生,老本是最小的,收益 是最大的。

WeakReference和SoftReference不是救命稻草

千万不要用WeakReference和SoftReference这货色来修复内存透露问题,它们基本就不是用来修复内存透露问题的。

再说一遍,内存透露是由生命周期凌乱造成的。

如果强行应用WeakReference来代替原来的强援用,就会造成想应用对象的时候它却被回收了,这时你的失常逻辑就没法走了,而且如何正确的解决这种异样case,也是很难失当 的解决的。

WeakReference这货色最最正当,最为适宜的场景就是缓存外面,也就是说它自身是用于一种可有可无的援用关系,这样一旦被GC了,也不会影响原有逻辑,因为对象原本就可能在(Cache Hit),也可能不在缓存外面(Cache Miss),使用者必须解决在或者不在两种case。因为缓存的清理可能不够及时(必须由编码人员手动设置条件去清理,比方在退出的时候),当JVM须要GC时,因为都是WeakReference,GC就能够疾速的回收对象开释内存。

不要到处给对象援用置为null

很多有过C++教训的同学,可能会习惯在对象应用实现后,手动把对象置为null。但其实这是齐全没有必要的,只会造成不必要的凌乱,JVM会本人去追踪每个对象,它到底还有没有被援用持有着。咱们要把精力重点放在对象生命周期的把控下面,简略的置为null,不会缩减对象的生命周期,所以它对解决和避免透露方面没有任何帮忙。

内存应用优化形式

除了防止内存透露,其余一些形式也是有很多技术能够用于优化的。

缩小内存节约

内存节约,就是应用了没必要的内存,尽管可能不会引发问题,然而还是会减少危险,比方同样都是后盾过程,你的利用占用内存稍大了一些,被杀的危险就高了一些。

缩小内存节约,外围的办法就是按需申请,特地像图片这种内存占用小户,肯定要按须要来加载,何为须要就是指标View的大小,具体能够看官网教程Loading Large Bitmaps Efficiently。以及尽可能的要复用bitmap。

再如资源图片,设置正当的分辨率,没有必要啥都上高清,且要为低精度设施提供独自的一套资源。

以及像不是要求那么清晰的场景就用RGB_565,而非RGBA_8888等等,这些都是在编码的时候就能够进步内存应用的办法。

应用缓存

缓存是计算机史上最平凡的创造,甚至是人类史上最平凡的创造,它无处不在从硬件到软件都会应用缓存,并且它在各种货色的设计之中都是很重要的一部分。

后面提到的内存抖动问题,就须要用缓存来解决,以防止频繁创建对象。特地是波及图片的场景,比方风行的图片加载开源库外面都有专门的缓存的机制,有些是二级,有些是三级。当须要设计缓存时,能够重点参考图片加载库中的缓存设计。

另外,SDK中也有规范的缓存组件能够用,LruCache,这是针对内存层面的缓存,能够看这篇文章来具体理解应用办法。

正当复用对象

这里的意思是应用像享元这样的设计模式,来正当的复用对象。

须要留神的是享元(Flyweight Pattern)的实用场景,它实用于创建对象的老本较高,比方创建对象须要的一些资源较低廉,不同的对象仅是有不同的属性,或者说对象自身在应用的时候的体现是不同的。

一个典型的例子就是绘图的形态,比方一个页面有大量的不同的形态须要绘制,无方的,有圆的,有红色的,有黑白的,有实边的有虚线的。惯例的思路是一个基类叫Shape,外面有各种属性,还有一个draw办法,子类能够定义不同的属性,各自实现draw办法。而后依据需要创立一大坨具体的对象,遍历调用draw办法。这是面向对象编程(OOP)中的十分规范的多态(Polymophsim)。事实上,你只须要创立一个对象就够了,它会依据不同的属性画出不同的成果。这就是设计模式中的享元模式,具体能够参考这篇文章来具体理解。

意识几种不同的内存类型

通过各种工具查看的内存时,如通过meminfo以及像Memory profiler,但能够发现有不同品种,须要重点关注以几种:

Java Heap

也即通常意义上的heap内存(堆内存),名字可能会是Java,Java Heap,或者Java allocate,但都是一样就是指纯Java代码中通过new创建对象时应用的内存。

Native Heap

因为Java是反对JNI与C/C++接通,也即native办法,那么通过native办法创立的对象是计算在Native之中的,它与Java层是离开的,当然通过native办法(malloc或者new)创立的对象,要记得去开释,否则是肯定会透露的。

因为Android的大部分是由C/C++实现的,Java层仅是封装,Frameworks层大部分性能都由JNI转到native层去实现的,因而native这部分的内存也是很多的,并且因为Frameworks自身会大量调用JNI native层,所以即便你的应用程序基本没有用到JNI,然而还是会看到Native内存应用。

Graphics

次要是波及OpenGL ES的相干内存占用,如GL Surfaces,如Texture或者如Framebuffer等,它们所占用的内存。

这里须要特地留神的是,即便你的利用没有用到OpenGL相干的货色,但仍可能会有此局部内存占用,这是因为硬件加速自身也是通过OpenGL ES实现的。

ion内存

这个是为了效率,间接从kernel层开出shared buffer,以减速内存应用效率,这个是偏底层的,大部分一般app是用不到的。

能够参考一下这个The Android ION memory allocator。

共享内存

能够了解为Linux中的匿名共享内存,能够用来实现IPC通信,但它并不会被Profiler计算在Java或者Native外面。非死不可出品的Fresco当初牛逼的中央就在于把Bitmap放在匿名共享内存外面,从而不占用利用本人的Heap空间。

能够参考这两个文章:

  • Ashmem(Android共享内存)应用办法和原理
  • Android shared memory

学无止境

深刻学习GC相干常识,如JVM的GC如何演进。

也能够学习一下其余编程语言的GC机制。

不要过早优化,更不能适度优化

性能优化这个事件是要在架构设计和产品设计阶段就须要思考的事件,比方是否要退出缓存。

但如果后期想太多,会造成重大的扭曲,会让你陷入有限的简单问题外面,难以自拔(本是问题1,然而变成了问题A,问题B,直到问题z,最后的问题1却被忽略了),反倒不是好事件。

最为想理的状况就是小步迭代,先提出能满足需要的最小版本,而后逐渐迭代。比如说做一个新的feature的时候,先用最简略的架构和设计来实现,而后思考补充细节,解决异样case,再思考可能的扩大,而后思考性能优化。

剩下的是态度

不是说一线开发的态度,而是老板们的态度。

性能问题是间接影响体验,所以只有器重体验的老板才会器重性能问题。而且这也不是研发猿的问题,须要测试,产品经理都要能器重性能问题,能力最终把性能做好。产品同学不能只顾着提需要,也要均衡性能,并且给研发同学肯定的工夫去重视性能问题,而测试同学更加重要,须要一直精进你的测试方法,帮忙研发同学更好的解决问题,并且要有监控伎俩,比如说A版本做了性能优化专项,那么为了保留反动果实,须要有一种监控伎俩,以防性能呈现重大回撤。

很多事件不能怪研发,就像有一位技术相当不错的共事说过的话,过后大家聊起性能优化的事件,他说:『情理大家都懂,但当右边是产品经理在那里催需要,左边是设计师在那说按扭还差几个象素,测试同学在那崔你连忙发版本啊,我还等着测完回家呢!当你处在这种条件下,谁TMD的还管性能啊,先实现了再说吧,甚至代码格局都懒得改了。』

所以,这是整个工程体系的事件,只有整个研发体系都重视性能,性能才会好,体验才会好,而这就须要一个老板的反对了,否则,性能不可能好,产品汪们只顾着提需要,设计师只顾着画面精美,研发同学光实现需求都做不完,哪有精力去搞性能啊!测试同学也不能只用浅显的测试方法,只说性能不好,具体哪不好,不应该都让研发本人去调试,去发现问题。另外,也须要做好性能监控机制,以保住反动果实。要不然,A版本辛辛苦苦搞了一轮性能优化,也有大幅改善,而后到了B版本,或者几个月后,再来一轮。

这就是很骨感的事实。所以,在现实生活中只有大厂头部利用 才真的器重性能和体验,并且能力把性能和体验做好。

参考资料

  • Overview of memory management
  • Profile your app performance
  • Android内存管理机制
  • 最全的Android内存优化技巧
  • Android性能优化之内存优化
  • 深刻摸索Android内存优化
  • Android性能优化之内存优化
  • 深刻摸索 Android 内存优化(炼狱级别-上)
  • 深刻摸索 Android 内存优化(炼狱级别-下)
  • 内存优化深刻版
  • Dealing with Large Memory Requirements on Android

原创不易,打赏点赞在看珍藏分享 总要有一个吧