杂志的 内存管控 和 OAPM相关

杂志的 内存管控 和 OAPM相关

一、背景

杂志进程基本属于常驻状态,开机或进程死掉就会被 systemui 重新拉起,软工对此类应用的内存有较严格的标准:

  • 开机内存:39M
  • 后台内存:80M
  • 峰值内存:(按标准管控)

目前每个迭代内存基本都在疯狂增长,已远超原标准,对手机内置内存造成威胁。


二、杂志对内存的管控要求和标准

1. 接入或更新 SDK 时

  • 1.1 确认接入的 SDK 体积增量,大于 100K(待讨论) 时需精简:
    • 让 SDK 提供方精简
    • 不影响功能的前提下进行混淆优化
  • 1.2 非必要不启动:确认是否需要开机/进程拉起时初始化 SDK 接口,不需要的一律禁止调用;需要调用的需说明理由。
  • 1.3 SDK 版本统一:一个项目尽量保持 SDK 版本一致,通过 version.gradle 统一管理,避免不同组件使用不同版本。
  • 1.4 按需引入 SDK:若仅需 SDK 部分功能,考虑裁剪后直接引入部分源码,避免全量引入。
  • 1.5 禁止 snapshot 版本上用户版:集成前需接入正式版本,灰度前必须替换掉所有 snapshot 版本。

2. 新增图片等资源

  • 新增图片资源必须进行压缩(需 UI 确认压缩后效果满足需求),大于 100K(待讨论) 需再次评估是否保留。

3. 新增 assets 资源

  • 大于 100K 的 assets 资源需考虑优化,尤其是视频等大资源,需与产品、设计讨论是否有必要放入本地。

4. 无用代码与依赖清理

  • 不提前预埋无用代码;相关功能下线时,必须同步移除所有相关代码及引入的 SDK。

5. 任务与 Service 管理

  • 任务、Service 执行结束后,需及时停用/释放资源。

6. 字符串优化

  • 固定字符串优先用常量定义,避免调用时直接拼接创建。
  • 频繁调用且固定的拼接字符串,建议在返回值中增加 intern() 方法复用实例,减少重复创建。val TAG: String get() = ("VM.$tag").intern()

7. 集合类资源管理

  • 集合类数据不再使用时,需及时调用 clear() 释放,避免积少成多占用内存。

8. 线程池与 HandlerThread 优化

  • 线程池需限制线程数,尤其是核心线程数;可设置核心线程超时退出:allowCoreThreadTimeOut(true)
  • HandlerThread 不再使用时,需及时调用 quit() 退出。

9. 内存峰值与抖动优化

  • 避免短时间内申请大块内存,防止内存峰值飙升、内存抖动,甚至引发 OOM。

10. 递归函数使用

  • 谨慎使用递归函数,避免栈溢出。

11. 常见内存泄漏规避

可参考链接:内存泄漏

  1. Context 正确使用
    • 生命周期比 Activity 长的对象,优先使用 Application 的 Context;需注意 Context 的应用场景。
  2. 静态内部类避免内存泄漏
    • 静态内部类中使用非静态外部成员变量(如 Context、View)时,需用弱引用持有外部类变量。
  3. 无用对象主动释放
    • 不再使用的对象及时赋值为 null,如 Bitmap 使用完后调用 recycle() 再赋值为 null
  4. 关注对象生命周期
    • 重点关注单例、静态对象、全局集合等的生命周期,避免不当持有导致泄漏。
  5. Activity 内部类泄漏规避
    • 对于生命周期比 Activity 长的内部类对象,并且内部类中使用了外部类的成员变量,如下方法可以避免内存泄漏:
      • 1.将内部类改为静态内部类
      • 2.静态内部类总使用弱引用来引用外部类的成员变量。

三、OAPM概述

内存泄露监控,可以监控APP使用过程中,出现 activity/fragment 等组件销毁后不能被回收的内存泄露问题。

四、检测原理

通过监听 activity/fragment 相关生命周期函数,监控其在 GC 时不能被虚拟机正常回收的情形、dump 出 hprof 文件并上报分析,实现内存泄露监控。

四、检测场景

以 Activity 为例,在其回调 onDestroy() 时由于其引用被其他长生命周期对象持有,导致 GC 时 Activity 占用的内存空间不能被正常回收,从而判断发生内存泄漏。

内存泄漏检测场景分为自动检测和主动检测:

  • 自动检测:无需接入方额外接入工作即可自动实现
  • 主动检测:需要接入方主动调用相关 API 实现

4.1 自动检测场景

自动检测场景包含 Activity、Fragment 及 Fragment 对应 View,分别监听如下生命周期方法实现:

  • Activity#onDestroy()
  • Fragment#onFragmentDestroyed()(Android O 以上)
  • Fragment#onFragmentViewDestroyed()(Android O 以上)

4.2 主动检测场景

主动检测场景需要接入方选取合适的时机,主动调用 PerfTest#leakWatch(Object watchedReference) 方法,实现监控任意对象泄漏。

如果是组件化开发,在非主模块中,用反射调用实现主动检测:

