0
点赞
收藏
分享

微信扫一扫

Android App 稳定性优化

王小沫 2022-04-13 阅读 52
性能优化

文章目录

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 日志文件(未还原前)

Java Crash 日志文件截图

4.3 混淆代码还原

  • 工具位于 Android SDK 中 /tools/proguard/bin/目录
    Java 混淆代码还原工具目录截图
4.3.1 使用 GUI 工具:
  • (1)terminal 命令终端中目录切换到工具所在的目录,执行运行 proguardgui.sh(mac)脚本;
  • (2)在左边的菜单选择 ReTrace;
  • (3)在上面的 mapping file 文件中选择你的 mapping.txt 文件,在下面输入框输出你要还原的代码;
  • (4)点击右下角的 ReTrace 按钮。
    Java 混淆代码还原 GUI 工具截图
  • mapping.txt 文件所在的位置:
    mapping 文件所在位置截图
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。
举报

相关推荐

0 条评论