文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

React Native release包全局错误处理——Android篇

2022-06-06 13:23

关注

当我们在开发React Native应用时,如果在调试状态下,获取错误的异常信息是非常简单的,JS异常会立即在真机上显示(或者打开调试模式在浏览器控制台中显示),原生层的java闪退异常则可以通过Android Studio的Logcat进行查看。

但是当我们将应用打包成apk包,并提交测试的时候,一旦出现异常或闪退都会比较棘手。如果复现步骤简单可能还好,我们可以尝试在开发环境下复现,可是一旦碰上小概率,且复现步骤不明确的bug就很难了,而且改bug的效率也会非常低。

这里就需要我们及时的将全局未捕获异常以文件的形式写入到手机本地存储中。接下来我将分两种情况介绍具体的处理方法:

1. JS异常

JS异常即RN层抛出的异常,如果异常不是很严重,往往会出现这种情况:APP未闪退,但是界面显示为白屏。

捕获React全局未捕获异常,需要借助componentDidCatch方法,我们可以先编写一个全局错误处理组件:

AppErrorHandler.js



import React, { Component } from 'react';
import { saveJSExceptionsToStorage } from '@nativeModules';
import { LogUtil } from '@utils';
class AppErrorHandler extends Component {
  componentDidCatch(error, errorInfo) {
    // 全局未捕获JS异常处理,DEBUG模式输出日志,RELEASE模式存储日志到本地
    LogUtil("AppErrorHandler: ", error.stack);
    if (!__DEV__) {
      saveJSExceptionsToStorage(error.stack);
    }
  }
  render() {
    const { children } = this.props;
    return children;
  }
}
export default AppErrorHandler;

上面的saveJSExceptionsToStorage方法是我封装的将错误信息写入手机存储的原生方法,具体可以查看我的这篇博客。

error.stack是字符串形式的错误异常栈信息,这样获取到的异常信息比error.message要更加完整。

对__DEV__进行判断的作用是:只有在release模式中,才将JS异常写入本地,因为如果是调试状态,我们根本不需要存储异常信息。

然后就是在我们的根组件中:

App.js


// ......
const App = () => {
  useEffect(() => {
    isMountedRef.current = true;
    return () => {
      isMountedRef.current = false;
    }
  });
  return (
             null }}>
              {AuthScreens()}
              {MainScreens()}
  );
};
export default App;

可以看到,我们把AppErrorHandler组件作为整个页面组件的根组件使用。这样在release包中抛出JS异常时,我们就能到

/storage/emulated/0/Android/data/${packageName}/file/jsExceptionLogs/log_yyyy_MM_dd_HH_mm_ss.txt中拿到异常信息。

注:建议将app中生成的日志文件保存在/storage/emulated/0/Android/data/${packageName}/file/路径下,这个路径的内容会在APP被卸载时清除。与他同级的还有一个目录/storage/emulated/0/Android/data/${packageName}/caches,这个目录中往往用来存放一些应用缓存的临时内容,会在用户手动清除应用缓存时一并删除。上述的两个路径应该是属于Android外部存储的私有路径,我们往这里写入文件是不需要动态申请外部存储(EXTERNAL_STORAGE)写入权限的。 

2. 原生java异常

这类异常就比较严重了,一旦出现,APP会立即停止运行并闪退,造成极差的用户体验。为了捕获java中的全局未捕获异常,我们需要编写一个CrashHandler类并实现Thread.UncaughtExceptionHandler接口。代码比较简单,我就直接贴出整个类的实现吧:

CrashHandler.java


package com.smarthome;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import com.smarthome.utils.FileUtils;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

public class CrashHandler implements Thread.UncaughtExceptionHandler {
    private Context mContext;
    private static final String TAG = CrashHandler.class.getSimpleName();
    private CrashHandler() { }
    private static volatile CrashHandler instance;
    static CrashHandler getInstance() {
        if (instance == null) {
            synchronized (CrashHandler.class) {
                if (instance == null) {
                    instance = new CrashHandler();
                }
            }
        }
        return instance;
    }
    private Map crashInfos = new HashMap();  // 键值对的形式收集错误日志
    
    void init() {
        mContext = MainApplication.getAppContext();
        // 设置CrashHandler为程序默认的处理器
        Thread.setDefaultUncaughtExceptionHandler(this);
    }
    
    @Override
    public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
        handleException(throwable);
    }
    
    private void handleException(Throwable throwable) {
        getPackageInfo();
        String crashMsg = saveCrashInfo(throwable);
        writeLogToStorage(crashMsg);
        uploadCrashToServer(crashMsg);
        remindUserOfCrash();
    }
    
    private void getPackageInfo() {
        PackageManager packageManager = mContext.getPackageManager();
        try {
            PackageInfo packageInfo = packageManager.getPackageInfo(mContext.getPackageName(), PackageManager.GET_CONFIGURATIONS);
            if (packageInfo != null) {
                String packageName = packageInfo.packageName;
                String versionName = packageInfo.versionName;
                String versionCode;
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                    versionCode = String.valueOf(packageInfo.getLongVersionCode());
                } else {
                    versionCode = String.valueOf(packageInfo.versionCode);
                }
                crashInfos.put("packageName", packageName);
                crashInfos.put("versionName", versionName);
                crashInfos.put("versionCode", versionCode);
            }
        } catch(PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        Field[] fields = Build.class.getDeclaredFields();
        for (Field field : fields) {
            try {
                field.setAccessible(true);
                crashInfos.put(field.getName(), String.valueOf(field.get(null)));
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
    
    private String saveCrashInfo(Throwable throwable) {
        StringBuilder sb = new StringBuilder();
        sb.append("Crash Log start: ==========================\n");
        for (Map.Entry entry : crashInfos.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            sb.append(key).append(": ").append(value).append("\n");
        }
        Writer writer = new StringWriter();
        PrintWriter printWriter = new PrintWriter(writer);
        throwable.printStackTrace(printWriter);
        Throwable cause = throwable.getCause();
        // 回溯异常抛出链
        while (cause != null) {
            cause.printStackTrace(printWriter);
            cause = cause.getCause();
        }
        printWriter.close();
        String crashResult = writer.toString();
        sb.append("crashResult: ").append(crashResult);
        sb.append("end========================================\n");
        return sb.toString();
    }
    
    private void writeLogToStorage(String crashMsg) {
        String externalFilePath = FileUtils.getExternalFileDir().getPath();
        String targetDir = externalFilePath + File.separator + "crashlogs";
        File log = new File(targetDir);
        if (log.exists() ||
                (!log.exists() && log.mkdir())) {
            StringBuilder targetFilePath = new StringBuilder();
            Date now = new Date();
            DateFormat dateFormat = new SimpleDateFormat("yyyy_MM_dd_HH_mm_ss", Locale.getDefault());
            targetFilePath.append(targetDir).append(File.separator).append("crashlog_").append(dateFormat.format(now)).append(".txt");
            File targetFile = new File(targetFilePath.toString());
            FileUtils.writeTextToFiles(targetFile, crashMsg);
        }
    }
    
    private void uploadCrashToServer(String crashMsg) {
        Log.d(TAG, crashMsg);
    }
    
    private void remindUserOfCrash() {
        new Thread(() -> {
            Looper.prepare();
            Toast.makeText(mContext, "抱歉,程序发生异常,即将退出o(╥﹏╥)o", Toast.LENGTH_LONG).show();
            Looper.loop();
        }).start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        android.os.Process.killProcess(android.os.Process.myPid());
        System.exit(1);
    }
}

上面代码中值得关注的就以下几点:

(1)需要通过Thread.setDefaultUncaughtExceptionHandler(this);方法将当前类设置为全局未捕获异常处理类;

(2)我们需要重写uncaughtException方法来自定义监听到全局未捕获异常时的处理逻辑(收集包信息以及错误信息,导出本地文件或上传服务器等)

(3)uncaughtException方法中获取到的是throwable对象,如果想要获取异常抛出的堆栈信息,我们需要回溯异常抛出链。通过throwable.getCause();能够获取到导致当前异常抛出的异常

(4)当我们的异常收集工作完成后,在停止APP进程运行前,如果我们想要尽量友好的提示用户,比如用Toast。我们需要新开一个线程去执行他,否则会抛出如下异常:

导致Toast无法正常弹出。

同时,如果在新线程中想要Toast提示信息,必须在该线程中启动事件循环,否则会收到java.lang.RuntimeException: Can't toast on  a thread that has not called Looper.prepare();

所以我们需要在新线程中初始化Looper。也就是调用Looper.prepare()以及Looper.loop();。(主线程中不需要主动初始化Looper,是因为系统已经自动为我们的主线程开启了事件循环)

最后,我们只需要在入口文件MainApplication.java的onCreate方法中调用:

CrashHandler.getInstance().init();即可。

通过以上的步骤,我们基本上就构建了完备的release包异常捕获流程,当提测包出现异常时,我们只需要让测试人员把测试机上的对应时间的日志文件发给我们即可。这样就再也不用担心小概率bug始终无法复现啦~


作者:KarmaGut


阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-移动开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