ShareSDK causes an BUG cause analysis of the App crash as well as an Fix method

  • 2020-05-30 21:03:25
  • OfStack

Recently, I studied Game App for social sharing, and finally chose ShareSDK for integration, not only because ShareSDK supports mainstream social platforms at home and abroad, but more importantly, ShareSDK provides a special integration scheme of cocos2d-x, with special documents and code Demo for developers' reference.

Three types of integration are mentioned in the document: pure Java, plugin-x, and Cocos2d-x dedicated components. Here, ShareSDK Cocos2d-x dedicated components (v2.3.7) are selected. According to document the steps described in the relatively smooth, in each of the social platform appkey effects, we demo app tested, unexpectedly found app often collapse of randomness, sometimes even collapse every time, after a thorough analysis, found this to be ShareSDK Cocos2d - a serious Bug x special components, detailed below 1 Bug causes and Fix method.

1. Scenario and code location of App crash

The crash scenario is as follows:
There is a "Share" button in App Demo. By clicking this button, App Demo shares some Test Content to the authorized social platform, while App Demo crashes when it receives the response of the Shared results.

The code location is roughly as follows:


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;
    }
}

The crash is roughly at a random position before and after the shareResultHandler callback.

2. Phenomenon analysis

By looking at the debug logs in the Eclipse logcat window, we found some patterns, some crashes after "Share Ok" printed the following logs:


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 x 00000003 (code=1), thread 1829 (Thread-122)

At a guess of 1, it appears that some CCObject has been freed before the real Release, which then triggers an illegal memory access when it is subsequently referenced. The memory management mechanism used by Cocos2d-x is memory counting, which is described in my article "Cocos2d-x memory management - the impossible hurdle" 1. Understanding the memory management mechanism of Cocos2d-x is a prerequisite for understanding this Bug.


3. Reason analysis

Looks like we'll have to dig for the code for the ShareSDK component. AppDemo ShareSDK components in the code is divided into two parts: AppDemo/Classes/C2DXShareSDK and AppDemo/proj android/src/cn/sharesdk. The former is the C++ code, and the latter is the Java code. The two are connected through the jni call at 1. We'll focus on finding the key connections when the Shared response comes back.

The integrated ShareSDK Cocos2d-x program calls ShareSDKUtils.prepare () in the onCreate method of the main Activity;

Let's look at the implementation of the prepare method:


//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);
            }
     ...   ... 
}

It can be seen that listener, which is listening for the Complete event, gives all the processing of message to cb, while cb calls the onJavaCallback method.

onJavaCallback method is jni derived, the method of its implementation in AppDemo/Classes/C2DXShareSDK/Android/ShareSDKUtils cpp inside.


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();
}

This is the key link between the two pieces of code. The problem seems to be in onJavaCallback, because we see that it USES the data structure class Cocos2d-x.

Let's look at 1 and see in which thread the onJavaCallback method is executed. Cocos2d-x App has at least two threads, one UI Thread (Activity), one Render Thread. Obviously onJavaCallback is implemented in UI Thread. However, we know that Cocos2d-x AutoreleasePool is managed in Render Thread and is released during frame switching.

We seem to smell a problem. Cocos2d-x is basically a "single-threaded" game architecture. All rendering operations, logical management of rendering tree nodes, and most of the game logic are carried out in Render Thread. UI Thread is more about receiving system events and passing them to Render Thread for processing. The memory management of Cocos2d-x is not a big problem in this "single-threaded" background, it is all serial operations, there is no thread racing situation. However, once another thread also calls the memory management interface to perform object memory operations, the problem arises. The memory pool management of Cocos2d-x is not thread-safe.

Let's go back to the above code and focus on the json to dic method, which converts the Shared reply string into an internal dictionary structure:


//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;
        }
    }
}

You can see that the whole parsing process is directly using the traditional Cocos2d-x object constructor: create. In the create of each object, the code calls the autorelease method of that object. This method itself is not thread safe, and even if autorelease calls ok, when the next frame is switched, these objects will be dropped by release. If the address of these objects is referenced again in UI Thread, it is bound to cause illegal access to memory and cause the program to crash.

4. Fix method

Some friends may ask, after create, can I retain1? The answer is no. Therefore, the creation of create is not thread-safe, and there is a time lag between the two calls create and retain, during which time the object may be released by render thread.

The Fix method is very simple. Instead of using the Cocos2d-x memory management mechanism in UI Thread, we use the traditional new to replace create, and change Java_cn_sharesdk_ShareSDKUtils_onJavaCallback autorelease to release, so that we don't have to bother Render Thread to free up memory. CCDictionary's destructor call also automatically frees all Element inside Dictionarny.


Related articles: