cocos tips 之autorelease

首先autorelease是个怎么回事呢?引用计数的原理就不用说了,凡是继承自Ref的类new出得对象,都使用引用计数来进行内存管理。对象被new出来时,初始引用计数为1,每次被retain时,引用计数+1,被release时,引用计数-1。当引用计数降为0时,则执行析构函数,内存被回收。
所以retain必须和release成对出现,才能避免内存泄漏(例如被加到父容器和从父容器移除时,就分别执行了retain和release),而且最初的new也必须对应一次release,例如

1
2
Node* node = new Node();	//引用计数1
this->addChild(node); //引用计数2

之后在合适的时候移除

1
node->removeFromParent();	//引用计数1

可见,此时如果不再执行一次release的话,引用计数一直为1,内存得不到释放,内存就泄漏了。对于一个被引用了很多的对象,要找到最终执行release的时机,显然很难,autoRelease就是为了解决这个问题的,当执行autoRelease函数时,这个对象将会被加到一个autoreleasePool中(并不会执行retain),在这帧结束时,这个pool内所有Ref对象都会执行一次release,这次release就抵消掉了new时候的那个引用计数,因此只要保证其他引用它的地方retain和release成对,就可以保证内存不会泄露。

除了使用系统自带的

1
PoolManager::getInstance()->getCurrentPool()

外,也可以自己新建一个autoRelease,因为要确保执行析构函数,所以不建议使用new来构造,直接在栈上创造一个即可,例如

1
2
AutoreleasePool p;
p.addObject(obj)

当退出执行函数时,p会被析构,此时所有被加入的Ref对象都会被执行一次release。什么时候需要自己新建一个autoreleasePool来使用呢,例如一帧内生成了大量autorelease对象(通常在循环中),如果使用默认的autoreleasePool,则全部集中在这帧结束时释放,可能导致性能降低,此时可以手动创建一个autoreleasePool来进行管理。

cocos tips之jsbinding热加载

所谓热加载,就是运行时,不用重启模拟器而重新加载js文件,提高开发效率。要实现热加载,首先要找到ocos是怎么加载js代码的,入口就是
ScriptingCore::runScript方法,包括在js代码内require,最终也是执行的这个方法。

在ScriptingCore::compileScript这个方法里,有

1
2
3
if (getScript(path)) {    
return;
}

这么一段代码,它读取了已加载文件的缓存,所以这几句必须注释掉,否则读取缓存的话,就没办法热加载了。

做了这步以后,在开启模拟器调试时,修改js代码后,不重启模拟器,只需要重新require一下文件,就实现了热加载了(比如做一个按钮,点击后重新require)。

然后如果你这么做,肯定会发现没有生效,原因是什么呢?那是因为我们在执行

1
require("a.js")

这句代码时,在模拟器上运行的时候,它实际加载的是

1
/Users/yangguang/Library/Developer/CoreSimulator/Devices/A25124E2-3204-43C7-A9F9-638FDF466587/data/Containers/Bundle/Application/69FC175C-06EB-4868-85D4-89F008164167/xxx.app/a.js

这样的路径,而我们修改的是项目中的代码,除非把修改后的代码拷贝到这个文件夹去,否则重新require的也是老代码,那要怎么解决这个问题呢?我们仍然看源代码,在读取js代码时,会获取这个文件的全路径,相关代码为

1
2
3
4
5
6
7
8
9
10
11
std::string FileUtils::fullPathForFilename(const std::string &filename)
{
if (filename.empty())
{
return "";
}

if (isAbsolutePath(filename))
{
return filename;
}

可以看到,如果文件路径写的是全路径,就不会再拼成模拟器应用所在路径了,所以解决的办法就出来了,我们将需要require的js文件的路径,在debug状态下设为开发目录下的全路径即可,在release状态下则为相对路径。

对于其他资源,也可以采用类似的思路,只要去掉资源缓存,以及使用全路径加载,就可以很容易实现资源热加载,从而避免调试时频繁的启动模拟器

javaScript中的无符号右移运算符

今天刷leetcode上的题目revert bits的时候,用到了js里的位移操作,然后就出现了问题。题目里给出的数字是32位无符号整数,但是js里默认的整数是32位有符号整数,所以在执行

1
var a = 1 << 31

时, a的值是-2147483648,而不是期望中的2147483648

如果要得到正确的值,就需要用到另一个位操作符:无符号右移运算符 >>>

它与普通的右移运算符的区别只是在右移补位时只补0,而普通的右移操作符对有符号整数进行操作,当符号位为1时补1,符号位为0时补0

当需要将一个有符号数转成无符号数时,使用 >>> 0即可,将一个无符号数转成有符号数,使用<< 0即可

1
2
3
var a = 1 << 31;	//a = -2147483648
var b = a >>> 0; //b = 2147483648
var c = b << 0; //c = -2147483648

知道了>>>操作符之后,这个leetcode问题就很好解决了,顺便把我的答案附上

1
2
3
4
5
6
7
8
9
10
11
12
13
var reverseBits = function(n) {
var m = 0;
var index = 31;
while(n) {
var tmp = n & 1;
if(tmp) {
m = m | (tmp << index);
}
index -= 1;
n = n >>> 1; //注意,使用无符号右移
}
return m>>>0; //m默认是有符号数,强制转换成无符号数
};

googleClosureCompiler使用

官方文档在此

最简单的使用命令如下

1
java -jar compiler.jar --js hello.js --js_output_file hello-compiled.js

使用

1
java -jar compiler.jar -help

可以显示帮助,通过它可以查询有哪些可配置参数,及它们的可选值,默认值

这里再介绍一下其他的一些常用参数,像–charset这种不是很常用到的参数就不一一举例了,用help查看即可,需要注意的是,随着compiler.jar的版本不一样,可选参数以及默认参数也可能会不一样,以help给出的文档为准即可

  • –js指定输入文件名
  • –js_output_file 指定输出文件名,如果不设置此参数,则默认输出到stdout
  • –compilation_level 混淆级别,可选参数WHITESPACE_ONLY,SIMPLE_OPTIMIZATIONS,ADVANCED_OPTIMIZATIONS。默认值为SIMPLE_OPTIMIZATIONS。WHITESPACE_ONLY只会移除掉空格和注释,SIMPLE_OPTIMIZATIONS会简化局部变量名,ADVANCED_OPTIMIZATIONS通常称为深度混淆,他简化局部以及全局变量名,移除deadCode,并且对一些函数inlining。
  • –externs 在使用深度混淆时,如果不希望一些变量名被混淆,则需要使用此参数。可以指定多个extern参数,但每个参数都需要使用一次–externs
  • –language_in 可选值包括ECMASCRIPT3,ECMASCRIPT5,ECMASCRIPT5_STRICT,默认值是ECMASCRIPT3,所以如果需要混淆的代码中包含ECMASCRIPT5特性,则必须指定此参数,ECMASCRIPT5_STRICT会使用严格模式

使用SIMPLE_OPTIMIZATIONS没什么好说的,主要是使用ADVANCED_OPTIMIZATIONS时需要注意一些,避免被坑,建议详细阅读下官方文档

简单而言:

  1. 为了避免未调用方法被移除,可以进行一次调用,或者export出来
  2. 对成员属性调用的方式有.key和[“key”]两种,因为字符串不会被混淆,所以对于不希望被混淆的变量名,应该采取第二种方式。而且对同一个成员属性,不应该两种方式混用,否则有的地方呗混淆,有的地方被保留,就肯定出错了。这里有个小细节,第一次赋值时使用.key的方式,key不会被混淆。
  3. 因为深度混淆会简化全局变量名,所以必须将需要混淆的所有代码统一进行混淆,否则定义时的变量名被混淆成了a,使用时混淆为b
  4. 混淆和未混淆代码的相互调用。这其中还包括一种隐藏的形式eval,因为eval内的代码是字符串形式所以不会被混淆。未混淆代码调用混淆代码,解决方案为混淆代码使用字符串为key的方式将接口导出。混淆代码调用未混淆代码,解决方案是使用extern参数

addTouchToNode的实现

直接上代码

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function addTouchToNode(node, touchEndCall, target, params) { //给node添加触摸事件
var bTouchCanceled = false;
var s = node.getContentSize();
var rect = cc.rect(0, 0, s.width, s.height);
var listener = node._tyTouchListener = cc.EventListener.create({
event: cc.EventListener.TOUCH_ONE_BY_ONE,
swallowTouches: true,
onTouchBegan: function(touch, event) {
var locationInNode = node.convertToNodeSpace(touch.getLocation());
if (cc.rectContainsPoint(rect, locationInNode)) {
bTouchCanceled = false;
return true;
}
return false;
},
onTouchEnded: function(touch, event) {
if (bTouchCanceled) {
return;
}
var locationInNode = node.convertToNodeSpace(touch.getLocation());
if (cc.rectContainsPoint(rect, locationInNode)) {
if (touchEndCall) {
touchEndCall.call(target, locationInNode, params);
}
}
},
onTouchCancelled: function() {
bTouchCanceled = true;
},

onTouchMoved: function(touch, event) {
var locationInNode = node.convertToNodeSpace(touch.getLocation());
if (cc.rectContainsPoint(rect, locationInNode) == false) {
bTouchCanceled = true;
}
}
});
cc.eventManager.addListener(listener, node);
node.onEnter = function(){
node.__proto__.onEnter.call(this);
var listener = this._tyTouchListener;
if(listener._isRegistered() === false) {
cc.eventManager.addListener(listener, this);
}
};
};

这个方法经过了几次陆续的改进

1 .刚开始只实现了onTouchBegan和onTouchEnded,而且在onTouchEnded的时候,没有判断坐标是否在区域内,产生的bug是在区域内点击后,移动到区域外松手,也产生了点击。于是增加了onTouchEnded内的区域判断
2. 增加了onTouchCancelled和onTouchMoved接口,当触摸移动出区域时,取消掉触摸事件,如果模拟按钮,此时应该将图片缩小回初始大小
3. 重写onEnter方法,在onEnter内重新添加事件。为了解决当它自身或者父容器被移除后,重新添加时触摸事件失效的问题

经过这些完善后,基本上可以代替cc.MenuItemSprite来使用了,再稍微加上放大缩小的效果,就能当cc.ControlButton来使用。

重用ccnode需要注意的触摸事件处理

在使用cocos开发过程中,对node的重用非常常见,例如TableViewCell就是重用的,这些被重用的node会频繁的被添加到父节点以及从父节点中移除,当被移除时会调用

1
removeFromParent(cleanup)

这个cleanup参数很关键,如果为true,会导致其cleanup方法被调用。如果不带此参数,则默认为true。

1
2
3
4
5
6
cleanup: function () {
this.stopAllActions();
this.unscheduleAllCallbacks();
cc.eventManager.removeListeners(this);
this._arrayMakeObjectsPerformSelector(this._children, cc.Node._stateCallbackType.cleanup);
}

可以看到,所有被加到此节点上的eventListener都被清除掉了,同时还递归调用了所有子节点的cleanup方法,也就意味着一个node执行了cleanup的话,其自身以及所有子孙结点注册的eventListener都会被移除。在重用这个node时,很可能就会发现给它添加的eventListener不生效了。

但cocos对一些node做了相应的功能完善,专门针对的是触摸事件。在其添加触摸事件时,用成员变量_touchListener保存起来,在onEnter时重新添加。这也是我们在碰到此类问题时的解决方案。cocos只对三个类做了这种实现,他们是cc.Control, cc.Menu, ccui.widget。理所当然的,继承自这三个类的也都有此功能,例如cc.ControlButton被重用时,它的触摸事件不会消失。

有人会说,tableViewCell一直在被重用,也不继承自上面三个类,为什么对它的触摸回调tableCellTouched一直能生效呢?这是因为tableCellTouched并不是在tableViewCell上添加的触摸事件,而是在tableView上添加的,如果你尝试重用tableView或者其父容器,就会发现触摸失效了,我的解决方案是重用时调用一次tableview的setTouchEnable接口。

总之在实现重用node时,要注意添加给它和它的子孙节点的事件,需要在onEnter时重新添加,例如某个节点的一个子孙节点是一个cc.TableView,那么在重用这个节点时,这个tableview的触摸肯定失效了,解决办法要么修改源代码在tableView的onEnter方法里重新添加,或者在重用时手动调用tableview的setTouchEnabled方法(这个方法继承自scrollview)

js中的string和unicode

javaScript中的String是UTF-16字符集合,但是要注意,因为js中并没有一种类型叫“字符”,所以charAt() 方法返回的是一个字符串。而charCodeAt()方法,则返回的是0-65535之间的一个整数。fromCharCode()方法是把一个unicode编码转换成String对象,这里是例如

1
2
3
4
'大'.charCodeAt(0)  ->  22823
"🐄".charCodeAt(1) -> 56324
String.fromCharCode(22823) -> 大
String.fromCharCode(667736) -> じ

因为string是utf-16字符的合集,所以也可以直接用UTF-16编码来组成字符串,例如

1
var a = '\u0061\u5927' -> a大

如果这个utf-16字符是4个字节的,则它在length中反应的长度为2,例如

1
2
3
4
var a = "🐄";
console.log(a.length) ->2
a.charCodeAt(0) -> 55357
a.charCodeAt(1) -> 56324

这里顺便提一下utf-16的编码方式,如果是两个字节,则直接用两个字节表示,这其中0xD800到0xDFFF的字段,是被永久保留不被映射字符的,被用来做标记。超过两个字节,则把codePoint减去0x10000,得到一个长度为20bit的值,这个值的高10位被加上0xD800后,范围为0xD800到0xDBFF。后10位加上0xDC00后,范围为0xDC00到0xDFFF。

NDK开发之Android.mk文件编写

现在我们把android稍微写复杂些。在项目根目录下创建一个lib1文件夹

结构如图:

img

test10.h和test11.h很简单,就是声明了两个方法

1
2
int test10();
int test11();

我们先看这个lib1文件夹内的Android.mk文件

1
2
3
4
5
6
7
8
9
10
11
12
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := test1
LOCAL_MODULE_FILENAME := libtest1

LOCAL_SRC_FILES := test1.cpp
LOCAL_C_INCLUDES := $(LOCAL_PATH)
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)

include $(BUILD_STATIC_LIBRARY)

这里LOCAL_MODULE是外面引用这个库时候需要用到的名字,LOCAL_MODULE_FILENAME不写也可以,默认就是在LOCAL_MODULE前面加上lib

LOCAL_SRC_FILES和LOCAL_C_INCLUDES分别表示需要编译的源文件,以及头文件路径,像这里只使用了当前目录为头文件查找目录,所以test1.cpp里写法就是

1
2
3
4
5
6
7
8
9
10
#include <test10.h>
#include <test1/test11.h>

int test10(){
return 1;
}

int test11(){
return 2;
}

LOCAL_EXPORT_C_INCLUDES是对外提供的头文件搜索路径,他决定了外面在引用这个头文件时的搜索相对路径。它跟LOCAL_C_INCLUDES完全可以不一样。

知道LOCAL_C_INCLUDES和LOCAL_EXPORT_C_INCLUDES的作用,编译时出现No such file or directory错误就完全不用害怕了,哪个文件找不到,去看一下这两个路径是否对就可以了。

再来看看jni目录下Android.mk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := learnNDK

LOCAL_SRC_FILES := learnNDK.cpp
LOCAL_C_INCLUDES := $(LOCAL_PATH)

LOCAL_STATIC_LIBRARIES := test1


include $(BUILD_SHARED_LIBRARY)

$(call import-module, lib1)

比原来就多了两行,LOCAL_STATIC_LIBRARIES := test1 表示引用名字为test1的库,这个名字就是上个文件里的LOCAL_MODULE,$(call import-module, lib1)这里指定了搜索引用库的Android.mk的路径,它是相对于NDK_MODULE_PATH,我们可以在ndk-build命令中指定这个参数,多个路径间使用:分隔

之后我们增加一个库lib2,它依赖于lib1,
它的cpp文件为

1
2
3
4
5
6
#include <test20.h>
#include <test11.h>

int test20(){
return test11() + 2;
}

它的Android.mk如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := test2

LOCAL_SRC_FILES := test2.cpp
LOCAL_C_INCLUDES := $(LOCAL_PATH)
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)

LOCAL_STATIC_LIBRARIES := test1

include $(BUILD_STATIC_LIBRARY)


$(call import-module, lib1)

加上对lib1的引用即可。

jni目录下的Android.mk加上

1
$(call import-module, lib2)

同时修改

1
LOCAL_STATIC_LIBRARIES := test2 test1

这里记住依赖顺序为从左到右,被依赖的基础库必须往后放

最后是执行的ndk-build命令,正确指定NDK_MODULE_PATH即可

1
ndk-build NDK_MODULE_PATH=/Users/imac-0003/Documents/workspace/learnNDK

NDK开发之java调用C++

首先创建一个空的android项目,默认生成的MainActivity,修改如下

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
public class MainActivity extends Activity {

static {
System.loadLibrary("learnNDK");
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

TextView t = (TextView)this.findViewById(R.id.textview);
t.setText("" + hello(1));
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}

public static native int hello(int num);

}

这里通过System.loadLibrary(“learnNDK”);来加载咱们等下要生成的动态库leanNDK

通过public static native int hello(int num);来声明一个C++层可以供我们调用的方法,返回值为int,函数名hello,接受一个形参int num

在项目根目录创建一个jni文件夹,执行命令

1
javah -d jni -classpath bin/classes/ com.example.learnndk.MainActivity

关于javah的详细参数介绍请参考javah

这里简单介绍一下,-d 表示输出的头文件目录,-classpath表示class文件的路径

然后就可以在jni目录下看到生成的文件com_example_learnndk_MainActivity.h

我们新建一个cpp文件来实现这个hello方法,新建一个learnNDK.cpp内容如下

1
2
3
4
5
6
#include <com_example_learnndk_MainActivity.h>

JNIEXPORT jint JNICALL Java_com_example_learnndk_MainActivity_hello
(JNIEnv *, jclass, jint) {
return 99;
}

然后在jni目录下,我们新建个Android.mk文件,内容如下

1
2
3
4
5
6
7
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE := learnNDK
LOCAL_SRC_FILES := learnNDK.cpp

include $(BUILD_SHARED_LIBRARY)

然后使用ndk编译

1
2
ndk-build clean
ndk-build

成功之后,运行这个android程序,就可以看到结果了。

实际中Android.mk可以编写很复杂,也可能需要在jni目录下通过Application.mk来设定一些参数,如果不正确设定可能导致编译出错。

使用closureCompiler深度混淆的一个坑

混淆命令为:

1
java -jar compiler.jar --compilation_level ADVANCED_OPTIMIZATIONS --js a.js --js_output_file b.js

混淆前代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function test(result){
console.log(result.fuck.text);
console.log(result.text.fuck);
console.log(result.color.colour);
var tmp = {
test1:Math.random(),
"test2":Math.random()
}
for(var i = 0; i < result.length; i++) {
console.log(result[i].text);
console.log(result[i].text.text1.text2.text3);
tmp.test1 += 1;
tmp.test2 += 2;
console.log(tmp.test1);
console.log(tmp.test1.test11.test111);
console.log(tmp.test2);
}
result.tmp = tmp;
}

混淆后代码

1
2
3
4
5
6
7
8
9
10
function d(b) {
console.log(b.b.text);
console.log(b.text.b);
console.log(b.color.d);
for (var a = {
a: Math.random(),
test2: Math.random()
}, c = 0; c < b.length; c++) console.log(b[c].text), console.log(b[c].text.c.j.k), a.a += 1, a.b += 2, console.log(a.a), console.log(a.a.h.i), console.log(a.b);
b.l = a
}

目前可以看到 text和color,在深度混淆中不会被混淆。是否还有别的一些关键字不被混淆,还不确定。比如colour就会被混淆= =