移动性能测试 专项:Android 内存泄露实践分析

Heyniu · August 30, 2016 · Last by jerry ding replied at April 24, 2019 · 6101 hits
本帖已被设为精华帖!

专项:Android内存泄露实践分析

定义

​ 内存泄漏也称作“存储渗漏”,用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。(其实说白了就是该内存空间使用完毕之后未回收)即所谓内存泄漏。

内存泄漏形象的比喻是“操作系统可提供给所有进程的存储空间正在被某个进程榨干”,最终结果是程序运行时间越长,占用存储空间越来越多,最终用尽全部存储空间,整个系统崩溃。所以“内存泄漏”是从操作系统的角度来看的。这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。由程序申请的一块内存,如果没有任何一个指针指向它,那么这块内存就泄漏了。

​ ——来自《百度百科》

影响

  • 导致OOM
  • 糟糕的用户体验
  • 鸡肋的App存活率

成效

  • 内存泄露是一个持续的过程,随着版本的迭代,效果越明显
  • 由于某些原因无法改善的泄露(如框架限制),则尽量降低泄露的内存大小
  • 内存泄露实施后的版本,一定要验证,不必马上推行到正式版,可作为beta版持续观察是否影响/引发其他功能/问题

内存泄露实施后,项目的收获:

  • OOM减少30%以上
  • 平均使用内存从80M稳定到40M左右
  • 用户体验上升,流畅度提升
  • 存活率上升,推送到达率提升

类型

  • IO
    • FileStream
    • Cursor
  • Bitmap
  • Context

    • 单例
    • Callback
  • Service

    • BraodcastReceiver
    • ContentObserver
  • Handler

  • Thread

技巧

  • 慎用Context

    • Context概念
    • 四大组件Context和Application的context使用参见下表

  • 善用Reference

    • Java引用介绍
    • Java四种引用由高到低依次为:强引用  >  软引用  >  弱引用  >  虚引用
    • 表格说明
    类型 垃圾回收时间 生存时间
    强引用 永远不会 JVM停止运行时终止
    软引用 内存不足时 内存不足时终止
    弱引用 垃圾回收时 垃圾回收时终止
    虚引用 垃圾回收时 垃圾回收时终止
  • 复用ConvertView

  • 对象释放

    • 遵循谁创建谁释放的原则
    • 示例:显示调用clear列表、对象赋空值

分析

原理

根本原因

  • 关注堆内存

怎么解决

  • 详见方案

实践分析

  • 详见实践

方案

  • StrictMode

    • 使用方法:AppContext的onCreate()方法加上
    StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy
    .Builder()
    .detectAll()
    .penaltyLog()
    .build());
    StrictMode.setVmPolicy(new StrictMode.VmPolicy
    .Builder()
    .detectAll()
    .penaltyLog()
    .build());
    • 主要检查项:内存泄露、耗时操作等
  • Leakcanary

  • Leakcanary + StrictMode + monkey (推荐)

    • 使用阶段:功能测试完成后,稳定性测试开始时
    • 使用方法:安装集成了Leakcanary的包,跑monkey
    • 收获阶段:一段时间后,会发现出现N个泄露
    • 实战分析:逐条分析每个泄露并改善/修复
    • StrictMode:查看日志搜索StrictMode关键字
  • Adb命令

    • 手动触发GC
    • 通过adb shell dumpsys meminfo packagename -d查看
    • 查看Activity以及View的数量
    • 越接近0越好
    • 对比进入Activity以及View前的数量和退出Activity以及View后的数量判断
  • Android Monitor

  • MAT

实践(示例)

Bitmap泄露

Bitmap泄露一般会泄露较多内存,视图片大小、位图而定

  • 经典场景:App启动图

  • 解决内存泄露前后内存相差10M+,可谓惊人

  • 解决方案:

App启动图Activity的onDestroy()中及时回收内存

@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
recycleImageView(imgv_load_ad);
}

public static void recycleImageView(View view){
if(view==null) return;
if(view instanceof ImageView){
Drawable drawable=((ImageView) view).getDrawable();
if(drawable instanceof BitmapDrawable){
Bitmap bmp = ((BitmapDrawable)drawable).getBitmap();
if (bmp != null && !bmp.isRecycled()){
((ImageView) view).setImageBitmap(null);
bmp.recycle();
bmp=null;
}
}
}
}

IO流未关闭

  • 分析:通过日志可知FileOutputStream()未关闭

  • 问题代码:

public static void copyFile(File source, File dest) {
FileChannel inChannel = null;
FileChannel outChannel = null;
Log.i(TAG, "source path: " + source.getAbsolutePath());
Log.i(TAG, "dest path: " + dest.getAbsolutePath());
try {
inChannel = new FileInputStream(source).getChannel();
outChannel = new FileOutputStream(dest).getChannel();
inChannel.transferTo(0, inChannel.size(), outChannel);
} catch (IOException e) {
e.printStackTrace();
}
}
  • 解决方案:

    • 及时关闭IO流,避免泄露
