(二)Android 性能优化 Memory Profiler

·  阅读 971
(二)Android 性能优化 Memory Profiler

学贵有疑,小疑则小进,大疑则大进。

什么是内存泄漏?

内存泄露:无用对象持续占有内存或无用对象的内存得不到及时的释放(程序申请分配内存空间后,使用完毕后未释放。结果会导致一直占据该内存单元,也无法再使用该内存单元,直到程序结束)。内存泄漏的影响:容易使得应用程序发生内存溢出,即 OOM(Out of Memory) 。
内存溢出:OOM(这个就很严重了),程序向系统申请的内存空间超出了系统能给的最大内存单元。内存溢出的影响:导致ANR,甚至应用Crash。
内存抖动:内存抖动是指在短时间内有大量的对象被创建或者被回收的现象,主要是循环中大量创建、回收对象,从而使UI线程被频繁阻塞,导致画面卡顿。
 它们三者的重要等级分别:内存溢出 > 内存泄露 > 内存抖动。

内存泄漏的根本原因

 本该回收的对象,因为某些原因(对象的强引用被另外一个正在使用的对象所持有,且没有及时释放),进而造成内存单元一直被占用,造成内存泄漏,甚至可能造成内存溢出!
 简言之:持有引用者的生命周期 > 被引用者的生命周期。
 原理基础知识:关于JVM的内存分区和GC机制(先请大家自行百度,后续我会为此单开一篇)

Android中内存泄漏分类

内存泄漏分类

(一)线程未结束(举栗子:Handler)

  • 主线程的Looper对象的生命周期 = 该应用程序的生命周期
  • 在Java中,非静态内部类和匿名内部类都默认持有外部类的引用
  • 引用关系: 未被处理 / 正处理的消息 -> Handler实例 -> 外部类
  • Handler的生命周期 > 外部类的生命周期。外部类销毁时,外部类无法被垃圾回收器(GC)回收。
  • 解决方案:(1)静态内部类 + 弱引用;(2)当外部类结束生命周期时,清空Handler内消息队列。

(二)非静态内部类

  • 因为非静态内部类持有外部类的隐式引用,容易导致意料之外的泄漏。比如我们创建一个内部类,而且持有一个静态变量的引用。
  • 解决方案:(1)使用静态内部类(静态内部类不持有外部类的引用,打破了链式引用);(2)注意管理引用的生命周期;(3)避免静态变量。

(三)单例模式

  • 单例的特性导致它和应用的生命周期一样长。一般泄露发生都是因为传入了一个Activity的Context。
  • 解决方案:需要使用Context,不要传入Activity的Context, 正确的做法是使用Application的Context。

(四)static变量引用

  • 因为static变量的生命周期是在类加载时开始、类卸载时结束,也就是说static变量是在程序进程死亡时才释放,如果在static变量中 引用了Activity/View ,那么 这个Activity由于被引用,便会随static变量的生命周期一样,一直无法被释放,造成内存泄漏。
  • 解决方案:(1)使用Application Context;(2)弱引用;(3)生命周期结束,主动断开引用链。

(五)集合中对象没清理造成的内存泄漏

  • 当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。
  • 解决方案:在Activity退出之前,将集合里的东西clear,然后置为null,再退出程序。

(六)BitMap占用过多内存

  • bitmap的解析需要占用内存,但是内存只提供8M的空间给BitMap,如果图片过多,并且没有及时 recycle bitmap 那么就会造成内存溢出。
  • 解决方案:(1)及时recycle,并显示置为null;(2)压缩图片之后加载图片;(3)当需要大量使用Bitmap的时候,试着把它们缓存在数组中实现复用。

(七)属性动画

  • 动画会持有view对象,进而持有activity
  • 解决方案:(1)在页面退出(destroy)时取消动画;(2)如果同时从当前页面跳转到其他页面,也应该在onPause中暂停动画,以避免资源浪费,在回到该页面时在onResume中继续动画播放。

