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

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

相关推荐
臣臣臣臣臣什么臣5 小时前
uni-app 多文件上传:直接循环调用 uni.uploadFile 实现(并行 / 串行双模式)
android·前端
Kapaseker6 小时前
每个Kotlin开发者应该掌握的最佳实践,第三趴
android·kotlin
lar_slw6 小时前
flutter json转实体类
android·flutter·json
用户2018792831677 小时前
轻松理解Ashmem实现原理
android
苏世-顾长歌8 小时前
Android studio导入OpenCV报“Unresolved reference: android“
android·opencv·android studio
qq_252924198 小时前
PHP 8.0+ 高级特性深度探索:架构设计与性能优化
android·性能优化·php
恋猫de小郭8 小时前
基于 Dart 的 Terminal UI ,pixel_prompt 这个 TUI 库了解下
android·前端·flutter
怪兽20148 小时前
谈一谈Java成员变量,局部变量和静态变量的创建和回收时机
android·面试
usabcd210 小时前
如何重新编译HyperLPR原生库以消除16k对齐警告
android·c++·cmake·ndk·mnn·16k对齐·hyperlpr