标签 github 下的文章

ShareSDK Cocos2d-x专用组件的一个Bug

近期研究了一下Game App做社交分享,最后选择了ShareSDK来集成,不仅是因为ShareSDK支持国内外主流社交平台,更重要的是ShareSDK提供了专门的 cocos2d-x集成方案,有专门的文档代码Demo供开发者参考。

文档中提到了三种集成方式:纯Java方式、plugin-x方式以及Cocos2d-x专用组件方式,这里选择了ShareSDK Cocos2d-x专用组件(v2.3.7版本)的方式。按照文档中描述的步骤进行的相对顺利,在各个社交平台的appkey生效后,我们对demo app进行了测试,居然发现app经常随机性的崩溃,有时甚至是每次都崩溃,经过深入分析,发现这是ShareSDK Cocos2d-x专用组件的一个严重Bug,下面详细说明一下Bug的产生原因以及Fix方法。

一、App崩溃的场景和代码位置

发生崩溃的场景如下:
    App Demo中有一个"Share"按钮,点击该按钮,App Demo向已经授权的社交平台分享一些Test Content,而App Demo就在收到分享结果应答时发生了崩溃。

代码位置大致如下:

void AppDemo::onShareClick(CCObject* sender)
{
    … …
    C2DXShareSDK::showShareMenu(NULL, content,
                                CCPointMake(100, 100),
                                C2DXMenuArrowDirectionLeft,
                                shareResultHandler);
}

void shareResultHandler(C2DXResponseState state, C2DXPlatType platType,
                        CCDictionary *shareInfo, CCDictionary *error)
{
    switch (state) {
        case C2DXResponseStateSuccess:
            CCLog("Share Ok");
            break;
        case C2DXResponseStateFail:
            CCLog("Share Failed");
            break;
        default:
            break;
    }
}

崩溃的位置大致就在回调shareResultHandler前后的某个位 置,比较随机。

二、现象分析

通过查看Eclipse logcat窗口的调试日志,我们发现一些规律,一些在“Share Ok后的崩溃打印出如下日志:

