svn忽略文件

在svn项目中,对于不需要加入版本管理的文件,可以使用svn propedit svn:ignore来进行设置

在执行这句之前,需要先设置好SVN_EDITOR环境变量,例如在~/.bash_profile内添加

1
export SVN_EDITOR=emacs

然后执行

1
svn propedit svn:ignore .

这里的.表示对当前目录进行设置,此时会打开你指定的编辑器界面,在里面写上你需要忽略的文件名或者正则表达式即可.
但这句命令,只会影响你指定的路径的子文件及文件夹,而不会递归影响到更深层次。例如在编辑界面填上

1
2
dir1
dir2/dir21/dir211

这里dir1会被忽略,而dir211不会,如果需要忽略dir211,要么在当前目录下执行svn propedit svn:ignore dir2/dir21 要么切换到dir2/dir21目录下执行svn propedit svn:ignore . 然后填写的内容都是dir211

在网上看到一个解释

1
2
3
4
5
6
7
8
9
Each and every directory in Subversion can be thought of its own
module, so there's no real way for Subversion to know that
foo/bar/barfoo is a directory in module /foo/bar, or a another
separate module module.

That means there's no way for Subversion to know how to handle
properties that can affect an entire directory tree. Plus, it would be
difficult to know exactly what parent directory is affecting a child
directory.

最后提醒一下,只有还没有被加入版本管理的文件会被忽略,如果文件已经加入了版本管理,那设置了忽略也没用。其次,设置了忽略的文件,仍然可以被添加进版本管理中。

cocos屏幕适配策略

cocos的屏幕适配包括两个方面,一个涉及到屏幕的宽高比,一个涉及到缩放因子。

宽高比

我打算尽量用通俗的语言来说这件事情,cocos处理不同宽高比的适配,使用了设计分辨率这么个东西,就是

1
void GLView::setDesignResolutionSize(float width, float height, ResolutionPolicy resolutionPolicy)

resolutionPolicy有好几种选择,但我们通常只使用FIXED_HEIGHT和FIXED_WIDTH。

设计分辨率的意义就是:你不用管实际上屏幕是什么样,你通过代码或者工具给一个node设定尺寸和坐标时,都只想象它是放在设计分辨率的屏幕里,然后给他设计尺寸和坐标,最后显示在实际屏幕上时,引擎会进行适当的比例缩放。

我们一般在项目中使用的ResolutionPolicy是FIXED_HEIGHT和FIXED_WIDTH,前者一般用于手机横屏,后者用于手机竖屏。我们拿FIXED_WIDTH举例,使用这个模式,意味着采用宽度的缩放比例,那么UI节点的x坐标和width就不用担心了,系统会帮你完美适配好来,y坐标和height则必须使用屏幕高度的百分比来进行设置,只要在需要适配的宽高比最大(通俗说就是最“扁”)的屏幕上不会发生冲突即可。FIXED_HEIGHT也是类似的道理,因为使用了高度的缩放比例,那么x坐标和width必须使用屏幕宽度的百分比来进行设置,保证在需要适配的宽高比最小的屏幕上不发生冲突即可。当然实际中我们对不管xy坐标还是width,height都习惯性采用百分比坐标。这两种方案的适用情况不能靠死记硬背,只要明白了原理,其实很容易理解。

缩放因子

缩放因子可以理解为图片资源尺寸与设计分辨率的比值。一般我们的资源都是配合设计分辨率而出的,但如果不是,就需要设置缩放因子了,比如一种情况就是,同样是1024*768的设计分辨率,我们有一套1024*768的资源,这套资源在2048*1536的屏幕上会被拉伸两倍显示,可能效果就不好了,那我们再准备一套2048*1536的资源,因为设计分辨率还是1024*768,所以就需要将缩放因子设为2了。

设置缩放因子的接口是:

1
void Director::setContentScaleFactor(float scaleFactor)

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