内存优化深刻版

内存优化之路

最近一直想着本身之后的路如何走,Android的坑位愈来愈少,对于能力的要求也愈来愈高。曾想着换一个方向,可是最终都放弃了,毕竟这是本身喜欢的东西。因此,继续下去,不断的在Android方向发展吧。机会是给准备的人的,不断的充实本身,时刻准备着~html

进入正题。优化工做是一个开发工程师进阶必备的一种能力。包括内存优化,电量优化,网络优化等等。这些优化所须要的能力,实际上是对于各类知识的一种综合运用处理能力。java

img

概念

内存优化,是对于应用程序的内存使用、空间占用进行必定的优化处理。android

做用

经过内存优化,可以有效的避免内存泄漏、内存溢出、内存占用过大等问题,从而最大程度上保证应用的流畅性、稳定性、以及存活几率算法

基础知识

内存管理机制

内存的管理,主要是对于进程、对象以及变量分配以及回收数据库

内存区域

Android将内存分为了:堆区、方法区、栈区。其中堆区是内存优化的主要区域。c#

对象的生命周期以及大小都是有区别的。为了可以更合理的利用堆区,将其又按照生命周期的长短区分为了年轻代、老年代和持久代缓存

img

内存的分配

Android应用在启动时,会经过Zygote进行来孵化应用所须要的进程。当进程建立以后,会交由Framework层来进行托管。性能优化

而对于对象和变量的内存分配,采用一种动态内存分配策略,可是并不会说能够无限的增加,会有一个上限的限制。而这个最大值则跟具体的设备有关。毕竟随着手机性能的增长,手机的处理能力更强了,从原来的512M到如今的6G内存,单个应用的可处理能力也在增长。网络

对于应用中建立的不一样对象的具体分配策略,则以下图所示ide

img

这里强调一下静态分配区域中的静态常量,这种常量会一直存活于程序的整个运行期间。后面咱们会讲到这种常量致使的问题。

内存的回收

对于Android系统,依赖gc来自动的执行对于内存的回收工做。而回收工做主要依赖于各类回收算法。在Android的ART虚拟机,用到的算法主要有4种:

img

ART虚拟机会自动的根据实际的状况,自动的选择回收算法来进行内存的回收工做。好比说,当咱们的应用处于前台的时候,显示速度对于咱们来讲是最重要的,这时候ART虚拟机会选择简单的标记清除算法。当应用处于后台的时候,对于速度要求就低一些,这时候可能会采用标记整理算法来进行垃圾的回收工做。

ART虚拟机还具有对于内存的整理能力,从而减小内存空洞的问题

Low Memory Killer 机制

应用进程建立之后,ActivityManagerService就会根据进程的状态计算一个其对应的OomAdj值,而后将这个值传递给Kernel中,kernel存在一个低内存回收机制(LMK)。当内存达到必定阈值时,触发清理OomAdj较高的进程。

img

对于一些后台占用过大的程序,其回收以后的效益最大,其OomAdj对应的值高一些,回收的几率更大。

经过LMK机制可以保证资源的合理利用,防止过大的后台应用影响到前台程序的正常使用。

四种引用

在Java中,对象的引用主要分为四种。

image-20200614133204982

强引用没法回收,当对象用完之后,不移除对应的引用关系,就会致使对象没法回收,而发生内存泄漏等状况。因此在Android中要注意这种问题。

内存问题

对于对象的使用不当的话,可能会致使3种内存问题:内存抖动、内存泄漏,内存溢出

内存抖动

定义

内存抖动是指内存的频繁分配和回收会致使内存不稳定。在内存上一般呈现一种锯齿状频繁进行GC

内存抖动一般会致使应用的页面卡顿,长时间的内存抖动可能会致使应用的OOM。

抖动致使OOM?
  • 当应用在前台运行时,为了保证流畅度,会使用标记清除算法,这种算法会致使大量的内存碎片。
  • 内存碎片可能会由于空间特别小而没法被分配使用,当聚沙成塔的时候,可能会致使OOM

实战解决

对于内存抖动的问题,通常可使用Memory Profiler来进行排查处理便可处理。

测试案例:

@SuppressLint("HandlerLeak")
    private static Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            for (int i = 0; i < 100; i++) {
                String[] strings = new String[100000];
            }
            handler.sendEmptyMessageDelayed(0, 100);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_splash);
        handler.sendEmptyMessage(0);
    }
复制代码

这里咱们模拟了一建立大对象的操做。当程序运行起来之后,咱们点击Android Studio中的Profile按钮,就能够看到咱们的内存状况。这里看到内存呈现了锯齿状,也就是所谓的内存抖动。

image-20200614154041323

当遇到这种状况的时候,就能够知道发生了内存的抖动,那么就须要查找哪些占用内存比较高。

image-20200614160302585

