文章目录
 
 
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;
    
    
    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();
        
        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();
            }
            
            ActivityManager.getService().handleApplicationCrash(
                    mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
        } catch (Throwable t2) {
            if (t2 instanceof DeadObjectException) {
                
            } else {
                try {
                    Clog_e(TAG, "Error reporting crash", t2);
                } catch (Throwable t3) {
                    
                }
            }
        } 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) {
                
            }
        }
    }
}
 
- 其实在 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) {
    ...
    
    
    addErrorToDropBox(
            eventType, r, processName, null, null, null, null, null, null, crashInfo);
    
    mAppErrors.crashApplication(r, crashInfo);
}
 
3.4 native_crash 如何监控?
 
- native_crash,顾名思义,就是 native 层发送的 crash。其实他是通过一个 NativeCrashListener 线程去监控的。
 - SystemServer–>ActivityManagerService–>startObservingNativeCrashes()
 
 
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);
            
            while (true) {
                FileDescriptor peerFd = null;
                try {
                    if (MORE_DEBUG) Slog.v(TAG, "Waiting for debuggerd connection");
                    peerFd = Os.accept(serverFd, null );
                    if (MORE_DEBUG) Slog.v(TAG, "Got debuggerd socket " + peerFd);
                    if (peerFd != null) {
                        
                        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];
        
        final ByteArrayOutputStream os = new ByteArrayOutputStream(4096);
        try {
            ...
             do {
                
                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);
                    }
                    
                    if (buf[bytes-1] == 0) {
                        os.write(buf, 0, bytes-1);  
                        break;
                    }
                    
                    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 {
            
            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()");
            
            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"
    
    
    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()
            }
        }
        
        private fun collectDeviceInfo(e: Throwable): String {
            val sb = StringBuilder()
            sb.append("brand=${Build.BRAND}\n")
            sb.append("rom=${Build.MODEL}\n") 
            sb.append("os=${Build.VERSION.RELEASE}\n")
            sb.append("sdk=${Build.VERSION.SDK_INT}\n")
            sb.append("launch_time=${LAUNCH_TIME}\n")
            sb.append("crash_time=${formatter.format(Date())}\n")
            sb.append("forground=${ActivityManager.instance.front}\n")
            sb.append("thread=${Thread.currentThread().name}\n")
            sb.append("cpu_arch=${Build.CPU_ABI}\n")
            
            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
 
 
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。