04-16 01:28:33.890: D/cocos2d-x debug info(1748): Share Ok
04-16 01:28:34.090: D/cocos2d-x debug info(1748): Assert failed: reference count should greater than 0
04-16 01:28:34.090: E/cocos2d-x assert(1748): /home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/temp/AppDemo/proj.android/../../../../../cocos2dx/cocoa/CCObject.cpp function:release line:81
04-16 01:28:34.130: A/libc(1748): Fatal signal 11 (SIGSEGV) at 0×00000003 (code=1), thread 1829 (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

public class ShareSDKUtils {
    private static boolean DEBUG = true;
    private static Context context;
    private static PlatformActionListener paListaner;
    private static Hashon hashon;
    … …
   
    public static void prepare() {
        UIHandler.prepare();
        context = Cocos2dxActivity.getContext().getApplicationContext();
        hashon = new Hashon();
        final Callback cb = new Callback() {
            public boolean handleMessage(Message msg) {
                onJavaCallback((String) msg.obj);
                return false;
            }
        };

        paListaner = new PlatformActionListener() {
            public void onComplete(Platform platform, int action, HashMap<String, Object> res) {
                if (DEBUG) {
                    System.out.println("onComplete");
                    System.out.println(res == null ? "" : res.toString());
                }
                HashMap<String, Object> map = new HashMap<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);
                Message msg = new Message();
                msg.obj = hashon.fromHashMap(map);
                UIHandler.sendMessage(msg, cb);
            }

    … …
}

可以看出监听Complete事件的listener将message的处理都交给了cb,而cb调用了onJavaCallback方法。

onJavaCallback方法是jni导出的方法,它的实现在 AppDemo/Classes/C2DXShareSDK/Android/ShareSDKUtils.cpp里面。

JNIEXPORT void JNICALL Java_cn_sharesdk_ShareSDKUtils_onJavaCallback
  (JNIEnv * env, jclass thiz, jstring resp) {
    CCJSONConverter* json = CCJSONConverter::sharedConverter();
    const char* 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");
    // TODO add codes here
    if(1 == status->getIntValue()){
        callBackComplete(action->getIntValue(), platform->getIntValue(), res);
    }else if(2 == status->getIntValue()){
        callBackError(action->getIntValue(), platform->getIntValue(), res);
    }else{
        callBackCancel(action->getIntValue(), platform->getIntValue(), res);
    }

    dic->autorelease();
}

这就是两块代码的关键联系。而问题似乎就出在onJavaCallback方 法里,因为我们看到了该方法中使用了Cocos2d-x的数据结构类。

我们来看一下onJavaCallback方法是在哪个线程里执行的。Cocos2d-x App至少有两个线程,一个UI Thread(Activity),一个Render Thread。显然onJavaCallback是在UI Thread中被执行的。但是我们知道Cocos2d-x的AutoreleasePool是在Render Thread中管理的,并在帧切换时进行释放操作的。

我们似乎闻到了问题的味道。Cocos2d-x基本上算是一个"单线程"游戏架构,所有的渲染操作、渲染树节点逻辑管理、绝大多数游戏逻辑都在 Render Thread中进行,UI Thread更多的是接收系统事件,并传递给Render Thread处理。Cocos2d-x的内存管理在这样的“单线程”背景下是没有大问题的,都是串行操作,不存在thread racing的情况。但一旦另外一个线程也调用内存管理接口进行对象内存操作时,问题就出现了,Cocos2d-x的内存池管理不是线程安全的。

我们回到上面代码,重点看一下json转dic的方法,该方法将分享应答字符串转换为内部的dictionary结构:

//AppDemo/Classes/C2DXShareSDK/Android/JSON/CCJSONConverter.cpp

CCDictionary * CCJSONConverter::dictionaryFrom(const char *str)
{
    cJSON * json = cJSON_Parse(str);
    if (!json || json->type!=cJSON_Object) {
        if (json) {
            cJSON_Delete(json);
        }
        return NULL;
    }
    CCAssert(json && json->type==cJSON_Object, "CCJSONConverter:wrong json format");
    CCDictionary * dictionary = CCDictionary::create();
    convertJsonToDictionary(json, dictionary);
    cJSON_Delete(json);
    return dictionary;
}

void CCJSONConverter::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) {
        case cJSON_Object:
        {
            CCDictionary * dictionary = CCDictionary::create();           
            convertJsonToDictionary(json, dictionary);
            return dictionary;
        }
        case cJSON_Array:
        {
            CCArray * array = CCArray::create();
            convertJsonToArray(json, array);
            return array;
        }
        case cJSON_String:
        {
            CCString * string = CCString::create(json->valuestring);
            return string;
        }
        case cJSON_Number:
        {
            CCNumber * number = CCNumber::create(json->valuedouble);
            return number;
        }
        case cJSON_True:
        {
            CCNumber * boolean = CCNumber::create(1);
            return boolean;
        }
        case cJSON_False:
       {
            CCNumber * boolean = CCNumber::create(0);
            return boolean;
        }
        case cJSON_NULL:
        {
            CCNull * null = CCNull::create();
            return null;
        }
        default:
        {
            CCLog("CCJSONConverter encountered an unrecognized type");
            return NULL;
        }
    }
}

可以看出整个解析过程,都直接用的是传统的Cocos2d-x对象构造方法:create。在每个对象的create中,代码都会调用该对象的 autorelease方法。而这个方法本身就是线程不安全的,且即便autorelease调用ok,在下一帧切换时,这些对象将都会被release 掉,如果在UI Thread中再引用这些对象的地址,那势必造成内存的非法访问,而引发程序崩溃。

四、Fix方法

可能有朋友会问,create后,我retain一下可否?答案是否。因此create的创建不是线程安全的,create和retain两个调 用之间存在时间差,而在这段时间内,该对象就有可能被render thread释放掉。

Fix方法很简单,就是在UI Thread中不使用Cocos2d-x的内存管理机制,我们用传统的new来替代create,并将 Java_cn_sharesdk_ShareSDKUtils_onJavaCallback最后的autorelease改为release,这样就 不用劳烦Render Thread来帮我们释放内存了。CCDictionary的destructor调用时还会将Dictionarny内部所有Element自动释放 掉。

2013小结

2013年的个人年终总结比以往来得晚了一些,至于原因,我也说不清楚,拖延症也罢,其他原因也罢,总之是晚了。

写年终小结已经有小几年了,风格一直如一,无非是老三样:工作得失、生活酸甜以及新年展望,今年也不利外。

* 工作篇

我们部门在所在行业里已经摸爬滚打了10多年了,经 历和见证了这个行业从诞生、增长、成熟到如今的衰退的整个过程。也正是由于处于行业的衰退期,2013年部门的运营十分艰难。十年对于任何一个行业来说, 可能都已经过了其巅峰期,真心不能再期望这个行业还能会有下一个高峰了,对于个人来说也是如此。转型、业务突破变成了领导常挂在嘴边的词汇,但做起来又何 其艰难。

