崩溃代码
-- 创建一个node,但是并不加入到scene中,在下一帧就会被回收
local wildPointer = cc.Label:create();
wildPointer:setString("123");
-- 在下一帧,此时的wildPointer已经是野指针了,再次调用就会触发崩溃
wildPointer:setString("");
这种lua代码非常容易出现,比如一个lua变量持有了一个node,但是这个node被remove了,此时lua层再次操作这个node,就会直接出问题。
崩溃日志
com.game.test A Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 in tid 17314 (GLThread 4719), pid 17231 (com.game.test)
pid-18570 A pid: 17231, tid: 17314, name: GLThread 4719 >>> com.game.test <<<
pid-18570 A #00 pc 005b9674 /data/app/~~2dSXjw-Pwc5t1uuaelP0sA==/com.game.test-HVaqUS1t_su3ikG9VESEIQ==/lib/arm/libcocos2dlua.so (lua_cocos2dx_Label_setString(lua_State*)+180) (BuildId: 2198ce3de806dfb68c9d6346167edc344b301702)
pid-18570 A #01 pc 009165cc /data/app/~~2dSXjw-Pwc5t1uuaelP0sA==/com.game.test-HVaqUS1t_su3ikG9VESEIQ==/lib/arm/libcocos2dlua.so (BuildId: 2198ce3de806dfb68c9d6346167edc344b301702)
pid-18570 A #02 pc 00909055 /data/app/~~2dSXjw-Pwc5t1uuaelP0sA==/com.game.test-HVaqUS1t_su3ikG9VESEIQ==/lib/arm/libcocos2dlua.so (lua_pcall+20) (BuildId: 2198ce3de806dfb68c9d6346167edc344b301702)
通过ida,005b9674 反汇编结果
对应的代码如下,可以看到就是转换之后的cobj出现了问题
cobj此时是null,因为COCOS2D_DEBUG的原因,这个问题在release模式下直接崩溃闪退,debug版本还能提示异常。
解决办法
造成这个问题的根本原因是这种引用持有野指针的情况非常极限,想要复现都非常困难。
可以通过bugly后台,看到哪个函数崩溃,就在哪个函数里面将
if(!cobj){return 0;}的逻辑打开,避免崩溃
但是这样要不停地强更新,而且频繁更新app主包,会造成用户流失的,所以这种办法治标不治本,在c++层做各种保护根本起不到任何作用,问题根源还是lua脚本的逻辑,所以只能在lua层修复了这个问题。
要修复问题必须复现才行,但是这种情况又非常极限难以复现,所以只能尽可能的还原现场,根据收集到的信息进行排查,顺着这个思路,我们首先想到的是堆栈,是tolua_tousertype转为null导致的崩溃,那么我们可以增加一个机制,发现转换类型指针失败,,立即收集堆栈相关的数据并上报到后台。
这对于解决问题非常有帮助,当收到这些数据后,可以尝试复现下现场,这样才能从根源上解决问题,并且修复的肯定是lua代码,直接热更就行了,根本不需要更新app。
具体实现思路
在函数tolua_tousertype下个钩子
- tolua++.h
TOLUA_API void set_tousertype_nullhandler(void(*func)); // 设置钩子函数
- tolua++.c
void (*_tousertype_nullhandler)() = NULL; // 钩子函数
TOLUA_API void set_tousertype_nullhandler(void(* func))
{
_tousertype_nullhandler = func;
}
TOLUA_API void* tolua_tousertype (lua_State* L, int narg, void* def)
{
if (lua_gettop(L)<abs(narg))
return def;
else
{
void* u;
if (!lua_isuserdata(L, narg)) {
if (!push_table_instance(L, narg)) return NULL;
};
u = lua_touserdata(L,narg);
void* ret = (u==NULL) ? NULL : *((void**)u); /* nil represents NULL */
if (ret == NULL && _tousertype_nullhandler != NULL)
{
_tousertype_nullhandler(); // 如果发现类型转换失败了,触发钩子函数
}
return ret;
}
}
然后在钩子函数里面进行lua堆栈数据的收集工作,这里面用到了lua_getstack,lua_getinfo获取每一层堆栈,通过lua_getlocal获取每层堆栈的变量情况,这里就不再贴代码了。
堆栈数据格式
示例
<setString> at =[C]:-1 # 第1层
<func> at .\crash-lua/index.lua:82 # 第2层
a:10 # 第2层的相关变量
b:{a=1,c="c",d={d1=1,d2="d2",},}
c:<function>0F5F5C78
[Lua] at .\core/testLayer.lua:122 # 第3层
ref:<userdata>
type:0
不过考虑到网络传输带宽,需要将数据压缩下再发送到服务器,格式就不需要精简了,没有必要。
这里就要开发配套的后台日志系统,又是一个新坑。