Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> ShareSDK造成App崩潰的一個BUG原因分析以及Fix方法

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

編輯:關於Android編程

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

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved