android 内存优化

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个步骤。

  1. 自定义崩溃处理器,实现Thread.UncaughtExceptionHandler定义的方法。
  2. 在崩溃发生时,会执行其中的uncaughtException方法,并且传递崩溃线程和崩溃类型,我们可以通过崩溃类型判断崩溃是否为内存不足导致的。
  3. 记录当前内存使用数据并上报。
  4. 为线程注册崩溃处理器。

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 可以用于判断是否前后台

//待续.................

相关推荐
千里马学框架7 分钟前
如何改进车载三分屏SplitScreen启动交互方式?
android·智能手机·分屏·aaos·安卓framework开发·车载开发·3分屏
REDcker2 小时前
Android WebView 版本升级方案详解
android·音视频·实时音视频·webview·js·编解码
麦兜*2 小时前
【springboot】图文详解Spring Boot自动配置原理:为什么@SpringBootApplication是核心?
android·java·spring boot·spring·spring cloud·tomcat
le1616162 小时前
Android 依赖种类及区别:远程仓库依赖、打包依赖、模块依赖、本地仓库依赖
android
lxysbly2 小时前
psp模拟器安卓版带金手指
android
云上凯歌3 小时前
02 Spring Boot企业级配置详解
android·spring boot·后端
hqiangtai3 小时前
Android 高级专家技术能力图谱
android·职场和发展
aqi003 小时前
FFmpeg开发笔记(九十七)国产的开源视频剪辑工具AndroidVideoEditor
android·ffmpeg·音视频·直播·流媒体
stevenzqzq3 小时前
Android Koin 注入入门教程
android·kotlin
炼金术4 小时前
SkyPlayer v1.1.0 - 在线视频播放功能更新
android·ffmpeg