脚本宝典收集整理的这篇文章主要介绍了深入探索 Android 内存优化(炼狱级别-下),脚本宝典觉得挺不错的,现在分享给大家,也给大家做个参考。
本篇是 AndROId 内存优化的进阶篇,难度可以说达到了炼狱级别,建议对内存优化不是非常熟悉的仔细看看前篇文章: Android性能优化之内存优化,其中详细分析了以下几大模块:
如果你对以上基础内容都比较了解了,那么我们便开始 Android 内存优化的探索之旅吧。
本篇文章非常长,建议收藏后慢慢享用~
内存达到阈值后自动触发 Hprof Dump,将得到的 Hprof 存档后由人工通过 MAT 进行分析。
检测和分析报告都在一起,批量自动化测试和事后分析都不太方便。
Matrix => ResourceCanary 实现原理
目前,它的主要功能有 三个部分,如下所示:
自动化测试由测试平台进行,分析则由监控平台的服务端离线完成,最后再通知相关开发解决问题。
获取 需要的类和对象相关的字符串 信息即可,其它数据都可以在客户端裁剪,一般能 Hprof 大小会减小至原来的 1/10 左右。
方便通过减少冗余 Bitmap 的数量,以降低内存消耗。
在研发阶段需要不断实现 更多的工具和组件,以此系统化地提升自动化程度,以最终 提升发现问题的效率。
除了常用的内存分析工具 Memory Profiler、MAT、LeakCanary 之外,还有一些其它的内存分析工具,下面我将一一为大家进行介绍。
top 命令是 Linux 下常用的性能分析工具,能够 实时显示系统中各个进程的资源占用状况,类似于 Windows 的任务管理器。top 命令提供了 实时的对系统处理器的状态监视。它将 显示系统中 CPU 最“敏感”的任务列表。该命令可以按 CPU使用、内存使用和执行时间 对任务进行排序。
接下来,我们输入以下命令查看top命令的用法:
quchao@quchaodeMacBook-Pro ~ % adb shell top --help
usage: top [-Hbq] [-k FIELD,] [-o FIELD,] [-s SORT] [-n NUMBER] [-d SECONDS] [-p PID,] [-u USER,]
Show process activity in real time.
-H Show threads
-k Fallback sort FIELDS (default -S,-%CPU,-ETIME,-PID)
-o Show FIELDS (def PID,USER,PR,NI,VIRT,RES,SHR,S,%CPU,%MEM,TIME+,CMDLINE)
-O Add FIELDS (replacing PR,NI,VIRT,RES,SHR,S From default)
-s Sort by field number (1-X, default 9)
-b Batch mode (no tty)
-d Delay SECONDS between each cycle (default 3)
-n Exit after NUMBER iterations
-p Show these PIDs
-u Show these USERs
-q Quiet (no header lines)
Cursor LEFT/RIGHT to change sort, UP/DOWN move list, space to force
update, R to reverse sort, Q to exit.
这里使用 top 仅显示一次进程信息,以便来讲解进程信息中各字段的含义。
前四行 是当前系统情况 整体的统计信息区。下面我们看每一行信息的具体意义。
具体信息说明如下所示:
系统现在共有 729 个进程,其中处于 运行中 的有 1 个,715 个在 休眠(sleep),stoped 状态的有0个,zombie 状态(僵尸)的有 8 个。
具体信息如下所示:
具体信息说明如下所示:
具体属性说明如下所示:
对于内存监控,在 top 里我们要时刻监控 第三行 swap 交换分区的 used,如果这个数值在不断的变化,说明内核在不断进行内存和 swap 的数据交换,这是真正的内存不够用了。
在 第五行及以下,就是各进程(任务)的状态监控,项目列信息说明如下所示:
从上图中可以看到,第一行的就是 Awesome-WanAndroid 这个应用的进程,它的进程名称为 json.chao.COM.w+,PID 为 23104,进程所有者 USER 为 u0_a714,进程优先级 PR 为 10,nice 置 NI 为 -10。进程使用的虚拟内存总量 VIRT 为 4.3GB,进程使用的、未被换出的物理内存大小 RES 为138M,共享内存大小 SHR 为 66M,进程状态 S 是睡眠状态,上次更新到现在的 CPU 时间占用百分比 %CPU 为 21.2。进程使用的物理内存百分比 %MEM 为 2.4%,进程使用的 CPU 时间 TIME+ 为 1:47.58 / 100小时。
在讲解 dumpsys meminfo 命令之前,我们必须先了解下 Android 中最重要的 四大内存指标 的概念,如下表所示:
内存指标 | 英文全称 | 含义 | 等价 |
---|---|---|---|
USS | unique Set Size | 物理内存 | 进程独占的内存 |
PSS | Proportional Set Size | 物理内存 | PSS = USS + 按比例包含共享库 |
RSS | Resident Set Size | 物理内存 | RSS= USS+ 包含共享库 |
VSS | Virtual Set Size | 虚拟内存 | VSS= RSS+ 未分配实际物理内存 |
从上可知,它们之间内存的大小关系为 VSS >= RSS >= PSS >= USS。
RSS 与 PSS 相似,也包含进程共享内存,但比较麻烦的是 RSS 并没有把共享内存大小全都平分到使用共享的进程头上,以至于所有进程的 RSS 相加会超过物理内存很多。而 VSS 是虚拟地址,它的上限与进程的可访问地址空间有关,和当前进程的内存使用关系并不大。比如有很多的 map 内存也被算在其中,我们都知道,file 的 map 内存对应的可能是一个文件或硬盘,或者某个奇怪的设备,它与进程使用内存并没有多少关系。
而 PSS、USS 最大的不同在于 “共享内存“(比如两个 App 使用 MMAP 方式打开同一个文件,那么打开文件而使用的这部分内存就是共享的),USS不包含进程间共享的内存,而PSS包含。这也造成了USS因为缺少共享内存,所有进程的USS相加要小于物理内存大小的原因。
最早的时候官方就推荐使用 PSS 曲线图来衡量 App 的物理内存占用,而 Android 4.4 之后才加入 USS。但是 PSS,有个很大的问题,就是 ”共享内存“,考虑一种情况,如果 A 进程与 B 进程都会使用一个共享 SO 库,那么 So 库中初始化所用掉的那部分内存就会平分到 A 与 B 的头上。但是 A 是在 B 之后启动的,那么对于 B 的 PSS 曲线而言,在 A 启动的那一刻,即使 B 没有做任何事情,也会出现一个比较大的阶梯状下滑,这会给用曲线图分析软件内存的行为造成致命的麻烦。
USS 虽然没有这个问题,但是由于 Dalvik 虚拟机申请内存牵扯到 GC 时延和多种 GC 策略,这些都会影响到曲线的异常波动。例如异步 GC 是 Android 4.0 以上系统很重要的特性,但是 GC 什么时候结束?曲线什么时候”降低“?就 无法预计 了。还有 GC 策略,什么时候开始增加 Dalvik 虚拟机的预申请内存大小(Dalvik 启动时是有一个标称的 start 内存大小,它是为 Java 代码运行时预留的,避免 Java 运行时再申请而造成卡顿),但是这个 预申请大小是动态变化的,这一点也会 造成 USS 忽大忽小。
了解完 Android 内存的性能指标之后,下面我们便来说说 dumpsys meminfo 这个命令的用法,首先我们输入 adb shell dumpsys meminfo -h 查看它的帮助文档:
quchao@quchaodeMacBook-Pro ~ % adb shell dumpsys meminfo -h
meminfo dump options: [-a] [-d] [-c] [-s] [--oom] [process]
-a: include all available information for each process.
-d: include dalvik details.
-c: dump in a compact machine-parseable representation.
-s: dump only summary of application memory usage.
-S: dump also SwapPss.
--oom: only show processes organized by oom adj.
--local: only collect details locally, don't call process.
--package: interpret process arg as package, dumping all
processes that have loaded that package.
--checkin: dump data for a checkin
If [process] is specified it can be the name or
pid of a specific process to dump.
接着,我们之间输入adb shell dumpsys meminfo命令:
quchao@quchaodeMacBook-Pro ~ % adb shell dumpsys meminfo
Applications Memory Usage (in Kilobytes):
Uptime: 257501238 Realtime: 257501238
// 根据进程PSS占用值从大到小排序
Total PSS by process:
308,049K: com.tencent.mm (pid 3760 / activities)
225,081K: system (pid 2088)
189,038K: com.android.systemui (pid 2297 / activities)
188,877K: com.miui.home (pid 2672 / activities)
176,665K: com.plan.kot32.tomatotime (pid 22744 / activities)
175,231K: json.chao.com.wanandroid (pid 23104 / activities)
126,918K: com.tencent.mobileQQ (pid 23741)
...
// 以oom来划分,会详细列举所有的类别的进程
Total PSS by OOM adjustment:
432,013K: Native
76,700K: Surfaceflinger (pid 784)
59,084K: android.hardware.camera.provider@2.4-service (pid 743)
26,524K: transport (pid 23418)
25,249K: logd (pid 597)
11,413K: media.codec (pid 1303)
10,648K: rild (pid 1304)
9,283K: media.extractor (pid 1297)
...
661,294K: Persistent
225,081K: system (pid 2088)
189,038K: com.android.systemui (pid 2297 / activities)
103,050K: com.xiaomi.finddevice (pid 3134)
39,098K: com.android.phone (pid 2656)
25,583K: com.miui.daemon (pid 3078)
...
219,795K: Foreground
175,231K: json.chao.com.wanandroid (pid 23104 / activities)
44,564K: com.miui.securitycenter.remote (pid 2986)
246,529K: Visible
71,002K: com.sohu.inputmethod.sogou.xiaomi (pid 4820)
52,305K: com.miui.miwallpaper (pid 2579)
40,982K: com.miui.powerkeeper (pid 3218)
24,604K: com.miui.systemAdSolution (pid 7986)
14,198K: com.xiaomi.metoknlp (pid 3506)
13,820K: com.miui.voiceassist:core (pid 8722)
13,222K: com.miui.Analytics (pid 8037)
7,046K: com.miui.hybrid:entrance (pid 7922)
5,104K: com.miui.wmsvc (pid 7887)
4,246K: com.android.smspush (pid 8126)
213,027K: Perceptible
89,780K: com.eg.android.AlipayGphone (pid 8238)
49,033K: com.eg.android.AlipayGphone:push (pid 8204)
23,181K: com.android.thememanager (pid 11057)
13,253K: com.xiaomi.joyose (pid 5558)
10,292K: com.android.updater (pid 3488)
9,807K: com.lbe.security.miui (pid 23060)
9,734K: com.GOOGLE.android.webview:sandboxed_process0 (pid 11150)
7,947K: com.xiaomi.location.fused (pid 3524)
308,049K: Backup
308,049K: com.tencent.mm (pid 3760 / activities)
74,250K: A Services
59,701K: com.tencent.mm:push (pid 7234)
9,247K: com.android.settings:remote (pid 27053)
5,302K: com.xiaomi.drivemode (pid 27009)
199,638K: Home
188,877K: com.miui.home (pid 2672 / activities)
10,761K: com.miui.hybrid (pid 7945)
53,934K: B Services
35,583K: com.tencent.mobileqq:MSF (pid 14119)
6,753K: com.qualcomm.qti.autoregistration (pid 8786)
4,086K: com.qualcomm.qti.callenhancement (pid 26958)
3,809K: com.qualcomm.qti.statsPollManager (pid 26993)
3,703K: com.qualcomm.qti.smcinvokepkgmgr (pid 26976)
692,588K: Cached
176,665K: com.plan.kot32.tomatotime (pid 22744 / activities)
126,918K: com.tencent.mobileqq (pid 23741)
72,928K: com.tencent.mm:tools (pid 18598)
68,208K: com.tencent.mm:sandbox (pid 27333)
55,270K: com.tencent.mm:toolsmp (pid 18842)
24,477K: com.android.mms (pid 27192)
23,865K: com.xiaomi.market (pid 27825)
...
// 按内存的类别来进行划分
Total PSS by category:
957,931K: Native
284,006K: Dalvik
199,750K: Unknown
193,236K: .dex mmap
191,521K: .art mmap
110,581K: .oat mmap
101,472K: .so mmap
94,984K: EGL mtrack
87,321K: Dalvik Other
84,924K: Gfx dev
77,300K: GL mtrack
64,963K: .apk mmap
17,112K: Other mmap
12,935K: Ashmem
3,364K: Stack
2,343K: .ttf mmap
1,375K: Other dev
1,071K: .jar mmap
20K: Cursor
0K: Other mtrack
// 手机整体内存使用情况
Total RAM: 5,847,124K (status normal)
Free RAM: 3,711,324K ( 692,588K cached pss + 2,428,616K cached kernel + 117,492K cached ion + 472,628K free)
Used RAM: 2,864,761K (2,408,529K used pss + 456,232K kernel)
Lost RAM: 184,330K
ZRAM: 174,628K physical used for 625,388K in swap (2,621,436K total swap)
Tuning: 256 (large 512), oom 322,560K, reStore limit 107,520K (high-end-gfx)
根据 dumpsys meminfo 的输出结果,可归结为如下表格:
划分类型 | 排序指标 | 含义 |
---|---|---|
process | PSS | 以进程的PSS从大到小依次排序显示,每行显示一个进程,一般用来做初步的竞品分析 |
OOM adj | PSS | 展示当前系统内部运行的所有Android进程的内存状态和被杀顺序,越靠近下方的进程越容易被杀,排序按照一套复杂的算法,算法涵盖了前后台、服务或节目、可见与否、老化等 |
category | PSS | 以Dalvik/Native/.art mmap/.dex map等划分并按降序列出各类进程的总PSS分布情况 |
total | - | 总内存、剩余内存、可用内存、其他内存 |
此外,为了 查看单个 App 进程的内存信息,我们可以输入如下命令:
dumpsys meminfo <pid> // 输出指定pid的某一进程
dumpsys meminfo --package <packagename> // 输出指定包名的进程,可能包含多个进程
这里我们输入 adb shell dumpsys meminfo 23104 这条命令,其中 23104 为 Awesome-WanAndroid App 的 pid,结果如下所示:
quchao@quchaodeMacBook-Pro ~ % adb shell dumpsys meminfo 23104
Applications Memory Usage (in Kilobytes):
Uptime: 258375231 Realtime: 258375231
** MEMINFO in pid 23104 [json.chao.com.wanandroid] **
Pss Private Private SwapPss Heap Heap Heap
Total Dirty Clean Dirty Size Alloc Free
------ ------ ------ ------ ------ ------ ------
Native Heap 46674 46620 0 164 80384 60559 19824
Dalvik Heap 6949 6912 16 23 12064 6032 6032
Dalvik Other 7672 7672 0 0
Stack 108 108 0 0
Ashmem 134 132 0 0
Gfx dev 16036 16036 0 0
Other dev 12 0 12 0
.so mmap 3360 228 1084 27
.jar mmap 8 8 0 0
.apk mmap 28279 11328 11584 0
.ttf mmap 295 0 80 0
.dex mmap 7780 20 4908 0
.oat mmap 660 0 92 0
.art mmap 8509 8028 104 69
Other mmap 982 8 848 0
EGL mtrack 29388 29388 0 0
GL mtrack 14864 14864 0 0
Unknown 2532 2500 8 20
TOTAL 174545 143852 18736 303 92448 66591 25856
App Summary
Pss(KB)
------
Java Heap: 15044
Native Heap: 46620
Code: 29332
Stack: 108
Graphics: 60288
Private Other: 11196
System: 11957
TOTAL: 174545 TOTAL SWAP PSS: 303
Objects
Views: 171 ViewRootImpl: 1
AppContexts: 3 Activities: 1
Assets: 18 AssetManagers: 6
Local Binders: 32 Proxy Binders: 27
Parcel memory: 11 Parcel count: 45
Death Recipients: 1 OpenSSL Sockets: 0
WebViews: 0
SQL
MEMORY_USED: 371
PAGECACHE_OVERFLOW: 72 MALLOC_Size: 117
DATABASES
pgsz dbsz Lookaside(b) cache Dbname
4 60 109 151/32/18 /data/user/0/json.chao.com.wanandroid/databases/bugly_db_
4 20 19 0/15/1 /data/user/0/json.chao.com.wanandroid/databases/aws_wan_android.db
该命令输出了 进程的内存概要,我们应该着重关注 四个要点,下面我将一一进行讲解。
如果 Views 与 Activities、AppContexts 持续上升,则表明有内存泄漏的风险。
LeakInspector 是腾讯内部的使用的 一站式内存泄漏解决方案,它是 Android 手机经过长期积累和提炼、集内存泄漏检测、自动修复系统Bug、自动回收已泄露Activity内资源、自动分析GC链、白名单过滤 等功能于一体,并 深度对接研发流程、自动分析责任人并提缺陷单的全链路体系。
它们之间主要有 四个方面 的不同,如下所示:
它们都支持对 Activity、Fragment 及其它自定义类的泄漏检测,但是,LeakInspector 还 增加了 BTiamp 的检测能力,如下所示:
这一个部分的实现原理,我们可以采用 ARTHook 的方式来实现,还不清楚的朋友请再仔细看看大图检测的部分。
两个工具的泄漏检测原理都是在 onDestroy 时检查弱引用,不同之处在于 LeakInspector 直接使用 WeakReference 来检测对象是否已经被释放,而 LeakCanary 则使用 ReferenceQueue,两者效果是一样的。
并且针对 Activity,我们通常都会使用 Application的 registerActivityLifecycleCallbacks 来注册 Activity 的生命周期,以重写 onActivityDestroyed 方法实现。但是在 Android 4.0 以下,系统并没有提供这个方法,为了避免手动在每一个 Activity 的 onDestroy 中去添加这份代码,我们可以使用 反射 Instrumentation 来截获 onDestory,以降低接入成本。代码如下所示:
Class<?> clazz = Class.forName("android.app.ActivityThread");
Method method = clazz.getDeclaredMethod("currentActivityThread", null);
method.setAccessible(true);
sCurrentActivityThread = method.invoke(null, null);
Field field = sCurrentActivityThread.getClass().getDeclareDField("mInstumentation");
field.setAccessible(true);
field.set(sCurrentActivityThread, new MonitorInstumentation());
两者都能采集 dump,但是 LeakInspector 提供了回调方法,我们可以增加更多的自定义信息,如运行时 Log、trace、dumpsys meminfo 等信息,以辅助分析定位问题。
这里的白名单是为了处理一些系统引起的泄漏问题,以及一些因为 业务逻辑要开后门的情形而设置 的。分析时如果碰到白名单上标识的类,则不对这个泄漏做后续的处理。二者的配置差异有如下两点:
1)、LeakInspector 的白名单以 XML 配置的形式存放在服务器上。
1)、而LeakCanary的白名单是直接写死在其源码的AndroidExcludedRefs类里。
2)、LeakCanary 的系统白名单里定义的类比 LeakInspector 中定义的多很多,因为它没有自动修复系统泄漏功能。
针对系统泄漏,LeakInspector 通过 反射自动修复 了目前碰到的一些系统泄漏,只要在 onDestory 里面 调用 一个修复系统泄漏的方法即可。而 LeakCanary 虽然能识别系统泄漏,但是它仅仅对该类问题给出了分析,没有提供实际可用的解决方案。
如果检测到发生了内存泄漏,LeakInspector 会对整个 Activity 的 View 进行遍历,把图片资源等一些占内存的数据释放掉,保证此次泄漏只会泄漏一个Activity的空壳,尽量减少对内存的影响。代码大致如下所示:
if (View instanceof ImageView) {
// ImageView ImageButton处理
recycleimageView(app, (ImageView) view);
} else if (view instanceof TextView) {
// 释放TextView、Button周边图片资源
recycleTextView((TextView) view);
} else if (View instanceof ProgressBar) {
recycleProgressBar((ProgressBar) view);
} else {
if (view instancof android.widget.ListView) {
recycleListView((android.widget.ListView) view);
} else if (view instanceof android.support.v7.widget.RecyclerView) {
recycleRecyclerView((android.support.v7.widget.RecyclerView) view);
} else if (view instanceof FrameLayout) {
recycleFrameLayout((FrameLayout) view);
} else if (view instanceof LinearLayout) {
recycleLinearLayout((LinearLayout) view);
}
if (view instanceof ViewGroup) {
recycleViewGroup(app, (ViewGroup) view);
}
}
这里以 recycleTextView 为例,它回收资源的方式如下所示:
private static void recycleTextView(TextView tv) {
Drawable[] ds = tv.getCompoundDrawables();
for (Drawable d : ds) {
if (d != null) {
d.setCallback(null);
}
}
tv.setCompoundDrawables(null, null, null, null);
// 取消焦点,让Editor$Blink这个Runnable不再被post,解决内存泄漏。
tv.setCursorVisible(false);
}
采集 dump 之后,LeakInspector 会上传 dump 文件,并* 调用 MAT 命令行来进行分析*,得到这次泄漏的 GC 链。而 LeakCanary 则用开源组件 HAHA 来分析得到一个 GC 链。但是 LeakCanary 得到的 GC 链包含被 hold 住的类对象,一般都不需要用 MAT 打开 Hporf 即可解决问题。而 LeakInpsector 得到的 GC 链只有类名,还需要 MAT 打开 Hprof 才能具体去定位问题,不是很方便。
LeakInspector 在 dump 分析结束之后,会提交缺陷单,并且把缺陷单分配给对应类的负责人。如果发现重复的问题则更新旧单,同时具备重新打开单等状态转换逻辑。而 LeakCanary 仅会在通知栏提醒用户,需要用户自己记录该问题并做后续处理。
LeakInspector 跟自动化测试可以无缝结合,当自动化脚本执行中发现内存泄漏,可以由它采集 dump 并发送到服务进行分析,最后提单,整个流程是不需要人力介入的。而 LeakCanary 则把分析结果通过通知栏告知用户,需要人工介入才能进入下一个流程。
JHat 是 oracle 推出的一款 Hprof 分析软件,它和 MAT 并称为 Java 内存静态分析利器。不同于 MAT 的单人界面式分析,jHat 使用多人界面式分析。它被 内置在 JDK 中,在命令行中输入 jhat 命令可查看有没有相应的命令。
quchao@quchaodeMacBook-Pro ~ % jhat
ERROR: No arguments supplied
Usage: jhat [-stack <bool>] [-refs <bool>] [-port <port>] [-baseline <file>] [-debug <int>] [-version] [-h|-help] <file>
-J<flag> Pass <flag> directly to the runtime system. For
example, -J-mx512m to use a maximum heap size of 512MB
-stack false: Turn off tracking object allocation call stack.
-refs false: Turn off tracking of references to objects
-port <port>: Set the port for the HTTP server. defaults to 7000
-exclude <file>: Specify a file that lists data members that should
be excluded from the reachableFrom query.
-baseline <file>: Specify a baseline object dump. Objects in
both heap dumps with the same ID and same class will
be marked as not being "new".
-debug <int>: Set debug level.
0: No debug output
1: Debug hprof file parsing
2: Debug hprof file parsing, no server
-version Report version number
-h|-help Print this help and exit
<file> The file to read
For a dump file that contains multiple heap dumps,
you may specify which dump in the file
by appending "#<number>" to the file name, i.e. "foo.hprof#3".
出现如上输出,则表明存在 jhat 命令。它的使用很简单,直在命令行输入 jhat xxx.hprof 即可,如下所示:
quchao@quchaodeMacBook-Pro ~ % jhat Documents/heapdump/new-33.hprof
Snapshot read, resolving...
Resolving 408200 objects...
Chasing references, expect 81 dots.................................................................................
Eliminating duplicate references.................................................................................
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.
jHat 的执行过程是解析 Hprof 文件,然后启动 httpsrv 服务,默认是在 7000 端口监听 Web 客户端链接,维护 Hprof 解析后的数据,以持续供给 Web 客户端进行查询操作。
启动服务器后,我们打开 入口地址 127.0.0.1:7000 即可查看 All Classes 界面,如下图所示:
jHat 还有两个比较重要的功能,分别如下所示:
打开 127.0.0.1:7000/histo/,统计表界面如下所示:
可以到,按 Total Size 降序 排列了所有的 Class,并且,我们还可以查看到每一个 Class 与之对应的实例数量。
OQL 是一种模仿 SQL 语句的查询语句,通常用来查询某个类的实例数量,打开 127.0.0.1:7000/oql/ 并输入 java.lang.String 查询 String 实例的数量,结果如下图所示:
JHat 比 MAT 更加灵活,且符合大型团队安装简单、团队协作的需求。但是,并不适合中小型高效沟通型团队使用。
GC Log 分为 Dalvik 和 ART 的 GC 日志,关于 Dalvik 的 GC 日志,我们在前篇 Android性能优化之内存优化 中已经详细讲解过了,接下来我们说说 ART 的 GC 日志。
ART 的日志与 Dalvik 的日志差距非常大,除了格式不同之外,打印的时间也不同,而且,它只有在慢 GC 时才会打印出来。下面我们看看这条 ART GC Log:
Explicit | (full) | concurrent mark sweep GC | freed 104710 (7MB) AllocSpace objects, | 21(416KB) LOS objects, | 33% free,25MB/38MB | paused 1.230ms total 67.216ms |
---|---|---|---|---|---|---|
GC产生的原因 | GC类型 | 采集方法 | 释放的数量和占用的空间 | 释放的大对象数量和所占用的空间 | 堆中空闲空间的百分比和(对象的个数)/(堆的总空间) | 暂停耗时 |
GC 产生的原因有如下九种:
GC 类型有如下三种:
GC 采集的方法有如下四种:
通过 GC 日志,我们可以知道 GC 的量和 它对卡顿的影响,也可以 初步定位一些如主动调用GC、可分配的内存不足、过多使用Weak Reference 等问题。
对于 HTML5 页面而言,抓取 JavaScript 的内存需要使用 Chrome Devtools 来进行远程调试。方式有如下两种:
Android 4.4 及以上系统的原生浏览器就是 Chrome 浏览器,可以使用 Chrome Devtool 远程调试 WebView,前提是需要在 App 的代码里把调试开关打开,如下代码所示:
if (Build.VERSION_SDK_INT >= Build.VERSION_CODES.KITKAT && 是debug模式) {
WebView.setWebContentsDebuggingEnabled(ture);
}
打开后的调试方法跟纯 H5 页面调试方法一样,直接在 App 中打开 H5 页面,再到 PC Chrome 的 inpsector 页面就可以看到调试目标页面。
这里总结一下 JS 中几种常见的内存问题点:
若想更深入地学习 Chrome 开发者工具的使用方法,请查看 《Chrome开发者工具中文手册》。
在我们进行内存优化的过程中,有许多内存问题都可以归结为一类问题,为了便于以后快速地解决类似的内存问题,我将它们归结成了以下的多个要点:
说道内类就不得不提到 ”**this0“∗∗,它是一种奇特的内类成员,每个类实例都具有一个this0“**,它是一种奇特的内类成员,每个类实例都具有一个 this0“∗∗,它是一种奇特的内类成员,每个类实例都具有一个this0,当它的内类需要访问它的成员时,内类就会持有外类的 this0,通过this0,通过 this0,通过this0 就可以访问外部类所有的成员。
解决方案是在 Activity 关闭,即触发 onDestory 时解除内类和外部的引用关系。
这也是一个 this$0 间接引用的问题,对于 Handler 的解决方案一般可以归结为如下三个步骤:
这里需要在使用过程中注意对 WeakReference 进行判空。
如果在闪屏页跳转到登录界面时没有调用 finish(),则会造成闪屏页的内存泄漏,在碰到这种”过渡界面“的情况时,需要注意不要产生这样的内存 Bug。
我们通常都会使用 getSystemService 方法来获取系统服务,但是当在 Activity 中调用时,会默认把 Activity 的 Context 传给系统服务,在某些不确定的情况下,某些系统服务内部会产生异常,从而 hold 住外界传入的 Context。
解决方案是 直接使用 Applicaiton 的 Context 去获取系统服务。
我们都知道,对应 WebView 来说,其 网络延时、引擎 Session 管理、Cookies 管理、引擎内核线程、HTML5 调用系统声音、视频播放组件等产生的引用链条无法及时打断,造成的内存问题基本上可以用”无解“来形容。
解决方案是我们可以 把 WebView 装入另一个进程。 具体为在 AndroidManifes 中对当前的 Activity 设置 android:process 属性即可,最后,在 Activity 的 onDestory 中退出进程,这样即可基本上终结 WebView 造成的泄漏。
我们在平常开发过程中经常需要在Activity创建的时候去注册一些组件,如广播、定时器、事件总线等等。这个时候我们应该在适当的时候对组件进行注销,如 onPause 或 onDestory 方法中。
不仅在使用 Handler 的 sendMessage 方法时,我们需要在 onDestory 中使用 removeCallbackAndMessage 移除回调和消息,在使用到 Handler / FrameLayout 的 postDelyed 方法时,我们需要调用 removeCallbacks 去移除实现控件内部的延时器对 Runnable 内类的持有。
在做资源适配的时候,因为需要考虑到 APK 的瘦身问题,无法为每张图片在每个 drawable / mipmap 目录下安置一张适配图片的副本。很多同学不知道图片应该放哪个目录,如果放到分辨率低的目录如 hdpi 目录,则可能会造成内存问题,这个时候建议尽量问设计人员要高品质图片然后往高密度目录下方,如 xxhdpi 目录,这样 在低密屏上”放大倍数“是小于1的,在保证画质的前提下,内存也是可控的。也可以使用 Drawable.createFromSream 替换 getResources().getDrawable 来加载,这样便可以绕过 Android 的默认适配规则。
对于已经被用户使用物理“返回键”退回到后台的进程,如果包含了以下 两点,则 不会被轻易杀死。
但建议 在运行一段时间(如3小时)后主动保存界面进程(位于后台),然后重启它,这样可以有效地降低内存负载。
我们应该在 item 被回收不可见时去释放掉对图片的引用。如果你使用的是 ListView,由于每次 item 被回收后被再次利用都会去重新绑定数据,所以只需在 ImageView 回调其 onDetchFromWindow 方法的时候区释放掉图片的引用即可。如果你使用的是 RecyclerView,因为被回收不可见时第一次选择是放进 mCacheView中,但是这里面的 item 被复用时并不会去执行 bindViewHolder 来重新绑定数据,只有被回收进 mRecyclePool 后拿出来复用才会重新绑定数据。所以此时我们应该在 item 被回收进 RecyclePool 的时候去释放图片的引用,这里我们只要去 重写 Adapter 中的 onViewRecycled 方法 就可以了,代码如下所示:
@override
public void onViewRecycled(@Nullable VH holder) {
super.onViewRecycled(holder);
if (holder != null) {
//做释放图片引用的操作
}
}
我们应该使用 ViewStub 对那些没有马上用到的资源去做延迟加载,并且还有很多大概率不会出现的 View 更要去做懒加载,这样可以等到要使用时再去为它们分配相应的内存。
产品或者运营为了统计数据会在每个版本中不断地增加新的埋点。所以我们需要定期地去清理一些过时的埋点,以此来 适当地优化内存以及CPU的压力。
我们在做子线程操作的时候,喜欢使用匿名内部类 Runnable 来操作。但是,如果某个 Activity 放在线程池中的任务不能及时执行完毕,在 Activity 销毁时很容易导致内存泄漏。因为这个匿名内部类 Runnable 类持有一个指向 Outer 类的引用,这样一来如果 Activity 里面的 Runnable 不能及时执行,就会使它外围的 Activity 无法释放,产生内存泄漏。从上面的分析可知,只要在 Activity 退出时没有这个引用即可,那我们就通过反射,在 Runnable 进入线程池前先干掉它,代码如下所示:
Field f = job.getClass().getDeclaredField("this$0");
f.setAccessible(true);
f.set(job, null);
这个任务就是我们的 Runnable 对象,而 ”this$0“ 就是上面所指的外部类的引用了。这里注意使用 WeakReference 装起来,要执行了先 get 一下,如果是 null 则说明 Activity 已经回收,任务就放弃执行。
我们发现我们的 APP 在内存方面可能存在很大的问题,第一方面的原因是我们的线上的 OOM 率比较高。
第二点呢,我们经常会看到在我们的 Android Studio 的 Profiler 工具中内存的抖动比较频繁。
这是我们一个初步的现状,然后在我们知道了这个初步的现状之后,进行了问题的确认,我们经过一系列的调研以及深入研究,我们最终发现我们的项目中存在以下几点大问题,比如说:内存抖动、内存溢出、内存泄漏,还有我们的Bitmap 使用非常粗犷。
比如 内存抖动的解决 => Memory Profiler 工具的使用(呈现了锯齿张图形) => 分析到具体代码存在的问题(频繁被调用的方法中出现了日志字符串的拼接),也可以说说 内存泄漏或内存溢出的解决。
为了不增加业务同学的工作量,我们使用了一些工具类或 ARTHook 这样的 大图检测方案,没有任何的侵入性。同时,我们将这些技术教给了大家,然后让大家一起进行 工作效率上的提升。
我们对内存优化工具Profiler Memory、MAT 的使用比较熟悉,因此 针对一系列不同问题的情况,我们写了 一系列解决方案的文档,分享给大家。这样,我们 整个团队成员的内存优化意识就变强 了。
我们一开始并没有直接去分析项目中代码哪些地方存在内存问题,而是先去学习了 Google 官方的一些文档,比如说学习了 Memory Profiler 工具的使用、学习了 MAT 工具的使用,在我们将这些工具学习熟练之后,当在我们的项目中遇到内存问题时,我们就能够很快地进行排查定位问题进行解决。
一开始,我们做了整体 APP 运行阶段的一个内存上报,然后,我们在一些重点的内存消耗模块进行了一些监控,但是,后面发现这些监控并没有紧密地结合我们的业务代码,比如说在梳理完项目之后,发现我们项目中存在使用多个图片库的情况,多个图片库的内存缓存肯定是不公用的,所以 导致我们整个项目的内存使用量非常高。所以进行技术优化时必须结合我们的业务代码。
我们在做内存优化的过程中,不仅做了 Android 端的优化工作,还将我们 Android 端一些数据的采集上报到了我们的服务器,然后传到我们的 APM 后台,这样,方便我们的无论是 Bug 跟踪人员或者是 Crash 跟踪人员进行一系列问题的解决。
比如说 大图片的检测,我们最初的一个方案是通过继承 ImageView,重写 它的 onDraw 方法来实现。但是,我们在推广它的过程中,发现很多开发人员并不接受,因为很多 ImageView 之前已经写过了,你现在让他去替换,工作成本是比较高的。所以说,后来我们就想,有没有一种方案可以 免替换,最终我们就找到了 ARTHook 这样一个 Hook 的方案。
对于 内存优化的专项优化 而言,我们要着重注意两点,即 优化大方向 和 优化细节。
对于 优化的大方向,我们应该 优先去做见效快的地方,主要有以下三部分:
对于 优化细节,我们应该 注意一些系统属性或内存回调的使用 等等,主要可以细分为如下六部分:
在这篇文章中,我们除了建立了 内存的监控闭环 这一核心体系之外,还实现了以下 十大组件 / 策略:
最后,当监控到 应用内存超过阈值时,还定制了 完善的兜底策略 来 重启应用进程。
总的来看,要建立一套 全面且成体系的内存优化及监控 是非常重要也是极具挑战性的一项工作。并且,目前各大公司的 内存优化体系 也正处于 不断演进的历程 之中,其目的不外乎:实现更健全的功能、更深层次的定位问题、快速准确地发现线上问题。
路漫漫其修远兮,吾将上下而求索
1、国内Top团队大牛带你玩转Android性能分析与优化 第四章 内存优化
2、极客时间之Android开发高手课 内存优化
3、微信 Android 终端内存优化实践
4、GMTC-Android内存泄漏自动化链路分析组件Probe.key
5、Manage your app's memory
6、Overview of memory management
7、Android内存优化杂谈
8、Android性能优化之内存篇
9、管理应用的内存
10、《Android移动性能实战》第二章 内存
11、每天一个linux命令(44):top命令
12、Android内存分析命令
本文转自 https://juejin.cn/post/6872919545728729095,如有侵权,请联系删除。
【2021最新版】Android studio安装教程+Android(安卓)零基础教程视频(适合Android 0基础,Android初学入门)含音视频_哔哩哔哩_bilibili
Android进阶系统学习——Jetpack先天优秀的基因可以避免数据内存泄漏_哔哩哔哩_bilibili
以上是脚本宝典为你收集整理的深入探索 Android 内存优化(炼狱级别-下)全部内容,希望文章能够帮你解决深入探索 Android 内存优化(炼狱级别-下)所遇到的问题。
本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。