如上图,经过顺序操做之后,在4这个位置咱们发现了建立大批量对象位置是在SplashActivity的handlerMessage的方法中。经过双击就能够跳转到咱们上面写的那一部分代码了。

解决技巧

对于内存抖动,应该重点关注:循环或者频繁调用的地方。由于这两个地方很容易形成对象的频繁分配会回收。

常见的内存抖动案例

有一些常见的内存抖动的案例,在咱们进行代码编写的时候,应该尽可能避免的。

  • 尽可能避免在循环体内部建立对象,把对象的建立移到循环体之外。
  • 在自定义View时,onDraw方法会频繁调用,尽可能不要在方法内建立对象
  • 对于大对象的使用(Bitmap,线程)等,要使用缓存池技术,经过复用防止频繁的申请和释放。在结束使用的时候,须要手动的释放池中的对象。
  • 当涉及到字符串的变动时,使用StringBuilder来替代,并且要合理化进行初始化。

内存泄漏

定义

虚拟机进行GC时,会选择一些还存活的对象做为GC Roots,而后经过可达性分析来判断对象是否回收。而内存泄漏是指一些再也不使用的对象被长期持有,致使内存没法被释放,其本质是由于再也不使用的对象被其余生命周期更长的对象所持有。而在内存上的表现则是应用的内存逐渐上升,可用内存逐渐变小

发生内存泄漏会致使咱们的内存不足,频繁GC,若是泄漏状况严重,会致使OOM。

实战解决

对于内存泄漏一般会使用Memory Profile+MAT来进行内存泄漏的分析。

这里咱们写一个简单的内存泄漏的案例,而后经过案例排查。

public class CallBackManager {
        private static List<Activity> list=new ArrayList<Activity>();
        public static void addCallBack(Activity activity){
            list.add(activity);
        }
    }
    public class SecondActivity extends AppCompatActivity {
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_sedsd);
            CallBackManager.addCallBack(this);
        }
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_splash);
        findViewById(R.id.iv).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(SecondActivity.class);
            }
        });
    }

复制代码

这里咱们准备了两个页面,第二个页面注册回调管理。咱们看一下内存的状况。

image-20200614174532108

当咱们进入第二个页面,而后退出。通过循环屡次的点击以后,内存慢慢的上升了~~~这种状况基本就是内存泄漏致使的。

image-20200614175439168

按照上图的步骤点开之后,会看到内存中存在着18个对应的SecondActivity对象。也就是SecondActivity发生了内存泄漏。可是对于如何发生了泄漏,如今什么实例发生泄漏,在这里是没法看出来的,须要经过MAT来帮助咱们定位问题。

在AndroidStudio中,将dump的文件导出。而后经过Android SDK的自带转换工具hprof-conv.exe,将文件转化为可以被MAT识别的hprof文件。转化语句为:

./hprof-conv file.hprof converted.hprof
复制代码

而后经过MAT打开转化后的.hprof文件。

GIF 2020-6-14 18-49-28

gif可能效果不太好,这里只是演示一下如何去使用MAT,咱们看一下最后找到的结果。

image-20200614185316940

这里左边有小圆点的是咱们的gcRoot,也就是持有SecondActivity的实例,致使其没法回收的根源。能够看到持有实例的是CallBackManager的list对象

这时候咱们就能够回到项目中去找这个对象,而后进行一个处理了。

常见的内存泄漏案例

集合类

集合类添加元素后,会持有集合对象的引用信息,致使集合对象没法回收而内存泄漏。在程序退出,或者集合再也不使用的时候,先clear,再置为空。

List<Object> objectList = new ArrayList<>();        
       for (int i = 0; i < 10; i++) {
            Object o = new Object();
            objectList.add(o);
            o = null;
        }
        // 释放objectList
        objectList.clear();
        objectList=null;
复制代码

Static修饰的成员变量

Static修饰的成员变量的周期=应用的生命周期。若是static修饰的成员变量引用耗费资源的实例(例如Context),那么当实例结束生命周期的时候,由于静态变量的持有而没法被回收,从而出现内存泄漏。

解决方案:

  • 尽可能避免Static修饰的成员变量引用耗费资源过多的实例。若是是Context,尽可能使用ApplicationContext
  • 使用*弱引用(WakReference)*代替强引用持有实例。

典型:单例中的静态变量

对于单例中的,可能第二种弱引用并非很合适,这时候只能使用第一种方案。

非静态内部类/匿名内部类

非静态内部类或者匿名类都会持有外部类的引用。而静态的内部类则不会。

因此若是非静态内部类的实例若是没法回收的话,好比说用Static修饰,也会致使外部类没法释放而内存泄漏。

解决方案:

  • 非静态内部类设置为静态内部类。
  • 将内部类抽取出来封装为一个单例,若是须要使用Context的时候,尽可能使用ApplicationContext
  • 尽可能避免static修饰的内部非静态类实例对象

