Android app crash的问题排查思路与反思

前言

做安卓开发总是伴随着不同的设备适配和不同的设备性能,同一套代码可能在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, 7

I/art: Background partial concurrent mark sweep GC freed 194941(15MB) AllocSpace objects

I/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);;
    }


    @Override
    public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
        Log.e(TAG, "error : ", e);
        //收集设备参数信息
        collectDeviceInfo(mContext);
        //保存日志文件
        saveCrashInfo2File(e);
        //关闭app
        Toast.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 block
                Log.e(TAG, "an error occured while writing file...", e);
            } catch (IOException e) {
                // TODO Auto-generated catch block
                Log.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 block
            Log.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 block
                e.printStackTrace();
            } catch (IllegalArgumentException e) {
                // TODO Auto-generated catch block
                Log.e(TAG, "an error occured when collect crash info", e);
            }
        }
    }




}

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

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

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
基于AndroidApp设计与实现是指在Android操作系统上开发并实现一个应用程序的过程。在进行设计与实现之前,首先要确定App的功能、目标用户和需求等。接下来,可以按照以下步骤进行设计与实现: 1. 界面设计:根据App的功能和用户需求,设计用户界面。可以利用可视化界面设计工具进行操作界面的设计,如绘制界面布局、选择合适的图标和颜色等。 2. 功能开发:根据需求,开发App的功能模块。这包括实现用户交互逻辑、数据传输、数据处理等功能。可以使用Java编程语言和Android开发工具包(SDK)进行开发。 3. 数据库设计:根据功能需求,设计数据的存储方式和数据库结构。Android提供了SQLite数据库来存储应用程序的数据。开发人员可以使用SQL语句来创建和管理数据库,以及存取数据。 4. 测试与调试:进行各个功能模块的测试,并进行调试。可以使用模拟器或真实的Android设备对App进行测试,检查和修复可能出现的错误。 5. 发布与更新:当App开发完成并经过测试后,可以将其发布到Google Play等应用商店,并跟踪用户反馈和评价,以便进行后续的更新和改进。 在设计与实现过程中,需要注重以下几点: 1. 用户体验:根据用户需求和习惯,设计简洁、直观的界面,提供良好的用户体验。 2. 性能优化:保证App的运行速度和响应时间,在开发过程中优化代码,减少资源占用。 3. 兼容性考虑:考虑不同Android版本和不同屏幕尺寸的设备兼容性,确保App的功能在各种设备上正常运行。 4. 安全性:对于涉及用户隐私和敏感数据的功能,要采取必要的安全措施,如加密数据传输和存储。 通过以上步骤,可以设计和实现一个功能完善、界面友好的Android App,并为用户提供良好的使用体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我怀里的猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值