(八)资源未及时关闭

  • 资源性对象比如(Cursor游标,Stream流,BroadCastReceiver等)往往都用了一些缓冲,我们在不使用的时候或者在使用完之后,应该及时关闭它们比如close()方法,以便它们的缓冲及时回收内存。
  • 解决方案:及时关闭(close)。

(九)订阅/取消订阅不匹配(比如Service业务监听/广播反注册)

  • Service注册监听如果不取消,Service会一致持有页面引用,导致内存泄漏。
  • BroadcastReceiver不止被Activity引用,还可能会被AMS等系统服务、管理器等之类的引用,导致BroadcastReceiver无法被回收,而BroadcastReceiver中又持有着Activity的引用(即:onReceive方法中的参数Context),会导致Activity也无法被回收(虽然Activity回调了onDestroy方法,但并不意味着Activity被回收了),从而导致严重的内存泄漏。
  • 解决方案:及时取消订阅。

(十)任务不及时取消(Service/WebView/Retrofit/Rxjava等)

  • 任务未关闭,肯定会有内存泄漏,更可怕的是,如果在正常的销毁流程中做了一些重置工作,而再后续有任务继续反馈工作时,可能会空指针等各种异常。
  • 解决方案:任务及时取消,follow宿主的生命周期,不要让其苟活……

(十一)执行频率高的地方或循环中创建对象(onMeasure/onDraw)

  • 在循环体内创建对象、自定义View的onDraw()创建对象、大量初始化Bitmap、频繁创建大内存对象。
  • 解决方案:(1) 尽量避免在循环体内创建对象,应该把对象创建移到循环体外;(2)自定义View的onDraw()方法会被频繁调用,在这里面不应该频繁的创建对象;(3)对于能够复用的对象,可以使用对象池将它们缓存起来。

(十二)Toast显示

  • Toast里面有一个LinearLayout,这个Context就是作为LinearLayout初始化的参数,它会一直持有Activity,Toast显示是有时间限制的(异步任务),如果Toast还在显示Activity就销毁了,由于Toast显示没有结束不会结束生命周期,这个时候Activity就内存泄漏了。
  • 解决方案:(1)不要直接Toast,自己封装一个ToastUtil,使用ApplicationContext来调用(或者通过getApplicationContext来调用);(2)还有一种通过toast变量的cancel来取消这个显示

小憩一下,正式开始

为什么选择 memory profiler?

 Memory Profiler 是 Android Profiler中的一个组件,可帮助您识别可能会导致应用卡顿、冻结甚至崩溃的内存泄露和内存抖动。它显示一个应用内存使用量的实时图表,让您可以捕获堆转储、强制执行垃圾回收以及跟踪内存分配。  Memory Profiler 加入了自动检查 Activity 和 Fragment 中的内存泄漏的功能。使用这一功能非常的简单。

实操体验

(1)实操环境

 Android Studio 4.0  Gradle version 6.1.1  Android API version 30

(2)打开路径

  • profiler打开位置:View -> Tool Windows -> Profiler
  • 当然也可以直接运行程序并启用profiler监控:Run -> Profiler

(3)工具预览

profiler整体概览

(4)详情页面

memory-profiler详情页

窗口详细说明:

  • 窗口1:分别由以下几个功能按钮组成

 性能分类切换按钮,包括:cpu、memeory、network、energy。
 用于强制执行垃圾回收事件的按钮
 用于捕获堆转储]的按钮
 用于指定分析器多久捕获一次内存分配的下拉菜单:
  Full:捕获内存中的所有对象分配
  Sampled:定期对内存中的对象分配进行采样
  None:停止跟踪应用的内存分配

  • 窗口2:页面调整按钮集合,包括:缩小,放大、重置、暂停、开始等。
  • 窗口3:事件时间轴,显示Activity的生命周期不同状态,用户交互事件,如点击,旋转等。
  • 窗口4:内存计数图例:

 Java:从 Java 或 Kotlin 代码分配的对象的内存
 Native:从 C 或 C++ 代码分配的对象的内存
 Graphics:图形缓冲区队列向屏幕显示像素(包括 GL 表面、GL 纹理等等)所使用的内存
 Stack:您的应用中的原生堆栈和 Java 堆栈使用的内存
 Code:您的应用用于处理代码和资源(如 dex 字节码、 dex 代码、.so 库和字体)的内存
 Others:您的应用使用的系统不确定如何分类的内存
 Allocated:您的应用分配的 Java/Kotlin 对象数。此数字没有计入 C 或 C++ 中分配的对象

  • 窗口5:内存使用量时间轴,它会显示以下内容:

 1.一个堆叠图表,显示每个内存类别当前使用多少内存,如左侧的 y 轴以及顶部的彩色键所示
 2.一条虚线,表示分配的对象数,如右侧的 y 轴所示
 3.每个垃圾回收事件的图标。

(5)查看内存分配

 在时间轴上拖动以选择要查看哪个区域的分配

memory-profiler查看内存分配

对各个窗口进行说明:

  • 1.选定范围:可以拖动选择想分析的数据部分
  • 2.选择要检查的堆:

 image heap:系统启动映像,包含启动期间预加载的类。此处的分配保证绝不会移动或消失
 zygote heap:写时复制堆,其中的应用进程是从 Android 系统中派生的
 app heap:您的应用在其中分配内存的主堆
 JNI heap:显示 Java 原生接口 (JNI) 引用被分配和释放到什么位置的堆

  • 3.选择如何安排分配:

 Arrange by class:根据类名称对所有分配进行分组。这是默认选项
 Arrange by package:根据软件包名称对所有分配进行分组
 Arrange by callstack:将所有分配分组到其对应的调用堆栈

  • 4.过滤器:搜索过滤,matchCase区分大小写,Regex使用正则表达式
  • 5.类名窗口,点击可以进入Instance View(实例窗口)
  • 6.实例窗口,点击进入Call Stack(实例分配的线程)
  • 7.线程窗口,显示该实例被分配到何处以及在哪个线程中,右键 -> Jump to Source

从上图可以查看对象分配的以下信息:

  • 分配了哪些类型的对象以及它们使用多少空间。
  • 每个分配的堆栈轨迹,包括在哪个线程中。
  • 对象在何时被取消分配。

(6)捕获堆转储

memory-profiler捕获堆转储

窗口说明:

  • 1.执行堆转储:我在MemoryProfilerActivity页面做了一些操作后,back回到MainActivity,等待1s后,执行堆转储(本次主要分析MemoryProfilerActivity的泄漏问题)
  • 2.页面泄漏过滤:筛选Activity 和 Fragment 实例存在内存泄露的分析数据,过滤器显示的数据类型包括:

 已销毁但仍被引用的 Activity 实例(t调试技巧:旋转屏幕/应用切换)  没有有效的 FragmentManager 但仍被引用的 Fragment 实例

  • 3.本项目类过滤:只显示本项目的类
  • 4.类窗口详细信息:

Allocations:堆中的分配数
Native Size : 此对象类型使用的原生内存总量(以字节为单位),你会在此处看到采用 Java 分配的某些对象的内存,因为 Android 对某些框架类(如 Bitmap)使用原生内存
Shallow Size:此对象类型使用的 Java 内存总量(以字节为单位)
Retained Size:为此类的所有实例而保留的内存总大小(以字节为单位)

  • 5.实例窗口详细信息:

Depth:从任意 GC 根到选定实例的最短跳数
Native Size:原生内存中此实例的大小
Shallow Size:Java 内存中此实例的大小
Retained Size:此实例所支配内存的大小

  • 6.实例引用:显示对应实例对象的每个引用,可以右键 -> Jump to Source/Go to Instance(跳转到相对应的实例进行查看)

