文章目录
1、如何提升 App 的稳定性?
- (1)提升应用代码质量;
- (2)建立有效的 Code Review 机制;
- (3)Java Crash 监控;
- (4)Java 混淆代码还原;
- (5)Native Crash 监控;
- (6)addr2line 堆栈还原;
- (7)拓展成 DebugTool 工具类,提供在开发阶段开发和测试同学使用。
- 重在预防、贵在治理。
- 长效保持需要科学流程,我们可以从开发阶段、测试阶段、发布阶段、运维阶段、降级容灾阶段这五个阶段来处理。
1.1 开发阶段
- 技术评审、CodeReview 机制、主动容错处理。
1.1.1 技术评审
- 技术评审的目的是以技术的角度来评估本次项目功能的实现方式、业务架构、可能遇到的重难点、可以采取的降级策略进行论证。
- 重要逻辑统一两端的实现方式。
- 可以采取的降级策略:
- (1)兜底策略:首次安装时,网络请求失败,此时展示跟包数据;
- (2)缓存策略:先展示缓存数据–再更新接口数据–接口失败再考虑兜底策略;
- (3)远程开关策略:通过配置平台配置功能开关,清除脏数据。
1.1.2 强制 Code Review 机制
- 经过理论及实践表明,定期进行 Code Review 有如下几点好处:
- (1)能够学习他人代码,能够开阔思路,并且提升代码健壮性,改掉边界条件考虑不周的情况;
- (2)对于测试同学没有能够测试到的 bug 提前进行修复,降低线上 bug 及 crash 率;
- (3)在 Code Review 会议中集思广益,促进团队成员交流,有助于营造团队协助的团队氛围。
- 开发的时候一把梭,上线前没有 Code Review,上线后风险还是自己承担!!!
1.1.3 主动容错
- 我们开发时候正常运行,但是到了测试后者用户手上的时候问题不断。究其原因:一方面是数据源处理问题,另外一方面是我们对这些部分没有主动进行容错处理。通过需要我们对这些地方进行异常捕捉,也许某个功能无法正常使用,但是终究不会引发崩溃。
- (1)字符串、数组、集合操作
- 字符串变换,截取等等,常见异常有空指针,长度越界,蹦到这类操作的时候,我们最好封装一个工具类,对整个方法进行异常捕捉。
- (2)数据转换
- 空数组,数据结构不匹配,解析时可能会出现异常,进而导致功能无法正常使用。
- (3)生命周期
- 页面关闭后异步任务回调,又没有判空,进而导致 NPE。
1.2 测试阶段
- 功能测试 check-list、回归测试、覆盖安装测试、边界特殊场景测试、机型兼容测试;
- 云测平台提供多种机型,进行兼容性,性能自动化测试。
1.3 发布阶段
- 灰度系统:逐步放量,观察灰度版本的 crash 情况,发现问题解决问题;
- 多轮灰度;
- ABTest 测试,一般用户接入优化逻辑,一半用户不接入。
1.4 运维阶段
- 发布到客户手里的程序必然是存在问题的,在这种情况下的日志收集就十分重要了。
1.4.1 Crash 监控日志收集
- (1)启动流程、重点流程 Crash:
- 处理策略:启动阶段 crash 建设安全模式,重点流程 crash 建设告警机制。
- (2)增量、存量 Crash 率:
- 增量->新出现的 Crash->新版的重点。
- 存量->老版本就有 Crash->继续啃的硬骨头。
- 处理策略:有限解决增量,持续跟进存量。
- (3)Crash 日志收集:
- 三方平台,比如说:友盟、bugly…
- 自己封装 UncaughtException,然后上传到自己的服务器。
1.4.2 非 Crash 的异常监控日志收集
- 用户反馈页面点击无反应,功能按键没展示,流程不正常,非 Crash 的异常,无法复现很难排查。
- 建设客户端运行时日志体系,远程日志按需回捞–Xlog 高性能日志库,以打点的形式记录关键的执行流程。
- (1)Catch 代码块,catch 导致的功能不可用;
- (2)异常逻辑,如某个方法返回 false 导致的功能不可用;
- (3)逻辑分支,如执行了 else 逻辑导致操作流程不正常。
// 异常业务监控
try{
// 一些业务逻辑
} catch (Exception e){
XLog.info('happen exception:' + e.getMessage());
}
// 逻辑监控
if (flag){
XLog.info('execute normal logic')
}else{
XLog.info('execute downgrade logic')
}
1.4.2 报警策略
- 阈值报警:crash 率超过某个值,舆情反馈超过多少数量,异常率超过一定次数;
- 趋势报警:昨天异常和今天异常的日常对比,超过某个百分比后报警。
try{
} catch (Exception e){
// 打点主动上报,模块名,页面名,方法名,异常信息描述
UTAnalyse.post('module_home','home_fragment','refresh','检测到顶部tab数据为空~');
}
1.5 降级容灾策略
1.5.1 配置平台
- 配置中心,功能开发。在一个可视化的配置平台上配置开关和它的值。App 启动时拉取最新的配置数据。
// 配置管理类
public class ConfigManager{
public static boolean sOpenClick = true;
}
// 在添加或者修改的代码中添加判断
if(ConfigManager.sOpenClick){
// 新逻辑
} else {
// 老逻辑
}
1.5.2 安全模式
- 根据 Crash 信息自动恢复,多次启动失败重置 App。
// 1. 通过 UncaughtExceptionHandler 记录崩溃
// 2. 在 Application
private int crashTimes;// 崩溃次数
@Override
protected void attachBaseContext(Context base){
super.attachBaseeContext(base);
if(crashTimes >= 3){
// 1. 删除缓存文件
// 2. 删除配置文件
// 3. 删除补丁文件
// 4. 删除动态下载的资源(so)文件
// 5. 重置到新安装的状态
}
}
1.5.3 统跳中心
- 模块化开发的路由,有问题的界面不进行跳转或者跳转至统一提示界面。
1.5.4 动态化修复
- 热修复;
- Weex、RN 增量更新;
- 动态化组件 VirtualView 更新组件。
2、建立有效的 Code Review 机制
2.1 什么是 Code Review?
- Code Review 就是代码评审,它能够在帮助团队找到代码缺陷这件事情上起到巨大的作用,代码审查一般可以找到以及移除 65% 的错误,最高可以到 85%。
- (1)传播知识;
- (2)增进代码质量;
- (3)找出潜在的 bug。
2.2 Code Review 需要做什么?
- 发现错误:对于测试同学没有能够测试找到的 bug 提前修复,降低线上 bug 及 crash 率;
- 代码健壮性检查:代码是否健壮,是否有潜在安全、性能风险。这里我们主要是检查对于异常情况是否有足够的容错处理,日志记录,告警埋点等;
- 代码质量检查:解决一个问题的实现方式有多重的,如果你的解决方案是 200 行代码,别人的代码是 50 行。那么为什么不使用更小的代码来解决呢?代码写的越多,潜在的问题就越多。这里主要是检查采用的数据结构是否合理,是否使用统一的线程管理库,组件库等等…
- 编码风格检查:对于整个团队来说,代码风格的统一很重要。这里主要是检查类名,方法名,字段名,资源文件名是否通俗易懂;
- 检查关键注释:检查代码中复杂实现是否有解释性的注释,紧急 hack 是否明确标注等,todo 是否有被解决等等。
2.3 配合工具建立强制 Code Review 机制
- 如果只是靠人的自觉,是很难长时间实施下去的,所以我们就需要配合工具建立一个强制的 Code Review 机制,当我们提交代码的时候,如果我们不经过别人的 Review,它是无法合并到 master 分支上的,当别人 Review 之后,提出了修改意见,我们也必须修改之后重新提交,再次经过别人的 Review,最终没有问题后才能够合并到 master 分支上去。
- 企业常用工具:GitLab 仓库管理平台
2.3.1 建立自动化的 Review 通知机制
- 当我们想要提交代码的时候,或者我们想要合并代码的时候,它会自动通知组内的成员进行代码的审查,审查通过之后就提交或合并上去。
- 个人使用:Gitee 仓库管理平台,对外提供了 WebHook 的能力,且免费。
- 企业使用:GitLab 仓库管理平台
3、FrameWork 层对 Java & Native Crash 监控
3.1 抛出异常程序为什么会崩溃?
- 线程中抛出异常以后的处理逻辑
- (1)默认情况了下,线程组处理未捕获异常的逻辑是,首先将异常消息通知给父线程组,然后利用一个默认的 defaultUncaughtExceptionHandler 来处理异常;
- (2)如果没有默认的异常处理器则将错误信息输出到 System.err;
- (3)也就是 JVM 提供给我们设置每个线程的具体的未捕获异常处理器,也提供了设置默认异常处理器的方法。
class Thread implements Runnable{
private ThreadGroup group;
// 当前线程单独设置的异常处理器
private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;
// new RuntimeException() 未被捕获-->分发异常消息
public final void dispatchUncaughtException(Throwable e){
getUncaughtExceptionHandler().uncaughtException(this, e);
}
UncaughtExceptionHandler getUncaughtExceptionHandler(){
// 默认情况,线程的异常处理器都是为空的,所以线程的异常都交由线程组统一处理
return uncaughtExceptionHandler != null ? uncaughtExceptionHandler : group;
}
// 普通方法,设置当前线程独有的异常处理器
public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh){
this.uncaughtExceptionHandler = eh;
}
}
- 然后看下一
ThreadGroup
中实现uncaughtException(Thread t, Throwable e)
方法: - (1)一旦线程出现抛出异常,并且我们没有捕捉的情况下,JVM 将调用 Thread 中的 dispatchUncaughtException()方法把异常传递给线程的未捕获异常处理器;
- (2)如果没有设置 uncaughtExceptionHandler,将使用线程所在的线程组来处理这个未捕获异常;
- (3)线程组 ThreadGroup 实现了 UncaughtExceptionHandler,所以可以用来处理未捕获异常。
class ThreadGroup implements Thread.UncaughtExceptionHandler{
public void uncaughtException(Thread t, Throwable e){
// 获取线程默认异常处理器
Thread.UncaughtExceptionHandler ueh = Thread.getDefaultUncaughtExceptionHandler();
if(ueh != null){
// 交由默认处理器处理
ueh.uncaughtException(t, e);
}eles if(!(e instanceof ThreadDeth)){
// 否则就打印在控制台
System.err.print("Exception in thread " + t.getName());
e.printStackTrace(system.err);
}
}
}
3.2 RuntimeInit 类分析
- 然后看一下 RuntimeInit 类,由于是 java 代码,所以首先找 main 方法入口:
class RuntimeInit{
public static final void main(String[] argv){
...
commonInit();
...
}
protected static final void commonInit(){
LoggingHandler loggingHandler = new LoggingHandler();
// 可以发现这里调用了 setDefaultUncaughtExceptionHandler()方法,设置了默认的异常处理器
Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));
}
}
- 接着看一下 KillApplicationHandler 类,可以发现该类实现了 Thread.UncaughtExceptionHandler 接口:
private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
private final LoggingHandler mLoggingHandler;
public KillApplicationHandler(LoggingHandler loggingHandler) {
this.mLoggingHandler = Objects.requireNonNull(loggingHandler);
}
@Override
public void uncaughtException(Thread t, Throwable e) {
try {
ensureLogging(t, e);
if (mCrashing) return;
mCrashing = true;
if (ActivityThread.currentActivityThread() != null) {
ActivityThread.currentActivityThread().stopProfiling();
}
// 弹出异常弹窗的 Dialog
ActivityManager.getService().handleApplicationCrash(
mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
} catch (Throwable t2) {
if (t2 instanceof DeadObjectException) {
// System process is dead; ignore
} else {
try {
Clog_e(TAG, "Error reporting crash", t2);
} catch (Throwable t3) {
// Even Clog_e() fails! Oh well.
}
}
} finally {
// 杀死当前进程
Process.killProcess(Process.myPid());
// 退出虚拟机并释放资源
System.exit(10);
}
}
private void ensureLogging(Thread t, Throwable e) {
if (!mLoggingHandler.mTriggered) {
try {
mLoggingHandler.uncaughtException(t, e);
} catch (Throwable loggingThrowable) {
// Ignored.
}
}
}
}
- 其实在 fork 出 app 进程的时候,系统已经为 app 设置了一个异常处理器,并且最终崩溃后悔直接导致执行该 handler 的 finally 方法最后杀死 app 直接退出 app。如果你要自己处理,就可以自己实现 Thread.UncaughtExceptionHandler。
3.3 ActivityManagerService#handleApplicationCrash
- 从下面可以看出,若传入 app 为 null 时,processName 就设置为 system_server
public void handleApplicationCrash(IBinder app,
ApplicationErrorReport.ParcelableCrashInfo crashInfo) {
ProcessRecord r = findAppProcess(app, "Crash");
final String processName = app == null ? "system_server"
: (r == null ? "unknown" : r.processName);
handleApplicationCrashInner("crash", r, processName, crashInfo);
}
- 然后接着看一下 handleApplicationCrashInner() 方法做了什么:
- (1)调用 addErrorToDropBox 将应用 crash 进行封装输出;
- (2)watchdog、anr、wtf(what a terrible failure)、lowmem、native_crash、crash(java crash)。
void handleApplicationCrashInner(String eventType, ProcessRecord r, String processName,
ApplicationErrorReport.CrashInfo crashInfo) {
...
// 把错误信息写入本地文件,如/data/data/anr.txt,并输出控制台
addErrorToDropBox(
eventType, r, processName, null, null, null, null, null, null, crashInfo);
// 显示 crash 的弹窗
mAppErrors.crashApplication(r, crashInfo);
}
3.4 native_crash 如何监控?
- native_crash,顾名思义,就是 native 层发送的 crash。其实他是通过一个 NativeCrashListener 线程去监控的。
- SystemServer–>ActivityManagerService–>startObservingNativeCrashes()
// ActivityManagerService.java
public void startObservingNativeCrashes() {
final NativeCrashListener ncl = new NativeCrashListener(this);
ncl.start();
}
final class NativeCrashListener extends Thread{
...
@Override
public void run() {
final byte[] ackSignal = new byte[1];
if (DEBUG) Slog.i(TAG, "Starting up");
{
File socketFile = new File(DEBUGGERD_SOCKET_PATH);
if (socketFile.exists()) {
socketFile.delete();
}
}
try {
FileDescriptor serverFd = Os.socket(AF_UNIX, SOCK_STREAM, 0);
final UnixSocketAddress sockAddr = UnixSocketAddress.createFileSystem(
DEBUGGERD_SOCKET_PATH);
Os.bind(serverFd, sockAddr);
Os.listen(serverFd, 1);
Os.chmod(DEBUGGERD_SOCKET_PATH, 0777);
// 1. 一直循环的读 peerFd 文件,若发生存在,则进入 consumeNativeCrashData
while (true) {
FileDescriptor peerFd = null;
try {
if (MORE_DEBUG) Slog.v(TAG, "Waiting for debuggerd connection");
peerFd = Os.accept(serverFd, null /* peerAddress */);
if (MORE_DEBUG) Slog.v(TAG, "Got debuggerd socket " + peerFd);
if (peerFd != null) {
// 2. 进入 native crash 数据处理流程
consumeNativeCrashData(peerFd);
}
} catch (Exception e) {
Slog.w(TAG, "Error handling connection", e);
} finally {
if (peerFd != null) {
try {
Os.write(peerFd, ackSignal, 0, 1);
} catch (Exception e) {
if (MORE_DEBUG) {
Slog.d(TAG, "Exception writing ack: " + e.getMessage());
}
}
try {
Os.close(peerFd);
} catch (ErrnoException e) {
if (MORE_DEBUG) {
Slog.d(TAG, "Exception closing socket: " + e.getMessage());
}
}
}
}
}
} catch (Exception e) {
Slog.e(TAG, "Unable to init native debug socket!", e);
}
}
void consumeNativeCrashData(FileDescriptor fd) {
if (MORE_DEBUG) Slog.i(TAG, "debuggerd connected");
final byte[] buf = new byte[4096];
// 3. 启动 NativeCrashReporter 作为上报错误的新线程
final ByteArrayOutputStream os = new ByteArrayOutputStream(4096);
try {
...
do {
// get some data
bytes = Os.read(fd, buf, 0, buf.length);
if (bytes > 0) {
if (MORE_DEBUG) {
String s = new String(buf, 0, bytes, "UTF-8");
Slog.v(TAG, "READ=" + bytes + "> " + s);
}
// did we just get the EOD null byte?
if (buf[bytes-1] == 0) {
os.write(buf, 0, bytes-1); // exclude the EOD token
break;
}
// no EOD, so collect it and read more
os.write(buf, 0, bytes);
}
} while (bytes > 0);
...
final String reportString = new String(os.toByteArray(), "UTF-8");
(new NativeCrashReporter(pr, signal, reportString)).start();
...
}catch(Exception e){
...
}
}
...
}
- 上报 native_crash 的线程–>NativeCrashReporter
class NativeCrashReporter extends Thread {
ProcessRecord mApp;
int mSignal;
String mCrashReport;
NativeCrashReporter(ProcessRecord app, int signal, String report) {
super("NativeCrashReport");
mApp = app;
mSignal = signal;
mCrashReport = report;
}
@Override
public void run() {
try {
// 1. 包装崩溃信息
CrashInfo ci = new CrashInfo();
ci.exceptionClassName = "Native crash";
ci.exceptionMessage = Os.strsignal(mSignal);
ci.throwFileName = "unknown";
ci.throwClassName = "unknown";
ci.throwMethodName = "unknown";
ci.stackTrace = mCrashReport;
if (DEBUG) Slog.v(TAG, "Calling handleApplicationCrash()");
// 2. 转到 ams 中处理,跟普通的 crash 一致,只是类型不一样
mAm.handleApplicationCrashInner("native_crash", mApp, mApp.processName, ci);
if (DEBUG) Slog.v(TAG, "<-- handleApplicationCrash() returned");
} catch (Exception e) {
Slog.e(TAG, "Unable to report native crash", e);
}
}
}
4、Java Crash 监控
4.1 如何收集 Java_Crash 日志
- UncaughtException Handler -> 收集设备+堆栈信息并写入文件 -> 杀进程重启 App
- 应该收集哪些设备信息?
- 设备类型、OS 版本、线程名、前后台、使用时长、App 版本、升级渠道
- CPU 架构、内存信息、存储信息、permission 权限
internal object CrashHandler {
var CRASH_DIR = "crash_dir"
// 提供给外部在 Application 的 onCreate() 方法进行初始化
fun init(crashDir: String) {
Thread.setDefaultUncaughtExceptionHandler(CaughtExceptionHandler())
this.CRASH_DIR = crashDir
}
private class CaughtExceptionHandler : Thread.UncaughtExceptionHandler {
private val context = AppGlobals.get()!!
private val formatter = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.CHINA)
private val LAUNCH_TIME = formatter.format(Date())
private val defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
override fun uncaughtException(t: Thread, e: Throwable) {
if (!handleException(e) && defaultExceptionHandler != null) {
defaultExceptionHandler.uncaughtException(t, e)
}
restartApp()
}
private fun restartApp() {
val intent: Intent? =
context.packageManager?.getLaunchIntentForPackage(context.packageName)
intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
context.startActivity(intent)
Process.killProcess(Process.myPid())
exitProcess(10)
}
private fun handleException(e: Throwable?): Boolean {
if (e == null) return false
val log = collectDeviceInfo(e)
if (BuildConfig.DEBUG) {
HiLog.e(log)
}
saveCrashInfo2File(log)
return true
}
private fun saveCrashInfo2File(log: String) {
val crashDir = File(CRASH_DIR)
if (!crashDir.exists()) {
crashDir.mkdirs()
}
val crashFile = File(crashDir, formatter.format(Date()) + "-crash.txt")
crashFile.createNewFile()
val fos = FileOutputStream(crashFile)
try {
fos.write(log.toByteArray())
fos.flush()
} catch (ex: Exception) {
ex.printStackTrace()
} finally {
fos.close()
}
}
/**
* 设备类型、OS本版、线程名、前后台、使用时长、App版本、升级渠道
* CPU架构、内存信息、存储信息、permission权限
*/
private fun collectDeviceInfo(e: Throwable): String {
val sb = StringBuilder()
sb.append("brand=${Build.BRAND}\n")// huawei,xiaomi
sb.append("rom=${Build.MODEL}\n") // sm-G9550
sb.append("os=${Build.VERSION.RELEASE}\n")// 9.0
sb.append("sdk=${Build.VERSION.SDK_INT}\n")// 28
sb.append("launch_time=${LAUNCH_TIME}\n")// 启动 APP 的时间
sb.append("crash_time=${formatter.format(Date())}\n")// crash 发生的时间
sb.append("forground=${ActivityManager.instance.front}\n")// 应用处于前后台
sb.append("thread=${Thread.currentThread().name}\n")// 异常线程名
sb.append("cpu_arch=${Build.CPU_ABI}\n")// armv7 armv8
// app 信息
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
sb.append("version_code=${packageInfo.versionCode}\n")
sb.append("version_name=${packageInfo.versionName}\n")
sb.append("package_name=${packageInfo.packageName}\n")
sb.append("requested_permission=${Arrays.toString(packageInfo.requestedPermissions)}\n")// 已申请到那些权限
// 统计一波 存储空间的信息
val memInfo = android.app.ActivityManager.MemoryInfo()
val ams =
context.getSystemService(Context.ACTIVITY_SERVICE) as android.app.ActivityManager
ams.getMemoryInfo(memInfo)
sb.append("availMem=${Formatter.formatFileSize(context, memInfo.availMem)}\n")//可用内存
sb.append("totalMem=${Formatter.formatFileSize(context, memInfo.totalMem)}\n")//设备总内存
val file = Environment.getExternalStorageDirectory()
val statFs = StatFs(file.path)
val availableSize = statFs.availableBlocks * statFs.blockSize
sb.append(
"availStorage=${
Formatter.formatFileSize(
context,
availableSize.toLong()
)
}\n"
)// 存储空间
val write: Writer = StringWriter()
val printWriter = PrintWriter(write)
e.printStackTrace(printWriter)
var cause = e.cause
while (cause != null) {
cause.printStackTrace(printWriter)
cause = cause.cause
}
printWriter.close()
sb.append(write.toString())
return sb.toString()
}
}
fun crashFiles(): Array<File> {
return File(
AppGlobals.get()?.cacheDir,
CRASH_DIR
).listFiles()
}
}
4.2 Java Crash 后收集到的 Crash 日志文件(未还原前)
4.3 混淆代码还原
- 工具位于 Android SDK 中 /tools/proguard/bin/目录
4.3.1 使用 GUI 工具:
- (1)terminal 命令终端中目录切换到工具所在的目录,执行运行 proguardgui.sh(mac)脚本;
- (2)在左边的菜单选择 ReTrace;
- (3)在上面的 mapping file 文件中选择你的 mapping.txt 文件,在下面输入框输出你要还原的代码;
- (4)点击右下角的 ReTrace 按钮。
- mapping.txt 文件所在的位置:
4.3.2 使用命令行工具:
- (1)准备好 mapping.txt 文件;
- (2)准备好咬还原的堆栈信息 stacktrace 文件;
- (3)根据文件位置执行以下命令(本例三个文件在同目录,文件名如下);
- (4)执行命令
sh retrace.sh -verbose mapping.txt stacktrace.txt > out.txt
5、Native Crash 监控
5.1 现有方案
方案 | 优点 | 缺点 |
---|---|---|
Google-breakpad(推荐使用) | 权威,跨平台 | |
logcat | 利用安卓系统实现 | 需要过滤掉无用日志 |
coffecatch | 实现简介,改动容易 | 存在兼容性问题 |
5.2 Native 崩溃的捕获流程
- (1)客户端:捕获到崩溃的时候,将收集到尽可能的有用信息写入日志文件,然后选择合适的时机上传到服务器;
- (2)服务端:读取客户端上报的日志文件,寻找适合的符号文件,生成可读的 C/C++ 调用栈。
5.3 接入 Google-breakpad
- breakpad GitHub 地址
5.3.1 编译本地的 minidump_stackwalk 可执行文件
minidump_stackwalk
可以把 breakpad 生成的 .dump 文件解析成 .txt 文件- (1)clone good-breakpad 源码;
- (2)在 breakpad 源码目录创建 artifact 文件夹,并进入
cd artifact
../ configure && make
make install
- (3)在
artifact/src/processor/
可以发现minidump_stackwalk
,将其拷贝到项目根目录下,方便测试使用(不需要集成)。
5.3.2 搭建 C++ 工程
- 选择 Native C++ template 模板工程;
- TODO:细节待完善
- 具体看 breakpad 如何配合使用,最终的结果可以生成一个 Native Crash 的 .txt 文件。
5.3.3 使用 addr2line 工具
- 使用 ndk aarch64-linux-android-addr2line 工具 将 crash 发生的内存地址解析成代码行号:
aarch64-linux-android-addr2line -f -C -e libnative-lib.so libbreakpad-core.so 0x5a4
5.3.4 开发阶段捕获 native crash
adb log | $NDK/ndk-stack -sym $PROJECT_PATH/obj/local/armeabi(项目 so 文件所在的目录)
5.3.5 如何监听 native crash 写入成功的回调事件?
6、拓展成 DebugTool 工具类
- 结合上述知识点,自己拓展实现一个 DebugTool 的工具类,然后集成在项目中,方便开发阶段时候提供给开发同学和测试同学使用,从而当 App crash 后能够快速查看反馈分析定位问题。
- 或者站在巨人的肩膀上利用爱奇艺的 xCrash 开源库。
- xCrash 是爱奇艺开源的在 Android 平台上面捕获异常的开源库,它能为 Android App 提供捕获 Java Crash、Native Crash 和 ANR Crash。