Android稳定性优化总结

App Crash对于用户来讲是一种最糟糕的体验,它会导致流程中断、app口碑变差、app卸载、用户流失、订单流失等。

Crash治理方法

常见Crash的处理方式:
• 在开发提交代码过程中,让同事之间codeReview检测潜在问题。

• 在功能上线之前,周期性地跑monkey,提前发现crash问题。

• 根据Crash统计平台的堆栈,用户日志,操作路径定位和解决。

• 寻找共性,机型、品牌、系统版本、所在页面、用户操作等辅助解决问题。

• 复现场景,能够复现通常就很容易解决,可以线下复现或者云真机复现。

• 对crash率较高的模块进行业务梳理,排查,重构等。

• 与第三方sdk沟通升级解决问题,修改SDK的使用方式。

Crash治理实践

1.预防Crash
对于稳定性来说,如果App已经到了线上才发现异常,那其实已经造成了损失,所以,对于稳定性的优化,其重点在于预防。Gerrit 是一个免费、开放源代码的代码审查软件,使用网页界面。我们公司大多数项目都使用Gerrit进行CodeReview,从项目开发阶段就会审查各种潜在的crash或者逻辑上的问题,严格按照代码+1、+2之后才能提交入库。

2.长效保持需要科学流程
应用稳定性的建设过程是一个细活,所以很容易出现这个版本优化好了,但是在接下来的版本中如果我们不管它,它就会发生持续恶化的情况,因此,我们必须从项目研发的每一个流程入手,建立科学完善的相关规范,才能保证长效的优化效果。在每个版本都要追踪崩溃监测平台,将对应的崩溃分发给对应业务的开发人员处理。

Crash率评价
性能指标 优秀值 及格值 极差值 行业参考值
崩溃率(%) <=0.1 0.6 >=1 0.5

那么,我们App的Crash率降低多少才能算是一个正常水平或优秀的水平呢?

Java与Native的总崩溃率必须在千分之二以下。
Crash率万分位为优秀:需要注意90%的Crash都是比较容易解决的,但是要解决最后的10%需要付出巨大的努力。

Crash关键问题

如果应用发生了Crash,我们应该尽可能还原Crash现场。因此,我们需要全面地采集应用发生Crash时的相关信息,如下所示:

  • 堆栈、设备、OS版本、进程、线程名、Logcat
  • 前后台、使用时长、App版本、小版本、渠道
  • CPU架构、内存信息、线程数、资源包信息、用户行为日志
Android中常见异常类型

在Android应用开发中,常见的异常崩溃情况有很多,以下是一些常见的异常类型:

  • 空指针异常(NullPointerException):

当试图调用一个对象的方法或访问其属性,而该对象为空时,就会抛出空指针异常。

  • 数组越界异常(ArrayIndexOutOfBoundsException):

当尝试访问数组中不存在的索引位置时,会引发数组越界异常。

  • 类型转换异常(ClassCastException):

当试图将一个对象转换为不兼容的类型时,会抛出类型转换异常。
并发修改异常(ConcurrentModificationException):

在使用迭代器遍历集合的同时,对集合进行了结构性修改(增加、删除元素)时,会抛出并发修改异常。

  • 资源未找到异常(NotFoundException):

在尝试获取某个资源(如布局文件、字符串等)而该资源不存在时,会抛出资源未找到异常。

  • SQLite异常:

包括但不限于SQLiteConstraintException(违反约束异常)、SQLiteException(SQLite数据库相关的通用异常)等。

  • 网络异常(IOException):

在网络请求、文件操作等场景中,由于网络不可用或文件不可访问等原因,可能会抛出IOException。

  • 线程异常(ThreadException):

在多线程编程中,可能会遇到一些线程异常,如IllegalThreadStateException、InterruptedException等。
视图相关异常:

包括但不限于InflateException(布局文件解析异常)、IllegalStateException(视图状态异常)等。

  • 运行时异常(RuntimeException):

运行时异常是一类不需要显式捕获的异常,通常是由程序员的错误导致的,如算术异常(ArithmeticException)、索引越界异常(IndexOutOfBoundsException)等。

  • SecurityException:

当应用尝试执行受系统安全性保护的敏感操作时,可能会引发SecurityException。

那么常见是如上崩溃类型,定位到了崩溃,要解决这些崩溃,就需要自己的实力基础了。空指针异常做判空容错处理,数组越界做判断处理,类型转换异常先做类型判断处理,并发修改用线程安全的集合如CopyOnWriteArrayList,IOException,SqliteException相关操作做try catch等等处理。

异常发生的崩溃是谁处理的

当发生了Exception或throwable时,如果我们手动在代码中做了try catch处理,那么异常会由catch捕获机制来处理异常。那如果没有自己try catch呢?那就会由Thread类的dispatchUncaughtException方法处理,在当前线程处理未捕获异常,打印崩溃日志,然后由系统类调用了killProcess退出程序。

    public final void dispatchUncaughtException(Throwable e) {
        // BEGIN Android-added: uncaughtExceptionPreHandler for use by platform.
        Thread.UncaughtExceptionHandler initialUeh =
                Thread.getUncaughtExceptionPreHandler();
        if (initialUeh != null) {
            try {
                initialUeh.uncaughtException(this, e);
            } catch (RuntimeException | Error ignored) {
                // Throwables thrown by the initial handler are ignored
            }
        }
        // END Android-added: uncaughtExceptionPreHandler for use by platform.
        getUncaughtExceptionHandler().uncaughtException(this, e);
    }

Crash监控方案

Java中的Thread定义了一个接口: UncaughtExceptionHandler ;用于处理未捕获的异常导致线程的终止(注意:catch了的是捕获不到的),当我们的应用crash的时候,就会走 UncaughtExceptionHandler.uncaughtException ,在该方法中可以获取到异常的信息,我们通过 Thread.setDefaultUncaughtExceptionHandler 该方法来设置线程的默认异常处理器,我们可以将异常信息保存到本地或者是上传到服务器,方便我们快速的定位问题。

class MyExceptionHandler implements Thread.UncaughtExceptionHandler {

    @Override
    public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
        File file = dealException(thread, throwable);
        //上传服务器
        ...
    }

    /*** 导出异常信息到SD卡 ** @param e */
    private File dealException(Thread thread, Throwable throwable) {
        String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        File crashFolder = new File(mContext.getExternalCacheDir().getAbsoluteFile(), CrashMonitor.DEFAULT_JAVA_CRASH_FOLDER_NAME);
        if (!crashFolder.exists()) {
            crashFolder.mkdirs();
        }
        File crashFile = new File(crashFolder, time + FILE_NAME_SUFFIX);
        try {
            // 往文件中写入数据
            PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(crashFile)));
            pw.println(time);
            pw.println(thread);
            pw.println(getPhoneInfo());
            throwable.printStackTrace(pw);  //将异常信息堆栈写入文件
            // 写入crash堆栈
            pw.close();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
        return crashFile;
    }

    private String getPhoneInfo() {
        PackageManager pm = mContext.getPackageManager();
        PackageInfo pi = null;
        StringBuilder sb = new StringBuilder();

        try {
            pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);

            // App版本
            sb.append("App Version: ");
            sb.append(pi.versionName);
            sb.append("_");
            sb.append(pi.versionCode + "\n");
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }

        // Android版本号
        sb.append("OS Version: ");
        sb.append(Build.VERSION.RELEASE);
        sb.append("_");
        sb.append(Build.VERSION.SDK_INT + "\n");

        // 手机制造商
        sb.append("Vendor: ");
        sb.append(Build.MANUFACTURER + "\n");

        // 手机型号
        sb.append("Model: ");
        sb.append(Build.MODEL + "\n");

        // CPU架构
        sb.append("CPU: ");
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            sb.append(Arrays.toString(Build.SUPPORTED_ABIS));
        } else {
            sb.append(Build.CPU_ABI);
        }
        return sb.toString();
    }
}

然后在application中设置自定义的UncaughtExceptionHandler。

Thread.setDefaultUncaughtExceptionHandler(MyExceptionHandler())

ANR问题分析

ANR发生的原因总结和解决办法


本质就是消息机制的message处理超过了5s消息还无法进行处理,因为前面有消息一直卡住,就会报 AppNotResponding。

最终由ActivityManager报出:

    /**
     * Method for the app to tell system that it's wedged and would like to trigger an ANR.
     *
     * @param reason The description of that what happened
     */
    public void appNotResponding(@NonNull final String reason) {
        try {
            getService().appNotResponding(reason);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

1.在主线程中,进行了触屏点击滑动等操作,在5秒之内对该事件没有响应,就会导致ANR(例如,按键按下,屏幕触摸)
2.BroadcastReceiver在10秒内没有执行完毕
3.service是20秒

根本原因是在主线程进行了耗时操作,导致后面的消息无法处理,比如:
1.耗时的网络访问
2.大量的数据读写
3.数据库操作
4.在主线程中调用thread的join()方法、sleep()方法、wait()方法
5.其他线程持有锁,导致主线程等待超时
6.子线程终止或崩溃导致主线程一直等待

解决的主要办法就是开启子线程去处理这些耗时操作。修改主线程去等待子线程的锁。

ANR排查流程

ANR定位解决实战:https://www.jianshu.com/p/f59bc75782f3

使用FileObserver监控data/anr目录,当文件状态发生改变、创建或删除时,说明当前发生了anr事件。

创建类继承FileObserver,监控data/anr文件:

class AnrFileObserver extends FileObserver {

    @Override
    public void onEvent(int event, @Nullable String path) {
        switch (event) {
            case FileObserver.ACCESS:

                break;
            case FileObserver.CREATE:

                break;
            case FileObserver.MODIFY:

                break;
        }
    }
}
线上ANR监控:

接入 bugly,根据App版本,手机型号,发生时间,次数,产生位置进行位置问题定位并解决。

ANR异常我们可分为线上监测和线下监测两个方向

线上监测主要是利用FileObserver进行ANR目录文件变化监听,以ANR-WatchDog进行补充。
FileObserver在使用过程中应注意高版本程序不可用以及预防死锁出现。
线下监测主要是在报错之后利用ADB命令将错误的日志导出并找到错误的类进行分析。

参考:
https://www.jianshu.com/p/8abce3ff4687

如何降低App的Crash率?

崩溃是由于消息机制里面的Looer.loop方法像外抛的异常,然后系统的CrashHandler类,最未捕获异常会打印堆栈日志,然后做killprocess。我们可以定义自己的UncatchExceptionHandler,然后hook掉系统的这个类,自定义处理日志,然后做重启looper,或者做退出当前activity,做程序崩溃处理。

将未捕获异常自行处理:

public class CrashHandler implements Thread.UncaughtExceptionHandler {

    private static CrashHandler instance;
    private Thread.UncaughtExceptionHandler defaultExceptionHandler;

    private CrashHandler() {
        // 获取默认的异常处理器
        defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
        // 设置当前实例为默认的异常处理器
        Thread.setDefaultUncaughtExceptionHandler(this);
    }

    public static synchronized CrashHandler getInstance() {
        if (instance == null) {
            instance = new CrashHandler();
        }
        return instance;
    }

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        // 在这里处理崩溃逻辑,例如记录日志、上传崩溃信息等

        // 默认的异常处理器处理崩溃
//        if (defaultExceptionHandler != null) {
//            defaultExceptionHandler.uncaughtException(t, e);
//        }
    }
}

OOM

Out of Memory(OOM)错误通常发生在应用程序尝试分配比其可用内存更多的内存时。

以下是一些可能导致OOM崩溃的常见场景的示例:

1.Bitmap操作:

// 创建过大的Bitmap
Bitmap largeBitmap = Bitmap.createBitmap(5000, 5000, Bitmap.Config.ARGB_8888);

解决方案:
对于大图,可以使用inSampleSize进行缩放。
使用BitmapFactory.Options.inJustDecodeBounds来获取图片大小信息,然后再根据需要进行合适的缩放。

2.大量资源未释放:

// 大量创建未释放的资源
for (int i = 0; i < 1000; i++) {
    Bitmap bitmap = Bitmap.createBitmap(1000, 1000, Bitmap.Config.ARGB_8888);
    // 使用bitmap...
}

解决方案:
及时释放不再需要的资源,例如使用bitmap.recycle()。

3.频繁创建对象:

// 在循环中频繁创建对象
for (int i = 0; i < 1000000; i++) {
    Object obj = new Object();
    // 使用obj...
}

解决方案:
避免在循环中频繁创建对象,考虑对象池的使用,获取使用内存缓存。

4.大量数据加载:

// 一次性加载大量数据到内存
List<SomeData> dataList = loadDataFromNetwork();

解决方案:使用分页加载或流式处理数据,而不是一次性加载所有数据。

  1. 内存泄漏:
    解决方案:
    避免在长时间生命周期对象中持有引用,使用弱引用或及时释放引用。
一些预防OOM的措施包括:

1.优化内存使用: 确保及时释放不再需要的对象,避免创建过多的临时对象。
2.使用内存分析工具: 使用工具如Android Profiler或MAT(Memory Analyzer Tool)来检测内存泄漏和分析内存使用情况。
3.合理使用Bitmap: 对于大图,使用合适的缩放技术,及时回收不再需要的Bitmap对象。
4.分页加载: 对于大量数据,采用分页加载或流式处理,而不是一次性加载所有数据。
5.使用内存缓存: 对于频繁使用的资源,考虑使用内存缓存来避免重复加载。

如果发生了异常情况,怎么快速止损?

1.功能开关
首先,需要让App具备一些高级的能力,我们对于任何要上线的新功能,要加上一个功能的开关,通过配置中心下发的开关呢,来决定是否要显示新功能的入口。如果有异常情况,可以紧急关闭新功能的入口,那就可以让这个App处于可控的状态了。

2.关闭灰度发布
如果问题更严重,范围更广,可以立即关闭灰度发布版本。

3.动态修复:热修复、资源包更新
目前热修复的方案其实已经比较成熟了,我们完全可以低成本地在我们的项目中添加热修复的能力,当然,如果有些功能是由RN或WeeX来实现就更好了,那就可以通过更新资源包的方式来实现动态更新。

参考:
https://mp.weixin.qq.com/s/Wwazb5HFd5hunq5GOkjWmQ

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容