最近在做跨平台迁移,之前写的iOS的控件现在需要在Android上也实现一遍,好久不用Java,手生的很,之前也完全不懂JNI,经过三天的挣扎,总算把控件写完了,其中最蛋疼的莫过于C++调用Java的方法。为了和iOS版本公用同一份C++逻辑代码,只在绘制部分调用Android的API,这样做导致的结果就是,C++调用Java方法时,需要把逻辑模块加工处理之后的结构体作为参数传递给Java,而且结构体中包括各种enum、vector和map。下面就讲讲经过这几天的挣扎如何从零开始学习JNI。
一开始的时候完全摸不到头脑,就想着是不是存在一个神奇的C++的函数,这个函数和Java是关联在一起的,传入参数,然后参数就被传递到JavaVM,执行Java的方法并返回返回值,返回值再被这个函数带回,就像调用一个普通的C++函数一样。
别说,还真有这个神奇的方法,但是用起来可不止普通C++函数那么简单。这就是<jni.h>头文件里提供的若干C函数:
Call<TYPE>Method(jobject obj, jmethodID methodID, …),调用Java的实例方法
CallStatic<TYPE>Method(jclass clazz, jmethodID methodID, …),调用Java的类方法
如果一个类方法返回一个Java对象,可以用:
jobject ret = env->CallStaticObjectMethod(clazz, methodId);
ret即为方法的返回值,env是JNIEnv *类型,Cocos2D-X帮我们设置好了,可以从JniHelper中得到,clazz是这个类方法的类型标识,可以通过:
jclass clazz = env->FindClass(“com/xxx/ClassA”)
获取,FindClass的参数为类的全名,‘.’用‘/’代替,methodId是这个类方法对应的方法Id,可以通过:
jmethodID methodID = env->GetStaticMethodID(clazz, “FuncA”, “()V”)
获取,第一个参数为之前获得的类标识,第二个参数是方法名,第三个参数比较奇怪,是这个方法的函数签名,用来区分不同重载的函数。得到以上参数之后,就可以从C++调用ClassA中的FuncA方法了。
如果FuncA有一个int参数的话,CallStaticObjectMethod需要把参数带上:
jobject ret = env->CallStaticObjectMethod(clazz, methodId, 10);
第三个参数实际是jint类型,由于jint是从typedef int而来,所以可以直接把整数10传入,同时,methodId也不一样了:
jmethodID methodID = env->GetStaticMethodID(clazz, “FuncA”, “(I)V”)
如果FuncA有一个String参数,在调用CallStaticObjectMethod时,是不能直接把char *或char[]或std::string或”xxx”直接作为参数传入的,必须创建一个jstring:
jstring jstr = env->NewStringUTF(“xxx”);
然后把str作为参数传入:
jobject ret = env->CallStaticObjectMethod(clazz, methodId, jstr);
之后需要把jstr删掉,否则JavaVM可能不会回收这个String的内存:
env->DeleteLocalRef(jstr);
如果FuncA有一个Java类作为参数,需要在JavaVM环境中创建参数类的实例,以jobject的形式返回到C++中,再把jobject作为参数调用CallStaticObjectMethod的时候,jobject对应的参数实例被传入FuncA方法。创建参数类实例相当于调用参数类的构造函数:
jclass paramClass = env->FindClass(“com/xxx/ParamA”);
jmethodID paramConstructor = env->GetMethodID(paramClass, “<init>”, “()V”);
也可以选择带参数的构造函数,只要修改函数签名,然后传入相应参数即可,如果参数不是基本数据类型,仍然需要先创建构造函数的参数类的实例。
jobject jParam = env->NewObject(paramClass, paramConstructor);
jobject ret = env->CallStaticObjectMethod(clazz, methodId, jParam);
env->DeleteLocalRef(jParam);
CallStaticObjectMethod返回之后同样需要释放之前创建的参数类实例。
总结:C++调用Java的函数,只要得到Java中对应的类(jclass)和方法(jmethodID)以及参数即可,参数如果是基本数据类型,可以直接传递,如果是String等非基本数据类型,则需要先创建实例(jstring/jobject)。
前几天遇到一个诡异的问题,从CCB文件创建出来的Node的引用计数始终多了1,导致资源无法正常释放,经过各种Debug之后,发现和Action有关,下面就举个栗子说说事情的原委。
ps:首先说明一下,导致问题的不算是Cocos2d-x的Bug,但确实容易被忽略了,称为坑应该不为过吧。