public static void leakWatch(Object object) {
    try {
        @SuppressLint("PrivateApi") Class<?> threadClazz = Class.forName(CLASS_PERF_TEST);
        Method method = threadClazz.getMethod("leakWatch", Object.class);
        method.invoke(null, object);
    } catch (Exception e) {
        LogUtils.d(TAG, "leakWatch Exception");
    }
}

五、数据上报

SDK 在监控到内存泄漏时会 dump 应用的内存信息并上报内存信息:

5.1 上报内存泄露相关信息

v2.2.9(不含)以前的版本,SDK 默认会上报如下信息:

  • 泄漏对象
  • 引用链
  • 内存快照

泄漏对象:

com.oapm.sample.test.TestLeakActivity

引用链:

* thread java.lang.Thread. (named Thread-2)
* L com.oapm.sample.test.TestLeakActivity$1.this$0 (anonymous implementation of java.lang.Runnable)
* L com.oapm.sample.test.TestLeakActivity

内存快照:

* Instance of java.lang.Thread
| static threadSeqNumber = 76625
| static defaultUncaughtExceptionHandler = com.android.internal.os.RuntimeInit$UncaughtHandler@316038480 (0x12d65d50)
| static $classOverhead = byte[224]@1863199241 (0x6f0e2a09)
| static SUBCLASS_IMPLEMENTATION_PERMISSION = java.lang.RuntimePermission@1863197248 (0x6f0e2240)
| static MAX_PRIORITY = 10
| static NANOS_PER_MILLI = 1000000
| static threadInitNumber = 4
| static NORM_PRIORITY = 5
| static EMPTY_STACK_TRACE = java.lang.StackTraceElement[0]@1863197232 (0x6f0e2230)
| static MIN_PRIORITY = 1
| blocks = null

5.2 上报 hprof 文件

v2.2.9 开始,支持上报 hprof 文件,由服务端完成内存泄露引用链的分析工作。

示例平台界面信息:

  • 应用:性能测试 DEMO
  • 状态:未处理
  • 场景:1 / 2 / 3 / 4 / 5 / 6 … 15
  • 泄漏对象:com.oapm.sample.ui.TestLeakActivity
  • 提供 hprof下载 功能

引用链(蓝色部分为泄漏分析时提取的关键引用链):

* thread android.os.HandlerThread (named 'perftest_time_update_thread')
* L static c.b.a.h.a.c.$class$dexCache
* L java.lang.DexCache.runtimeInternalObjects
* L array java.lang.Object[].[562]
* L static com.oapm.sample.ui.TestLeakActivity.w
* L com.oapm.sample.ui.TestLeakActivity

内存快照(部分):

* Instance of android.os.HandlerThread
| static $class$name = "android.os.HandlerThread"
| static $class$numReferenceInstanceFields = 2
| static $class$componentType = null
| static $class$iFields = 1892540532
| static $class$clinitThread = 0
| static $class$copiedMethodsOffset = 9
| static $class$referenceInstanceOffsets = -1073741824
| static $class$numReferenceStaticFields = 0

业务方也可自行在 OAPM 平台内存泄露详情页下载该泄露相关的 hprof 文件,并使用 MAT、profiler 等工具分析其他内存问题。

六、内存泄漏产生原因

一、内存泄漏如何产生?

Java 内存泄漏的根本原因是长生命周期的对象持有短生命周期对象的引用,导致短生命周期对象无法被 GC 回收,这就是 Java 中内存泄漏的发生场景。

具体主要有如下几大类:

  1. 静态集合类引起内存泄漏HashMapVector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象 Object 也不能被释放,因为他们也将一直被Vector等引用着。
  2. java运行1 Static Vector v = new Vector(10); 2 for (int i = 1; i<100; i++) 3 { 4 Object o = new Object(); 5 v.add(o); 6 o = null; 7 }监听器在 Java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。
  3. 各种连接比如数据库连接(dataSource.getConnection()),网络连接(socket 和 io 连接),除非显式的调用了其close()方法将其连接关闭,否则是不会自动被 GC 回收的。对于ResultSetStatement对象可以不进行显式回收,但Connection一定要显式回收,因为Connection在任何时候都无法自动回收,而Connection一旦回收,ResultSetStatement对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭ResultSetStatement对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去连接,在finally里面释放连接。
  4. 内部类和外部模块的引用内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。此外程序员还要小心外部模块不经意的引用,内部类是否提供相应的操作去移除外部引用。
  5. 单例模式由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏。单例对象在初始化后将在 JVM 的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么外部对象将不能被 JVM 正常回收,导致内存泄漏。

二、常见的内存泄漏的处理方式

  1. 集合类泄漏集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量(比如类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。因此,在编写代码的时候,集合类需要有成对出现添加和删除或者清空的操作。
  2. 单例造成的泄漏java运行1 public class AppManager { 2 private static AppManager instance; 3 private Context context; 4 private AppManager(Context context) { 5 this.context = context; 6 } 7 public static AppManager getInstance(Context context) { 8 if (instance == null) { 9 instance = new AppManager(context); 10 } 11 return instance; 12 } 13 } 这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要:
    • 如果此时传入的是ApplicationContext,因为Application的生命周期就是整个应用的生命周期,所以这将没有任何问题。
    • 如果此时传入的是ActivityContext,当这个Context对应的Activity退出时,由于该Context的引用被单例对象所持有,其生命周期等于整个应用程序的生命周期,所以当前Activity退出时它的内存并不会被回收,这就造成泄漏了。

3、非静态内部类创建静态实例造成的内存泄漏

public class MainActivity extends AppCompatActivity {
    private static TestResource mResource = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (mManager == null) {
            mManager = new TestResource();
        }
        //...
    }

    class TestResource {
        //...
    }
}

在 Activity 内部创建了一个非静态内部类的单例,每次启动 Activity 时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该 Activity 的引用,导致 Activity 的内存资源不能正常回收。

正确的做法为:将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例。


4、匿名内部类 / 异步线程

AsyncTask 和 Runnable 都使用了匿名内部类,那么它们将持有其所在 Activity 的隐式引用。如果任务在 Activity 销毁之前还未完成,那么将导致 Activity 的内存资源无法被回收,从而造成内存泄漏。

解决方法:将 AsyncTask 和 Runnable 类独立出来或者使用静态内部类,这样便可以避免内存泄漏。


5、Handle 造成的内存泄露

由于 Handler 属于 TLS (Thread Local Storage) 变量,生命周期和 Activity 是不一致的。因此这种实现方式一般很难保证跟 View 或者 Activity 的生命周期保持一致,故很容易导致无法正确释放。

从 Android 的角度

当 Android 应用程序启动时,该应用程序的主线程会自动创建一个 Looper 对象和与之关联的 MessageQueue。当主线程中实例化一个 Handler 对象后,它就会自动与主线程 Looper 的 MessageQueue 关联起来。所有发送到 MessageQueue 的 Message 都会持有 Handler 的引用,所以 Looper 会据此回调 Handle 的 handleMessage () 方法来处理消息。只要 MessageQueue 中有未处理的 Message,Looper 就会不断的从中取出并交给 Handler 处理。另外,主线程的 Looper 对象会伴随该应用程序的整个生命周期。

从 Java 角度

在 Java 中,非静态内部类和匿名内部类都会潜在持有它们所属的外部类的引用,但是静态内部类却不会。

当该 Activity 被 finish () 掉时,延迟执行任务的 Message 还会继续存在于主线程中,它持有该 Activity 的 Handler 引用,所以此时 finish 掉的 Activity 就不会被回收了从而造成内存泄漏(因 Handler 为非静态内部类,它会持有外部类的引用,在这里就是指 SampleActivity)。

修复方法:在 Activity 中避免使用非静态内部类,比如上面我们将 Handler 声明为静态的,则其存活期限跟 Activity 的生命周期就无关了。同时通过弱引用的方式引入 Activity,避免直接将 Activity 作为 context 传进去,见下面代码:

即推荐使用静态内部类 + WeakReference 这种方式


6、避免使用 static 成员变量

7、资源未关闭造成的内存泄露

对于使用了 BroadcastReceiver, ContentObserver, File, 游标 Cursor, Stream, Bitmap 等资源的使用,应该在 Activity 销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

1)比如在 Activity 中 register 了一个 BroadcastReceiver,但在 Activity 结束后没有 unregister 该 BroadcastReceiver。

2)资源性对象比如 Cursor,Stream、File 文件等往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于 java 虚拟机内,还存在于 java 虚拟机外。如果我们仅仅是把它的引用设置为 null, 而不关闭它们,往往会造成内存泄漏。

3)对于资源性对象在不使用的时候,应该调用它的 close () 函数将其关闭掉,然后再设置为 null。在我们的程序退出时一定要确保我们的资源性对象已经关闭。

4)Bitmap 对象不在使用时调用 recycle () 释放内存。2.3 以后的 bitmap 应该是不需要手动 recycle 了,内存已经在 java 层了。


8、listview 没用复用 contentview 造成的泄露

初始时 ListView 会从 BaseAdapter 中根据当前的屏幕布局实例化一定数量的 View 对象,同时 ListView 会将这些 View 对象缓存起来。当向上滚动 ListView 时,原先位于最上面的 Item 的 View 对象会被回收,然后被用来构造新出现在下面的 Item。这个构造过程就是由 getView () 方法完成的,getView () 的第二个形参 convertView 就是被缓存起来的 Item 的 View 对象(初始化时缓存中没有 View 对象则 convertView 是 null)。

构造 Adapter 时,没有使用缓存的 convertView。

解决方法:在构造 Adapter 时,使用缓存的 convertView。


9、webview 造成的内存泄露

当我们不要使用 WebView 对象时,应该调用它的 destory () 函数来销毁它,并释放其占用的内存,否则其长期占用的内存也不能被回收,从而造成内存泄露。

解决方法:为 WebView 另外开启一个进程,通过 AIDL 与主线程进行通信,WebView 所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。