捕获堆转储后,您可以查看以下信息:

  • 您的应用分配了哪些类型的对象,以及每种对象有多少。
  • 每个对象当前使用多少内存。
  • 在代码中的什么位置保持着对每个对象的引用。
  • 对象所分配到的调用堆栈

(7)示例代码

(一)Handler内存泄漏演练,先看下代码:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate: ");
        setContentView(R.layout.activity_memory_profiler);
        mResultTv = findViewById(R.id.memory_type_result);
        ((RadioGroup) findViewById(R.id.memory_handler_type)).
                setOnCheckedChangeListener(mOnCheckedChangeListener);
    }

    private RadioGroup.OnCheckedChangeListener mOnCheckedChangeListener =
            new RadioGroup.OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(RadioGroup radioGroup, int i) {
            switch (i) {
                case R.id.handler_no_static:
                    memoryTestType = MEMORY_ERROR_NO_STATIC;
                    break;
                case R.id.handler_inner_class:
                    memoryTestType = MEMORY_ERROR_INNER_CLASS;
                    break;
                case R.id.handler_static_weak:
                    memoryTestType = MEMORY_NORMAL_STATIC_WEAK;
                    break;
                case R.id.handler_no_static_clear_message:
                    memoryTestType = MEMORY_NORMAL_MESSAGE_CLEAR;
                    break;
                default:
                    break;
            }
        }
    };

    public void onMemoryHandlerMonitor(View view) {
        Log.d(TAG, "onMemoryHandlerMonitor: ");
        monitorHandler();
    }

    //模拟Handler泄漏(memory profiler以这个来演示实操)
    private static final int MESSAGE_DELAY = 5 * 1000;

    private static final int MEMORY_ERROR_NO_STATIC = 1;
    private static final int MEMORY_ERROR_INNER_CLASS = 2;
    private static final int MEMORY_NORMAL_STATIC_WEAK = 3;
    private static final int MEMORY_NORMAL_MESSAGE_CLEAR = 4;

    //只要动态修改此参数就行
    private int memoryTestType = MEMORY_ERROR_NO_STATIC;
    TextView mResultTv;
    private Handler mHandler;

    @Override
    protected void onDestroy() {
        Log.d(TAG, "onDestroy: ");
        if (MEMORY_NORMAL_MESSAGE_CLEAR == memoryTestType) {
            mHandler.removeCallbacksAndMessages(null);
        }
        super.onDestroy();
    }

    //Button click 触发模拟操作
    private void monitorHandler() {
        Log.d(TAG, "monitorHandler: ");
        switch (memoryTestType) {
            case MEMORY_ERROR_NO_STATIC:
                //泄漏1 : 使用no-static内部类
                mHandler = new NoStaticHandler();
                break;
            case MEMORY_ERROR_INNER_CLASS:
                //泄漏2:使用匿名内部类
                mHandler = new Handler() {
                    @Override
                    public void handleMessage(@NonNull Message msg) {
                        Log.d(TAG, "inner class handler handleMessage: " + msg.what);
                        mResultTv.setText("MEMORY_ERROR_INNER_CLASS");
                    }
                };
                break;
            case MEMORY_NORMAL_STATIC_WEAK:
                //不泄露3:静态+弱引用(为了保证Handler中消息队列中的所有消息都能被执行,
                //推荐使用 静态内部类 + 弱引用的方式)
                mHandler = new StaticHandler(this);
                break;
            case MEMORY_NORMAL_MESSAGE_CLEAR:
                //不泄露4:destroy()方法对handler进行消息队列清空,结束Handler生命周期
                mHandler = new NoStaticHandler();
                break;
            default:
                Log.e(TAG, "monitorHandler: default no handle");
                return;
        }

        //执行事件延迟队列发送
        mHandler.sendEmptyMessageDelayed(1, MESSAGE_DELAY);
    }

    //no-static内部类:自定义Handler子类
    class NoStaticHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            Log.d(TAG, "NoStaticHandler handleMessage: " + msg.what);
            mResultTv.setText(memoryTestType == MEMORY_ERROR_NO_STATIC ?
                    "MEMORY_ERROR_NO_STATIC" : "MEMORY_NORMAL_MESSAGE_CLEAR");
        }
    }

    private static class StaticHandler extends Handler {
        private WeakReference<Activity> reference;

        public StaticHandler(Activity activity) {
            super(activity.getMainLooper());
            reference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(@NonNull Message msg) {
            Log.d(TAG, "StaticHandler handleMessage: " + msg.what);
            Activity activity = reference.get();
            if (null != activity) {
                Log.d(TAG, "StaticHandler handleMessage: ui update");
                ((TextView) activity.findViewById(R.id.memory_type_result)).
                        setText("MEMORY_NORMAL_STATIC_WEAK");
            }
        }
    }
复制代码

(二)MemoryProfilerActivity界面如下:

内存优化模拟界面

(三)我们对第六步的截图【memory-profiler捕获堆转储】增加操作步骤说明:
 上面的堆转储测试的是Handler的非静态内部类,流程如下:
  1.进入主界面MainActivity,点击进入MemoryProfilerActivity
  2.选择【handler使用no-static内部类】,点击【模拟handler泄漏】
  3.点击【back】返回MainActivity,等待1秒,点击AS(AndroidStudio)的【堆转储按钮】
  4.生成堆转储,准备下面的分析

(四)分析上面的堆转储文件
  1.我们选择AS的【Activity/Fragment Leaks】查看发现确实有MemoryProfilerActivity泄漏;
  2.在类名窗口中我们点击泄漏的Actvity类,在 Instance View中跟踪发现是NoStaticHandler持有了MemoryProfilerActivity的引用,而NoStaticHandler的消息队列中仍然存在消息,所以导致MemoryProfilerActivity在执行堆转储时,是不满足GC条件的。
  3.AS的【Activity/Fragment Leaks】是指是否满足GC条件,而不是是否已经GC。举个栗子:在进入一个Activity后(不执行任何操作),然后退出,这当然不会引起内存泄漏,因为它满足GC条件,但是也不是说它会立马被GC掉,你会在堆转储文件中可以看到此实例任然存在(可能会存在很久),如果你尝试N次主动GC,然后再看堆转储文件,发现就被GC掉了。
  下面就让我们去优化它,走起!

(五)优化Handler内存泄漏:静态内部类 + 弱引用的方式
  1.同样按照第三步的操作顺序执行,只是要选择【handler静态内部类 + 弱引用】
  2.查看堆转储文件,查看【Activity/Fragment Leaks】下面,是不是MemoryProfilerActivity没有内存泄漏了?

memory-profiler捕获堆转储-优化后

(六)技巧分享
 AS这个IDE的代码风险提示做的很好,大家在开发过程中要注意特殊颜色的标识,然后按照提示将其优化掉,基本就能解决大部分问题。大家可以在SamplePop的代码中感受下那些可能会引起内存泄漏等问题的特殊颜色标志:

内存泄漏IDE提示-1

内存泄漏IDE提示-2

(七)课后小练习(奥里给)

    //模拟订阅/反订阅缺失泄漏
    @SuppressLint("MissingPermission")
    private void monitorRegister() {
        Log.d(TAG, "monitorRegister: ");
        LocationManager locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
        locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
                TimeUnit.MINUTES.toMillis(5), 100, new LocationListener() {
                    @Override
                    public void onLocationChanged(@NonNull Location location) {
                        Log.d(TAG, "onLocationChanged: ");
                    }
                });
    }

    //模拟非静态内部类
    private static Object inner;
    private void monitorNoStaticInnerClass() {
        Log.d(TAG, "monitorNoStaticInnerClass: ");
        class InnerClass {

        }
        inner = new InnerClass();
    }
复制代码

小编的扩展链接

参考链接

唯有美景不可辜负

   ❤ 比心 ❤  

分类:
Android
分类:
Android