为什么图片的三级缓存,内存是第一位
- 硬件快:内存本身读取、存入速度快
- 复用快:解码成果有效保存,复用时,直接使用解码后对象,而不是再做一次图像解码。
解码:常见的jpg,png等图片格式,都是把 “像素缓冲” 使用不同的手段压缩后的结果,所以这些格式的图片,
要在设备上展示,就 必须经过一次解码,它的 执行速度会受图片压缩比、尺寸等因素影响。
(官方建议:把从内存中淘汰的图片,降低压缩比后存储到本地,以备后用,这样可以最大限度地降低以后复用时的解码开销。)
Android 内存管理机制
- 应用程序的内存分配和垃圾回收都是由Android虚拟机完成的,在Android 5.0以下,使用的是Dalvik虚拟机,5.0及以上,则使用的是ART虚拟机;
- Java 对象生命周期
- Java代码编译后生成的字节码.class文件从文件系统中加载到虚拟机之后,便有了JVM上的Java对象,Java对象在JVM上运行有7个阶段:
- Created(创建): 分配存储空间,构造对象,超类及子类静态成员初始化,超类及子类成员变量按顺序初始化
- InUse(应用): 此时对象至少被一个强引用持有
- Invisible(不可见): 程序本身不再持有该对象的任何强引用, 但是,该对象仍可能被虚拟机下的某些已装载的静态变量线程或JNI等强引用持有,这些特殊的强引用称为“GC Root”, 导致该对象的内存泄漏,因而无法被GC回收;
- Unreachable(不可达): 该对象不再被任何强引用持有
- Collected(收集):当GC已经对该对象的内存空间重新分配做好准备时,对象进入收集阶段,如果该对象重写了finalize()方法,则执行它;
- Finalized(终结):等待垃圾回收器回收该对象空间
- Deallocated(对象空间重新分配):GC对该对象所占用的内存空间进行回收或者再分配,则该对象彻底消失;
- Java 内存分配模型
- JVM 将整个内存划分为了几块:
- 方法区:存储类信息、常量、静态变量等,所有线程共享(存静态常量)
- 虚拟机栈:存储局部变量表、操作数栈等(存Java变量引用)
- 本地方法栈:存native变量引用
- 堆:内存最大的区域,对象的实际存储位置,栈中只是对象的引用,GC和内存泄漏的主战场,所有线程共享(存对象)
- 程序计数器:计算当前线程的当前方法执行到多少行
- Android 内存回收机制
- Android 设备每打开一个 APP, 内存都是弹性分配的,并且其分配值与最大值受具体设备而定;
- 注意区分如下两种 OOM 场景:
- 内存真正不足:例如 APP 当前进程最大内存上限为 512 MB,当超过这个值就表明内存真正不足了;
- 可用内存不足:手机系统内存极度紧张,就算 APP 当前进程最大内存上限为 512 MB,我们只分配了 200 MB,也会产生内存溢出,因为系统的可用内存不足了;
- GC的三种类型
- kGcCauseForAlloc:分配内存不够引起的GC,会Stop World。由于是并发GC,其它线程都会停止,直到GC完成。
- kGcCauseBackground:内存达到一定阈值触发的GC,由于是一个后台GC,所以不会引起Stop World。
- kGcCauseExplicit:显示调用时进行的GC,当ART打开这个选项时,使用System.gc时会进行GC。
内存问题三个分类
- 内存抖动:内存波动图形呈锯齿状、GC导致卡顿,Dalvik上比较明显,ART在内存管理和回收策略上做了大量优化,内存分配和GC效率提升了5~10倍,抖动概率较小;
- 为什么内存抖动会导致 OOM?
- 频繁创建对象,导致内存不足及碎片(不连续)
- 不连续的内存片无法被分配,导致OOM
- 为什么内存抖动会导致 OOM?
- 内存泄漏:在当前应用周期内不再使用的对象被GC Roots引用,导致不能回收,使实际可使用内存变小
- 内存溢出:即OOM,Android设备出厂以后,java虚拟机对单个应用的最大内存分配就确定下来了,超出这个值就会OOM
内存可能造成的两个问题
- 异常: 包括 OOM、内存分配失败这些崩溃,也包括因为整体内存不足导致应用被杀死、设备重启等问题
- 卡顿: 除了频繁 GC 造成卡顿之外,物理内存不足时系统会触发 low memory killer 机制,系统负载过高是造成卡顿的另外一个原因
Low Memory Killer 机制
针对于手机系统所有进程而制定的,当我们手机内存不足的情况下,LMK 机制就会针对我们所有进程进行回收,而其对于不同的进程,它的回收力度也是有不同的,目前系统的进程类型主要有如下几种:前台进程, 可见进程, 服务进程, 后台进程, 空进程;从前台进程到空进程,进程优先级会越来越低,因此,它被 LMK 机制杀死的几率也会相应变大。此外,LMK 机制也会综合考虑回收收益,这样就能保证我们大多数进程不会出现内存不足的情况
内存优化主要包括两方面
- 优化RAM,即降低运行时内存
- 手机不使用 PC 的 DDR内存,采用的是 LPDDR RAM,即 ”低功耗双倍数据速率内存“。
- 优化ROM,即降低程序占ROM的体积
内存泄露的检测与修改
- 监控方案: Square的开源库 LeakCanary
- 或使用基于 LeakCanary 的改进版 ResourceCanary Matrix
- 对系统内存泄露的Hack Fix:
- AndroidExcludedRefs列出了一些由于系统原因导致引用无法释放的例子,同时对于大多数的例子,都会提供建议如何通过hack的建议去修复
- 通过兜底回收内存
- Activity泄漏会导致该Activity引用到的Bitmap、DrawingCache等无法释放,对内存造成大的压力,兜底回收是指对于已泄漏Activity,尝试回收其持有的资源,泄漏的仅仅是一个Activity空壳,从而降低对内存的压力。做法也非常简单,在Activity onDestory时候从view的rootview开始,递归释放所有子view涉及的图片,背景,DrawingCache,监听器等等资源,让Activity成为一个不占资源的空壳,泄露了也不会导致图片资源被持有。
使用MAT来查找内存泄漏
- 在https://eclipse.org/mat/downloads.php下载MAT客户端。
- 从Android Studio进入Profile的Memory视图,选择需要分析的应用进程,对应用进行怀疑有内存问题的操作,结束操作后,主动GC几次,最后export dump文件
- 因为Android Studio保存的是Android Dalvik/ART格式的.hprof文件,所以需要转换成J2SE HPROF格式才能被MAT识别和分析;
Android SDK自带了一个转换工具在SDK的platform-tools下,转换命令为:
./hprof-conv file.hprof converted.hprof
- 通过MAT打开转换后的HPROF文件。
优化内存的意义
- 减少OOM,提高应用稳定性。
- 减少卡顿,提高应用流畅度(如GC次数增多导致)。
- 减少内存占用,提高应用后台运行时的存活率(防止Low Memory Killer)。
- 减少异常发生和代码逻辑隐患。
常见内存泄漏场景
- 资源对象未关闭
- 解决方法:应该在对象不再使用或Activity销毁时及时调用close方法,再置为null;
- 注册对象未注销
- 如BroadcastReceiver、EventBus未注销;
- 解决方法:Activity销毁时注销
- 类的静态变量持有大数据对象
- 解决方法:应尽量避免使用静态变量存储数据,特别是大数据对象;
- 单例造成的内存泄漏
- 解决方法:优先使用Application的Context,如需使用Activity的Context,可以在传入Context时使用弱引用进行封装,然后,在使用到的地方从弱引用中获取Context,如果获取不到,则直接return即可;
- 非静态内部类的静态实例
- 该实例的生命周期和应用一样长,这就导致该静态实例一直持有该Activity的引用,Activity的内存资源不能正常回收。
- 解决方法:1. 静态内部类+弱引用; 2. Activity 关闭,即触发 onDestory 时解除内类和外部的引用关系
- Handler临时性内存泄漏
- handler发送的Message存储在MessageQueue中,Message中的target是handler的一个引用,如果handler是非静态的,或持有Activity或service的强引用,Activity退出时,消息队列中还有未处理的消息或者正在处理的消息,就会到主Activity的资源无法回收;
- 解决方法:1. 静态内部类+弱引用;2. Destroy或者Stop时removeCallbacksAndMessages;
- AsyncTask内部也是Handler机制, 对于类似AsyncTask或是线程造成的内存泄漏,我们也可以将AsyncTask和Runnable类独立出来或者使用静态内部类。
- 容器中的对象没清理
- 解决方法:退出之前,将集合clear,然后置为null
- WebView
- 在应用中只要使用一次WebView,内存就不会被释放掉
- 其 网络延时、引擎 Session 管理、Cookies 管理、引擎内核线程、HTML5 调用系统声音、视频播放组件等产生的引用链条无法及时打断,造成的内存问题基本上可以用”无解“来形容。
- 解决方法:把 WebView 装入另一个进程。具体为在 AndroidManifest 中对当前的 Activity 设置 android:process 属性即可,最后,在 Activity 的 onDestory 中退出进程,这样即可基本上终结 WebView 造成的泄漏。
- 使用ListView时造成的内存泄漏
- 解决方法:构造Adapter时,使用缓存的convertView 或使用RecyclerView
- 使用系统服务时产生的内存问题
- getSystemService 方法来获取系统服务,但是当在 Activity 中调用时,会默认把 Activity 的 Context 传给系统服务,在某些不确定的情况下,某些系统服务内部会产生异常,从而 hold 住外界传入的 Context。
- 解决方案是 直接使用 Application 的 Context 去获取系统服务
优化内存
- 尽量避免AutoBoxing)(自动装箱),减少字符串使用加号拼接,改为使用StringBuilder
- 读文件优化:读文件使用ByteArrayPool,初始设置capacity,减少expand
- 内存复用
- 资源复用:通用的字符串、颜色定义、简单页面布局的复用。
- 视图复用:可以使用ViewHolder实现ConvertView复用。
- 对象池:显示创建对象池,实现复用逻辑,对相同的类型数据使用同一块内存空间。
- Bitmap对象的复用:使用inBitmap属性可以告知Bitmap解码器尝试使用已经存在的内存区域,
新解码的bitmap会尝试使用之前那张bitmap在heap中占据的pixel data内存区域。
- 减少不必要或不合理的对象:例如在onDraw、getView中应减少对象申请,尽量重用。更多是一些逻辑上的东西,例如循环中不断申请局部变量等
- 选用合理的数据格式:
- 使用ArrayMap, SparseArray, SparseBooleanArray, and LongSparseArray来代替HashMap,
- 使用 IntDef和StringDef 替代枚举类型;
- 使用 LruCache 最近最少使用缓存
- Bitmap 优化 两种方法:
- 统一图片库
- 收拢图片的调用,这样我们可以做整体的控制策略。
- 例如低端机使用 565 格式、更加严格的缩放算法(如通过二次采样压缩,LuBan库,哈夫曼算法压缩),当显示小图片或对图片质量要求不高时可以考虑使用RGB_565,用户头像或圆角图片一般可以尝试ARGB_4444;
- 可以使用 Glide、Fresco 或者采取自研(如通过inBitmap+LruCache+软引用实现图片的三级缓存)都可以,而且需要进一步将所有 Bitmap.createBitmap、BitmapFactory 相关的接口也一并收拢;
- 图片资源优化:只需要UI提供一套高分辨率的图,图片建议放在drawable-xxhdpi文件夹下,这样在低分辨率设备中图片的大小只是压缩,不会存在内存增大的情况。如若遇到不需缩放的文件,放在drawable-nodpi文件夹下。
- 统一监控
- 统一图片库后就非常容易监控 Bitmap 的使用情况,主要有三点需要注意
- 大图片监控:可以计算有多少比例的图片会超过屏幕的大小,也就是图片的“超宽率”
- 重复图片监控:Bitmap 的像素数据完全一致,但是有多个不同的对象存在
- 图片总内存:在 OOM 崩溃的时候,也可以把图片占用的总内存、Top N 图片的内存都写到崩溃日志中,帮助我们排查问题。
- 统一图片库
- 在App可用内存过低时主动释放内存:
- onTrimMemory/onLowMemory 方法去释放掉图片缓存、静态缓存来自保;
- item被回收不可见时释放掉对图片的引用:
- ListView: 在ImageView onDetachFromWindow的时候释放掉图片引用;
- RecyclerView: 只有被回收进mRecyclePool中后拿出来复用才会重新绑定数据,因此重写Recycler.Adapter中的onViewRecycled()方法来使item被回收进RecyclePool的时候去释放图片引用;
- 其他:尽使用static final 优化成员变量,使用增强型for循环语法;
- 已经被用户使用物理“返回键”退回到后台的进程
- 如果包含了以下 两点,则 不会被轻易杀死:
- 进程包含了服务 startService,而服务本身调用了 startForeground(低版本需通过反射调用)
- 主 Activity 没有实现 onSaveInstanceState 接口
- 建议 在运行一段时间(如3小时)后主动保存界面进程(位于后台),然后重启它,这样可以有效地降低内存负载(详见下面的内存兜底策略)。
- 如果包含了以下 两点,则 不会被轻易杀死:
- 使用 ViewStub 进行占位:
- 对那些没有马上用到的资源去做延迟加载,并且还有很多大概率不会出现的 View 更要去做懒加载
- 定时清理 App 过时的埋点数据
设置内存兜底策略
- 目的是为了在用户无感知的情况下,在接近触发系统异常前,选择合适的场景杀死进程并将其重启,从而使得应用内存占用回到正常情况;
- 通常执行内存兜底策略时至少需要满足六个条件,只有在满足了以下条件之后,我们才会去杀死当前主进程并通过 push 进程重新拉起及初始化;
- 是否在主界面退到后台且位于后台时间超过 30min
- 当前时间为早上 2~5 点
- 不存在前台服务(通知栏、音乐播放栏等情况)
- Java heap 必须大于当前进程最大可分配的85% || native内存大于800MB
- vmsize 超过了4G(32bit)的85%
- 非大量的流量消耗(不超过1M/min) && 进程无大量CPU调度情况
更深入的内存优化策略
- 使 bitmap 资源在 native 中分配(参考Fresco)
- 图片加载时的降级处理:
- 使用 Glide、Fresco 等图片加载库,通过定制,在加载 bitmap 时,若发生 OOM,则使用 try catch 将其捕获,然后清除图片 cache,尝试降低 bitmap format(ARGB8888、RGB565、ARGB4444、ALPHA8)
- 前台每隔 3 分钟去获取当前应用内存占最大内存的比例,超过设定的危险阈值(如80%)则主动
释放应用 cache(Bitmap 为大头),并且显示地除去应用的 memory,以加速内存收集的过程。WindowManagerGlobal.getInstance().startTrimMemory(TRIM_MEMORY_COMPLETE);
- 由于 webview 存在内存系统泄漏,还有 图库占用内存过多 的问题,可以采用单独的进程。
- 用类似 Hack 的方式修复系统内存泄漏:(参考booster)
- 当应用使用的Service不再使用时应该销毁它,建议使用 IntentServcie
- 当UI隐藏时释放内存:在所有 UI 组件都隐藏的时候会接收到 Activity 的 onTrimMemory() 回调并带有参数 TRIM_MEMORY_UI_HIDDEN
- 谨慎使用第三方库,避免为了使用其中一两个功能而导入一个大而全的解决方案。
内存优化和架构设计时的两个误区
- 内存占用越少越好
- 当系统内存充足的时候,我们可以多用一些获得更好的性能。当系统内存不足的时候,希望可以做到“用时分配,及时释放”
- 回顾一下 Android Bitmap 内存分配的变化
- 在 Android 3.0 之前,Bitmap 对象放在 Java 堆,而像素数据是放在 Native 内存中。需要手动调用 recycle
- Android 3.0~Android 7.0 将 Bitmap 对象和像素数据统一放到 Java 堆中,无需手动调用 recycle; 不过 Bitmap 是内存消耗的大户,把它的内存放到 Java 堆中似乎不是那么美妙。即使华为 Mate 20,最大的 Java 堆限制也才到 512MB,可能我的物理内存还有 5GB,但是应用还是会因为 Java 堆内存不足导致 OOM
既然讲到了将图片的内存放到 Native 中,我们比较熟悉的是 Fresco 图片库在 Dalvik 会把图片放到 Native 内存中。 事实上在 Android 5.0~Android 7.0,也能做到相同的效果,只是流程相对复杂一些。 不过这个“黑科技”有两个主要问题,一个是兼容性问题,另外一个是频繁申请释放 Java Bitmap 容易导致内存抖动 // 步骤一:调用 libandroid_runtime.so 中的构造函数申请一张空的 Native Bitmap Bitmap nativeBitmap = nativeCreateBitmap(dstWidth, dstHeight, nativeConfig, 22); // 步骤二:申请一张普通的 Java Bitmap Bitmap srcBitmap = BitmapFactory.decodeResource(res, id); // 步骤三:使用 Java Bitmap 将内容绘制到 Native Bitmap 中 mNativeCanvas.setBitmap(nativeBitmap); mNativeCanvas.drawBitmap(srcBitmap, mSrcRect, mDstRect, mPaint); // 步骤四:释放 Java Bitmap 内存 srcBitmap.recycle(); srcBitmap = null;
- Android 8.0 使用NativeAllocationRegistry辅助回收 Native 内存的机制,来实现像素数据放到 Native 内存中。Android 8.0 还新增了硬件位图 Hardware Bitmap,它可以减少图片内存并提升绘制效率
- Native 内存不用管
- 当系统物理内存不足时,lmk 开始杀进程,从后台、桌面、服务、前台,直到手机重启
测量方法
- Java 内存分配
- 跟踪 Java 堆内存的使用情况,这个时候最常用的有 Allocation Tracker 和 MAT 这两个工具。
- Allocation Tracker 的三个缺点:
- 获取的信息过于分散,中间夹杂着不少其他的信息
- 跟 Traceview 一样,无法做到自动化分析,每次都需要开发者手工开始 / 结束
- 在停止的时候,直到把数据 dump 出来之前,经常会把手机完全卡死
- Native 内存分配
- Malloc 调试可以帮助我们去调试 Native 内存的一些使用问题,例如堆破坏、内存泄漏、非法地址等。Android 8.0 之后支持在非 root 的设备做 Native 内存调试,不过跟 AddressSanitize 一样,需要通过wrap.sh做包装
- Malloc 钩子是在 Android P 之后,Android 的 libc 支持拦截在程序执行期间发生的所有分配 / 释放调用,这样我们就可以构建出自定义的内存检测工具。
内存优化工具
除了常用的内存分析工具 Memory Profiler、MAT、LeakCanary 之外
- top
- top 命令是 Linux 下常用的性能分析工具,能够 实时显示系统中各个进程的资源占用状况,类似于 Windows 的任务管理器。top 命令提供了 实时的对系统处理器的状态监视。它将 显示系统中 CPU 最“敏感”的任务列表。该命令可以按 CPU使用、内存使用和执行时间 对任务进行排序。
- 输入adb shell top --help 查看它的帮助文档
- dumpsys meminfo
- 输入 adb shell dumpsys meminfo -h 查看它的帮助文档
- LeakInspector
- 腾讯内部的使用的 一站式内存泄漏解决方案,它是 Android 手机经过长期积累和提炼、集内存泄漏检测、自动修复系统Bug、自动回收已泄露Activity内资源、自动分析GC链、白名单过滤 等功能于一体,并 深度对接研发流程、自动分析责任人并提缺陷单的全链路体系。
- JHat
- Oracle 推出的一款 Hprof 分析软件,它和 MAT 并称为 Java 内存静态分析利器。不同于 MAT 的单人界面式分析,jHat 使用多人界面式分析。它被 内置在 JDK 中,在命令行中输入 jhat 命令可查看有没有相应的命令。
- ART GC Log
- GC Log 分为 Dalvik 和 ART 的 GC 日志
- Chrome DevTool
- 对于 HTML5 页面而言,抓取 JavaScript 的内存需要使用 Chrome Devtools 来进行远程调试。方式有如下两种:
- 直接把 URL 抓取出来放到 Chrome 里访问。
- 用 Android H5 远程调试
- 纯H5:
- 手机安装 Chrome,打开 USB 调试模式,通过 USB 连上电脑,在 Chrome 里打开一个页面,比如百度页面。
然后在 PC Chrome 地址栏里访问 Chrome://inspect - 直接点击 Chrome 下面的 inspect 选项即可弹出开发者工具界面
- 手机安装 Chrome,打开 USB 调试模式,通过 USB 连上电脑,在 Chrome 里打开一个页面,比如百度页面。
- Hybrid H5 调试
- (就是我们应用中内嵌h5)
- Android 4.4 及以上系统的原生浏览器就是 Chrome 浏览器,可以使用 Chrome Devtool 远程调试 WebView,前提是需要在 App 的代码里把调试开关打开,如下代码所示:
打开后的调试方法跟纯 H5 页面调试方法一样,直接在 App 中打开 H5 页面,再到 PC Chrome 的 inpsector 页面就可以看到调试目标页面。if (Build.VERSION_SDK_INT >= Build.VERSION_CODES.KITKAT && 是debug模式) { WebView.setWebContentsDebuggingEnabled(ture); }
- 对于 HTML5 页面而言,抓取 JavaScript 的内存需要使用 Chrome Devtools 来进行远程调试。方式有如下两种:
内存优化,应该从哪里着手
-
设备分级
- 内存优化首先需要根据设备环境来综合考虑,当然这需要有一个良好的架构设计支撑,在架构设计时需要做到以下几点
- 设备分级
- 低端机用户可以关闭复杂的动画,或者是某些功能;使用 565 格式的图片,使用更小的缓存内存等,
- 低端机避免使用多进程,一个空进程 也会占用 10MB 内存
(可以根据手机的内存、CPU 核心数和频率等信息决定设备属于哪一个年份) - 针对低端机用户推出 4MB 的轻量版本,例如各种极速版app,如抖音极速版,腾讯视频极速版
- 缓存管理
- 需要有一套统一的缓存管理机制,可以适当地使用内存;当“系统有难”时,也要义不容辞地归还。
- 我们可以使用 OnTrimMemory 回调,根据不同的状态决定释放多少内存。
- 方便监控每个模块的缓存大小;
- 进程模型
- 减少应用启动的进程数、减少常驻进程、有节操的保活,对低端机内存优化非常重要
- 安装包大小
- 安装包中的代码、资源、图片以及 so 库的体积,跟它们占用的内存有很大的关系。
统一图片库,重复图片检测,大图检测等,详见上面的 Bitmap 优化 两种方法:
-
内存泄漏
- 就是没有回收不再使用的内存,主要分两种情况
- 同一个对象泄漏
- 每次都会泄漏新的对象
- 优秀的框架设计可以减少甚至避免程序员犯错,当然这不是一件容易的事情,所以我们还需要对内存泄漏建立持续的监控:
- Java 内存泄漏:建立类似 LeakCanary 自动化检测方案,至少做到 Activity 和 Fragment 的泄漏检测;
- OOM 监控:美团有一个 Android 内存泄露自动化链路分析组件Probe,不过有二次崩溃风险
- Native 内存泄漏监控
- 就是没有回收不再使用的内存,主要分两种情况
内存监控
- 内存泄漏的监控存在一些性能的问题,一般只会对内部人员和极少部分的用户开启。
- 采集方式
- 用户在前台的时候,可以每 5 分钟采集一次 PSS、Java 堆、图片总内存, 建议通过采样只统计部分用户;
- 计算指标
- 内存异常率: 内存 UV 异常率 = PSS 超过 400MB 的 UV / 采集 UV
- 触顶率: 内存 UV 触顶率 = Java 堆占用超过最大堆限制的 85% 的 UV / 采集 UV
long javaMax = runtime.maxMemory(); long javaTotal = runtime.totalMemory(); long javaUsed = javaTotal - runtime.freeMemory(); // Java 内存使用超过最大限制的 85% float proportion = (float) javaUsed / javaMax;
- GC 监控
- Debug.startAllocCounting 来监控 Java 内存分配和 GC 的情况
long allocCount = Debug.getGlobalAllocCount(); long allocSize = Debug.getGlobalAllocSize(); long gcCount = Debug.getGlobalGcInvocationCount(); //上面的这些信息似乎不太容易定位问题,在 Android 6.0 之后系统可以拿到更加精准的 GC 信息。 // 运行的GC次数 Debug.getRuntimeStat("art.gc.gc-count"); // GC使用的总耗时,单位是毫秒 Debug.getRuntimeStat("art.gc.gc-time"); // 阻塞式GC的次数 Debug.getRuntimeStat("art.gc.blocking-gc-count"); // 阻塞式GC的总耗时 Debug.getRuntimeStat("art.gc.blocking-gc-time"); //阻塞式 GC 会暂停应用线程,可能导致应用发生卡顿
课后作业
- 脱离 Android Studio 实现一个自定义的 Allocation Tracker,这样就可以将它用到自动化分析中
- 通过分析内存文件hprof快速判断内存中是否存在重复的图片,并且将这些重复图片的PNG、堆栈等信息输出
参考文章
- Android内存优化杂谈
- Android开发高手课-内存优化(上)
- Android内存申请分析
- Android性能优化之内存优化
- 深入探索 Android 内存优化(炼狱级别-上)
- 深入探索 Android 内存优化(炼狱级别-下)
- 微信 Android 终端内存优化实践
- 必知必会 | Android 性能优化的方面方面都在这儿
- 吐血整理!究极深入Android内存优化(三)