public static void copyFile(File source, File dest) {
FileChannel inChannel = null;
FileChannel outChannel = null;
Log.i(TAG, "source path: " + source.getAbsolutePath());
Log.i(TAG, "dest path: " + dest.getAbsolutePath());
try {
inChannel = new FileInputStream(source).getChannel();
outChannel = new FileOutputStream(dest).getChannel();
inChannel.transferTo(0, inChannel.size(), outChannel);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inChannel != null) {
try {
inChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (outChannel != null) {
try {
outChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
E/StrictMode: A resource was acquired at attached stack trace but never released. 
See java.io.Closeable for information on avoiding resource leaks.
java.lang.Throwable: Explicit termination method 'close' not called
at dalvik.system.CloseGuard.open(CloseGuard.java:180)
at java.io.FileOutputStream.<init>(FileOutputStream.java:89)
at java.io.FileOutputStream.<init>(FileOutputStream.java:72)
at com.heyniu.lock.utils.FileUtil.copyFile(FileUtil.java:44)
at com.heyniu.lock.db.BackupData.backupData(BackupData.java:89)
at com.heyniu.lock.ui.HomeActivity$11.onClick(HomeActivity.java:675)
at android.support.v7.app.AlertController$ButtonHandler.handleMessage(AlertController.java:157)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)

单例模式泄露

  • 分析:通过截图我们发现SplashActivity被ActivityUtil的实例activityStack持有

  • 引用代码:

ActivityUtil.getAppManager().add(this);
  • 持有代码:
public void add(Activity activity) {
if (activityStack == null) {
synchronized (ActivityUtil.class){
if (activityStack == null) {
activityStack = new Stack<>();
}
}
}
activityStack.add(activity);
}
  • 解决方案:

    • 在SplashActivity的onDestroy()生命周期移除引用
@Override
protected void onDestroy() {
super.onDestroy();
ActivityUtil.getAppManager().remove(this);
}

静态变量持有Context实例泄露

  • 分析:长生命周期持有短什么周期引用导致泄露,详见上文四大组件Context和Application的context使用

  • 示例引用代码:

private static HttpRequest req;
public static void HttpUtilPost(Context context, int TaskId, String url, String requestBody,ArrayList<HttpHeader> Headers, RequestListener listener) {
// TODO Auto-generated constructor stub
req = new HttpRequest(context, url, TaskId, requestBody, Headers, listener);
req.post();
}
  • 解决方案:

    • 改为弱引用
    • pass:弱引用随时可能为空,使用前先判空
    • 示例代码:
    public static void cancel(int TaskId) {
    if(req != null && req.get() != null){
    req.get().AsyncCancel(TaskId);
    }
    }
    private static WeakReference<HttpRequest> req;
    public static void HttpUtilPost(Context context, int TaskId, String url, String requestBody,ArrayList<HttpHeader> Headers, RequestListener listener) {
    // TODO Auto-generated constructor stub
    req = new WeakReference<HttpRequest>(new HttpRequest(context, url, TaskId, requestBody, Headers, listener));
    req.get().post();
    }
    • 改为长生命周期
    private static HttpRequest req;
    public static void HttpUtilPost(Context context, int TaskId, String url, String requestBody,ArrayList<HttpHeader> Headers, RequestListener listener) {
    // TODO Auto-generated constructor stub
    req = new HttpRequest(context.getApplicationContext(), url, TaskId, requestBody, Headers, listener);
    req.post();
    }

Context泄露

Callback泄露

服务未解绑注册泄露

  • 分析:一般发生在注册了某服务,不用时未解绑服务导致泄露

  • 引用代码:

private void initSensor() {
// 获取传感器管理器
sm = (SensorManager) container.activity.getSystemService(Context.SENSOR_SERVICE);
// 获取距离传感器
acceleromererSensor = sm.getDefaultSensor(Sensor.TYPE_PROXIMITY);
// 设置传感器监听器
acceleromererListener = new SensorEventListener() {
......
};
sm.registerListener(acceleromererListener, acceleromererSensor, SensorManager.SENSOR_DELAY_NORMAL);
}
  • 解决方案:

    • 在Activity的onDestroy()方法解绑服务
@Override
protected void onDestroy() {
super.onDestroy();
sm.unregisterListener(acceleromererListener,acceleromererSensor);
}

Handler泄露

  • 分析:由于Activity已经关闭,Handler任务还未执行完成,其引用了Activity的实例导致内存泄露

  • 引用代码:

handler.sendEmptyMessage(0);
  • 解决方案:

    • 在Activity的onDestroy()方法回收Handler
@Override
protected void onDestroy() {
super.onDestroy();
handler.removeCallbacksAndMessages(null);
}
  • 图片后续遇到再补上

异步线程泄露

  • 分析:一般发生在线程执行耗时操作时,如下载,此时Activity关闭后,由于其被异步线程引用,导致无法被正常回收,从而内存泄露

  • 引用代码:

new Thread() {
public void run() {
imageArray = loadImageFromUrl(imageUrl);
}.start();
  • 解决方案:

    • 把线程作为对象提取出来
    • 在Activity的onDestroy()方法阻塞线程
thread = new Thread() {
public void run() {
imageArray = loadImageFromUrl(imageUrl);
};
thread.start();

@Override
protected void onDestroy() {
super.onDestroy();
if(thread != null){
thread.interrupt();
thread = null;
}
}

后面

  • 欢迎补充实际中遇到的泄露类型
  • 文章如有错误,欢迎指正
  • 如有更好的内存泄露分享方法,欢迎一起讨论

未完待续。。。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 33 条回复 时间 点赞

很高产啊

高产男士,手工mark来学习

Heyniu #3 · August 30, 2016 作者

#1楼 @mads 谢谢😃

Heyniu #4 · August 30, 2016 作者

#2楼 @darker50 赶紧收归麾下啊😂

思寒_seveniruby 将本帖设为了精华贴 30 Aug 15:42

加精理由: 列举了常见的内存泄漏检测方法和依赖工具. 并给了详细的解决过程

常见的泄露场景。赞下!

赞赞赞,你玩这么 6,你家珍妞知道吗😏

Heyniu #9 · August 30, 2016 作者

#8楼 @thinker 😅 低调,低调

这是停不下来的节奏呀

Heyniu #12 · August 30, 2016 作者

#11楼 @sycing 😆 赶紧分享点干货来

膜拜,收藏了,慢慢消化~

房网的员工啊

好文章,收藏了。

Heyniu #16 · August 31, 2016 作者

#14楼 @kasi 暴露了,低调

很详细啊,赞一个

Heyniu #18 · August 31, 2016 作者

#17楼 @neyo 谢谢

写得很详尽,加油期待后续文章

每次跑monkey的时候,都会进入Leakcanary 去删除内存泄露的点,请问如何避免进入Leakcanary

#20楼 @testsina 目前还没办法,或许可以从Leakcanary源码入手,去掉那个删除按钮

#21楼 @heyniu 请问你是如何做的?StrictMode集成到每一个Activity或Appliction中 我看logcat中无theadPlice和vmPlice

#22楼 @testsina StrictMode集成到Appliction就行了,无的话说明代码质量(线程、IO、内存泄露)没什么问题
monkey跑的时候,点到Leakcanary删除按钮的,我搞定了M你

#24楼 @testsina 搞定了,修改源码,重新打包,依赖到项目就好了

#25楼 @heyniu 能否给个QQ,请教下怎么修改呢?

列举的还是很全的,查内存泄漏原因应该是开发的基本任务之一,然而写起代码来就忘了一些细节。对QA我之前建议是,app启动记录好初始内存,然后不停的跑,定时回到初始页面对比内存情况,差别大就dump一个hprof丢给开发。当然敏感的QA能感觉到哪个页面泄漏,只要反复进入退出该activity,观察内存情况就能判断。凑贴,溜~

Heyniu [Topic was deleted] 中提及了此贴 13 Sep 19:37

有Leakcanary的详细示例吗

LeakCanary有时候蛮好用的,但是每个公司的APP的技术架构不一样,如Native APP、Web APP、Hybird APP,所以Leak报的内容也不一样。
我们公司是模块化开发,用起来不是太方便,如果AAR改成APP会方便很多。我们的机制是APP引用各个AAR,AAR里面是获取不到APP的对象,AAR是被引用关系。且如果APP中含第三方库必须在application中调用install方法,然后会返回一个watcher对象,不方便在于AAR都访问不到的application类。上次3个泄漏点只报了一个,所以其余2个是手动结合Monitor排查的。

如何准确定位某个问题导致卡顿或者oom,这个leakCanary日志表示看不懂,求问如何根据它确定问题

#32楼 @xiaozheng_QM oom可以直接看崩溃日志,卡顿通过性能分析,比如滑动下列表,然后查看此时的一些cpu占用等信息,通过traceview

请问作者是如何选择Android机型进行测试呢?

Heyniu 内存泄露配置 (优化版) 中提及了此贴 25 Jul 17:17

异步耗时线程导致的activity泄漏,等线程运算完成后,被持有的activity对象应该也会gc及时回收吧,所以应该算是暂时的内存泄漏?

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up