若是是Runnable,Thread等匿名类,若是内部耗时工做,那么工做线程实例持有外部类引用,也会形成实例没法回收的问题。

Handler临时性的内存泄漏

Handler在发送出去Message以后,会保存在MessageQueue中。因为Message中会保存着Handler的引用,因此若是是延迟处理的消息,那么极可能致使Handler没法回收,若是再使用了匿名内部类,那么所对应的的Activity实例也就没法回收,从而致使内存泄漏。

解决方案:

  • 使用静态的Handler内部类,而后使用弱引用,在使用的时候,使用get()获取引用的Activity信息进行判空处理。
  • 在Activity进行销毁的时候,经过removeCallbacksAndMessages移除对应的消息队列。
  • Android Weak Handler:能够避免内存泄漏的Handler库,参考:www.jcodecraeer.com/a/anzhuokai…

资源型文件未关闭

对于资源的使用(广播,文件流,数据库游标,图片资源BitMap等),若是不关闭或者注销,那么资源就不会回收,从而致使内存泄漏。应该在资源对象再也不使用的时候,调用其close()方法将其关闭,而后再置为null。

WebView

WebView因为其特性问题,只要在应用中使用一次,那么内存就不会进行释放。能够为WebView建立单独的进程,经过AIDL方式和主进程进行通讯,当WebView须要销毁时,直接销毁其进程来达到内存释放的目的。

Adapter

在快速滑动ListView或者RecyclerView的时候,会频繁的建立大量对象,不只浪费资源和时间,内存也会愈来愈大。可使用缓存的convertView和ViewHolder来进行缓存。

内存溢出

当内存使用过大的时候,会致使内存的溢出,也就是OOM。这种状况不必定发生在相对应的代码处。内存抖动和内存泄漏都有可能会致使最后的内存溢出。对于内存溢出的状况,除了要考虑内存抖动和内存泄漏的问题,还应该尽可能使用合适的类型来优化代码

常见内存优化案:
  • 使用Android优化事后的SparseArray,SparseInt等集合类。代替HashMap。

  • 使用IntDef、StringDef等,来替代枚举。

  • 经过LruCache方法来实现对大资源的缓存功能

  • BitMap优化。位图使用RGB_565或者ARGB_4444。

  • 重写 onTrimMemory/onLowMemory 方法去释放掉图片缓存、静态缓存来自保

  • RecyclView和ListView不可见时,释放掉对图片的

    • ListView的3级缓存:在ImageView的DetachFromWindows时释放掉

    • RecyclerView的5级缓存:在放入到mRecyclerPool时回收(重写Adapter的onViewRecycled方法)

  • 其余

    • 使用基本数据类型

    • 使用For加强

    • 使用软引用和弱引用

    • 采用内存缓存或者磁盘缓存

    • 对于建立复杂的使用池技术

    • 尽可能使用静态内部类

优化工具

哪怕对于常见的内存泄漏案例了如指掌,可是确定仍是会不免出现内存泄漏现象。这时候咱们就须要借用工具来检测内存的泄漏状况了。最经常使用的工具分别

  • MAT
  • Memory Profile
  • LeakCanary

对于前两种工具,咱们在以前的案例中进行了一部分的功能展现,这里咱们作一些总结的信息。

Memory Profiler:
  • 实时的内存使用状况,方便直观

  • 识别内存抖动、泄漏等,

  • 提供堆转储、强制GC以及跟踪内存分配的能力

  • 线下平时使用

Memory Analyzer(MAT)
  • 强大的Java Heap 分析工具,查找内存泄漏以及内存占用

  • 生成总体的报告,分析问题等等

  • 线下的深刻使用工具

  • 能够和Memory Profiler结合使用,一个定位问题,一个深刻分析

LeakCanary

LeakCanary是一种自动内存泄漏检测的神器,仅推荐使用于线下集成。可是其缺点也是比较明显的。

  • 使用了多进程以及idleHandler,可是进行堆转储和引用分析的时候,仍然会致使应用的卡顿。具体的能够看一下以前作的一个关于LeakCanary的源码解析

总结

这篇文章对于内存的常见问题以及处理方案进行了总结。

可是内存优化,若是深挖,会涉及到不少不少的知识点。

一个最须要优化的Bitmap,其内存占是如何计算的等等,为何就占用了那么多的内存;对于LMK机制,咱们如何保证程序在后台运行的时候可以保证最大程度不被杀掉;线上如何造成一套APM的机制,内存出现问题之后上报等等都是值得研究的地方。

参考

Android性能优化以内存优化

深刻探索 Android 内存优化(炼狱级别)

MAT使用教程

Android性能优化:手把手带你全面了解 内存泄露 & 解决方案

Top团队大牛带你玩转Android性能分析与优化

lowmemorykiller总结

这是一份全面&详细的内存优化指南

本文由 开了肯 发布!

同步公众号[开了肯]

image-20200404120045271
相关文章
相关标签/搜索