欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

ShareSDK造成App崩溃的一个BUG原因分析以及Fix方法

程序员文章站 2024-02-03 15:01:40
近期研究了一下game app做社交分享,最后选择了sharesdk来集成,不仅是因为sharesdk支持国内外主流社交平台,更重要的是sharesdk提供了专门的 coc...

近期研究了一下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自动释放掉。