ShareSDK造成App崩溃的一个BUG原因分析以及Fix方法
近期研究了一下GameApp做社交分享,最后选择了ShareSDK来集成,不仅是因为ShareSDK支持国内外主流社交平台,更重要的是ShareSDK提供了专门的cocos2d-x集成方案,有专门的文档和代码Demo供开发者参考。
文档中提到了三种集成方式:纯Java方式、plugin-x方式以及Cocos2d-x专用组件方式,这里选择了ShareSDKCocos2d-x专用组件(v2.3.7版本)的方式。按照文档中描述的步骤进行的相对顺利,在各个社交平台的appkey生效后,我们对demoapp进行了测试,居然发现app经常随机性的崩溃,有时甚至是每次都崩溃,经过深入分析,发现这是ShareSDKCocos2d-x专用组件的一个严重Bug,下面详细说明一下Bug的产生原因以及Fix方法。
一、App崩溃的场景和代码位置
发生崩溃的场景如下:
AppDemo中有一个"Share"按钮,点击该按钮,AppDemo向已经授权的社交平台分享一些TestContent,而AppDemo就在收到分享结果应答时发生了崩溃。
代码位置大致如下:
voidAppDemo::onShareClick(CCObject*sender)
{
……
C2DXShareSDK::showShareMenu(NULL,content,
CCPointMake(100,100),
C2DXMenuArrowDirectionLeft,
shareResultHandler);
}
voidshareResultHandler(C2DXResponseStatestate,C2DXPlatTypeplatType,
CCDictionary*shareInfo,CCDictionary*error)
{
switch(state){
caseC2DXResponseStateSuccess:
CCLog("ShareOk");
break;
caseC2DXResponseStateFail:
CCLog("ShareFailed");
break;
default:
break;
}
}
崩溃的位置大致就在回调shareResultHandler前后的某个位置,比较随机。
二、现象分析
通过查看Eclipselogcat窗口的调试日志,我们发现一些规律,一些在“ShareOk后的崩溃打印出如下日志:
04-1601:28:33.890:D/cocos2d-xdebuginfo(1748):ShareOk
04-1601:28:34.090:D/cocos2d-xdebuginfo(1748):Assertfailed:referencecountshouldgreaterthan0
04-1601:28:34.090:E/cocos2d-xassert(1748):/home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/temp/AppDemo/proj.android/../../../../../cocos2dx/cocoa/CCObject.cppfunction:releaseline:81
04-1601:28:34.130:A/libc(1748):Fatalsignal11(SIGSEGV)at0×00000003(code=1),thread1829(Thread-122)
猜测一下,似乎是某个CCObject在真正Release前已经被释放了,然后后续被引用时触发内存非法访问。Cocos2d-x采用的是内存计数的内存管理机制,在我的《Cocos2d-x内存管理-绕不过去的坎》一文中有描述。了解Cocos2d-x的内存管理机制是理解这个Bug的前提条件。
三、原因分析
看来不得不挖掘一下ShareSDK组件的代码了。AppDemo中ShareSDK组件的代码分为两个部分:AppDemo/Classes/C2DXShareSDK和AppDemo/proj.android/src/cn/sharesdk。前者是C++代码,后面则是Java代码,两者通过jni调用联系在一起。我们重点来找出分享应答返回来时的关键联系。
集成ShareSDK的Cocos2d-x程序会在主Activity的onCreate方法中调用ShareSDKUtils.prepare();
我们来看看prepare方法的实现:
//AppDemo/proj.android/src/cn/sharesdk/ShareSDKUtils.java
publicclassShareSDKUtils{
privatestaticbooleanDEBUG=true;
privatestaticContextcontext;
privatestaticPlatformActionListenerpaListaner;
privatestaticHashonhashon;
……
publicstaticvoidprepare(){
UIHandler.prepare();
context=Cocos2dxActivity.getContext().getApplicationContext();
hashon=newHashon();
finalCallbackcb=newCallback(){
publicbooleanhandleMessage(Messagemsg){
onJavaCallback((String)msg.obj);
returnfalse;
}
};
paListaner=newPlatformActionListener(){
publicvoidonComplete(Platformplatform,intaction,HashMap<String,Object>res){
if(DEBUG){
System.out.println("onComplete");
System.out.println(res==null?"":res.toString());
}
HashMap<String,Object>map=newHashMap<String,Object>();
map.put("platform",ShareSDK.platformNameToId(platform.getName()));
map.put("action",action);
map.put("status",1);//Success=1,Fail=2,Cancel=3
map.put("res",res);
Messagemsg=newMessage();
msg.obj=hashon.fromHashMap(map);
UIHandler.sendMessage(msg,cb);
}
……
}
可以看出监听Complete事件的listener将message的处理都交给了cb,而cb调用了onJavaCallback方法。
onJavaCallback方法是jni导出的方法,它的实现在AppDemo/Classes/C2DXShareSDK/Android/ShareSDKUtils.cpp里面。
JNIEXPORTvoidJNICALLJava_cn_sharesdk_ShareSDKUtils_onJavaCallback
(JNIEnv*env,jclassthiz,jstringresp){
CCJSONConverter*json=CCJSONConverter::sharedConverter();
constchar*ccResp=env->GetStringUTFChars(resp,JNI_FALSE);
CCLog("ccResp=%s",ccResp);
CCDictionary*dic=json->dictionaryFrom(ccResp);
env->ReleaseStringUTFChars(resp,ccResp);
CCNumber*status=(CCNumber*)dic->objectForKey("status");//Success=1,Fail=2,Cancel=3
CCNumber*action=(CCNumber*)dic->objectForKey("action");// 1=ACTION_AUTHORIZING, 8=ACTION_USER_INFOR,9=ACTION_SHARE
CCNumber*platform=(CCNumber*)dic->objectForKey("platform");
CCDictionary*res=(CCDictionary*)dic->objectForKey("res");
//TODOaddcodeshere
if(1==status->getIntValue()){
callBackComplete(action->getIntValue(),platform->getIntValue(),res);
}elseif(2==status->getIntValue()){
callBackError(action->getIntValue(),platform->getIntValue(),res);
}else{
callBackCancel(action->getIntValue(),platform->getIntValue(),res);
}
dic->autorelease();
}
这就是两块代码的关键联系。而问题似乎就出在onJavaCallback方法里,因为我们看到了该方法中使用了Cocos2d-x的数据结构类。
我们来看一下onJavaCallback方法是在哪个线程里执行的。Cocos2d-xApp至少有两个线程,一个UIThread(Activity),一个RenderThread。显然onJavaCallback是在UIThread中被执行的。但是我们知道Cocos2d-x的AutoreleasePool是在RenderThread中管理的,并在帧切换时进行释放操作的。
我们似乎闻到了问题的味道。Cocos2d-x基本上算是一个"单线程"游戏架构,所有的渲染操作、渲染树节点逻辑管理、绝大多数游戏逻辑都在RenderThread中进行,UIThread更多的是接收系统事件,并传递给RenderThread处理。Cocos2d-x的内存管理在这样的“单线程”背景下是没有大问题的,都是串行操作,不存在threadracing的情况。但一旦另外一个线程也调用内存管理接口进行对象内存操作时,问题就出现了,Cocos2d-x的内存池管理不是线程安全的。
我们回到上面代码,重点看一下json转dic的方法,该方法将分享应答字符串转换为内部的dictionary结构:
//AppDemo/Classes/C2DXShareSDK/Android/JSON/CCJSONConverter.cpp
CCDictionary*CCJSONConverter::dictionaryFrom(constchar*str)
{
cJSON*json=cJSON_Parse(str);
if(!json||json->type!=cJSON_Object){
if(json){
cJSON_Delete(json);
}
returnNULL;
}
CCAssert(json&&json->type==cJSON_Object,"CCJSONConverter:wrongjsonformat");
CCDictionary*dictionary=CCDictionary::create();
convertJsonToDictionary(json,dictionary);
cJSON_Delete(json);
returndictionary;
}
voidCCJSONConverter::convertJsonToDictionary(cJSON*json,CCDictionary*dictionary)
{
dictionary->removeAllObjects();
cJSON*j=json->child;
while(j){
CCObject*obj=getJsonObj(j);
dictionary->setObject(obj,j->string);
j=j->next;
}
}
CCObject*CCJSONConverter::getJsonObj(cJSON*json)
{
switch(json->type){
casecJSON_Object:
{
CCDictionary*dictionary=CCDictionary::create();
convertJsonToDictionary(json,dictionary);
returndictionary;
}
casecJSON_Array:
{
CCArray*array=CCArray::create();
convertJsonToArray(json,array);
returnarray;
}
casecJSON_String:
{
CCString*string=CCString::create(json->valuestring);
returnstring;
}
casecJSON_Number:
{
CCNumber*number=CCNumber::create(json->valuedouble);
returnnumber;
}
casecJSON_True:
{
CCNumber*boolean=CCNumber::create(1);
returnboolean;
}
casecJSON_False:
{
CCNumber*boolean=CCNumber::create(0);
returnboolean;
}
casecJSON_NULL:
{
CCNull*null=CCNull::create();
returnnull;
}
default:
{
CCLog("CCJSONConverterencounteredanunrecognizedtype");
returnNULL;
}
}
}
可以看出整个解析过程,都直接用的是传统的Cocos2d-x对象构造方法:create。在每个对象的create中,代码都会调用该对象的autorelease方法。而这个方法本身就是线程不安全的,且即便autorelease调用ok,在下一帧切换时,这些对象将都会被release掉,如果在UIThread中再引用这些对象的地址,那势必造成内存的非法访问,而引发程序崩溃。
四、Fix方法
可能有朋友会问,create后,我retain一下可否?答案是否。因此create的创建不是线程安全的,create和retain两个调用之间存在时间差,而在这段时间内,该对象就有可能被renderthread释放掉。
Fix方法很简单,就是在UIThread中不使用Cocos2d-x的内存管理机制,我们用传统的new来替代create,并将Java_cn_sharesdk_ShareSDKUtils_onJavaCallback最后的autorelease改为release,这样就不用劳烦RenderThread来帮我们释放内存了。CCDictionary的destructor调用时还会将Dictionarny内部所有Element自动释放掉。
相关文章
- 跨平台移动APP开发进阶(二):HTML5+、mui开发移动app教程[通俗易懂]
- uni-app APP地图移动时获取地图中心点经纬度坐标
- 【Bug解决】Unity Build GI data 卡住问题
- 解决移动端适配rem单位在华为和三星手机出现bug
- uni-app 安卓APP开发记录
- Redmine系统通过bug号解析页面内容及下载附件
- python TCP套接字服务器v1.1-新增服务端命令功能及修改bug(socket+PyQt5)
- 测试下班前提了个bug
- ChatGPT修bug横扫全场,准确率达78%!网友:程序员要开心了
- spring jdbctemplate 启动报错(oracle驱动bug)详解数据库
- 因存在诸多BUG 微软暂停Edge浏览器“启动增强”功能
- Debian 9 图形界面安装有bug 开发人员正在修复
- 增加了新功能并修复了1000个bug的VLC 2.1发布
- 轻松实现优化App与MySQL连接速度(app连接mysql很慢)
- 让App连接MySQL轻松实现数据库连接(app连mysql)
- APP访问MySQL从零开始(app 访问mysql)
- 移动端App使用MySQL数据库开发丰富应用(app用mysql)
- App开发之路MySQL源码指引(app源码 mysql)
- APP互联网化MySQL驱动开拓前沿(app和mysql的关系)
- App与MySQL数据库构建坚固的联系(app和mysql数据库)
- 破解Oracle数据库修复Bug问题(oracle修改bug)
- Oracle APP究竟有多重(oracle app太大)
- Oracle Bug文档之调查这些bug有何秘密(oracle bug文档)
- Firefox2中输入框丢失光标bug的解决方法
- 关于在IE下的一个安全BUG--可用于跟踪用户的系统鼠标位置
- 解析Tomcat6、7在EL表达式解析时存在的一个Bug