1. 为什么要做内存优化
- App崩溃
- 应用后台存活时间短,被系统杀掉
- 应用启动慢、流畅性变差、耗电增加
1.1 虚拟内存不足导致App崩溃
常见的OOM异常如下
- Java OOM
- Native OOM
- Graphics OOM
虚拟内存相关
- 32位设备的所有App,整体虚拟内存上限4GB,系统内核内存占用1GB,因此留给App的可用虚拟内存只有3GB
- 64位设备上的32位App,可用虚拟内存有4GB
- 64位可用设备上的64位App,理论上可用虚拟内存有256TB
1.2 物理内存不足导致App后台存活时间短
LMK杀进程逻辑:获取oom_socre_adj值最大的进程,选择占用内存最多的进程杀掉,如果内存还不足,继续杀;adb shell cat /proc/{pid}/oom_score_adj查看进程的分数。
1.3 GC对应用启动、流畅性的影响
- GC类型有很多种,有些类型的GC会阻塞线程执行,这无疑会影响线程执行速度
- 异步执行GC的线程(线程名HeapTaskDaemon)常常会占用大量的CPU时间片或者抢占大核,导致主线程无法被及时调度(CPU时间片变少,线程状态频繁切换、从大核切换到小核),从而影响应用启动速度、页面流畅性
- 部分版本的GC采用复制算法,会将数据复制到另一块内存,导致CPU缓存失效,代码执行效率降低
- GC过程会获取一把锁,导致主线程那个锁等待
2. 线上内存监控
就崩溃、后台存活时间短、卡顿3个问题,讲述线上内存监控方案
2.1 内存不足导致的崩溃如何监控
2.1.1 OOM次数
//1、自定义崩溃处理器
@Override public class JavaCrashHandler implements Thread.UncaughtExceptionHandier public void uncaughtException (@NonNu11 Thread t, @NonNull
Throwable e) {
//2、判断崩溃类型
if (e instanceof OutOfMemoryError) {
// 发生了 OutOfMemory
//3.记录当前内存使用数据并上报
//4.为线程注册崩溃处理器
Thread.currentThread().setUncaughtExceptionHandler (new JavaCrashHandler());
显示的代码主要分为以下4个步骤。
- 自定义崩溃处理器,实现Thread.UncaughtExceptionHandler定义的方法。
- 在崩溃发生时,会执行其中的uncaughtException方法,并且传递崩溃线程和崩溃类型,我们可以通过崩溃类型判断崩溃是否为内存不足导致的。
- 记录当前内存使用数据并上报。
- 为线程注册崩溃处理器。
2.1.2 内存使用情况
Android应用的内存使用类型可以细分为Java、Native、Graphics,因此我们的内存监控需要上报这些类型的内存的使用情况。 我们可以通过Android SDK中的Runtime和Debug等API获取App的Java内存使用情况。通过Runtime我们可以获取App的Java内存的上限和当前已使用的内存。使用方式如下。
/***通过 Runtime 获取 Java内存使用情况*/
private static void getByRuntime(){
//dalvik 堆最大可用内存
long maxMemory = Runtime.getRuntime () .maxMemory() ;
long freeMemory = Runtime.getRuntime() ,freeMemory () ;
long totalMemory = Runtime.getRuntime () .totalMemory ();
//已使用的内存
double memoryUsedPercent =(totalMemory - freeMemory) * 1.0f / maxMemory * 100;
Log.d(TAG,"memoryUsedPeroent: " + memoryUsedPercent + " %");
Log.d(TAG,"maxMemory:" + formatMB(maxMemory) + ",
totalMemory:" + formatMB(totalMemory) + ", used:" + formatMB(totalMemory-treeMemory));
备注:在AndroidManifest.xml文件中设置android:largeHeap="true"后,最大可用内存: 512MB。
通过Debug.MemoryInfo#getMemoryStats(),我们可以获取到Java、Native、Graphics 等类型的物理内存使用情况,它返回的是一个Map,保存了这些类型的数据。
// android.os.Debug.MemoryInfo的getMemorystats方法
publie Map<String, String> getMemorystats() (
Map<String, String> stats - new HashMap<String,String();
//Java 堆内存实际映射的物理内存
stats,put ("summary,java-heap", Integer,toString(getSummaryJavaHeap());
//Native 堆内存实际映射的物理内存
stats,put ("summary.native-heap",Integer,toString (getSummaryNativeHeap()))
//.dex文件.so文件.art文件.ttf文件等映射的物理内存
stats,put ("summary.code",Integer.toString (getSummaryCode());
//运行时栈空间映射的物理内存
stats.put ("summary.stack", Integer.toString (getSummaryStack()));
//Graphics 相关映射的物理内存
stats.put ("summary.graphics",Integer.toString(getSummaryGraphics());
//...
return stats;
使用方式如下
Debug.MemoryInfo memoryInfo = new Debug.MemoryInfo(): Debug.getMemoryInfo(memoryInfo);
Map<String, String> memoryStats - memoryInfo.getMemoryStats(); Set<Map.Entry<String, String>> entries - memoryStats.entrySet ();
for (Map,Entry<string, String> entry:entries) {
Log.d(TAG,"getByDebugMemoryInfo:"+ entry.getKey ()+":"+entry.getValue());
}
2.2 后台被强制杀掉的问题如何监控
- LMK次数
- 是否低内存设备
- 设备可用内存
- 进程的oom_score和优先级
2.2.1 LMK次数
从Android11开始,Android系统为我们提供了可以直接获取App上次退出的信息的API(ActivityManager.getHistoricalProcessExitReasons),通过它我们可以获取到App退出的原因、当时进程的优先级和物理内存等信息。如果App被LMK强制“杀掉”,下次启动应用时就能查询到被强制“杀掉”的信息。 获取方式:
@RequiresApi(api = Build.VERSION_CODES.R)
private static void getApplicationExitInfo() {
if (sContext == null) {
return;
}
String packageName = sContext.getPackageName();
ActivityManager activityManager =(ActivityManager) sContext.
getSystemService (Context.ACTIVITY_SERVICE);
List<ApplicationExitInfo> historicalProcessExitReasons = activityManager.
getHistoricalProcessExitReasons(packageName, 0, 0);
for (ApplicationExitInfo info : historicalProcessExitReasons) {
int importance = info.getImportance();
int reason = info.getReason();
String processName = info.getProcessName () ;
Log.d(TAG, "ApplicationExitInfo: processName:" + processName +",
reason: " + reason + " , importance:" + importance);
}
}
当进程因为下面这些原因退出时可以查询到记录:
@IntDef(prefix={"REASON_” ),value=
REASON_UNKNOWN,
REASON_EXIT_SELF,
REASON_SIGNALED,
REASON_LOW_MEMORY,
REASON_CRASH,
REASON_CRASH_NATIVE,
REASON_ANR,
REASON_INITIALIZATION_FAILURE,
REASON_PERMISSION_CHANGE,
REASON_EXCESSIVE_RESOURCE_USAGE,
REASON_USER_REQUESTED,
REASON_USER_STOPPED,
REASON_DEPENDENCY_DIED,
REASON_OTHER,
})
@Retention(RetentionPolicy.SOURCE)
public @interface Reason {}
当进程被LMK强制“杀掉”后,进程的退出原因是REASON_LOW_MEMORY。因此每次启动App后查询退出记录,我们就能获取到App不同版本的LMK次数。 另外,App被LMK强制“杀掉”时,也会有对应的Logcat日志:
ActivityManager: Killing 3281:top.shixinzhang.example (adj 900):stop top.
shixinzhang.example
因此在系统低于Android11的手机上,我们可以通过Logcat日志数据判断App是否被强制“杀掉”。具体方式:在崩溃时上报最近的Logcat日志数据,分析其中是否有Killing{pid}:{package Name} (adj xxx)等关键字,如果有则证明App被强制“杀掉”了。
2.2.2 是否为低物理内存设备
ActivityManager为我们提供了查询当前设备是否为低物理内存设备的API:
boolean lowRamDevice = activityManager.isLowRamDevice();
当设备物理内存小于等于1GB 时这个 API返回 true。 有时候我们需要更灵活的判断标准,那就需要获取到设备的物理内存总数及剩余可用存。我们可以通过ActivityManager.getMemoryInfo查询设备的物理内存总数及剩余可用内存:
ActivityManager activityManager = (ActivityManager) sContext.
getSystemService(Context.ACTIVITY_SERVICE);
ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
activityManager,getMemoryInfo (memoryInfo);
printSection("手机操作系统的物理内存是否够用(ActivityManager.getMemoryInfo):"); Log.d(TAG,"RAM size of the device:" + formatMB(memoryInfo.totalMem)+",
availMem:"+formatMB(memoryInfo.availMem)+ ", lowMemory:" + memoryInfo.lowMemory +",
threshold: " + formatMB(memoryInfo.threshold));
- totalMem 整体运行内存上限
- availMem 剩余可用物理内存
- lowMemory 可用内存是否处于很少的状态
- threshold lowMemory的阈值,一般为256MB
我们在发现某个版本App的LMK指标劣化后,可以结合上面的这4个数据,调整下一个版本App的内存使用策略,从而减少触发LMK的概率。
2.2.3 进程的oom_score和优先级
影响 App的后台存活时间的因素除了设备环境,还有App本身的状态,包括oom_score和优先级。在LMK指标有变化时,我们可以通过它们进一步分析是不是因为某个业务需求影响了 App整体的优先级。 首先来了解如何获取App的 oom_score。 什么是 oom_score 呢?可以理解为LMK机制对不同进程的评分,根据应用的优先级动态调整。 LMK在执行进程清理时会根据这个分数决定先清理谁:oom_score越大,进程越容易被杀。 我们可以通过读取/proc/{pid}/oom_score_adj来获取 App的 oom_score。App的oom_ score_adj范围为[-1000,1000]。
try{
String scoreAdjPath = String.format (Locale.CHINA,"/proc/%d/oom_score_adj",
Process.myPid());
String adjPath = String.format(Locale.CHINA,"/proc/%d/oom_adj",
Process. myPid());
String content = FileUtils.file2String(scoreAdjPath);
Log.d(TAG,"oom score_adj path: " + scoreAdjPath +":"+ content);
}
catch (Exception e){
e.printStackTrace();
}
经过测试,在部分使用较旧的操作系统的机型上,App没有权限读取这个节点的数据。不过不用担心,我们还可以通过ActivityManager.getMyMemoryState获取到App的优先级(也称重要性,本书统称“优先级”)数据,对于LMK机制,它的概念和oom_score很接近。
ActivityManager.RunningAppProcessInfo processInfo = new ActivityManager.
RunningAppProcessInfo();
ActivityManager.getMyMemoryState(processInfo);
//importance 可以用于判断是否前后台
//这个值可以结合 oom_score_adj一起判斯 APP 的优先艇
Log.d(TAG,"process importance:"*processInfo.importance+",lastTrimLevel: "+ processInfo.lastTrimLevel);
Android 系统定义的优先级有这些:
@IntDef (prefix = ( "IMPORTANCE_"), value=(
IMPORTANCE_FOREGROUND,
IMPORTANCE_FOREGROUND_SERVICE,
IMPORTANCE_TOP_SLEEPING,
IMPORTANCE_VISIBLE,
IMPORTANCE_PERCEPTIBLE,
IMPORTANCE_CANT_SAVE_STATE,
IMPORTANCE_SERVICE,
IMPORTANCE_CACHED,
IMPORTANCE_GONE,
})
@Retention (RetentionPolicy.SOURCE)
public @interface Importance()
- IMPORTANCE_FOREGROUND 100 用户正在交互的前台、最上层UI进程
- IMPORTANCE_ FOREGROUND_SERVICE 125 在做比较重要的事,比如播放音乐前台服务进程,虽然没有直接和用户交互,但正
- IMPORTANCE_VISIBLE 200 可见但不是最上层的进程,或者是系统级服务
- IMPORTANCE PERCEPTIBLE 230 用户不可见,但可以感知到的进程
- IMPORTANCE SERVICE 300 拥有后台服务的进程
- IMPORTANCE_TOP_SLEEPING 325 前台可见进程,但设备处于休眠状态 无法保存状态的进程,因此在后台时不可被轻易
- IMPORTANCE_CANT_SAVE_STATE 350 置android:cantSaveState-"true"可以让进程具有 “杀掉”,通过在AndroidManifest.xml 文件中设 这个优先级
- IMPORTANCE_CACHED 400 后台进程
- IMPORTANCE_GONE 1000 App优先级最大值
通过查看进程上报的优先级,我们就可以知道进程被“杀掉”时所处的状态。另外,我们也可以根据系统对优先级的判断标准,通过一些手段提升进程的优先级,降低进程被强制“杀掉”的概率。 到这里我们通过LMK次数、设备物理内存情况,以及进程的oom_score和优先,实现了对后台存活相关数据的全方位监控。
2.3 GC对流畅性的影响如何监控
要衡量App是否因为GC而卡顿,需要获取的数据
- GC次数
- GC耗时
- GC线程
- CPU使用时间
- 场景 FPS
2.3.1 GC次数和耗时
GC可以分为两种类型:阻塞式、非阻塞式。
- 阻塞式 GC 是指在进行GC时,会阻塞GC发起线程。
- 非阻塞式GC是指并发执行的GC,不会显式阻塞其他线程。 我们可以通过Debug.getRuntimeStat获取到App当前的GC次数和耗时:
public static long getGCInfoSafely(String info) {
try {
return Long.parseLong(Debug.getRuntimeStat(info));
catch (Throwable throwable) {
throwable。printStackTrace();
return -1;
}
}
private static void getGCInfo() {
long gcCount = getGcInfoSafely("art.gc.gc-count");
long gcTime = getGcInfoSafely("art.gc.gc-time");
long blockGcCount = getGcInfoSafely("art.gc.blocking-gc-count");
long blockGcTime - getGcInfoSafely("art.gc.blocking-gc-time");
}
上面的返回值中,blockGcCount和blockGcTime是App从启动到被查询时阻塞式GC的次数和耗时;gcCount和gcTime是App从启动到被查询时的非阻塞式GC的次数和耗时。 通过对比不同场景的FPS、GC次数、耗时差值,我们可以得出不同场景下的GC对流畅性的影响,从而决定是否需要对当前业务进行GC优化。
2.3.2 GC线程是否频繁
除了GC次数,并发执行的GC线程HeapTaskDaemon的繁忙程度,也可以用于衡量GC的影响。 如果在启动、页面加载等核心场景,HeapTaskDacmon线程的CPU使用时间比主线程的还长,就说明GC对App的性能有严重影响。所以我们需要追踪HeapTaskDaemon的CPU使用时间。 在运行时我们可以通过遍历进程的/proc/{pid}/task,找到名称为HeapTaskDaemon的tid.然后从/proc/ {pid}/task/{tid}/stat中读取CPU使用时间相关数据:
blueline:/proc/2718/task $ ls
20010 2742 2747 2749 2751 2755 2758 2777 2815 2819 2826 2992 3864 4295 4297 4975 4977 4980 4997 5085 5104 5106 5108 5110 5221 5667
2718 2743 2748 2750 2752 2756 2759 2791 2818 2824 2991 3848 4294 4296 4298 4976 4979 4981 4998 5086 5105 5107 5109 5220 5254 8550
blueline:/proc/2718/task $ cat 2749/stat
2749 (HeapTaskDaemon) S 1141 1141 0 0 -1 1077952576 13240 0 0 0 90 10 0 0 24 4 52 0 1217 15975903232 57947 18446744073709551615 1 1 0 0 0 0 20996 1 1073775864 0 0 0 -1 4 0 0 0 0 0 0 0 0 0
0 0 0 0
返回的内容比较复杂,这里我们只关心第14~17部分,它们分别表示HeapTaskDaemon线程的用户态时长和内核态时长,把它们累加起来,就是线程从创建到被查询时的CPU使用时间。 通过将其与主线程的CPU使用时间对比,我们可以判断出HeapTaskDaemon是否执行太频繁,从而决定是否需要对其进行优先级降低。