android和iOS平台的崩溃捕获和收集
转自:http://www.cnblogs.com/lancidie/archive/2013/04/13/3019349.html
通过崩溃捕获和收集,可以收集到已发布应用(游戏)的异常,以便开发人员发现和修改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[]);
void InitCrashReport()
{
// 1 linux错误信号捕获
for (int i = ; 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[];
int frames = backtrace(callstack, );
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、技巧---崩溃后程序保持运行状态而不退出
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++代码如何进行崩溃捕获。
#include <signal.h>
#include <stdlib.h>
static struct sigaction old_sa[NSIG];
void InitCrashReport()
{
CCLOG("InitCrashReport"); // Try to catch crashes...
struct sigaction handler;
memset(&handler, , 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实现如下:
publicclass CrashHandler extends Activity
{
publicstaticfinal String TAG = "CrashHandler";
protectedvoid 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(){
publicvoid 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(){
publicvoid 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();
}}, );
}});
c.setOnClickListener(new View.OnClickListener(){
publicvoid onClick(View v){
finish();
}});
} static String getVersion(Context c)
{
try {
return c.getPackageManager().getPackageInfo(c.getPackageName(),).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","","-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();
}
returnnull;
} @Override
protectedvoid onCancelled() {
Log.e("crash", "onCancelled");
process.destroy();
} @Override
protectedvoid 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"/>
3、获取到错误日志后,就可以写到sd卡(同样不要忘记添加权限),或者是上传。 代码很容易google到,不多说了。 最后再说下如何解析这个错误日志。
我们在获取到的错误日志中,可以截取到如下信息:
- ::31.807 I DEBUG :
- ::31.847 I DEBUG : # pc 004931f8 /data/data/org.cocos2dx.wing/lib/libhelloworld.so
- ::31.847 I DEBUG : # pc 005b3a5e /data/data/org.cocos2dx.wing/lib/libhelloworld.so
- ::31.847 I DEBUG : # pc 005aab68 /data/data/org.cocos2dx.wing/lib/libhelloworld.so
- ::31.847 I DEBUG : # pc 005ad8aa /data/data/org.cocos2dx.wing/lib/libhelloworld.so
- ::31.847 I DEBUG : # pc 005924a4 /data/data/org.cocos2dx.wing/lib/libhelloworld.so
- ::31.847 I DEBUG : # pc 005929b6 /data/data/org.cocos2dx.wing/lib/libhelloworld.so
004931f8
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
android和iOS平台的崩溃捕获和收集的更多相关文章
- 经典好文:android和iOS平台的崩溃捕获和收集
通过崩溃捕获和收集,可以收集到已发布应用(游戏)的异常,以便开发人员发现和修改bug,对于提高软件质量有着极大的帮助.本文介绍了iOS和android平台下崩溃捕获和收集的原理及步骤,不过如果是个人开 ...
- 学习笔记:APP切图那点事儿–详细介绍android和ios平台
学习笔记:APP切图那点事儿–详细介绍android和ios平台 转载自:http://www.woofeng.cn/articles/168.html 版权归原作者所有 作者:亚茹有李 原文地址 ...
- JSBridge(Android和IOS平台)的设计和实现
前言 对于商务类的app,随着app注册使用人数递增,app的运营者们就会逐渐考虑在应用中开展一些推广活动.大多数活动具备时效性强.运营时间短的特征,一般产品们和运营者们都是通过wap页面快速投放到产 ...
- 多媒体开发(7):编译Android与iOS平台的FFmpeg
编译FFmpeg,一个古老的话题,但小程还是介绍一遍,就当记录.之前介绍怎么给视频添加水印时,就已经提到FFmpeg的编译,并且在编译时指定了滤镜的功能. 但是,在手机盛行的时代,读者可能更需要的是能 ...
- 教你pomeloclient包libpomelo增加cocos2d-x 3.0工程(Windows、Android、IOS平台)
Windows平台 操作系统:Windows7(64-bit) VS版本号:2013 Cocos2d-x版本号:3.0 project路径:E:\cocos2d-prj\ 1.从github下载lib ...
- Android、iOS平台RTMP/RTSP播放器实时音量调节
介绍移动端RTMP.RTSP播放器实时音量调节之前,我们之前也写过,为什么windows播放端加这样的接口,windows端播放器在多窗口大屏显示的场景下尤其需要,尽管我们老早就有了实时静音接口,相对 ...
- JS对于Android和IOS平台的点击响应的适配
IOS点击事件 Click 300毫秒点击延迟 解决办法: 参考:http://cuiqingcai.com/1687.html 可判断设备 if (/(iPhone|iPad|iPod|iOS)/i ...
- ReactNative: Android与iOS平台兼容处理
方法一: 创建不同的文件扩展名:*.android.js*.io.js 方法二: import { Platform } from 'react-native'; if (Platform.OS == ...
- Unity在Android和iOS中如何调用Native API
本文主要是对unity中如何在Android和iOS中调用Native API进行介绍. 首先unity支持在C#中调用C++ dll,这样可以在Android和iOS中提供C++接口在unity中调 ...
随机推荐
- Unity3D 与 objective-c 之间数据交互。iOS SDK接口封装Unity3D接口 .-- 转载
Unity 3D 简单工程的创建.与Xcode 导出到iOS 平台请看这 Unity3D 学习 创建简单的按钮.相应事件 Unity C# 代码 using UnityEngine; using Sy ...
- NDK以及C语言基础语法(一)
一.什么是NDK? Native Development Kit (本地开发工具包): NDK中提供了一系列的工具,帮助我们快速开发C/C++的动态库,并能自动将so文件和java文件一起打包成apk ...
- EasyPlayer播放海康大华RTSP流时RTSPClient客户端连接兼容问题的解决
在之前的博客<EasyPlayer RTSP播放器对RTSP播放地址url的通用兼容修改意见>中,我描述了遇到的一个客户在播放大华某款摄像机时地址不兼容的问题,这不,团队刚刚参考我的这个意 ...
- await 暂停 等待 暂停的是什么
体验异步的终极解决方案-ES7的Async/Await var sleep = function (time) { return new Promise(function (resolve, reje ...
- Java for LeetCode 087 Scramble String
Given a string s1, we may represent it as a binary tree by partitioning it to two non-empty substrin ...
- 后端CORS解决跨域问题
一 . 为什么会有跨域问题 是因为浏览器的同源策略是对ajax请求进行阻拦了,但是不是所有的请求都给做跨域,像是一般的href 属性,a标签什么的都不拦截. 二 . 解决跨域的方法 解决跨域有两种方法 ...
- centos下安装nodejs及websocket
软件环境: VMware Workstation CentOS 6.5 NodeJS v0.12.5 安装过程: Step 1.确认服务器有nodejs编译及依赖相关软件,如果没有可通过运行以下命令安 ...
- js正則函數 match、exec、test、search、replace、split 使用介紹集合
match 方法 使用正則表達式模式對字元串執行查找,並將包含查找的結果作為數組返回. stringObj.match(rgExp) 參數 stringObj 必選項.對其進行查找的 String 對 ...
- TopCoder SRM420 Div1 RedIsGood —— 期望
题目链接:https://vjudge.net/problem/TopCoder-9915 (论文上的题) 题解: 更正:, i>0, j>0 代码如下: #include <ios ...
- Java常用四大线程池用法以及ThreadPoolExecutor详解
为什么用线程池? 1.创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度上影响处-理效率 2.线程并发数量过多,抢占系统资源从而导致阻塞 3.对线程进行一些简单的管理 在Java中,线 ...