如上图所示,FriendPanel里需要若干个AvatarNode,为了节省AvatarNode创建的时间,当FriendPanel关闭的时候,回收所有AvatarNode到AvatarPool中,再次打开FriendPanel时,从AvatarPool中直接获取AvatarNode并重新设置参数,但是,为了节省内存,切换游戏场景的时候,需要把FriendPanel以及已经使用的AvatarNode和AvatarPool中未使用的AvatarNode全部释放。
由于Friend的数量可能会变,所以AvatarPool中的Avatar可能会有剩余,此时,在FriendPanel中的AvatarNode的parent为FriendPanel,AvatarPool中的AvatarNode的parent为NULL。

切换场景的时候,会调用到oldScene->onExit()和oldScene->release(),onExit()中会停止所有的Scheduler和Actions,并递归调用所有child的onExit(),调用oldScene->release()之后,如果没有在其他地方retain过oldScene的话,则会调用到oldScene的析构函数,析构函数里又会release所有的child,如果child没有在其他地方retain的话,则会调用child的析构函数。
因此,已经加入FriendPanel的AvatarNode,可以在FriendPanel析构的时候一起释放掉。
那么,AvatarPool里的AvatarNode应该如何释放呢? 继续阅读 →
如果有很多相同的CCSprite(下文简称Sprite)需要绘制,使用CCSpriteBatchNode(下文简称SpriteBatch)能大大提高绘制效率。这里“相同的”是指来自同一个图片文件,比如,有一张虫子的图,在屏幕上贴上100只(很容易脑补,就不贴图了)。
大致步骤:
|
CCSpriteBatchNode *
batch
=
CCSpriteBatchNode
::
(
"img.png"
)
;
{
// loop
CCSprite *
sprite
=
CCSprite
::
createWithTexture
(
batch
->
getTexture
(
)
)
;
batch
->
addChild
(
sprite
)
;
// ...set position or something else
}
// loop end
addChild
(
batch
)
;
|
还有一种情况,比如有10种虫子的图,把十张图打包到一张TextureAtlas上,如果用TexturePacker的话,会生成一个.png和.plist文件,对于这十种虫子,也算是来自“相同的”图,用CCSpriteFrame创建每种虫子的Sprite,然后用SpriteBatch绘制不同的虫子(也请自行脑补)。
使用SpriteBatch的条件:
1.所有Sprite必须是同一张图
SpriteBatch的本质是一个TextureAtlas,把Sprite加到SpriteBatch时,不仅把Sprite加到SpriteBatch的children列表里,还把Sprite的Quad加到SpriteBatch的TextureAtlas里,SpriteBatch的draw()实际上调用了TextureAtlas的drawQuads(),它只调用一次glBindTexture,然后遍历Quad列表,绘制每个Quad对应的Texture。由于TextureAtlas里地Quad对应的都是同一张图素,即绘制前绑定的那张,所以添加到SpriteBatch里地所有Sprite都必须来自同一张图。
如果不使用SpriteBatch,每次绘制CCSprite时,都要调用glBindTexture来绑定Sprite对应的Texture,如果绘制100个Sprite,则会调用100次glBindTexture,如果把这100个Sprite放到SpriteBatch里,则只需要调用一次glBindTexture,这就是为什么SpriteBatch可以高效绘制大量相同Sprite的原因。
2.同一个SpriteBatch里的Sprite不能和外部其他Sprite存在遮挡冲突
“遮挡冲突”是指,假设Sprite1和Sprite2满足条件1,Sprite3不满足条件1,如果Sprite3的zOrder在Sprite1和Sprite2之间,导致无法先绘制Sprite1和Sprite2,再绘制Sprite3,或者先绘制Sprite3,再绘制Sprite1和Sprite2,那么Sprite1和Sprite2与Sprite3存在遮挡冲突,此时Sprite1和Sprite2不能放到同一个SpriteBatch里。因此,可以把SpriteBatch里的所有Sprite看做一个整体,对于SpriteBatch外的其他Sprite而言,要么会遮挡SpriteBatch,要么被遮挡,不会出现遮挡一部分的情况。
加到SpriteBatch里的Sprite,也有相对zOrder,SpriteBatch在绘制之前会根据Sprite的zOrder排序,保证了Sprite的绘制顺序。如果Sprite有子节点,且子节点也是Sprite,SpriteBatch会按先序遍历Sprite,把子节点加到SpriteBatch里。
Tips:
Sprite在加到SpriteBatch里的时候,会给Sprite分配一个atlasIndex,可以作为Sprite的Quad在TextureAtlas里的唯一索引,SpriteBatch对Sprite排序或者Sprite发生transform改变时,可以据此索引更新TextureAtlas里的Quad,所以加到SpriteBatch里的Sprite可以像普通Sprite一样使用,可以执行Action,transform也是相互独立互不干扰的。
创建SpriteBatch之前最好估算一下会加入多少Sprite,设置一个初始容量,每次添加Sprite时都会检查剩余容量是否够用,不够的话TextureAtlas需要扩容,开销不容小觑。
之前写过一篇《不规则形状按钮的点击判定》,利用了CCRenderTexture创建一块画布,可以在上面随意作画,这次,美术同学又本着把程序员折腾到底的态度,提出了又一奇葩需求,由于原需求设计商业机密,这里仅举个同理的例子说明。
附带福利图一张:

神马?没看够?还想看看其他人?请看耐心完全文
要做到上面的效果,glBlendFunc是个很好的选择。glBlendFunc是一个设置图像叠加方式的函数,就是把一张图绘制在画布上的时候,用指定的混色模式,使被绘制的图和原画布上的图进行混色运算,来实现各种混色效果。关于glBlendFunc的使用方法网上有很多教程,具体计算原理这里不再赘述,可参见微软的文档(官网文档排版实在蛋疼):http://msdn.microsoft.com/en-us/library/ms537046,下面介绍下上述效果的实现过程。
继续阅读 →
一般按钮的hitTest只需要判断是否在按钮的frame内部,其实就是判断点是否在矩形内,对于不规则形状的按钮就复杂一些。如果是多边形之类的,还可以考虑人工取点构成多边形,然后判断点是否在多边形内,而且需要涉及到如何取点和加载配置表,无论从人工开销还是能耗环保角度看,都是最蛋疼的方法没有之一。

如果策划让你把上面那张地图区块变成按钮,那么你会连死得心都有了。
幸运的是,可以很容易想到一个方法,当被点击像素的alpha值不等于0或者大于一个阈值时就是点中了,反之就是没点中。
接下来的问题就是,如何取得指定位置的alpha值呢?cocos2d-x并没有提供获取图片某点像素的方法,不过OpeGL ES提供了glReadPixels函数,来获取framebuffer上的像素数据,Cocos2D-X提供了一个CCRenderTexture,它会帮我们初始化一块framebuffer,相当于创建一块画布,我们把图片绘制到画布上,然后去画布上的像素值。
继续阅读 →
先介绍下宕机现象:用Cocos2D-X自带的cURL下载玩家头像,开了6个线程,各自维护一个下载队列,刷新一次玩家大概要下载50多个头像(最坏情况下本地还没有头像缓存),前面几个头像下载基本顺利,后面总有几个请求超时,然后就宕机了。
看了下cURL官网介绍
libcurl is completely thread safe, except for two issues: signals and SSL/TLS handlers. Signals are used for timing out name resolves (during DNS lookup) – when built without c-ares support and not on Windows.—-参见http://curl.haxx.se/libcurl/c/libcurl-tutorial.htmlMulti-threading Issues部分
由于libcurl是线程安全的,而且没有用到SSL,所以问题应该出在Signals上,基本意思就是DNS超时,而且不支持c-ares,导致crash。
官网也同时提供了解决方案:
When using multiple threads you should set the CURLOPT_NOSIGNAL option to 1 for all handles. Everything will or might work fine except that timeouts are not honored during the DNS lookup – which you can work around by building libcurl with c-ares support. c-ares is a library that provides asynchronous name resolves.
使用多线程的适合需要为所有handles设置 CURLOPT_NOSIGNAL为1,或者build一个支持c-ares的libcurl版本,c-ares用来提供异步的域名解析。
这里有关于SIGALRM和c-ares的解释:http://curl.haxx.se/mail/lib-2010-11/0188.html
还有一点需要注意的是,CURLcode curl_global_init(long flags )函数不是线程安全的,关于这一点不知道为什么之前的Multi-threading Issues里没有强调,但是在该函数的Reference页里有强调:
This function is not thread safe. You must not call it when any other thread in the program (i.e. a thread sharing the same memory) is running.—-参见http://curl.haxx.se/libcurl/c/curl_global_init.html
所以在多线程环境下使用curl时需要显示地调用curl_global_init,因为在工作线程调用CURL *curl_easy_init( )的时候,如果之前没有调用过curl_global_init,会自动调用之,将会存在一定概率导致调用冲突,对此官方并不推荐。
You are strongly advised to not allow this automatic behaviour, by calling curl_global_init(3) yourself properly.—-参见http://curl.haxx.se/libcurl/c/curl_easy_init.html
对此,官方推荐的做法是,在程序一开始调用curl_global_init,程序结束时调用curl_global_cleanup。
libcurl has a global constant environment that you must set up and maintain while using libcurl. This essentially means you call curl_global_init(3) at the start of your program and curl_global_cleanup(3) at the end.—-参见http://curl.haxx.se/libcurl/c/libcurl.html
一般情况下,从CCNode::update(float delta)方法可以得到delta当我们想在其他方法中获得delta时该怎么办呢?
可以参照CCDirector::calculateDeltaTime(void)的实现方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
void
CCDirector
::
calculateDeltaTime
(
void
)
{
struct
cc_timeval
now
;
if
(
CCTime
::
gettimeofdayCocos2d
(
&
now
,
NULL
)
!=
0
)
{
CCLOG
(
"error in gettimeofday"
)
;
m_fDeltaTime
=
0
;
return
;
}
// new delta time. Re-fixed issue #1277
if
(
m_bNextDeltaTimeZero
)
{
m_fDeltaTime
=
0
;
m_bNextDeltaTimeZero
=
false
;
}
else
{
// delta = 秒差+微妙差/1e6
m_fDeltaTime
=
(
now
.
tv_sec
-
m_pLastUpdate
->
tv_sec
)
+
(
now
.
tv_usec
-
m_pLastUpdate
->
tv_usec
)
/
1000000.0f
;
m_fDeltaTime
=
MAX
(
0
,
m_fDeltaTime
)
;
}
*
m_pLastUpdate
=
now
;
}
|
可以利用CCTime::gettimeofdayCocos2d(&now, NULL)获得系统当前时间戳,计算差值时是秒和微秒分开计算的,并且需要自己维护lastUpdate的时间。
前几天在重构代码的时候突然发现一处内存泄露,具体情况是这样:加载ccb文件后,通过cocos定义的一个宏“CCB_MEMBERVARIABLEASSIGNER_GLUE”获取指定Node的引用,类似加载xib文件后用IBOutlet得到Reference,由于没有在析构时做release,导致引用计数不对称,所谓隐性的内存泄露。
宏的实现如下:
|
#define CCB_MEMBERVARIABLEASSIGNER_GLUE(TARGET, MEMBERVARIABLENAME, MEMBERVARIABLETYPE, MEMBERVARIABLE) \
if
(
pTarget
==
TARGET
&&
0
==
strcmp
(
pMemberVariableName
,
(
MEMBERVARIABLENAME
)
)
)
{
\
MEMBERVARIABLETYPE
pOldVar
=
MEMBERVARIABLE
;
\
MEMBERVARIABLE
=
dynamic_cast
<
MEMBERVARIABLETYPE
>
(
pNode
)
;
\
CC_ASSERT
(
MEMBERVARIABLE
)
;
\
if
(
pOldVar
!=
MEMBERVARIABLE
)
{
\
CC_SAFE_RELEASE
(
pOldVar
)
;
\
MEMBERVARIABLE
->
retain
(
)
;
\
}
\
return
true
;
\
}
|
注意到这个宏会把原来的var做release,新的member做retain,这就意味着指向该Node的member拥有该Node的所有权(Owner),所以在类的析构函数中必须对member做release。
官方Demo节选:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
HelloCocosBuilderLayer
::
HelloCocosBuilderLayer
(
)
:
mBurstSprite
(
NULL
)
,
mTestTitleLabelTTF
(
NULL
)
{
}
HelloCocosBuilderLayer
::
~
HelloCocosBuilderLayer
(
)
{
CC_SAFE_RELEASE
(
mBurstSprite
)
;
CC_SAFE_RELEASE
(
mTestTitleLabelTTF
)
;
}
bool
HelloCocosBuilderLayer
::
onAssignCCBMemberVariable
(
CCObject *
pTarget
,
const
char
*
pMemberVariableName
,
CCNode *
pNode
)
{
CCB_MEMBERVARIABLEASSIGNER_GLUE
(
this
,
"mBurstSprite"
,
CCSprite *
,
this
->
mBurstSprite
)
;
CCB_MEMBERVARIABLEASSIGNER_GLUE
(
this
,
"mTestTitleLabelTTF"
,
CCLabelTTF *
,
this
->
mTestTitleLabelTTF
)
;
return
false
;
}
|
在用IBOutlet的时候,因为声明为IBOutlet的对象的accessor method property往往是retain的,所以需要在viewDidLoad和dealloc中对IBOutlet的Reference做release或赋值为nil。
苹果官方Sample节选:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
@
interface
AdvancedGetController
:
UIViewController
{
UIImageView *
_imageView
;
// ...
}
@
property
(
nonatomic
,
retain
,
readwrite
)
IBOutlet
UIImageView *
imageView
;
// ...
@
synthesize
imageView
=
_imageView
;
// ...
-
(
void
)
viewDidUnload
{
[
super
viewDidUnload
]
;
self
.
imageView
=
nil
;
// ...
}
-
(
void
)
dealloc
{
[
self
->
_imageView
release
]
;
// ...
[
super
dealloc
]
;
}
|
这是个很基本的做法,按理说不应该犯这么低级的错误,但是由于种种原因导致我们的项目中所有用宏得到的Node都没有做release,而且用到的地方都是只会加载一遍ccb,在程序运行中不会释放,所以没有发现这个问题。
谨以此文防止同样错误再犯。