2013年,我们的业务转型依旧是围绕着我们的“金主”,虽然他们的业务营收也受到了微信等OTT业务的极大影响,传统业务投资也在缩减。对于个人而言, 除了负责传统产品线,转型、业务突破也成了我的绩效目标的一部分。于是在2013年,写文档比写代码多了一点,出差比常年多了一点,周六周日的连续加班也 多出了一点。在这些尝试中,以5月份某运营商某信的重构项目最为让人印象深刻。为了这个合同额几个亿的项目,我们近30人连续奋战了一个多月编写技术建议 书和投标方案,过程辛苦但却颇感充实,最终我们拿到了两个第二、两个第三的成绩。也许这个结果对于公司来说算是一种失败,但对于我个人来说,我获得了些许 转型的信心,以至于在后续的几次投标资料编写过程中,面对较新的领域,我也可以镇定自若。

掐指算来,这一年我以咨询顾问的临时角色参与了8个大大小小项目的前期交流以及投标支持工作,其中六个标以失败或不了了之而告终,还有两个标尚未有最终结 果。对于这样的结果,我也只能表示无奈。虽然我心里也十分清楚,对于国内这类解决方案项目的投标,技术往往不是最重要的,况且对于这些新领域,我们的技术 储备还不够系统,积累较为浅薄,落地的也的确较少。但面对这样的局面,我们还能怎么做呢?我也期待新一年能得到一个新的答案。

当然2013的工作中不全是遗憾,年末之前新系统的上线算是为我的2013划上了一个还算不错的句号,毕竟这是我两年来为之付出最多,也是最重要的一个工作目标。另外2013年继续整理和总结自己的一些管理经验工作原则。在过程方面继续深入改善,尤其在代码质量方面。

在技术精深方面,今年没有太多进步。年初的时候曾探讨过如何在现有项目中使用一些成熟的开源技术和产品,比如memcachedzookeeper等, 为了保持手热,还尝试做些算法类的编码,这个在experiments库中有体现。在其他方面,可谓是“三无状态”:无技术书籍翻译、无技术杂志投稿、无 新开源项目发起。另外今年没有尝试去学习什么新语言,理由在此

在年末的绩效评审时,观察到一些现象:那些绩效最末尾的人,往往并非是自身不够努力,而是领导赋予的目标不明确,这会给下属带来更多的不安,多数下属也会因为工作目标的不明确,而表现出更为糟糕的绩效。

* 生活篇

我个人十分注重工作和生活的平衡,不知道这种理念对于一个革命尚未成功的人来说算不算正确。

今年写了56篇博文,只完成了计划值的3/4,算是可接受范围,博文质量有所提升,访问次数和评论反馈也多了许多。文章以技术理解偏多,深入的偏少。技术攻关还是留给年轻人去吧。另外就是经验总结和感悟偏多,这也许与工作年头多有关系吧。

读书方面,据豆瓣不完全统计一共读了61本,这照比去年要多出不少,想必是有了Kindle PaperWhite的缘故吧,使得碎片时间得到充分利用。技术、商业书籍依旧占较大比例,小说尤其是科幻小说也不少。同样是因为电子书,今年纸质书籍购 买减少了(痛定思痛后的决定),双十一、双十二以及圣诞促销均没有出手。不过豆瓣上想读的书单依旧还有上百本^_^,任重道远啊。

今年爱上了跑步,坚持到11月末,因出差和天气转深寒等原因,决定暂停一段时间,等春节后气温回升时再拾起这个好习惯,相信不是大问题。跑步的确让我的身体状况大为好转,至少感冒次数大为下降。                 

今年的一些家庭目标也多已实现,比如和老婆一起去香港、带孩子去海边玩等。数码装备也更新了一圈。

果果这个小家伙那叫一个茁壮成长啊。年中给她换了一个大的幼儿园,她也变得十分喜欢和小朋友在一起玩了,有时候还觉得在家里没有意思。每周果果还要上一节 她最喜欢的舞蹈课,我们的初衷就是让她多与小朋友老师接触,也不指望她能学出什么样子来,不过她学得倒是有模有样,十分认真。现在的果果简直就是一个小大 人,每天从早到晚说个不停,精力那叫一个充沛,有时候不得不强迫她去睡觉^_^。

* 新年展望

感觉这一年的进步有些差强人意,心底真心感觉自己的努力还是太少了,于是立下了“少睡觉,多干活”的目标。

新的一年,无论是个人还是工作,都要更多的思考如何将知识、技能和经验转化为更多价值,如何将业务经验、技术积累转化为合同。

新的一年,要主动适应转型,无论是工作上的还是个人方向上的,争取在这一年里能找到正确的方向,并成功入门。最好给自己做一个三年到五年的布局。

新的一年,尝试继续保持生活与工作的平衡,也许这将变成一种奢侈的期望。

新的一年,还有什么比全家健康快乐更重要的呢。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats