文章目录

  • 前言
  • 一、Android 中几种优雅的退出APP方式
  • 二、第三方库导致,程序异常,内存泄漏
  • 三、RXJava引起的内存泄漏
  • 四、内存泄漏的总览
    • 1.资源对象没关闭造成的内存泄漏
    • 2.构造 Adapter 时,没有使用缓存的 convertView
    • 3.Bitmap 对象不在使用时调用 recycle()释放内存
    • 4.试着使用关于 application 的 context 来替代和 activity 相关的 context
    • 5.注册没取消造成的内存泄漏
    • 6.集合中对象没清理造成的内存泄漏
  • 五、内存泄露排查
    • 1、使用 AndroidProfiler 的 MEMORY 工具:
    • 2、使用 MAT:
  • 总结(实用小技巧)

前言

做安卓开发总是伴随着不同的设备适配和不同的设备性能,同一套代码可能在Android5.0上跑着相安无事,但是在Android7.0上就有各种各样的问题,再加上安卓厂商众多以及良萎不齐的设备性能,如果没有经验,很容易就会踩坑。如果是普遍的内存泄露问题,靠水磨工夫一点一点慢慢查还是能弄清楚的,但是总有些莫名其妙的问题,于是便有了本篇文章。

一、Android 中几种优雅的退出APP方式

参考链接:https://mp.weixin.qq.com/s/DAeD5YHS9oNYA-wzcHqJEg.

一开始是测试发现app有不能必现的闪退问题,通常是发生在关闭app重启过后,于是我从关闭的代码开始看起,发现app的关闭代码是直接杀死app的进程!
代码如下(示例):

    android.os.Process.killProcess(android.os.Process.myPid());System.exit(0);Error: That port is already in use

这种方式我个人觉得很不好,于是在网上找到了同道中人后就开始根据自己项目选择了合适的退出方式,自测了一下发现问题并没有解决。

二、第三方库导致,程序异常,内存泄漏

测试反应问题没有出现的那么频繁了,但是还是有,而且会影响到再次开启,再次开启app时,就完全无响应了,重启才有用;看了日志发现app有的端口被占用了,类似这样的

报错如下(示例):

  Error: That port is already in use和connect: already connected (cur=1 req=1)

因为项目在本地起了一个服务(不是Android的service)AndServer,然后在github上面看了下这个开源项目发现如果在关闭的时候没有关闭这个三方服务它就会占用端口导致重启app也起不起来,看了半天发现是有同事重启app时,没有调用关闭服务的方法,加上后,问题还是没有解决,我又化身为日志阅读器(bushi),在一个咔咔发现了内存泄露
报错如下(示例):


I/art: Background sticky concurrent mark sweep GC freed 115518(12MB) AllocSpace objects,I/art: Background sticky concurrent mark sweep GC freed 83705(8MB) AllocSpace objects, 7I/art: Background partial concurrent mark sweep GC freed 194941(15MB) AllocSpace objectsI/art: Background sticky concurrent mark sweep GC freed 125262(11MB) AllocSpace objects,

大概是这样的日志连续出现了很多次,我就开始考虑内存抖动的问题,这个就是老生常谈了,有各种各样的原因,出现这个问题,只能说自己平常写代码的时候不规范,查问题的时候两行泪;漫长的找问题后发现之前对接一个称重SdK的时候胡乱把厂家给的示例demo代码复制粘贴过去没问题就过了,它的入参使用了Handler作为参数,但是Handler会有内存泄漏问题,但是因为这个内存泄漏其实也只是暂时的,厂家的示例demo并没有做处理,所以写代码一定要细心再细心,5555.

修复这个问题呢,实际上就是让使用Handler的activity弱引用,这样就不会因为handleMassage中还有消息队列导致activity无法销毁导致crash了
参考这篇博文:Handler内存泄漏详解及其解决方案.

三、RXJava引起的内存泄漏

这是另外一个项目的优化,所以单独拎出来说,项目架构使用了retrofit和rxjava的mvp的架构,这个架构啥都好,就是presenter的请求返回来后如果activity被销毁了直接就crash了,道理是这个道理,怎么解决呢,我记得我当时是在presenter中很粗糙的就在返回的时候判断activity如果为null,就直接结束了,直到我知道有个叫rxlifecycle的第三方库

implementation 'com.trello.rxlifecycle2:rxlifecycle-components:2.1.0'

这个库是干什么的呢,他会关联rxjava的Observable,让Observable和activity,Fragment这些组件绑定起来,共享生命周期,这样就不会发生上面那种情况了,看到这玩意我泪流满面,天知道我当初写了多少
if(activity==null),果然我是真的菜啊。
详细忘记当时看的那篇博客了,大概是这篇解决rxjava导致的内存泄漏.(ps:评论区好像说这第一种方法有问题)

四、内存泄漏的总览

实际开发中遇到那么多内存泄漏的情况,有时候真的也不太好查,把看到的内存泄漏贴在这里方便自己反思回顾

1.资源对象没关闭造成的内存泄漏

描述: 资源性对象比如(Cursor,File 文件等)往往都用了一些缓冲,我们在不使 用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅 存在于 java 虚拟机内,还存在于 java 虚拟机外。如果我们仅仅是把它的引用设 置为 null,而不关闭它们,往往会造成内存泄漏。因为有些资源性对象,比如 SQLiteCursor(在析构函数 finalize(),如果我们没有关闭它,它自己会调 close()关 闭),如果我们没有关闭它,系统在回收它时也会关闭它,但是这样的效率太低 了。因此对于资源性对象在不使用的时候,应该调用它的 close()函数,将其关闭
掉,然后才置为 null.在我们的程序退出时一定要确保我们的资源性对象已经关 闭。程序中经常会进行查询数据库的操作,但是经常会有使用完毕 Cursor 后没有关 闭的情况。如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在 常时间大量操作的情况下才会复现内存问题,这样就会给以后的测试和问题排查 带来困难和风险。

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

描述: 以构造 ListView 的 BaseAdapter 为例,在 BaseAdapter 中提供了方法: public View getView(int position, ViewconvertView, ViewGroup parent) 来向 ListView 提供每一个 item 所需要的 view 对象。初始时 ListView 会从 BaseAdapter 中根据当前的屏幕布局实例化一定数量的 view 对象,同时 ListView 会将这些 view 对象缓存起来。当向上滚动 ListView 时,原先位于最上面的 list item 的 view 对象会被回收,然后被用来构造新出现的最下面的 list item。这个构造过程就是 由 getView()方法完成的,getView()的第二个形参 View convertView 就是被缓存 起来的 list item 的 view 对象(初始化时缓存中没有 view 对象则 convertView 是 null)。由此可以看出,如果我们不去使用 convertView,而是每次都在 getView() 中重新实例化一个 View 对象的话,即浪费资源也浪费时间,也会使得内存占用 越来越大。 ListView 回收 list item 的 view 对象的过程可以查看: android.widget.AbsListView.java --> voidaddScrapView(View scrap) 方法。

示例代码:

public View getView(int position, ViewconvertView, ViewGroup parent) { View view = new Xxx(...); ... ... return view;
}

修正示例代码:

public View getView(int position, ViewconvertView, ViewGroup parent) { View view = null; if (convertView != null) { view = convertView; populate(view, getItem(position)); ... } else { view = new Xxx(...);... }return view; }

3.Bitmap 对象不在使用时调用 recycle()释放内存

描述: 有时我们会手工的操作 Bitmap 对象,如果一个 Bitmap 对象比较占内存, 当它不在被使用的时候,可以调用 Bitmap.recycle()方法回收此对象的像素所占 用的内存,但这不是必须的,视情况而定。可以看一下代码中的注释:

/* •Free up the memory associated with thisbitmap's pixels, and mark the •bitmap as "dead", meaning itwill throw an exception if getPixels() or •setPixels() is called, and will drawnothing. This operation cannot be •reversed, so it should only be called ifyou are sure there are no •further uses for the bitmap. This is anadvanced call, and normally need •not be called, since thenormal GCprocess will free up this memory when •there are no more references to thisbitmap. /

/*•释放与此位图像素相关的内存,并将•位图标记为“死”,这意味着如果调用getPixels()或•setPixels(),它将引发异常,并且不会绘制任何内容。此操作无法•反转,因此只有在确定位图没有•其他用途时,才应调用此操作。这是高级呼叫,通常不需要呼叫,因为当不再引用此位图时,正常GCprocess将释放此内存/

4.试着使用关于 application 的 context 来替代和 activity 相关的 context

这是一个很隐晦的内存泄漏的情况。有一种简单的方法来避免 context 相关的内 存泄漏。最显著地一个是避免 context 逃出他自己的范围之外。使用 Application context。这个 context 的生存周期和你的应用的生存周期一样长,而不是取决于 activity 的生存周期。如果你想保持一个长期生存的对象,并且这个对象需要一 个 context,记得使用 application 对象。你可以通过调用 Context.getApplicationContext() or Activity.getApplication()来获得。更多的请 看这篇文章如何避免 Android 内存泄漏。

5.注册没取消造成的内存泄漏

一些 Android 程序可能引用我们的 Anroid 程序的对象(比如注册机制)。即使我 们的 Android 程序已经结束了,但是别的引用程序仍然还有对我们的 Android 程序的某个对象的引用,泄漏的内存依然不能被垃圾回收。调用 registerReceiver 后未调用 unregisterReceiver。 比如:假设我们希望在锁屏界面(LockScreen)中, 监听系统中的电话服务以获取一些信息(如信号强度等),则可以在 LockScreen 中定义一个 PhoneStateListener 的对象,同时将它注册到 TelephonyManager 服务中。对于 LockScreen 对象,当需要显示锁屏界面的时候就会创建一个 LockScreen 对象,而当锁屏界面消失的时候 LockScreen 对象就会被释放掉。 但 是如果在释放 LockScreen 对象的时候忘记取消我们之前注册的 PhoneStateListener 对象,则会导致 LockScreen 无法被垃圾回收。如果不断的
使锁屏界面显示和消失,则最终会由于大量的 LockScreen 对象没有办法被回收 而引起 OutOfMemory,使得 system_process 进程挂掉。 虽然有些系统程序,它 本身好像是可以自动取消注册的(当然不及时),但是我们还是应该在我们的程序 中明确的取消注册,程序结束时应该把所有的注册都取消掉。

6.集合中对象没清理造成的内存泄漏

我们通常把一些对象的引用加入到了集合中,当我们不需要该对象时,并没有把 它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是 static 的话,那情况就更严重了。 查找内存泄漏可以使用 Android Studio 自带的 AndroidProfiler 工具或 MAT,也 可以使用 Square 产品的 LeakCanary.

五、内存泄露排查

1、使用 AndroidProfiler 的 MEMORY 工具:

运行程序,对每一个页面进行内存分析检查。首先,反复打开关闭页面 5 次,然 后收到 GC(点击 Profile MEMORY 左上角的垃圾桶图标),如果此时 total 内存 还没有恢复到之前的数值,则可能发生了内存泄露。此时,再点击 Profile MEMORY 左上角的垃圾桶图标旁的 heap dump 按钮查看当前的内存堆栈情况, 选择按包名查找,找到当前测试的 Activity,如果引用了多个实例,则表明发生 了内存泄露。

2、使用 MAT:

运行程序,所有功能跑一遍,确保没有改出问题,完全退出程序,手动触发 GC,然后使用 adb shell dumpsys meminfo packagename -d 命令查看退出界面 后 Objects 下的 Views 和 Activities 数目是否为 0,如果不是则通过 Leakcanary 检查可能存在内存泄露的地方,最后通过 MAT 分析,如此反复,改善满意为止。

在使用 MAT 之前,先使用 as 的 Profile 中的 Memory 去获取要分析的堆内存 快照文件.hprof,如果要测试某个页面是否产生内存泄漏,可以先 dump 出没进 入该页面的内存快照文件.hprof,然后,通常执行 5 次进入/退出该页面,然后再 dump 出此刻的内存快照文件.hprof,最后,将两者比较,如果内存相除明显, 则可能发生内存泄露。(注意:MAT 需要标准的.hprof 文件,因此在 as 的 Profiler 中 GC 后 dump 出的内存快照文件.hprof 必须手动使用 android sdk platform-tools 下的 hprof-conv 程序进行转换才能被 MAT 打开)

然后,使用 MAT 打开前面保存的 2 份.hprof 文件,打开 Overview 界面,在 Overview 界面下面有 4 中 action,其中最常用的就是 Histogram 和 Dominator Tree。 Dominator Tree:支配树,按对象大小降序列出对象和其所引用的对象,注重引 用关系分析。选择 Group by package,找到当前要检测的类(或者使用顶部的 Regex 直接搜索),查看它的 Object 数目是否正确,如果多了,则判断发生了 内存泄露。然后,右击该类,选择 Merge Shortest Paths to GC Root 中的 exclude all phantom/weak/soft etc.references 选项来查看该类的 GC 强引用链。最后, 通过引用链即可看到最终强引用该类的对象。 Histogram:直方图注重量的分析。使用方式与 Dominator Tree 类似。

对比 hprof 文件,检测出复杂情况下的内存泄露: 通用对比方式:在 Navigation History 下面选择想要对比的 dominator_tree/histogram,右击选择 Add to Compare Basket,然后在 Compare Basket 一栏中点击红色感叹号(Compare the results)生成对比表格(Compared
Tables),在顶部 Regex 输入要检测的类,查看引用关系或对象数量去进行分析 即可。 针对于 Historam 的快速对比方式:直接选择 Histogram 上方的 Compare to another Heap Dump 选择要比较的 hprof 文件的 Historam 即可。

总结(实用小技巧)

实际上,内存泄露有各种各样的原因,大到activity,小到一个bitmap甚至一个没有置空GC的new对象,多多利用日志和现有开发工具抓取错误日志,多一点耐心,总能解决问题,拒绝开摆鸟

参考这篇博文分享一个抓取错误日志的工具类,参考:App crash原因以及解决办法.

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Environment;
import android.util.Log;
import android.widget.Toast;import com.aw.ccpos.FullscreenApplication;
import com.aw.ccpos.client.Client;import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;import java.lang.reflect.Field;import androidx.annotation.NonNull;/*** @ProjectName : ....* @Author : yifeng_zeng* @Time : 2022/2/28 10:27* @Description : app异常捕获*/
public class CrashHandler implements Thread.UncaughtExceptionHandler {private FullscreenApplication softApp;public static final String TAG="CrashHandler";//系统默认的UncaughtException处理类private Thread.UncaughtExceptionHandler mDefaultHandler;//程序的Context对象private Context mContext;//用来存储设备信息和异常信息private Map<String,String> infos=new HashMap<String,String>();//用于格式化日期,作为日志文件名的一部分private DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");/**保证只有一个CrashHandler实例**/public  CrashHandler(FullscreenApplication app){softApp=app;}/*** 初始化*/public void init(Context context){mContext=context;//获取系统默认的UnCaughtException处理器mDefaultHandler=Thread.getDefaultUncaughtExceptionHandler();//设置该CrashHandler为程序默认处理器Thread.setDefaultUncaughtExceptionHandler(this);;}@Overridepublic void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {Log.e(TAG, "error : ", e);//收集设备参数信息collectDeviceInfo(mContext);//保存日志文件saveCrashInfo2File(e);//关闭appToast.makeText(Client.i().getActivity(),"app程序异常,即将关闭",Toast.LENGTH_LONG).show();//这里重启或关闭app}private String  saveCrashInfo2File(Throwable ex) {StringBuffer sb=new StringBuffer();for(Map.Entry<String, String> entry : infos.entrySet()){String key=entry.getKey();String value=entry.getValue();sb.append(key + "=" + value + "\n");}Writer writer=new StringWriter();PrintWriter printWriter=new PrintWriter(writer);ex.printStackTrace(printWriter);Throwable cause = ex.getCause();while(cause!=null){cause.printStackTrace(printWriter);cause=cause.getCause();}printWriter.close();String result=writer.toString();sb.append(result);long timestamp=System.currentTimeMillis();String time=formatter.format(new Date());String fileName = "crash-" + time + "-" + timestamp + ".log";if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){String path="/sdcard/crash";File dir=new File(path);if(!dir.exists()){dir.mkdirs();}try {FileOutputStream fos=new FileOutputStream(path + fileName);fos.write(sb.toString().getBytes());} catch (FileNotFoundException e) {// TODO Auto-generated catch blockLog.e(TAG, "an error occured while writing file...", e);} catch (IOException e) {// TODO Auto-generated catch blockLog.e(TAG, "an error occured while writing file...", e);}}return null;}private void collectDeviceInfo(Context ctx) {PackageManager pm=ctx.getPackageManager();PackageInfo pi;try {pi = pm.getPackageInfo(ctx.getPackageCodePath(),PackageManager.GET_ACTIVITIES);if(pi!=null){String versionName=pi.versionName==null?"null":pi.versionName;String versionCode=pi.versionCode+"";infos.put("versionName", versionName);infos.put("versionCode", versionCode);}} catch (PackageManager.NameNotFoundException e) {// TODO Auto-generated catch blockLog.e(TAG, "an error occured when collect package info", e);}Field[] fields= Build.class.getDeclaredFields();for(Field field:fields){try {field.setAccessible(true);infos.put(field.getName(), field.get(null).toString());Log.d(TAG, field.getName() + " : " + field.get(null));} catch (IllegalAccessException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (IllegalArgumentException e) {// TODO Auto-generated catch blockLog.e(TAG, "an error occured when collect crash info", e);}}}}

抓取到的错误日志保存在/sdcard/crash目录下,有助于快速解决问题

其实碍于公司代码不方便粘贴出来,还有很多问题没有写出来,如果你们也遇到什么问题欢迎在评论区讨论,共勉

Android app crash的问题排查思路与反思相关推荐

