Android Native 崩溃日志收集
通过崩溃捕获和收集,可以收集到已发布应用(游戏)的异常,以便开发人员发现和修改bug,对于提高软件质量有着极大的帮助。本文介绍了iOS和android平台下崩溃捕获和收集的原理及步骤,不过如果是个人开发应用或者没有特殊限制的话,就不用往下看了,直接把友盟sdk(一个统计分析sdk)加入到工程中就万事大吉了,其中的错误日志功能完全能够满足需求,而且不需要额外准备接收服务器。 但是如果你对其原理更感兴趣,或者像我一样必须要兼容公司现有的bug收集系统,那么下面的东西就值得一看了。
要实现崩溃捕获和收集的困难主要有这么几个:
1、如何捕获崩溃(比如c++常见的野指针错误或是内存读写越界,当发生这些情况时程序不是异常退出了吗,我们如何捕获它呢)
2、如何获取堆栈信息(告诉我们崩溃是哪个函数,甚至是第几行发生的,这样我们才可能重现并修改问题)
3、将错误日志上传到指定服务器(这个最好办)
我们先进行一个简单的综述。会引发崩溃的代码本质上就两类,一个是c++语言层面的错误,比如野指针,除零,内存访问异常等等;另一类是未捕获异常(Uncaught Exception),iOS下面最常见的就是objective-c的NSException(通过@throw抛出,比如,NSArray访问元素越界),android下面就是java抛出的异常了。这些异常如果没有在最上层try住,那么程序就崩溃了。 无论是iOS还是android系统,其底层都是unix或者是类unix系统,对于第一类语言层面的错误,可以通过信号机制来捕获(signal或者是sigaction,不要跟qt的信号插槽弄混了),即任何系统错误都会抛出一个错误信号,我们可以通过设定一个回调函数,然后在回调函数里面打印并发送错误日志。
一、iOS平台的崩溃捕获和收集
1、设置开启崩溃捕获
- static int s_fatal_signals[] = {
- SIGABRT,
- SIGBUS,
- SIGFPE,
- SIGILL,
- SIGSEGV,
- SIGTRAP,
- SIGTERM,
- SIGKILL,
- };
- static const char* s_fatal_signal_names[] = {
- "SIGABRT",
- "SIGBUS",
- "SIGFPE",
- "SIGILL",
- "SIGSEGV",
- "SIGTRAP",
- "SIGTERM",
- "SIGKILL",
- };
- static int s_fatal_signal_num = sizeof(s_fatal_signals) / sizeof(s_fatal_signals[0]);
- void InitCrashReport()
- {
- // 1 linux错误信号捕获
- for (int i = 0; i < s_fatal_signal_num; ++i) {
- signal(s_fatal_signals[i], SignalHandler);
- }
- // 2 objective-c未捕获异常的捕获
- NSSetUncaughtExceptionHandler(&HandleException);
- }
在游戏的最开始调用InitCrashReport()函数来开启崩溃捕获。 注释1处对应上文所说的第一类崩溃,注释2处对应objective-c(或者说是UIKit Framework)抛出但是没有被处理的异常。
2、打印堆栈信息
- + (NSArray *)backtrace
- {
- void* callstack[128];
- int frames = backtrace(callstack, 128);
- char **strs = backtrace_symbols(callstack, frames);
- int i;
- NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
- for (i = kSkipAddressCount;
- i < __min(kSkipAddressCount + kReportAddressCount, frames);
- ++i) {
- [backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
- }
- free(strs);
- return backtrace;
- }
幸好,苹果的iOS系统支持backtrace,通过这个函数可以直接打印出程序崩溃的调用堆栈。优点是,什么符号函数表都不需要,也不需要保存发布出去的对应版本,直接查看崩溃堆栈。缺点是,不能打印出具体哪一行崩溃,很多问题知道了是哪个函数崩的,但是还是查不出是因为什么崩的
3、日志上传,这个需要看实际需求,比如我们公司就是把崩溃信息http post到一个php服务器。这里就不多做声明了。
4、技巧---崩溃后程序保持运行状态而不退出
CFRelease(allModes);
- CFRunLoopRef runLoop = CFRunLoopGetCurrent();
- CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
- while (!dismissed)
- {
- for (NSString *mode in (__bridge NSArray *)allModes)
- {
- CFRunLoopRunInMode((__bridge CFStringRef)mode, 0.001, false);
- }
- }
- CFRelease(allModes);
在崩溃处理函数上传完日志信息后,调用上述代码,可以重新构建程序主循环。这样,程序即便崩溃了,依然可以正常运行(当然,这个时候是处于不稳定状态,但是由于手持游戏和应用大多是短期操作,不会有挂机这种说法,所以稳定与否就无关紧要了)。玩家甚至感受不到崩溃。
这里要在说明一个感念,那就是“可重入(reentrant)”。简单来说,当我们的崩溃回调函数是可重入的时候,那么再次发生崩溃的时候,依然可以正常运行这个新的函数;但是如果是不可重入的,则无法运行(这个时候就彻底死了)。要实现上面描述的效果,并且还要保证回调函数是可重入的几乎不可能。所以,我测试的结果是,objective-c的异常触发多少次都可以正常运行。但是如果多次触发错误信号,那么程序就会卡死。 所以要慎重决定是否要应用这个技巧。
二、android崩溃捕获和收集
1、android开启崩溃捕获
首先是java代码的崩溃捕获,这个可以仿照最下面的完整代码写一个UncaughtExceptionHandler,然后在所有的Activity的onCreate函数最开始调用
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler(this));
这样,当发生崩溃的时候,就会自动调用UncaughtExceptionHandler的public void uncaughtException(Thread thread, Throwable exception)函数,其中的exception包含堆栈信息,我们可以在这个函数里面打印我们需要的信息,并且上传错误日志
然后是重中之重,jni的c++代码如何进行崩溃捕获。
- void InitCrashReport()
- {
- CCLOG("InitCrashReport");
- // Try to catch crashes...
- struct sigaction handler;
- memset(&handler, 0, sizeof(struct sigaction));
- handler.sa_sigaction = android_sigaction;
- handler.sa_flags = SA_RESETHAND;
- #define CATCHSIG(X) sigaction(X, &handler, &old_sa[X])
- CATCHSIG(SIGILL);
- CATCHSIG(SIGABRT);
- CATCHSIG(SIGBUS);
- CATCHSIG(SIGFPE);
- CATCHSIG(SIGSEGV);
- CATCHSIG(SIGSTKFLT);
- CATCHSIG(SIGPIPE);
- }
通过singal的设置,当崩溃发生的时候就会调用android_sigaction函数。这同样是linux的信号机制。 此处设置信号回调函数的代码跟iOS有点不同,这个只是同一个功能的两种不同写法,没有本质区别。有兴趣的可以google下两者的区别。
2、打印堆栈
java语法可以直接通过exception获取到堆栈信息,但是jni代码不支持backtrace,那么我们如何获取堆栈信息呢? 这里有个我想尝试的新方法,就是使用google breakpad,貌似它现在完整的跨平台了(支持windows, mac, linux, iOS和android等),它自己实现了一套minidump,在android上面限制会小很多。 但是这个库有些大,估计要加到我们的工程中不是一件非常容易的事,所以我们还是使用了简洁的“传统”方案。 思路是,当发生崩溃的时候,在回调函数里面调用一个我们在Activity写好的静态函数。在这个函数里面通过执行命令获取logcat的输出信息(输出信息里面包含了jni的崩溃地址),然后上传这个崩溃信息。 当我们获取到崩溃信息后,可以通过arm-linux-androideabi-addr2line(具体可能不是这个名字,在android ndk里面搜索*addr2line,找到实际的程序)解析崩溃信息。
jni的崩溃回调函数如下:
- void android_sigaction(int signal, siginfo_t *info, void *reserved)
- {
- if (!g_env) {
- return;
- }
- jclass classID = g_env->FindClass(CLASS_NAME);
- if (!classID) {
- return;
- }
- jmethodID methodID = g_env->GetStaticMethodID(classID, "onNativeCrashed", "()V");
- if (!methodID) {
- return;
- }
- g_env->CallStaticVoidMethod(classID, methodID);
- old_sa[signal].sa_handler(signal);
- }
可以看到,我们仅仅是通过jni调用了java的一个函数,然后所有的处理都是在java层面完成。
java对应的函数实现如下:
- public static void onNativeCrashed() {
- // http://stackoverflow.com/questions/1083154/how-can-i-catch-sigsegv-segmentation-fault-and-get-a-stack-trace-under-jni-on-a
- Log.e("handller", "handle");
- new RuntimeException("crashed here (native trace should follow after the Java trace)").printStackTrace();
- s_instance.startActivity(new Intent(s_instance, CrashHandler.class));
- }
我们开启了一个新的activity,因为当jni发生崩溃的时候,原始的activity可能已经结束掉了。 这个新的activity实现如下:
- public class CrashHandler extends Activity
- {
- public static final String TAG = "CrashHandler";
- protected void onCreate(Bundle state)
- {
- super.onCreate(state);
- setTitle(R.string.crash_title);
- setContentView(R.layout.crashhandler);
- TextView v = (TextView)findViewById(R.id.crashText);
- v.setText(MessageFormat.format(getString(R.string.crashed), getString(R.string.app_name)));
- final Button b = (Button)findViewById(R.id.report),
- c = (Button)findViewById(R.id.close);
- b.setOnClickListener(new View.OnClickListener(){
- public void onClick(View v){
- final ProgressDialog progress = new ProgressDialog(CrashHandler.this);
- progress.setMessage(getString(R.string.getting_log));
- progress.setIndeterminate(true);
- progress.setCancelable(false);
- progress.show();
- final AsyncTask task = new LogTask(CrashHandler.this, progress).execute();
- b.postDelayed(new Runnable(){
- public void run(){
- if (task.getStatus() == AsyncTask.Status.FINISHED)
- return;
- // It's probably one of these devices where some fool broke logcat.
- progress.dismiss();
- task.cancel(true);
- new AlertDialog.Builder(CrashHandler.this)
- .setMessage(MessageFormat.format(getString(R.string.get_log_failed), getString(R.string.author_email)))
- .setCancelable(true)
- .setIcon(android.R.drawable.ic_dialog_alert)
- .show();
- }}, 3000);
- }});
- c.setOnClickListener(new View.OnClickListener(){
- public void onClick(View v){
- finish();
- }});
- }
- static String getVersion(Context c)
- {
- try {
- return c.getPackageManager().getPackageInfo(c.getPackageName(),0).versionName;
- } catch(Exception e) {
- return c.getString(R.string.unknown_version);
- }
- }
- }
- class LogTask extends AsyncTask<Void, Void, Void>
- {
- Activity activity;
- String logText;
- Process process;
- ProgressDialog progress;
- LogTask(Activity a, ProgressDialog p) {
- activity = a;
- progress = p;
- }
- @Override
- protected Void doInBackground(Void... v) {
- try {
- Log.e("crash", "doInBackground begin");
- process = Runtime.getRuntime().exec(new String[]{"logcat","-d","-t","500","-v","threadtime"});
- logText = UncaughtExceptionHandler.readFromLogcat(process.getInputStream());
- Log.e("crash", "doInBackground end");
- } catch (IOException e) {
- e.printStackTrace();
- Toast.makeText(activity, e.toString(), Toast.LENGTH_LONG).show();
- }
- return null;
- }
- @Override
- protected void onCancelled() {
- Log.e("crash", "onCancelled");
- process.destroy();
- }
- @Override
- protected void onPostExecute(Void v) {
- Log.e("crash", "onPostExecute");
- progress.setMessage(activity.getString(R.string.starting_email));
- UncaughtExceptionHandler.sendLog(logText, activity);
- progress.dismiss();
- activity.finish();
- Log.e("crash", "onPostExecute over");
- }
最主要的地方是doInBackground函数,这个函数通过logcat获取了崩溃信息。 不要忘记在AndroidManifest.xml添加读取LOG的权限
- <uses-permissionandroid:name="android.permission.READ_LOGS"/>
- <uses-permission android:name="android.permission.READ_LOGS" />
3、获取到错误日志后,就可以写到sd卡(同样不要忘记添加权限),或者是上传。 代码很容易google到,不多说了。 最后再说下如何解析这个错误日志。
我们在获取到的错误日志中,可以截取到如下信息:
- 12-12 20:41:31.807 24206 24206 I DEBUG :
- 12-12 20:41:31.847 24206 24206 I DEBUG : #00 pc 004931f8 /data/data/org.cocos2dx.wing/lib/libhelloworld.so
- 12-12 20:41:31.847 24206 24206 I DEBUG : #01 pc 005b3a5e /data/data/org.cocos2dx.wing/lib/libhelloworld.so
- 12-12 20:41:31.847 24206 24206 I DEBUG : #02 pc 005aab68 /data/data/org.cocos2dx.wing/lib/libhelloworld.so
- 12-12 20:41:31.847 24206 24206 I DEBUG : #03 pc 005ad8aa /data/data/org.cocos2dx.wing/lib/libhelloworld.so
- 12-12 20:41:31.847 24206 24206 I DEBUG : #04 pc 005924a4 /data/data/org.cocos2dx.wing/lib/libhelloworld.so
- 12-12 20:41:31.847 24206 24206 I DEBUG : #05 pc 005929b6 /data/data/org.cocos2dx.wing/lib/libhelloworld.so
- 004931f8
这个就是我们崩溃函数的地址, libhelloworld.so就是崩溃的动态库。我们要使用addr2line对这个动态库进行解析(注意要是obj/local目录下的那个比较大的,含有符号文件的动态库,不是Libs目录下比较小的,同时发布版本时,这个动态库也要保存好,之后查log都要有对应的动态库)。命令如下:
arm-linux-androideabi-addr2line.exe -e 动态库名称 崩溃地址
例如:
- $ /cygdrive/d/devandroid/android-ndk-r8c-windows/android-ndk-r8c/toolchains/arm-linux-androideabi-4.6/prebuilt/windows/bin/arm-linux-androideabi-addr2line.exe -e obj/local/armeabi-v7a/libhelloworld.so 004931f8
得到的结果就是哪个cpp文件第几行崩溃。 如果动态库信息不对,返回的就是 ?:0
原文地址:http://www.cnblogs.com/lancidie/archive/2013/04/13/3019349.html
Android Native 崩溃日志收集相关推荐
- 代码:android崩溃日志收集和处理
用来处理android崩溃日志收集的代码,详情的使用请转:android崩溃日志收集和处理 第一个类 /** * 异常捕捉实现类 */ public class ErrorCaughtimplemen ...
- Android 应用崩溃日志的收集和上传
如何将应用崩溃日志收集起来? Android 应用难以避免的会 crash ,也称为崩溃,无论你的程序多完美,总是无法避免 crash 的发生.这对用户来说是很不友好的,也是开发者所不愿意看到的.更糟 ...
- Android 异常崩溃日志,捕捉并保存到本地
Android 异常崩溃日志,捕捉并保存到本地: 前几天因为在省公安厅做一个通讯类之类的应用:碰到个问题,就是download人员信息将信息保存到本地数据库完成的时候,菊花转还没有dismission ...
- Android捕捉崩溃日志并输出日志文件
Android捕捉崩溃日志并输出日志文件 当程序与运行时发生崩溃,可以捕捉到当前崩溃的日志信息并写入文件保存到指定的目录下.这里还做了最大文件数量限制,超过数量即删除旧日志文件. import jav ...
- ios崩溃日志收集_漫谈iOS Crash收集框架
为了能够第一时间发现程序问题,应用程序需要实现自己的崩溃日志收集服务,成熟的开源项目很多,如 KSCrash,plcrashreporter,CrashKit 等.追求方便省心,对于保密性要求不高的程 ...
- 移动应用崩溃日志收集工具对比
背景 移动互联网时代,由于 Android 设备的碎片化,客服人员每天要接到很多用户反馈在各种不同机型上的崩溃问题,又没有办法提供具体的 Crash 日志给开发人员.测试人员每天需要对用户的反馈进行 ...
- ios崩溃日志收集_iOS崩溃与日志分析
在iOS开发中经常需要靠记录日志来调试应用程序.解决崩溃问题等,整理常用的日志输出和崩溃日志分析. 最新更新:2018-11-30 基于CocoaLumberjack 的 Swift使用封装库 一.崩 ...
- android崩溃日志收集
https://blog.csdn.net/wxx_csdn/article/details/79238842 转载于:https://blog.51cto.com/xuguohongai/21289 ...
- android查找邮件程序,Android 程序崩溃日志邮件获取
版权声明:本文为博主原创文章,未经博主允许不得转载. 在我们开发Android应用程序的时候,BUG的出现是难以避免的,时不时还会出现崩溃的情况,这个时候,我们急需知道造成问题的原因是什么,但是,在没 ...
- Android 保存崩溃日志到本地目录下
代码如下可以直接复制过去,别人的代码修改了下 package com.hly.rtxt; import android.annotation.SuppressLint; import android. ...
最新文章
- Adnroid文件存储路径getFilesDir()与getExternalFilesDir的区别
- 尝试用单元测试做spring注入调用service
- 桌面虚拟化之用户评估指南 (翻译)
- xwpftemplate的时间设置_数据导出生成word附件使用POI的XWPFTemplate对象
- GStreamer基础教程01 - Hello World
- Android之汽车音频
- 关于访问远程服务器的一些基本操作
- microsoft html help workshop_云话科技 | 奥比中光Workshop技术研讨线上沙龙
- 在VS2013平台下,用VB.net 连接Access数据库
- Unity之FBX文件操作学习笔记(一)
- Revit二次开发资料汇总
- Python:火山小视频-无水印视频-多线程-批量采集实现和完整代码
- 如何使用PC3000检测硬盘
- MySQL 从入门到实践,万字详解!
- adobe cs4系列套装及注册机下载
- 成都开发者看过来!百度资深研发工程师将出席超级账本成都见面会
- iPhone 13发布前迎来坏消息,摩托车会损坏手机相机?
- C# 名称空间的别名
- 零基础学C语言(C语言入门)
- IAR代码溢出问题处理section placement failed
热门文章
- java parser_愿你走出半生,归来仍是Java Parser
- java判断是否是英文_Java 判断输入是否为英文字符
- 股票交易接口程序概述
- 【安全】如何防止他人恶意调试你的web程序
- Codeforces 235C Cyclical Quest(后缀自动机)
- DWT(离散小波变换)与其简单应用
- oracle数据库exec用法,Sql中exec的用法
- Elastic 7.13.0 版重磅发布:在 Elastic 上搜索和存储更多数据
- .7z文件 合并、解压
- android imageview图片崩溃,安卓 ImageView 的使用及崩溃闪退、空白原因