标签 C 下的文章

Cocos2d-x 3.0rc0集成Google AdMob SDK

话说Cocos2d-x 3.0上一周迫不及待地发布了正式版,本是一件值得庆幸的事情。但由于不可解决的技术问题,引擎无奈将Android平台的NativeActivity 实现重新回退到了Cocos2d-x 2.2.x版本的实现方案。由于之前已经将 GameDemo移植到了Cocos2d-x 3.0rc0版,直观感受到了NativeActivity方案带来的游戏操作体验上的提升(触屏事件的响应),因此心里总是“挂念”引擎的 NativeActivity方案。按照Cocos2d-x官方人士的说法,只要NDK的问题修复,NativeActivity还是未来引擎在 Android平台上的第一选择。我的理解,将来Cocos2d-x的某个版本中还会恢复NativeActivity的实现方案。

上次只是将GameDemo的核心逻辑移植到了Cocos2d-x 3.0rc0上,但一些外部SDK集成尚未做完,这两天闲遐时开始着手研究如何做,而首先要移植的就是Google AdMob SDK。关于Cocos2d-x 3.0rc0集成Google AdMob SDK,网上已经有了技术方案原型,最初应该是一个外国开发者提出的方案,后来被CocoaChina cocos2d-x社区版主翻译成了中文教程,这里基本上也是参考的这个方案,只是做了局部细化和进一步说明,希望能帮助大家进一步解惑。

一、功能说明

GameDemo游戏分为三个场景:WelcomeScene、GameScene以及EndScene。集成AdMob SDK的功能要求如下:

    – 当游戏进入到WelcomeScene时,AdMob SDK完成初始化,发出Ad Request,当收到Ad的时候才会在屏幕上方显示带有Ad的窗口;
    – 当点击Start进入到GameScene时,隐藏Ad窗口,以不干扰玩家的游戏操作为优先;
    – 当游戏Over进入到EndScene的时候,恢复显示Ad窗口;
    – 当玩家点击“Retry”回到GameScene时,隐藏Ad窗口。

二、方案原理

这个方案就是通过android.widget.PopupWindow实现广告窗口悬浮在当前窗口之上。按照Android官方Doc中的说 明,PopupWindow用于实现一个弹出框,它可以使用任意布局的View作为其内容,这个弹出框是悬浮在当前activity之上的。

至于PopupWindow为何能显示在当前Activity之上,可以查看PopupWindow的源码,大致思路就是PopupWindow通过 new时传入的context(当前Activity)获得了当前WindowManager,并将AdView作为子View添加到该窗口中。这里简要列出一些关键源码,大家可粗略理解一下:

    public PopupWindow(Context context, AttributeSet attrs,
         int defStyleAttr, int defStyleRes) {
        mContext = context;
        mWindowManager = (WindowManager)context
               .getSystemService(Context.WINDOW_SERVICE);
        …. …
    }

    public void setContentView(View contentView) {
        if (isShowing()) {
            return;
        }

        mContentView = contentView;

        if (mContext == null && mContentView != null) {
            mContext = mContentView.getContext();
        }

        if (mWindowManager == null && mContentView != null) {
            mWindowManager = (WindowManager) mContext
                  .getSystemService(Context.WINDOW_SERVICE);
        }
    }

    public void showAtLocation(View parent, int gravity, int x, int y) {
        showAtLocation(parent.getWindowToken(), gravity, x, y);
    }

    public void showAtLocation(IBinder token, int gravity, int x, int y) {
        if (isShowing() || mContentView == null) {
            return;
        }

        unregisterForScrollChanged();

        mIsShowing = true;
        mIsDropdown = false;

        WindowManager.LayoutParams p = createPopupLayout(token);
        p.windowAnimations = computeAnimationResource();
      
        preparePopup(p);
        if (gravity == Gravity.NO_GRAVITY) {
            gravity = Gravity.TOP | Gravity.START;
        }
        p.gravity = gravity;
        p.x = x;
        p.y = y;
        if (mHeightMode < 0) p.height = mLastHeight = mHeightMode;
        if (mWidthMode < 0) p.width = mLastWidth = mWidthMode;
        invokePopup(p);
    }
   
    private void invokePopup(WindowManager.LayoutParams p) {
        if (mContext != null) {
            p.packageName = mContext.getPackageName();
        }
        mPopupView.setFitsSystemWindows(mLayoutInsetDecor);
        setLayoutDirectionFromAnchor();
        mWindowManager.addView(mPopupView, p);
    }

UI线程负责更新窗口,因此popupWindow的创建与操作都应该通过runOnUiThread传递给UI线程执行。

游戏逻辑的C++层代码通过Jni控制PopupWindow的Show和dismiss。别忘了C++层的代码是在渲染线程执行的哦,这也是为何要用handler和runOnUIThread的原因。

在Cocos2d-x 2.2.2版本集成AdMob时,AdMob只是在收到ad后才显示出广告Banner,但是在cocos2d-x 3.0rc0中,当广告加载未成功时,android.widget.PopupWindow会显示一个小黑框,特难看,因此我们需要自己来控制。

三、集成步骤

【AdMob准备】

  到Google AdMob注册一个帐号,如果你有Google帐号,那直接开通AdMob(需填写更加详细的信息)。
  下载Google AdMob SDK,之前一直用GoogleAdMobAdsSdk-6.4.1.jar,这里还以这个版本为例。但Google官方已经不推荐这个版本了,你可以下 载Google Play Services版本的Mobile Ads API,但代码与这里会稍有不同。将下载的jar包放到GameDemo/proj.android/libs中。
 
【修改AndroidManifest.xml】

  在<Application>标签里添加:

  <activity
     android:name="com.google.ads.AdActivity"
     android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize" />
  <meta-data
     android:name="ADMOB_PUBLISHER_ID"
     android:value="YOUR_PUBLISHER_ID_VALUE" />

  权限方面至少包含如下两个:
   <uses-permission android:name="android.permission.INTERNET"/>
   <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

【Java层代码集成】

我们只需要修改GameDemoActivity.java这个文件。

import android.app.NativeActivity;
import android.os.Bundle;
import android.util.Log;

import android.os.Handler;
import android.os.Message;
import android.view.Gravity;
import android.view.View;
import android.view.Gravity;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.WindowManager;
import android.widget.LinearLayout;

import android.widget.PopupWindow;
import com.google.ads.AdRequest;
import com.google.ads.AdSize;
import com.google.ads.AdView;
import com.google.ads.AdListener;
import com.google.ads.Ad;

public class GameDemoActivity extends NativeActivity{
    private static GameDemoActivity context;

    private AdView adView;
    private PopupWindow popUpWindow;
    private LinearLayout popupWindowLayout;
    private LinearLayout mainActivityLayout;
    private boolean hasAdReceived;

    private static Handler adHandler = new Handler() {
        public void handleMessage(Message msg) {
            if (!context.hasAdReceived)
                return;

            switch (msg.what) {
                case 0:
                    if (View.VISIBLE ==
                        context.adView.getVisibility()) {
                        context.adView.setVisibility(View.GONE);
                        context.popUpWindow.dismiss();
                    }
                    break;
                case 1:
                    if (View.VISIBLE !=
                        context.adView.getVisibility()) {
                        context.adView.setVisibility(View.VISIBLE);
                        context.popUpWindow.showAtLocation(
                           context.mainActivityLayout, Gravity.TOP, 0, 0);
                    }
                    break;
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        context = this;
        adView = new AdView(this, AdSize.BANNER, "a1533d6de900e31");
        adView.setAdListener(new AdmobListener());
        //初始时,广告View隐藏起来
        adView.setVisibility(View.GONE);
    }

    public static void setupAds(){
        context.initAdPopupWindow();
    }

    public void initAdPopupWindow() {
        if (adView != null) {
            context.runOnUiThread(new Runnable() {
                 @Override
                 public void run() {
                     MarginLayoutParams params = new MarginLayoutParams(
                                     LayoutParams.WRAP_CONTENT,
                                     LayoutParams.WRAP_CONTENT);
                     params.setMargins(0, 0, 0, 0);

                     popupWindowLayout = new LinearLayout(context);
                     popupWindowLayout.setPadding(-5, -5, -5, -5);
                     popupWindowLayout.setOrientation(LinearLayout.VERTICAL);
                     popupWindowLayout.addView(adView, params);

                     popUpWindow = new PopupWindow(context);
                     popUpWindow.setWidth(320);
                     popUpWindow.setHeight(50);
                     popUpWindow.setWindowLayoutMode(LayoutParams.WRAP_CONTENT,
                                               LayoutParams.WRAP_CONTENT);
                     popUpWindow.setClippingEnabled(false);
                     popUpWindow.setContentView(popupWindowLayout);

                     mainActivityLayout = new LinearLayout(context);
                     context.setContentView(mainActivityLayout, params);

                     context.hasAdReceived = false;

                     AdRequest adRequest = new AdRequest();
                     //测试Admob时使用测试Device,发布版需要去掉这行代码
                     adRequest.addTestDevice("CCE4220B2509A406B515E7C9A205AEE1");
                     context.adView.loadAd(adRequest);

                     popUpWindow.update();
                }
            });
        }
    }

    //GoogleAdMobAdsSdk-6.4.1版本中的AdListener是interface,因此
    //我们需要override所有接口,但只有onReceiveAd是我们关心的。
    private class AdmobListener implements AdListener {
        @Override
        public void onReceiveAd(Ad ad) {
            //只有第一次成功接收Ad后,我们后续才显示广告窗口,否则
            //popupWindow会显示为一个小黑框,特难看。
            Log.d("GameDemo", "onReceiveAd");
            if (!hasAdReceived){
                hasAdReceived = true;               
            }
        }

        @Override
        public void onFailedToReceiveAd(Ad ad,
                            AdRequest.ErrorCode error) {
            Log.d("GameDemo",
              "failed to receive ad (" + error+ ")");
        }

        @Override
        public void onPresentScreen(Ad ad) {
            Log.d("GameDemo", "onPresentScreen");
        }

        @Override
        public void onDismissScreen(Ad ad) {
            Log.d("GameDemo", "onDismisScreen");
        }

        @Override
        public void onLeaveApplication(Ad ad) {
            Log.d("GameDemo", "onLeaveApp");
        }
    }

    public static void setAdVisible(boolean b) {
       Message msg = new Message();
        if (b) {
            msg.what = 1;
        } else {
            msg.what = 0;
        }
        adHandler.sendMessage(msg);
    }
}

【C++层代码集成】

在AppDelegate.cpp中添加setupAds方法:

void AppDelegate::setupAds()
{
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
    JniMethodInfo t;
    if (JniHelper::getStaticMethodInfo(t, "com/tonybai/game/GameDemoActivity", "setupAds", "()V")) {
        t.env->CallStaticVoidMethod(t.classID, t.methodID);
        if (t.env->ExceptionOccurred()) {
            t.env->ExceptionDescribe();
            t.env->ExceptionClear();
            return;
        }
        t.env->DeleteLocalRef(t.classID);
    }
#endif
}

在WelcomeScene的init方法中调用setupAds:

bool WelcomeScene::init()
{
    bool bRet = false;
    do {
       … …
       AppDelegate *app = (AppDelegate*)Application::getInstance();
       app->setupAds();

       bRet=true;
    } while(0);

    return bRet;
}

在点击Start按钮时,隐藏Ad:

void WelcomeScene::menuStartCallback(Ref* pSender)
{
    AppDelegate *app = (AppDelegate*)Application::getInstance();

    app->setAdVisible(false);
    GameScene *gameScene = GameScene::create();
    Director::getInstance()->replaceScene(gameScene);
}

集成步骤到此就结束了,编译ok就可以部署到模拟器上运行测试一番了。

四、使用Cocos2d-x 3.0rc0引擎遇到的两个问题

【问题1】用cocos2d-x 3.0rc0的cocos编译高版本引擎生成的工程遇到的问题

 cocos2d-x 3.0rc0生成的proj.android/build-cfg.json与高版本(rc1~正式版)  cocos生成的工程的build-cfg.json稍有不同,用cocos2d-x 3.0rc0版cocos编译高版本cocos生成的project,会提示build_android组件无法找到“copy_to_assets”。 因此需要手动修改proj.android/build-cfg.json,将其中的:

    "copy_resources": [
        {
            "from": "../Resources",
            "to": ""
        }
    ],

改为:

   "copy_to_assets" :[
            "../Resources/"
            ],

【问题2】 W/dalvikvm(1194): dvmFindClassByName rejecting 'org/cocos2dx/lib/Cocos2dxHelper'

使用cocos2d-x 3.0rc0编译的项目,总是出现如下问题,但似乎这个错误还不影响程序的运行:

04-29 09:44:34.968: D/JniHelper(1194): JniHelper::setJavaVM(0xb8835730), pthread_self() = B897B518
04-29 09:44:34.968: W/dalvikvm(1194): dvmFindClassByName rejecting 'org/cocos2dx/lib/Cocos2dxHelper'

Google了一下,这个问题很多童鞋都遇到了,但给出的solution却不甚令我满意,于是我就求甚解的细致挖掘了一下,终于找到了一个让我还算满意 的答案。我们分析一下这两条日志,rejecting那条日志总是伴随setJavaVM后面出现的。可引擎什么时候setJavaVM了呢?显然是在 native_app_glue创建的子进程中,引擎需要attachCurrentThread,获得JniEnv时才做的。于是我们打开cocos2d-x-3.0rc0/cocos/2d/platform/android/nativeactivity.cpp一看究竟。

在nativeactivity.cpp中有两处"org/cocos2dx/lib/Cocos2dxHelper",我们先看engine_handle_cmd中的那处:

static void engine_handle_cmd(struct android_app* app, int32_t cmd)
{
        …. ….
        case APP_CMD_INIT_WINDOW:
            LOG_RENDER_DEBUG("android_main : APP_CMD_INIT_WINDOW");
            // The window is being shown, get it ready.
            if (engine->app->window != NULL) {
                cocos_dimensions d = engine_init_display(engine);
                if ((d.w > 0) &&
                    (d.h > 0)) {
                    cocos2d::JniHelper::setJavaVM(app->activity->vm);
                    cocos2d::JniHelper::setClassLoaderFrom(app->activity->clazz);

                    // call Cocos2dxHelper.init()
                    cocos2d::JniMethodInfo ccxhelperInit;
                    if (!cocos2d::JniHelper::getStaticMethodInfo(ccxhelperInit,
                                                                 "org/cocos2dx/lib/Cocos2dxHelper",
                                                                 "init",
                                                                 "(Landroid/app/Activity;)V")) {
                        LOGI("cocos2d::JniHelper::getStaticMethodInfo(ccxhelperInit) FAILED");
                    }
                    ccxhelperInit.env->CallStaticVoidMethod(ccxhelperInit.classID,
                                                            ccxhelperInit.methodID,
                                                            app->activity->clazz);

                    cocos_init(d, app);
                }
                engine->animating = 1;
                engine_draw_frame(engine);
            }
            break;
    … …
}

初步可以断定,那两条日志就是执行到这里输出的,但为何dvmFindClassByName方法会rejecting “org/cocos2dx/lib/Cocos2dxHelper”这个类名呢?我们还得翻看dalvik虚拟机源码:

 /dalvik/vm/native/InternalNative.cpp

/*
 * Find a class by name, initializing it if requested.
 */
ClassObject* dvmFindClassByName(StringObject* nameObj, Object* loader,
        bool doInit)
{
    ClassObject* clazz = NULL;
    char* name = NULL;
    char* descriptor = NULL;
    if (nameObj == NULL) {
        dvmThrowNullPointerException("name == null");
        goto bail;
    }
    name = dvmCreateCstrFromString(nameObj);
    /*
     * We need to validate and convert the name (from x.y.z to x/y/z). This
     * is especially handy for array types, since we want to avoid
     * auto-generating bogus array classes.
     */
    if (!dexIsValidClassName(name, true)) {
        ALOGW("dvmFindClassByName rejecting '%s'", name);
        dvmThrowClassNotFoundException(name);
        goto bail;
    }
    … …
}

我们的确找到了输出rejecting日志的地方,通过注释我们可以看到这个方法是用来验证名字对象,并将x.y.z形式的名字转换成x/y/z的。但引 擎中传入的就是“x/y/z”格式,因此这个方法输出了错误日志。我尝试将上面engine_handle_cmd中的"org/cocos2dx/lib/Cocos2dxHelper"改成"org.cocos2dx.lib.Cocos2dxHelper",错误日志果然消失了。

不过目前仍不能解释一点:为何在其他位置,比如在前面的AppDelegate::setupAds中,使用x/y/z格式的jni调用参数却没有输出错误日志!难道两个位置dalvik虚拟机使用的是不同的名字对象查找和加载方法?这个目前尚无定论。

Cocos2d-x 3.0多线程异步资源加载

Cocos2d-x2.x版本到上周刚刚才发布的Cocos2d-x 3.0 Final版,其引擎驱动核心依旧是一个单线程的“死循环”,一旦某一帧遇到了“大活儿”,比如Size很大的纹理资源加载或网络IO或大量计算,画面将 不可避免出现卡顿以及响应迟缓的现象。从古老的Win32 GUI编程那时起,Guru们就告诉我们:别阻塞主线程(UI线程),让Worker线程去做那些“大活儿”吧。

手机游戏,即便是休闲类的小游戏,往往也涉及大量纹理资源、音视频资源、文件读写以及网络通信,处理的稍有不甚就会出现画面卡顿,交互不畅的情况。虽然引 擎在某些方面提供了一些支持,但有些时候还是自己祭出Worker线程这个法宝比较灵活,下面就以Cocos2d-x 3.0 Final版游戏初始化为例(针对Android平台),说说如何进行多线程资源加载。

我们经常看到一些手机游戏,启动之后首先会显示一个带有公司Logo的闪屏画面(Flash Screen),然后才会进入一个游戏Welcome场景,点击“开始”才正式进入游戏主场景。而这里Flash Screen的展示环节往往在后台还会做另外一件事,那就是加载游戏的图片资源,音乐音效资源以及配置数据读取,这算是一个“障眼法”吧,目的就是提高用 户体验,这样后续场景渲染以及场景切换直接使用已经cache到内存中的数据即可,无需再行加载。

一、为游戏添加FlashScene

在游戏App初始化时,我们首先创建FlashScene,让游戏尽快显示FlashScene画面:

// AppDelegate.cpp
bool AppDelegate::applicationDidFinishLaunching() {
    … …
    FlashScene* scene = FlashScene::create();
    pDirector->runWithScene(scene);

    return true;
}

在FlashScene init时,我们创建一个Resource Load Thread,我们用一个ResourceLoadIndicator作为渲染线程与Worker线程之间交互的媒介。

//FlashScene.h

struct ResourceLoadIndicator {
    pthread_mutex_t mutex;
    bool load_done;
    void *context;
};

class FlashScene : public Scene
{
public:
    FlashScene(void);
    ~FlashScene(void);

    virtual bool init();

    CREATE_FUNC(FlashScene);
    bool getResourceLoadIndicator();
    void setResourceLoadIndicator(bool flag);

private:
     void updateScene(float dt);

private:
     ResourceLoadIndicator rli;
};

// FlashScene.cpp
bool FlashScene::init()
{
    bool bRet = false;
    do {
        CC_BREAK_IF(!CCScene::init());
        Size winSize = Director::getInstance()->getWinSize();

        //FlashScene自己的资源只能同步加载了
        Sprite *bg = Sprite::create("FlashSceenBg.png");
        CC_BREAK_IF(!bg);
        bg->setPosition(ccp(winSize.width/2, winSize.height/2));
        this->addChild(bg, 0);

        this->schedule(schedule_selector(FlashScene::updateScene)
                       , 0.01f);

        //start the resource loading thread
        rli.load_done = false;
        rli.context = (void*)this;
        pthread_mutex_init(&rli.mutex, NULL);
        pthread_attr_t attr;
        pthread_attr_init(&attr);
        pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
        pthread_t thread;
        pthread_create(&thread, &attr,
                    resource_load_thread_entry, &rli);

        bRet=true;
    } while(0);

    return bRet;
}

static void* resource_load_thread_entry(void* param)
{
    AppDelegate *app = (AppDelegate*)Application::getInstance();
    ResourceLoadIndicator *rli = (ResourceLoadIndicator*)param;
    FlashScene *scene = (FlashScene*)rli->context;

    //load music effect resource
    … …

    //init from config files
    … …

    //load images data in worker thread
    SpriteFrameCache::getInstance()->addSpriteFramesWithFile(
                                       "All-Sprites.plist");
    … …

    //set loading done
    scene->setResourceLoadIndicator(true);
    return NULL;
}

bool FlashScene::getResourceLoadIndicator()
{
    bool flag;
    pthread_mutex_lock(&rli.mutex);
    flag = rli.load_done;
    pthread_mutex_unlock(&rli.mutex);
    return flag;
}

void FlashScene::setResourceLoadIndicator(bool flag)
{
    pthread_mutex_lock(&rli.mutex);
    rli.load_done = flag;
    pthread_mutex_unlock(&rli.mutex);
    return;
}

我们在定时器回调函数中对indicator标志位进行检查,当发现加载ok后,切换到接下来的游戏开始场景:

void FlashScene::updateScene(float dt)
{
    if (getResourceLoadIndicator()) {
        Director::getInstance()->replaceScene(
                              WelcomeScene::create());
    }
}

到此,FlashScene的初始设计和实现完成了。Run一下试试吧。

二、崩溃

GenyMotion的4.4.2模拟器上,游戏运行的结果并没有如我期望,FlashScreen显现后游戏就异常崩溃退出了。

通过monitor分析游戏的运行日志,我们看到了如下一些异常日志:

threadid=24: thread exiting, not yet detached (count=0)
threadid=24: thread exiting, not yet detached (count=1)
threadid=24: native thread exited without detaching

很是奇怪啊,我们在创建线程时,明明设置了 PTHREAD_CREATE_DETACHED属性了啊:

pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

怎么还会出现这个问题,而且居然有三条日志。翻看了一下引擎内核的代码TextureCache::addImageAsync,在线程创建以及线程主函数中也没有发现什么特别的设置。为何内核可以创建线程,我自己创建就会崩溃呢。Debug多个来回,问题似乎聚焦在resource_load_thread_entry中执行的任务。在我的代码里,我利用SimpleAudioEngine加载了音效资源、利用UserDefault读取了一些持久化的数据,把这两个任务去掉,游戏就会进入到下一个环节而不会崩溃。

SimpleAudioEngine和UserDefault能有什么共同点呢?Jni调用。没错,这两个接口底层要适配多个平台,而对于Android 平台,他们都用到了Jni提供的接口去调用Java中的方法。而Jni对多线程是有约束的。Android开发者官网上有这么一段话:

All threads are Linux threads, scheduled by the kernel. They're usually started from managed code (using Thread.start), but they can also be created elsewhere and then attached to the JavaVM. For example, a thread started with pthread_create can be attached with the JNI AttachCurrentThread or AttachCurrentThreadAsDaemon functions. Until a thread is attached, it has no JNIEnv, and cannot make JNI calls.

由此看来pthread_create创建的新线程默认情况下是不能进行Jni接口调用的,除非Attach到Vm,获得一个JniEnv对象,并且在线 程exit前要Detach Vm。好,我们来尝试一下,Cocos2d-x引擎提供了一些JniHelper方法,可以方便进行Jni相关操作。

#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
#include "platform/android/jni/JniHelper.h"
#include <jni.h>
#endif

static void* resource_load_thread_entry(void* param)
{
    … …

    JavaVM *vm;
    JNIEnv *env;
    vm = JniHelper::getJavaVM();

    JavaVMAttachArgs thread_args;

    thread_args.name = "Resource Load";
    thread_args.version = JNI_VERSION_1_4;
    thread_args.group = NULL;

    vm->AttachCurrentThread(&env, &thread_args);
    … …
    //Your Jni Calls
    … …

    vm->DetachCurrentThread();
    … …
    return NULL;
}

关于什么是JavaVM,什么是JniEnv,Android Developer官方文档中是这样描述的:

The JavaVM provides the "invocation interface" functions, which allow you to create and destroy a JavaVM. In theory you can have multiple JavaVMs per process, but Android only allows one.
The JNIEnv provides most of the JNI functions. Your native functions all receive a JNIEnv as the first argument.
The JNIEnv is used for thread-local storage. For this reason, you cannot share a JNIEnv between threads.

三、黑屏

上面的代码成功解决了线程崩溃的问题,但问题还没完,因为接下来我们又遇到了“黑屏”事件。所谓的“黑屏”,其实并不是全黑。但进入游戏 WelcomScene时,只有Scene中的LabelTTF实例能显示出来,其余Sprite都无法显示。显然肯定与我们在Worker线程加载纹理 资源有关了:

SpriteFrameCache::getInstance()->addSpriteFramesWithFile("All-Sprites.plist");

我们通过碎图压缩到一张大纹理的方式建立SpriteFrame,这是Cocos2d-x推荐的优化手段。但要想找到这个问题的根源,还得看monitor日志。我们的确发现了一些异常日志:

libEGL: call to OpenGL ES API with no current context (logged once per thread)

通过Google得知,只有Renderer Thread才能进行egl调用,因为egl的context是在Renderer Thread创建的,Worker Thread并没有EGL的context,在进行egl操作时,无法找到context,因此操作都是失败的,纹理也就无法显示出来。要解决这个问题就 得查看一下TextureCache::addImageAsync是如何做的了。

TextureCache::addImageAsync只是在worker线程进行了image数据的加载,而纹理对象Texture2D instance则是在addImageAsyncCallBack中创建的。也就是说纹理还是在Renderer线程中创建的,因此不会出现我们上面的 “黑屏”问题。模仿addImageAsync,我们来修改一下代码:

static void* resource_load_thread_entry(void* param)
{
    … …
    allSpritesImage = new Image();
    allSpritesImage->initWithImageFile("All-Sprites.png");
    … …
}

void FlashScene::updateScene(float dt)
{
    if (getResourceLoadIndicator()) {
        // construct texture with preloaded images
        Texture2D *allSpritesTexture = TextureCache::getInstance()->
                           addImage(allSpritesImage, "All-Sprites.png");
        allSpritesImage->release();
        SpriteFrameCache::getInstance()->addSpriteFramesWithFile(
                           "All-Sprites.plist", allSpritesTexture);
     
        Director::getInstance()->replaceScene(WelcomeScene::create());
    }
}

完成这一修改后,游戏画面就变得一切正常了,多线程资源加载机制正式生效。

Cocos2d-x 3.0rc2集成ShareSDK

给自己的手机游戏增加些社交分享功能,有助于游戏宣传和提升知名度,是一种不错的社交营销手段。国内这方面的第三方插件有不少,比如ShareSDK友 盟分享组件Baidu分享组件等,之前在研究2.2.2版本时,集成了ShareSDK这个组件,这次迁移到Cocos2d-x 3.0rc2依旧选择集成ShareSDK,这里就来说说集成的过程,遇到的一些问题以及解决方法。这里仅以Android平台游戏集成为例。

一、功能描述、SDK版本和帐号准备

功能大致是这样的:在游戏中设置一个按钮,点击这个按钮,弹出知名社交平台的分享图标集窗口,用户选择分享目标后,相关信息分享到对应的社交平台。分享结果通知通过Toast显示在屏幕的下方。

这次依旧使用ShareSDK for Android 2.3.7版本(ShareSDK-Android-2.3.7),Cocos2d-x的版本为3.0rc2

集成前,你需要有一个基于Cocos2d-x 3.0rc2的可运行的Android平台游戏project,我们的集成就基于该project,这里我们的project名为GameDemo,GameDemo的源码结构大致是:

GameDemo/
    – Classes/
    – proj.android/
    – Resources/
    – cocos2d/
    – CMakeLists.txt
    – … …

使用ShareSDK前,你需要在各大主流社交平台(微信微博)申请开发者帐号以及游戏接入权限(app_key、app_secret)等,当然在ShareSDK站点也应该有自己的帐号和应用AppKey,这些申请的审核需要几个工作日,甚至更长。

二、ShareSDK集成步骤

按照ShareSDK官方manual说法,Cocos2d-x集成ShareSDK有三种方式,之前在Cocos2d-x 2.2.2引擎中采用的是专用组件集成的方式,该组件(C2DXShareSDKSample)可以在这里下载(该组件近期已经fix了我之前发现的bug)。

1.  jar包集成

这次我们主要做微博、微信的社交分享,因此只需要微博、微信相关jar包。在C2DXShareSDKSample/proj.android/libs下,我们找到以下几个jar包:

  -rw-rw-r– 1 tonybai tonybai  97K  4月  8 18:10 mframework.jar
  -rw-rw-r– 1 tonybai tonybai 112K  4月  8 17:39 ShareSDK-Core-2.3.7.jar
  -rw-rw-r– 1 tonybai tonybai  19K  4月  8 17:39 ShareSDK-SinaWeibo-2.3.7.jar
  -rw-rw-r– 1 tonybai tonybai 4.3K  4月  8 17:39 ShareSDK-Wechat-2.3.7.jar
  -rw-rw-r– 1 tonybai tonybai  29K  4月  8 17:39 ShareSDK-Wechat-Core-2.3.7.jar
  -rw-rw-r– 1 tonybai tonybai 4.6K  4月  8 17:39 ShareSDK-Wechat-Favorite-2.3.7.jar
  -rw-rw-r– 1 tonybai tonybai 4.4K  4月  8 17:39 ShareSDK-Wechat-Moments-2.3.7.jar

把这些jar包文件Copy到GameDemo/proj.android/libs下。

2. 配置文件与资源部分集成

修改GameDemo/proj.android/AndroidManifest.xml文件,在application标签下,添加如下Activity标签:

        <activity
            android:name="cn.sharesdk.framework.ShareSDKUIShell"
            android:configChanges="keyboardHidden|orientation|screenSize"
            android:screenOrientation="portrait"
            android:theme="@android:style/Theme.Translucent.NoTitleBar"
            android:windowSoftInputMode="stateHidden|adjustResize" >
    </activity>
    <activity
            android:name=".wxapi.WXEntryActivity"
            android:configChanges="keyboardHidden|orientation|screenSize"
            android:exported="true"
            android:screenOrientation="portrait"
            android:theme="@android:style/Theme.Translucent.NoTitleBar" />

将C2DXShareSDKSample/proj.android/res下的如下目录中的文件复制到GameDemo/proj.android/res下:

   drawable-hdpi/  drawable-ldpi/  drawable-mdpi/ 
   drawable-xhdpi/  layout/  values/  values-en/

注意,类似icon.png这种文件就不要复制了,自己做一下判断就好。

3. C++部分代码集成

将C2DXShareSDKSample/Classes下的C2DXShareSDK文件夹Copy到GameDemo/Classes下面。

由于Cocos2d-x 3.0rc2的类命名发生了变化,我们需要对C2DXShareSDK中使用到的引擎中的类名以及方法名进行修改。但实际上Cocos2d-x 3.0rc2考虑到了一些兼容性的问题,大部分名字通过cocos2d/cocos/deprecated/CCDeprecated.h中定义的typedef得以保留,虽然这些名字已经被建议deprecated了。rc2中CCObject被改名为Ref了,这个我们需要手工在C2DXShareSDK进行修改。

另外ShareSDK组件在实现时大量使用了CCDictionaryCCArrayCCString,而这三个类在Cocos2d-x 3.0rc2中均被deprecated了,但我们依然可以使用,所以我们可以不做修改。但以后随着cocos2d-x版本的演进,这些类很可能被彻底移除出引擎,我们就需要重新使用其替代品进行实现了。

此外我们还需要手工修改一下C2DXShareSDK/Android/JSON/CCJSONConverter.cpp文件中的getObjJson方 法,因为rc2中CCDictionary、CCString、CCArray这些类的真实名称都已经换成了__Dictionary、__String 和__Array,CCDictionary、CCString、CCArray只是些typedef,因此要像下面这样做些修改(如果你是集成 cocos2d-x 2.x.x版本,则无需做下面修改):

cJSON * CCJSONConverter::getObjJson(Ref * obj)
{
    std::string s = typeid(*obj).name();
    if(s.find("__Dictionary")!=std::string::npos){
        cJSON * json = cJSON_CreateObject();
        convertDictionaryToJson((CCDictionary *)obj, json);
        return json;
    }else if(s.find("__Array")!=std::string::npos){
        cJSON * json = cJSON_CreateArray();
        convertArrayToJson((CCArray *)obj, json);
        return json;
    }else if(s.find("__String")!=std::string::npos){
        CCString * s = (CCString *)obj;
        cJSON * json = cJSON_CreateString(s->getCString());
        return json;
    }else if(s.find("CCNumber")!=std::string::npos){
        CCNumber * n = (CCNumber *)obj;
        cJSON * json = cJSON_CreateNumber(n->getDoubleValue());
        return json;
    }else if(s.find("CCNull")!=std::string::npos){
        cJSON * json = cJSON_CreateNull();
        return json;
    }
    CCLog("CCJSONConverter encountered an unrecognized type");
    return NULL;
}

CCNumber和CCNull是ShareSDK组件自己实现的类名,这里无需修改。

接下来我们需要在AppDelegate.cpp中对ShareSDK做初始化了:

bool AppDelegate::applicationDidFinishLaunching() {
    … …
    initShareSDK();
    … ..
}

void AppDelegate::initShareSDK()
{
    // sina weibo
    CCDictionary *sinaConfigDict = CCDictionary::create();
    sinaConfigDict->setObject(CCString::create("YOUR_WEIBO_APPKEY"), "app_key");
    sinaConfigDict->setObject(CCString::create("YOUR_WEBIO_APPSECRET"), "app_secret");
    sinaConfigDict->setObject(CCString::create("http://www.sharesdk.cn"), "redirect_uri");
    C2DXShareSDK::setPlatformConfig(C2DXPlatTypeSinaWeibo, sinaConfigDict);

    // wechat
    CCDictionary *wcConfigDict = CCDictionary::create();
    wcConfigDict->setObject(CCString::create("YOUR_WECHAT_APPID"), "app_id");
    C2DXShareSDK::setPlatformConfig(C2DXPlatTypeWeixiSession, wcConfigDict);
    C2DXShareSDK::setPlatformConfig(C2DXPlatTypeWeixiTimeline, wcConfigDict);
    C2DXShareSDK::setPlatformConfig(C2DXPlatTypeWeixiFav, wcConfigDict);

    C2DXShareSDK::open(CCString::create("YOUR_SHARESDK_APPKEY"), false);
}

在Share按钮的事件回调函数中调用ShareSDK的接口进行社交平台分享:

void GameScene::menuShareCallback(Ref* sender)
{
    Dictionary *content = Dictionary::create();

    content->setObject(String::create("ShareSDK for Cocos2d-x 3.0rc2社交分享测试。")
                        , "content");
    content->setObject(String::create("ShareSDK分享测试"), "title");
    content->setObject(String::create("http://tonybai.com"), "titleUrl");
    content->setObject(String::create("http://tonybai.com"), "url");
    content->setObject(String::create("Tony Bai"), "site");
    content->setObject(String::create("http://tonybai.com"), "siteUrl");
    content->setObject(String::createWithFormat("%s", YOUR_LOCAL_IMAGE_PATH)
                       , "image");
    content->setObject(String::createWithFormat("%d", C2DXContentTypeNews)
                       , "type");

    C2DXShareSDK::showShareMenu(NULL, content, CCPointMake(100, 100),
                          C2DXMenuArrowDirectionLeft, shareResultHandler);
}

void shareResultHandler(C2DXResponseState state,
                        C2DXPlatType platType,
                        Dictionary *shareInfo,
                        Dictionary *error)
{
    AppDelegate *app = (AppDelegate*)Application::getInstance();
    switch (state) {
        case C2DXResponseStateSuccess:
            CCLog("Share Ok");
            app->showShareResultToast("分享成功");
            break;
        case C2DXResponseStateFail:
            app->showShareResultToast("分享失败");
            CCLog("Share Failed");
            break;
        default:
            break;
    }
}

showShareResultToast实现如下:

void AppDelegate::showShareResultToast(const char *msg)
{
    JniMethodInfo t;
    if (JniHelper::getStaticMethodInfo(t, "YOUR_ACTIVITY_NAME",
        "showShareResultToast", "(Ljava/lang/String;)V")) {
        jstring jmsg = t.env->NewStringUTF(msg);
        t.env->CallStaticVoidMethod(t.classID, t.methodID, jmsg);
        if (t.env->ExceptionOccurred()) {
            t.env->ExceptionDescribe();
            t.env->ExceptionClear();
            return;
        }
        t.env->DeleteLocalRef(t.classID);
    }
}

4. Java部分代码集成

在GameDemo/proj.android/src下面建立cn/sharesdk路径,将C2DXShareSDKSample /proj.android/src/cn/sharesdk下的onekeyshare和ShareSDKUtils.java Copy到GameDemo/proj.android/src/cn/sharesdk下面。

将ShareSDK-Android-2.3.7.zip解压后的ShareSDK for Android/Src/wxapi Copy到GameDemo/proj.android/src/com.tonybai.game/下。

修改GameDemo/proj.android/src/com.tonybai.game/GameDemoActivity.java文件:

import android.widget.Toast;
import cn.sharesdk.ShareSDKUtils;

public class GameDemoActivity extends Cocos2dxActivity {

    private static Context context;

    private static Handler notifyHandler = new Handler() {
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 1:
                    String message = (String) msg.obj;
                    Toast.makeText(context, message,
                      Toast.LENGTH_SHORT).show();
                    break;
                default:
                    break;
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        context = this;
        ShareSDKUtils.prepare();
        ShareSDKUtils.initSDK("YOUR_SHARESDK_APPKEY", true);
    }

    public static void showShareResultToast(String result) {
        Message msg = new Message();
        msg.what = 1;
        msg.obj = result;
        notifyHandler.sendMessage(msg);
    }

    @Override
    public void onDestroy() {
        ShareSDKUtils.stopSDK();
        super.onDestroy();
    }
}

三、问题与解决方法

按照上面的集成方法修改后,通过cocos编译app,在模拟器运行GameDemo,点击Share,理论上屏幕下方会出现ShareSDK的分享窗口,选择“新浪微博”图标,会打开“图文分享”内容窗口,点击窗口右上角的“分享”即可。

问题1】“图文分享”窗口内容可编辑,并且总是弹出软键盘,影响体验。
 
 期望:内容不可编辑,默认不弹出软键盘
 解决方法:
      打开proj.android/src/cn/sharesdk/onekeyshare/EditPage.java,做如下修改:

      将窗口的软输入方式默认改为SOFT_INPUT_STATE_HIDDEN。

      public void setActivity(Activity activity) {
        super.setActivity(activity);
        if (dialogMode) {
            activity.setTheme(android.R.style.Theme_Dialog);
            activity.requestWindowFeature(Window.FEATURE_NO_TITLE);
        }
        activity.getWindow().setSoftInputMode(
                   //WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
                   WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);//default: hidden
    }

    在initPageView中增加一行:etContent.setKeyListener(null)。让窗口内容无法修改。
    private void initPageView() {
         … …
        // 文字输入区域
        etContent = new EditText(getContext());
        etContent.setGravity(Gravity.LEFT | Gravity.TOP);
        etContent.setBackgroundDrawable(null);
        etContent.setText(String.valueOf(reqData.get("text")));
        etContent.setKeyListener(null);//make the edittext uneditable
        etContent.setLayoutParams(lpEt);
        … …
    }

【问题2】向微博分享,点击“分享”后,过一会程序异常停止。

 原因分析:
        通过调试观察,发现ShareSDK在解析从Weibo收到的Json包时出现内存违法访问。具体位置是在解析一个数组对象时出现的问题。 ShareSDK用CCArray来存储Json中的数组对象。该问题在cocos2d-x 2.2.2版本中不会出现,但在cocos2d-x 3.0rc2版本中会出现。经代码对比发现,3.0rc2版本中的CCArray的实现与2.2.2 CCArray实现有很大不同,似乎是做了较大重构,暂不能确定是否是3.0rc2版本中CCArray实现的bug。

 解决方法:由于后续的分享结果通知成功与否只需要根据分享的状态来决定,因此我们只需解析出"status"、“action”和“platform” 这三个CCNumber类型字段的值即可。CCArray类型的对象我们并不需要,因此我们只需绕过对Array类型字段的解析和存储即可,修改如下:

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

void CCJSONConverter::convertJsonToDictionary(cJSON *json, CCDictionary *dictionary)
{
    dictionary->removeAllObjects();
    cJSON * j = json->child;
    while (j) {
        if (j->type == cJSON_Number) {
            Ref * obj = getJsonObj(j);
            dictionary->setObject(obj, j->string);
        }
        j = j->next;
    }
}

四、其他

在使用ShareSDK做社交分享时,注意下面两个现象:
1) 第一次进行微博或微信分享时,会打开授权页面,授权后才能分享成功;
2) 微信分享窗口只有在手机联网状态下才能打开。如果手机无法联网,那微信好友、朋友圈和收藏分享将无法打开分享窗口,也不会有什么提示。




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

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

如果您喜欢通过微信App浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:



本站Powered by Digital Ocean VPS。

选择Digital Ocean VPS主机,即可获得10美元现金充值,可免费使用两个月哟!

著名主机提供商Linode 10$优惠码:linode10,在这里注册即可免费获得。

阿里云推荐码:1WFZ0V立享9折!

View Tony Bai's profile on LinkedIn


文章

评论

  • 正在加载...

分类

标签

归档











更多