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次数
less
//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内存的上限和当前已使用的内存。使用方式如下。
csharp
/***通过 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,保存了这些类型的数据。
arduino
// 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;
使用方式如下
typescript
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强制"杀掉",下次启动应用时就能查询到被强制"杀掉"的信息。 获取方式:
ini
@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);
}
}
当进程因为下面这些原因退出时可以查询到记录:
less
@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日志:
ruby
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:
ini
boolean lowRamDevice = activityManager.isLowRamDevice();
当设备物理内存小于等于1GB 时这个 API返回 true。 有时候我们需要更灵活的判断标准,那就需要获取到设备的物理内存总数及剩余可用存。我们可以通过ActivityManager.getMemoryInfo查询设备的物理内存总数及剩余可用内存:
scss
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]。
ini
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很接近。
ini
ActivityManager.RunningAppProcessInfo processInfo = new ActivityManager.
RunningAppProcessInfo();
ActivityManager.getMyMemoryState(processInfo);//importance 可以用于判断是否前后台
//待续.................