  1. Android App Crash详解

    Android APP Crash通常是由未捕获的Exception或signal引起app异常退出. 本文主要从以下6个方面分析介绍 Android App Crash App Crash 检测 C ...

  2. Android APP Crash

    Android APP Crash 通常是由未捕获的Exception或signal引起app异常退出. App Crash 检测 Crash 问题 Android vitals 分析 App Cra ...

  3. Android防止App崩溃的,Android App Crash之后如何禁止Activity重启

    针对crash,android默认的处理方式是,退出app.弹一个提示框. /** * 初始化 * * @param context */ public void init(context conte ...

  4. Android APP报毒和排查解决

    APP遇到操作一些较高的用户权限,比如删除短信,定时开关机这些,在用户手机安装应用时会提示用户给用户. 但是最近一个项目出现了报毒的现象,被百度手机管家定位为木马应用,原因是添加了发送和删除短信的权限 ...

  5. android app crash测试,APP常见崩溃原因和测试方法整理

    测试过APP的人都应该发现,app崩溃是一类非常常见的问题,很多时候还是致命性的,这就要求我们测试人员要尽最大可能去找出软件当中的缺陷,减少app崩溃出现的概率,这里我将收集到的关于针对APP崩溃测试 ...

  6. Android URL scheme跳转失败(源码实现排查思路知识拓展)

    目录 问题 实现 排查思路 知识拓展 包可见性 scheme targetsdkVersion 问题 App A的Activity A通过scheme跳转至App B的Activity B,跳转失败. ...

  7. android内存占用分析,Android App性能评测分析-内存篇

    1.内存了解 在Android App的性能优化的各个部分里,内存方面的知识较多且不易理解,内存的问题绝对是最令人头疼的一部分,需要对内存基础知识.内存分配.内存管理机制等非常熟悉,才能排查问题. 1 ...

  8. android app性能测试工具,Android 性能测试 - 内存

    1.内存了解 在Android App的性能优化的各个部分里,内存方面的知识较多且不易理解,内存的问题绝对是最令人头疼的一部分,需要对内存基础知识.内存分配.内存管理机制等非常熟悉,才能排查题. 1. ...

  9. android patch app,AndroidN,O 加载到patch 以后重启app crash了.

    Issue/提问须知 在提交issue之前,我们应该先查询是否已经有相关的issue以及常见问题.提交issue时,我们需要写明issue的原因,以及编译或运行过程的日志(加载进程以及Patch进程) ...

  10. 02.Android崩溃Crash库之App崩溃分析

    目录总结 01.抛出异常导致崩溃分析 02.RuntimeInit类分析 03.Looper停止App就退出吗 04.handleApplicationCrash 05.native_crash如何监 ...

最新文章

  1. 改善代码设计 —— 简化条件表达式(Simplifying Conditional Expressions)
  2. 怎么逐步突破,成为Python高手?
  3. sap事务代码_SAP事务码太多,记不住怎么办?
  4. 自建ES迁移至阿里云ES(使用脚本及logstash同步)
  5. Yarn取代job/task tracker
  6. ML《集成学习(三)Boosting和Adaboosting回归树》
  7. 苹果x和xs买哪个好_苹果12和苹果11哪个值得买-苹果12和11哪个更值得买
  8. VC维含义的个人理解
  9. 重定向与请求转发的区别
  10. linux TP 阿帕奇 数据库 php的安装
  11. c#开发之八---mvc
  12. 宏文件下载_Catia常用宏文件分享(压箱底)
  13. 用户故事与敏捷方法笔记---Scrum与用户故事
  14. hive 常用函数操作
  15. 展辰涂料如何利用K2BPM加强流程管控?
  16. todo已完成任务_总结一下TODO的用法
  17. pyecharts 在地图上根据经纬度和量值,画出散点图/热力图
  18. 【JavaWeb】1—JavaWeb概述
  19. 大文件的切割与归并(数据分析案例三)
  20. 【17】 强化学习 17章 前沿技术

热门文章

  1. 订餐系统c语言代码大全,C语言订餐系统
  2. java中sql去除游标_SQL游标 - java.beggar - BlogJava
  3. Java课程寒假之开发记账本软件(网页版)之一
  4. postgresql 数据库执行计划 Nested Loop
  5. java开发tv上转盘抽奖_java实现大转盘抽奖的简单思路
  6. 三次方程求根公式例子
  7. 网络文件常常提到类似./run.sh的数据,这个命令的意义是什么?
  8. 家庭账本应该怎样记简洁明了
  9. dubbo中的group与version的存在意义
  10. 积分商城游戏化运营?积分游戏应该如何正确设置