最近一直在排查游戏崩溃相关的问题,游戏已经接入了bugly,但是对于无法复现的bug,bugly提供的信息很有限,而且大部分还都是native层的,看的是一头雾水,而且lua/js脚本层的信息非常少,不利于我们修复问题。
该教程就是实现一个定制版本的bugly,当游戏发生异常时,尽可能多的收集lua/js脚本层的信息,更详细的帮助我们分析定位bug。
原理: signal函数
当app产生崩溃时系统会向当前进程中发送一个信号量,表示进程异常。
我们可以拦截这个信号量,收集相关的信息,上传到服务器,方便我们后续排查,对应的函数是:
void (*signal(int sig, void (*func)(int)))(int)
信号 | 含义 |
SIGABRT | (Signal Abort) 程序异常终止。 |
SIGFPE | (Signal Floating-Point Exception) 算术运算出错,如除数为 0 或溢出(不一定是浮点运算)。 |
SIGILL | (Signal Illegal Instruction) 非法函数映象,如非法指令,通常是由于代码中的某个变体或者尝试执行数据导致的。 |
SIGINT | (Signal Interrupt) 中断信号,如 ctrl-C,通常由用户生成。 |
SIGTERM | (Signal Terminate) 发送给本程序的终止请求信号。 |
SIGSEGV |
(Signal Segmentation Violation) 非法访问存储器,如访问不存在的内存单元。 |
这里看到了熟悉的SIGSEGV
,其实bugly收集到的大部分崩溃都是这个。
实现案例
部分需要说明的我直接写到注释里面了,以下的实现是可以直接集成到游戏中,收集到的崩溃信息比bugyly更友好,虽然没有bugyly全面。
c
复制代码
#include <curl/curl.h> #include <signal.h> static size_t writeData(void* ptr, size_t size, size_t nmemb, void* stream) { std::vector<char>* recvBuffer = (std::vector<char>*)stream; size_t sizes = size * nmemb; // add data to the end of recvBuffer // write data maybe called more than once in a single request recvBuffer->insert(recvBuffer->end(), (char*)ptr, (char*)ptr + sizes); return sizes; } // 使用curl同步接口上传崩溃数据 void uploadData(const std::string& uploadUrl, const std::string& data) { curl_global_init(CURL_GLOBAL_ALL); std::vector<char> stream; CURL* curl = curl_easy_init(); curl_slist* _headers = nullptr; _headers = curl_slist_append(_headers, "Content-Type:text/plain"); if(curl){ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, _headers); curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); curl_easy_setopt(curl, CURLOPT_POST, 1L); // post curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str()); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, data.size()); curl_easy_setopt(curl, CURLOPT_URL, uploadUrl.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &stream); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeData); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); CURLcode res = curl_easy_perform(curl); if (res != CURLE_OK) { CCLOG("%s", curl_easy_strerror(res)); } else { CCLOG("%s", stream.data()); } curl_easy_cleanup(curl); curl_slist_free_all(_headers); } curl_global_cleanup(); } void uploadCrashInfo() { std::string data = getStackInfo(); // 收集的崩溃数据,至于详细的数据结构,可以自行设计 std::string url = "https://data-box.onrender.com/log/add-log-string"; // 日志服务器 uploadData(url, data); // 必须是同步函数,异步上传无法保证正确将数据上传到服务器。 } void sigsegv_handler(int signum) { std::string name = "unknown"; switch (signum){ case SIGSEGV: name = "SIGSEGV"; break; case SIGABRT: name = "SIGABRT"; break; case SIGFPE: name = "SIGFPE"; break; case SIGILL: name = "SIGILL"; break; case SIGINT: name = "SIGINT"; break; case SIGTERM: name = "SIGTERM"; break; default: break; } // 可以在这里收集一些堆栈数据和堆栈变量相关数据,上传到服务器 uploadCrashInfo(); signal(signum, SIG_DFL); // 将异常处理转交给系统,否则会继续触发sigsegv_handler } void main() { signal(SIGSEGV, sigsegv_handler); int* p = nullptr; *p = 1;// 主动触发崩溃 }
收集崩溃的堆栈数据
lua
lua_getstack
,lua_getinfo
获取每一层堆栈,通过lua_getlocal
获取每层堆栈的变量情况,具体代码如下:
c++
复制代码
std::string getStackInfo() { std::string ret; lua_State* state = cocos2d::LuaEngine::getInstance()->getLuaStack()->getLuaState(); if (state) { lua_Debug ar; int level = 0; std::string lua = ""; int deep = 2; while (lua_getstack(state, level, &ar)) { // lua的调用堆栈信息 lua_getinfo(state, "nSl", &ar); std::ostringstream ss; if (ar.name) { ss << "<" << ar.name << "> at "; } else if (ar.what) { ss << "[" << ar.what << "] at "; } ss << (ar.source ? ar.source : "?") << ":" << ar.currentline << std::endl; ret += ss.str(); // lua堆栈的变量信息 std::ostringstream vs; if (deep > 0) { deep--; int i = 0; while (++i) { const char* name = lua_getlocal(state, &ar, i); if (!name) { break; } if (name[0] != '(') { int type = lua_type(state, -1); vs << varPrefix << name << ":"; switch (type) { case LUA_TNUMBER: vs << lua_tonumber(state, -1); break; case LUA_TSTRING: vs << lua_tostring(state, -1); break; case LUA_TBOOLEAN: vs << lua_toboolean(state, -1); break; case LUA_TTABLE: { vs << "table"; break; } case LUA_TNIL: vs << "nil"; break; case LUA_TFUNCTION: vs << "<function>" << lua_topointer(state, -1); break; case LUA_TUSERDATA: vs << "<userdata>"; break; default: vs << "<type" << type<<">"; break; } vs << endl; } lua_pop(state, 1); } } ret += ss.str(); ++level; } } return ret; }
js
js引擎一般都会提供类似上边lua的接口,我没有用到js引擎,所以这里不再赘述。
日志服务器
我自己编写了一个简单的可用日志服务器,方便测试
上传地址为:https://data-box.onrender.com/log/add-log-string
,直接发送post数据即可。
上传的日志可以在 https://tidys.gitee.io/data-box-viewer/index.html
进行查看。