弱者才会相信运气,强者只相信因果

0%

学习使用 Jsb 时产生的笔记。

调试

1
2
3
4
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
// Enable debugger here
jsb_enable_debugger("0.0.0.0", 5086);
#endif

se::Object::root/unroot 与 se::Object::incRef/decRef 的区别?

root/unroot 用于控制 JS 对象是否受 GC 控制,root 表示不受 GC 控制,unroot 则相反,表示交由 GC 控制,对一个 se::Object 来说,root 和 unroot 可以被调用多次,se::Object 内部有_rootCount 变量用于表示 root 的次数。当 unroot 被调用,且_rootCount 为 0 时,se::Object 关联的 JS 对象将交由 GC 管理。还有一种情况,即如果 se::Object 的析构被触发了,如果_rootCount > 0,则强制把 JS 对象交由 GC 控制。

incRef/decRef 用于控制 se::Object 这个 cpp 对象的生命周期,前面章节已经提及,建议用户使用 se::HandleObject 来控制手动创建非绑定对象的方式控制 se::Object 的生命周期。因此,一般情况下,开发者不需要接触到 incRef/decRef。

对象生命周期的关联与解除关联

1
se::Object::attachObject/dettachObject

objA->attachObject(objB);类似于 JS 中执行 objA.nativeRefs[index] = objB,只有当 objA 被 GC 后,objB 才有可能被 GC objA->dettachObject(objB);类似于 JS 中执行 delete objA.nativeRefs[index];,这样 objB 的生命周期就不受 objA 控制了

三、SdkManager

上面提到的两座桥是真桥,这第三座则是我自己膨胀出的结果,让一个底层之上的东西比肩 JNI 和 JSB。为什么那么嚣张,因为这个东西直接 PK 掉了 JSB。

先让我们看看它的设计架构:

SdkManager 身处 C++层,向下通过 JNI 与 Java 层交互接受生命周期调用,向上通过 JSB 提供 Javascript 接口,同层管理各个 Sdk。

  • 它是一个 Lifecycle 分发器:当收到生命周期函数调用时,它会将其分发给其下管理的所有 Sdk。所以各个 Sdk 不需要前往各地注册生命周期调用函数,只需要继承并实现 Sdk 基类中对应的生命周期函数即可。
  • 它是一个 Sdk Method 路由器:通过 call 方法,SdkManager 可以找到对应的 Sdk 和 Method 调用它,并返回结果。
  • 它是一个中间件:通过 C++提供统一的接口,屏蔽了 iOS 与 Android 的接口差异。
  • 它很安全:它没有用到任何反射代码,不用为 iOS 多变的审核条款而提心吊胆。

下面是 SdkMannger 在头文件中的定义

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
47
48
49
50
51
52
53
54
55
// SdkManager.h

class SdkManager
{
public:
#ifndef SKIP_BY_AUTO_BINDINGS
static void addSdk(Sdk *sdk);
static void removeSdk(Sdk *sdk);
static Sdk *getSdk(const std::string &name);
#endif

static void call(const std::string &clazz, const std::string &method, const std::string &params, const Sdk::SdkCallback &callback);

#if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
public:
static void *appController;
static void *viewController;
static void *window;

static void applicationDidFinishLaunching(void *iosUIApplication, void *iosNSDictionary);
static void applicationWillResignActive(void *iosUIApplication);
static void applicationDidBecomeActive(void *iosUIApplication);
static void applicationDidEnterBackground(void *iosUIApplication);
static void applicationWillEnterForeground(void *iosUIApplication);
static void applicationWillTerminate(void *iosUIApplication);
static bool applicationOpenURL(void *iosUIApplication, void *iosNSURL, void *iosNSDictionary);
static void applicationdidRegisterForRemoteNotificationsWithDeviceToken(void *iosUIApplication, void *iosNSData);
static void applicationDidReceiveRemoteNotification(void *iosUIApplication, void *iosNSDictionary);
static void applicationDidReceiveLocalNotification(void *iosUIApplication, void *iosUILocalNotification);
static void applicationDidRegisterUserNotificationSettings(void *iosUIApplication, void *iosUIUserNotificationSettings);
static void applicationHandleActionWithIdentifierForRemoteNotification(void *iosUIApplication, void *iosNSString, void *iosNSDictionary, void *completionHandler);
static void applicationDidFailToRegisterForRemoteNotificationsWithError(void *iosUIApplication, void *iosNSError);
static int applicationSupportedInterfaceOrientationsForWindow(void *iosUIApplication, void *iosUIWindow);
#endif

#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID) && !defined(SKIP_BY_AUTO_BINDINGS)
public:
static void *appActivity;
static void *glSurfaceView;

static void activityOnCreate();
static void activityOnPause();
static void activityOnResume();
static void activityOnDestroy();
static void activityOnStart();
static void activityOnRestart();
static void activityOnStop();
static void activityOnNewIntent(void *intent);
static void activityOnActivityResult(int request, int result, void *intent);
static void activityOnBackPressed();
static void activityOnSaveInstanceState(void *bundle);
static void activityOnRestoreInstanceState(void *bundle);
static void activityOnConfigurationChanged(void *configuration);
#endif
};

可以看到,大部分的行数都被生命周期调用占据了,其他的包含 Sdk 的add, removeget方法,最后是一个call方法 。

1
2
3
4
5
6
7
8
9
typedef std::function<void(const std::string &result)> SdkCallback;

static void call(const std::string &clazz, const std::string &method, const std::string &params, const Sdk::SdkCallback &callback)
{
Sdk *sdk = getSdk(clazz);
if(sdk) {
sdk->call(method, params, callback);
}
}

call方法的参数列表分别表示类名方法名参数、和返回值

  • 类名:用于确定业务层需要使用哪一个 Sdk
  • 方法名:用于确定业务层需要使用哪一个方法
  • 参数:作为输入传参,它是字符串类型的,你可以用任何方式将你的参数转化为一串字符传递给 Sdk,然后在 Sdk 中将其再拆解开来。推荐的格式是 Json,当然你也可以使用自己的方式进行组合和拆解,SdkManager 对此并不关心。
  • 返回值:返回值使用 callback 的形式,并且 callback 的参数也是字符串类型(同参数使用方法)。之所以使用 callback 的方式,是因为这样可以同时解决同步与异步的问题。举个例子:登录时你可以在一系列用户操作之后拿到 token,再使用 callback 返回这个 token,而不用另外想办法将其传回 Js 层。

Sdk 是所有业务 Sdk 的基类,它提供的生命周期函数都等待着业务 Sdk 的实现,call方法少了类名,同样等待业务 Sdk 的实现。

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
47
48
49
class Sdk
{
friend SdkManager;

public:
CC_SYNTHESIZE(std::string, _name, Name);

Sdk(const std::string &name);
virtual ~Sdk();

typedef std::function<void(const std::string &result)> SdkCallback;

virtual void call(const std::string &method, const std::string &params, const SdkCallback &callback) {}

#if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
public:
virtual void applicationDidFinishLaunching(void *iosUIApplication, void *iosNSDictionary) {}
virtual void applicationWillResignActive(void *iosUIApplication) {}
virtual void applicationDidBecomeActive(void *iosUIApplication) {}
virtual void applicationDidEnterBackground(void *iosUIApplication) {}
virtual void applicationWillEnterForeground(void *iosUIApplication) {}
virtual void applicationWillTerminate(void *iosUIApplication) {}
virtual bool applicationOpenURL(void *iosUIApplication, void *iosNSURL, void *iosNSDictionary) { return false; }
virtual void applicationdidRegisterForRemoteNotificationsWithDeviceToken(void *iosUIApplication, void *iosNSData) {}
virtual void applicationDidReceiveRemoteNotification(void *iosUIApplication, void *iosNSDictionary) {}
virtual void applicationDidReceiveLocalNotification(void *iosUIApplication, void *iosUILocalNotification) {}
virtual void applicationDidRegisterUserNotificationSettings(void *iosUIApplication, void *iosUIUserNotificationSettings) {}
virtual void applicationHandleActionWithIdentifierForRemoteNotification(void *iosUIApplication, void *iosNSString, void *iosNSDictionary, void *completionHandler) {}
virtual void applicationDidFailToRegisterForRemoteNotificationsWithError(void *iosUIApplication, void *iosNSError) {}
virtual int applicationSupportedInterfaceOrientationsForWindow(void *iosUIApplication, void *iosUIWindow) { return /*UIInterfaceOrientationMaskPortrait*/0x02; }
#endif

#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
public:
virtual void activityOnCreate() {}
virtual void activityOnPause() {}
virtual void activityOnResume() {}
virtual void activityOnDestroy() {}
virtual void activityOnStart() {}
virtual void activityOnRestart() {}
virtual void activityOnStop() {}
virtual void activityOnNewIntent(void *intent) {}
virtual void activityOnActivityResult(int request, int result, void *intent) {}
virtual void activityOnBackPressed() {}
virtual void activityOnSaveInstanceState(void *bundle) {}
virtual void activityOnRestoreInstanceState(void *bundle) {}
virtual void activityOnConfigurationChanged(void *configuration) {}
#endif
};

这是微信 Sdk 重写call的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void WechatSdk::call(const std::string &method, const std::string &params, const SdkCallback &callback)
{
if(method == "login") {
login(callback);
}
else if(method == "loginWithQrcode") {
loginWithQrcode(callback);
}
else if(method == "isWXAppInstalled") {
bool ret = isWXAppInstalled();
callback(ret ? "true" : "false");
}
else if(method == "getWXAppInstallUrl") {
std::string url = getWXAppInstallUrl();
callback(url);
}
}

为了能在 Javascript 层有一个 漂亮 的 Sdk 接口,或许你还应该写一个 Js 的封装,把 SdkManager 的调用封装一下。

JSB(Javascript Binding)

JSB 是第二座桥,它与 JNI 类似,是提供 Native 层与 Javascript 层互相访问的一种方式,支持双向调用。

上面一章节提到了普通的 Android 程序架构,下面的是 Cocos 程序的架构,它是这样的:

在 Java 之上又通过 JNI 回到了 Cocos 的 Native 层,此层是通过动态链接库.so 加载的。然后再经过一个 JSB 的桥与 Javascript 进行交互。游戏则是使用 Javascript 编写的。

JSB 有好几种,有 Mozilla 家的 SpiderMonkey 和 Google 家的 V8,最新的 iOS 里则用到了 Apple 自家的 JSB 接口。使用哪种 JSB 接口主要取决于使用哪种 Javascript 虚拟机。

排除掉各种 JS 虚拟机的不同,Cocos 为我们提供了统一的接口,让我们可以使用。它能实现 Javascript 中的一个方法与一个 C 方法进行绑定。在 Javascript 中调用该方法时,则在 C++中被绑定的方法会被调用。

1、Javascript 调用 C++

如果我们在 C++中看见这样的函数名 js**_(se::State& s)那么它就是为 JSB 而写的了。它不像 JNI 一样有严格的命名规范,可以时任意函数名,但为了可以方便快捷的定位方法,所以人为的定了这么一个命名规范。

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
static bool js_cocos2dx_FileUtils_addSearchPath(se::State& s)
{
cocos2d::FileUtils* cobj = (cocos2d::FileUtils*)s.nativeThisObject();
SE_PRECONDITION2(cobj, false, "js_cocos2dx_FileUtils_addSearchPath : Invalid Native Object");
const auto& args = s.args();
size_t argc = args.size();
CC_UNUSED bool ok = true;
if (argc == 1) {
std::string arg0;
ok &= seval_to_std_string(args[0], &arg0);
SE_PRECONDITION2(ok, false, "js_cocos2dx_FileUtils_addSearchPath : Error processing arguments");
cobj->addSearchPath(arg0);
return true;
}
if (argc == 2) {
std::string arg0;
bool arg1;
ok &= seval_to_std_string(args[0], &arg0);
ok &= seval_to_boolean(args[1], &arg1);
SE_PRECONDITION2(ok, false, "js_cocos2dx_FileUtils_addSearchPath : Error processing arguments");
cobj->addSearchPath(arg0, arg1);
return true;
}
SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", (int)argc, 2);
return false;
}
SE_BIND_FUNC(js_cocos2dx_FileUtils_addSearchPath)

这时一段典型的 JSB 代码,通过函数名我们就知道了这个 JSB 绑定的是 FileUtils 的 addSearchPath 方法。

addSearchPath 是个重载函数,一个有 1 参数,一个有 2 参数。我们可以看到 JSB 中是以 逻辑 来区分函数重载的。

乍一看,JSB 也辣么变态,1 行代码变成 27 行。如果我只有一个函数绑定 JSB 还好,如果我有成百上千和函数要绑定,岂不是这辈子都要搭里面了?Noooooo…

好在 Cocos 又提供的方案,就是自动生成 JSB 代码。只需要简(bìng)单(bù)配置一个配置文件,然后让生成脚本跑一下,那成百上千的 JSB 绑定函数就变出来了。

在我们的项目中,我将这个工具放在了 tools/bindings-generator 下。配置文件是 test/test.ini,生成脚本是 test/test.py。

不细讲了,自己看吧。

2、C++ 调用 Javascript

不推荐使用 C++调用 Javascript,因为需要明确地写明方法名来找到该 JS 方法并调用。而 C++是编译语言不支持热更,Javascript 则是脚本语言可以作为资源进行热更新。这很容易造成 Js 中的方法名修改后导致调用失效。

解决方案就是将 Js 方法作为参数传入 C++中,在 C++中调用该注册方法。上面提到的 bindings-generator 支持生成将函数作为参数的代码,好好利用吧。

Cocos Creator ts 项目构建后在 Andorid 上的开发,我们会用到三种语言:Java、C++和 Javascript。往往一个简单的功能就需要同时用到这几种语言,例如微信的登录,需要在 Javascript 响应按钮的点击操作,在 Java 中调用 SDK 的 login 接口,最后再将 SDK 的结果返回给 Javascript 层用于进行后续逻辑。

在 Java、C++、Javascript 各自的语言上进行开发我们这里不做详解。基本上有编程经验的人都能很快上手。

一、JNI(Java native interface)

JNI 是第一座桥,用于在 Java 和 C++间进行交互。

Java jni 本意是 Java native interface(Java 本地接口),是为了方便 Java 调用 c、c++等本地代码所封装的一层接口。

Java 的跨平台特性导致其本地交互的能力不够强大,一些和操作系统相关的特性 Java 无法完成,于是 Java 提供了 JNI 专门用于和本地代码交互,这样就增强了 Java 语言的本地交互能力。

通过 Java jni,用户可以调用用 c、c++所编写的 native code。Native code 也可以调用 Java code。

一个普通的 Android 程序是这样的架构

通过 JNI 在 Native(C++)和 Java 间进行交互,Android App 使用 Java 进行开发。

1、Java 调用 C++

当 Java 要使用 C++的方法时,方法的实现是在 C++中编写的,首先需要在 Java 中使用 native 关键字对该方法进行声明。

1
2
3
4
5
6
// Baz.java
package com.foo.bar
public class Baz {
public static void native qux();
public static int native quux(int param1, boolean param2, String param3);
}

然后在 C++中仅仅需要按照规定的函数命名方式进行函数命名即可。

注意: C++编译器可能会对普通的c函数进行函数名变形,所以别忘了加上extern "C"。
1
2
3
4
5
6
7
8
9
10
11
// BazJni.cpp
#include <jni.h>
extern "C" {
JNIEXPORT void JNICALL Java_com_foo_bar_Baz_qux(JNIEnv *env, jobject thiz) {
// balabala
}
JNIEXPORT jint JNICALL Java_com_foo_bar_Baz_quux(JNIEnv *env, jobject thiz, jint param1, jboolean param2, jobject param3) {
jobject ref = env->NewLocalRef(param3);
// balabala
}
}

这样,JNI 会自动将两者关联并进行调用,无需我们做更多处理。

需要注意的是,虽然 C++和 Java 之间可以交互,对于传入 C++的 Java 对象进行简单赋值是不会在 Java 内对其产生引用效果的,所以需要显式地创建 Java 引用来防止其在 Java 内被回收掉。

1
2
jobject ref = env->NewGlobalRef(param) // 创建全局引用
jobject ref = env->NewLocalRef(param) // 创建局部引用

2、C++调用 Java

C++调用 Java 相对麻烦点,需要采用一系列 C++函数来 找到 并 执行 它。可想而知,一个简简单单的 Java 语句到了 C++层来调用,要花掉好几行。

例如在 Java 中有这么一个方法需要在 C++中被调用

1
2
3
4
5
// Baz.java
package com.foo.bar
public class Baz {
public static int qux(int param1, boolean param2, String param3);
}

C++中是这么写的

1
2
3
4
5
6
7
8
// BazJni.cpp
#include <jni.h>
// Baz.qux(123, true, "HelloWorld");
env = getEnv();
jclass classID = _getClassID("com/foo/bar/Baz");
jmethodID methodID = env->GetStaticMethodID(classID, "qux", "(IZLjava/lang/String;)I");
jobject jtext = env->NewStringUTF("HelloWorld");
int ret = env->CallStaticIntMethod(classID, methodID, 123, true, jtext);

这里 getEnv 和_getClassID 里面是如何实现我就不展开了,那又是一大段故事了。

简单的一行 Baz.qux(123, true, “HelloWorld”);竟然花了这么多行!!!而且其中还混入了奇怪的东西,”(IZLjava/lang/String;)I”又是什么鬼啊???

这又要引入新的概念了,就是数据类型签名。我们知道像 Java 这种高级语言是有 重载 功能的,即允许在同一范围中声明几个功能类似的同名函数,但是这些同名函数的形式参数必须不同。那么问题就来了,我想调用 qux 这个方法,但它如果有好几个重载,JNI 如何知道该调用哪一个呢?程序又不会意念感应,当然不知道你想使用哪一个。

1
2
3
4
int qux();
int qux(int param1);
int qux(int param1, boolean param2);
int qux(int param1, boolean param2, String param3);

数据类型签名就是解决这个问题的,它需要程序员明确地注册参数列表样式,让 JNI 知道被调用的是哪一个方法。

数据签名如下表:

本地类型 JNI 类型 Java 类型 类型签名(signature) 描述
void void V - -
bool jboolean boolean Z 无符号 8 位 -
signed char jbyte byte B
short jshort short C 无符号 16 位
int jint int I 有符号 32 位
long long jlong long J
float jfloat float F 32 位浮点
double jdouble double D 64 位浮点
[] [ 数组 ] 数组

使用方式是直接拼接成一个字符串

如上面的(int, boolean, String)就是变成了”(IZLjava/lang/String;)”,其中 String 的签名是 Ljava/lang/String;,因为它的 package 是 package java.lang。同样,返回值也是需要签名的。放在括号的后面,例如”(IZLjava/lang/String;)I”表示返回值是 int 类型。

讲了辣么多,是不是觉得 C++ 调用 Java 简直就是 地狱模式。

所幸 Cocos 的 JniHelper 类为我们提供了简单的方法,也得益于 C++的模版功能,我们有了这么一系列简单的接口。

1
2
3
4
5
6
template <typename... Ts>
void callStaticVoidMethod(const std::string& className, const std::string& methodName, Ts... xs);
bool callStaticBooleanMethod(const std::string& className, const std::string& methodName, Ts... xs);
int callStaticIntMethod(const std::string& className, const std::string& methodName, Ts... xs);
float callStaticFloatMethod(const std::string& className, const std::string& methodName, Ts... xs);
... etc.

只需要在后面直接列出参数即可,如此神奇。如下:

1
int ret = JniHelper::callStaticIntMethod("com/foo/bar/Baz", "qux", 123, true, "HelloWorld");

具体实现,可以移步”JniHelper.h”中了解。

Javascript Binding

Cocos Creator 使用的是 Cocos2d-X 引擎的 js 绑定,开发语言也是 js 了。这里顺带提一下,关于用 Lua 还是 js 绑定的问题。主要的方式如下:引擎开启一个 脚本运行( Lua 是 Lua State,JavaScript 用的是 V8 等等),然后把 C++ 写的代码,注入到这个引擎内。这样,引擎内就可以以调用注入函数的形式,调用底层代码。

而对于我们的用户而言,所看到的据,我们所编写的 Js/Lua 脚本,居然能够产生就跟原生代码一样的效果。

安卓的启动

jsb-default会把CocosCreator中的Cocos2dx引擎C++代码复制在build中,方便做自定义的引擎修改。

jsb-link不会复制C++代码出来,所以使用默认引擎

我们用 Cocos Creator 打包好的安卓项目内,与通常的安卓项目没有什么太大的差异,不过是利用了一些 Jni 技术来加载底层代码。但我们现在只关注启动的流程。

启动的 Activity 是一个叫做 AppActivity 的东西,在其 onCreate() 函数内进行了 SDK 的初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Workaround in https://stackoverflow.com/questions/16283079/re-launch-of-activity-on-home-button-but-only-the-first-time/16447508
if (!isTaskRoot()) {
// Android launched another instance of the root activity into an existing task
// so just quietly finish and go away, dropping the user back into the activity
// at the top of the stack (ie: the last state of this task)
// Don't need to finish it again since it's finished in super.onCreate .
return;
}
// DO OTHER INITIALIZATION BELOW

SDKWrapper.getInstance().init(this);

// 我们的项目中使用 SDKManager

}

最终都会执行到引擎的 C++ 类: AppDelegate.cpp。

在其中的一个方法内就能看到,加载初始化的代码:

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
47
48
49
50
51
bool AppDelegate::applicationDidFinishLaunching()
{
#if CC_TARGET_PLATFORM == CC_PLATFORM_IOS && PACKAGE_AS
SDKManager::getInstance()->loadAllPlugins();
#endif
// initialize director
auto director = Director::getInstance();
auto glview = director->getOpenGLView();
if(!glview) {
#if(CC_TARGET_PLATFORM == CC_PLATFORM_WP8) || (CC_TARGET_PLATFORM == CC_PLATFORM_WINRT)
glview = GLViewImpl::create("SCMJ");
#else
glview = GLViewImpl::createWithRect("SCMJ", cocos2d::Rect(0,0,900,640));
#endif
director->setOpenGLView(glview);
}

// set FPS. the default value is 1.0/60 if you don't call this
director->setAnimationInterval(1.0 / 60);

ScriptingCore* sc = ScriptingCore::getInstance();
ScriptEngineManager::getInstance()->setScriptEngine(sc);

se::ScriptEngine* se = se::ScriptEngine::getInstance();

jsb_set_xxtea_key("");
jsb_init_file_operation_delegate();

#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
// Enable debugger here
jsb_enable_debugger("0.0.0.0", 5086);
#endif

se->setExceptionCallback([](const char* location, const char* message, const char* stack){
// Send exception information to server like Tencent Bugly.

});

jsb_register_all_modules();

#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID || CC_TARGET_PLATFORM == CC_PLATFORM_IOS) && PACKAGE_AS
se->addRegisterCallback(register_all_anysdk_framework);
se->addRegisterCallback(register_all_anysdk_manual);
#endif

se->start();

jsb_run_script("main.js");

return true;
}

简单的解释:

初始化 openGL 视图。
初始化脚本核心引擎。
注入所有模块。
启动脚本引擎
执行脚本 main.js。
这样就将控制权转交给了脚本引擎中的 main.js。

main.js

当加载完我们项目相关的 js 后,就会把这些参数,传递给给 引擎的 game 对象 run 方法。启动游戏了

jsb_register_all_modules 将相关的底层接口注册到js引擎

在这个函数中,首先获取一个 ScriptEngine(se) 的实例,然后就会进行一系列的加载操作。这样我们就有了 JavaScript 调用 C++ (Cocos Native 以及 SDKManager) 和 Java 的能力(因register_javascript_java_bridge)了,js 绑定完成。

余额操作在大多数系统都是不可缺少和不允许出现问题的 如何修改余额 , 这个问题可能在实际项目中 没那么简单;
如何修改余额
假设一个用户数据 :
id⇒12 | user_name⇒ 放放 | fee⇒ 30 | updated_at ⇒ 2019-09-06 15:51:33
修改余额

1
2
3
4
5
6
7
8
9
10
11
12
13
//消费金额
$spend = 10;

//查询用户余额
$user = select id,fee from `users` where id = 12;

//计算金额
$newFee = $user['fee']-$spend;

//.. 这里检查余额是否足够

//更新余额
update `users` set fee = $newFee where id = 12 ;

按照正常逻辑来说上面更新余额是没问题的
但是如果发生并发 当 A 跟 B 同时发起请求时
请求 A 查询出余额为 30
请求 B 查询出余额为 30
请求 B 更新余额为 20
请求 A 更新余额为 20
最终用户余额为 20
也就是说 用户余额为 30 两个请求都消费了 10 元 即 30 - 10 -10 =10 但是当发生并发请求时 余额最终为 20 这里的金额是错误的
在请求 B 修改了 余额之后 请求 A 的查询出来的余额已经不是正确的了 导致了 余额更新错误
常见的解决办法 添加数据库的行锁
当请求 A 执行时 先加入锁 阻塞 请求 B 直到 请求 A 完成之后 请求 B 才继续执行
当然使用事务 并不是那么明智

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
//开始事务
begin;

//消费金额
$spend = 10;

//查询用户余额
$user = select id,fee from `users` where id = 12 for update;

//计算金额
$newFee = $user['fee']-$spend;

//.. 这里检查余额是否足够

//更新余额
update `users` set fee = $newFee where id = 12 ;

//确认成功之后 提交事务
commit


CAS 业务层面乐观锁
什么是 CAS
在更新的时候使用初始值(即查询出来的当前余额)作为条件 compare 限制只有当初始值没有改变时才允许更新成功 set
Compare And Set(CAS)
使用该方式 修改 更新语句

//消金额
$spend = 10;

//查询用户余额
$user = select id,fee from `users` where id = 12;
$oldFee = $user['fee'];
//计算金额
$newFee = $user['fee']-$spend;

//.. 这里检查余额是否足够

//更新余额
update `users` set fee = $newFee where id = 12 and fee = $oldFee ;

这里如果发生同时修改产生并发 将只有一边修改成功 这时候如果产生失败 可以对他进行重试等操作
为什么不使用 减等于 的 sql 语句
例如 :
update users set fee = fee - $spend where id = 12 ;
这里要再加上余额的判断避免出现 负数金额
update users set fee = fee - $spend where id = 12 and fee >= $spend ;
稍微改一下这里的更新 语句 也能完成正确的更新 就算是并发也都将正常
但是这样做将产生一个问题 不幂等
什么是不幂等 ?
在相同的条件下 , 执行同一请求,得到的结果相同才符合幂等性

也即是说
fee = fee - $spend 不幂等
fee = $newFee 幂等

不幂等的情况下 如果发生重复执行的情况将产生重复扣款
事实上实际业务如何正确的扣款 根据业务的实际情况 可能需要注意更多细节 , 越大的业务量,需要面对更多的问题处理更多的细节. 以上的方案也仅仅是最基础的处理 实际情况需要更多的耐心和思考 共勉之 ~
以上.

DOM

DOM 全称是 Document Object Model,即文档对象模型。DOM 是 W3C 的标准,定义了访问 HTML 和 XML 文档的标准。

“文档对象模型 (DOM) 是中立于平台和语言的接口,它允许程序和脚本动态地访问和更新文档的内容、结构和样式。”

DOM 事件

DOM 使 Javascript 有能力对 HTML 上的事件做出反应。这些事件包括鼠标键盘的点击事件、移动事件以及页面中内容的变化等。HTML 元素事件是浏览器内在自动产生的,当有事件发生时 html 元素会向外界(这里主要指元素事件的订阅者)发出各种事件,如 click,onmouseover,onmouseout 等等。

DOM 事件流及事件模型

DOM 的结构是一个树形,每当 HTML 元素产生事件时,该事件就会在树的根节点和元素节点之间传播,所有经过的节点都会收到该事件。

DOM 事件模型分为两类:一类是 IE 所使用的冒泡型事件(Bubbling);另一类是 DOM 标准定义的冒泡型与捕获型(Capture)的事件。除 IE 外的其他浏览器都支持标准的 DOM 事件处理模型。

  • 冒泡型事件处理模型(Bubbling)

如上图所示,冒泡型事件处理模型在事件发生时,首先在最精确的元素上触发,然后向上传播,直到根节点。反映到 DOM 树上就是事件从叶子节点传播到根节点。

  • 捕获型事件处理模型(Captrue)

相反地,捕获型在事件发生时首先在最顶级的元素上触发,传播到最低级的元素上。在 DOM 树上的表现就是由根节点传播到叶子节点。

标准的 DOM 事件处理模型

标准的事件处理模型分为三个阶段:

  • 父元素中所有的捕捉型事件(如果有)自上而下地执行
  • 目标元素的冒泡型事件(如果有)
  • 父元素中所有的冒泡型事件(如果有)自下而上地执行

注册事件监听

1、传统方式的事件模型即直接在 DOM 元素上绑定事件处理器,例如—

1
2
3
window.onload=function(){…}
obj.onmouseover=function(e){…}
obj.onclick=function(){…}

首先这种方式是无论在 IE 还是 Firefox 等其他浏览器上都可以成功运行的通用方式。这便是它最大的优势了,而且在 Event 处理函数内部的 this 变量无一例外的都只想被绑定的 DOM 元素,这使得 Js 程序员可以大大利用 this 关键字做很多事情。

至于它的缺点也很明显,就是传统方式只支持 Bubbling,而不支持 Capturing,并且一次只能绑定一个事件处理器在 DOM 元素上,无法实现多 Handler 绑定。最后就是 function 参数中的 event 参数只对非 IE 浏览器有效果(因为 IE 浏览器有特制的 window.event)。

2、W3C (Firefox.e.g) Event Module

Firefox 等浏览器很坚决的遵循 W3C 标准来制定浏览器事件模型,使用 addEventListener 和 removeEventListener 两个函数,看几个例子—

1
2
3
4
window.addEventListener(‘load’,function(){…},false);
document.body.addEventListener(‘keypress’,function{…},false);
obj.addEventListener(‘mouseover’,MV,true);
function MV(){…}

addEventListener 带有三个参数,第一个参数是事件类型,就是我们熟知的那些事件名字去掉前面的’on’,第二个参数是处理函数,可以直接给函数字面量或者函数名,第三个参数是 boolean 值,表示事件是否支持 Capturing。

W3C 的事件模型优点是 Bubbling 和 Capturing 都支持,并且可以在一个 DOM 元素上绑定多个事件处理器,各自并不会冲突。并且在处理函数内部,this 关键字仍然可以使用只想被绑定的 DOM 元素。另外 function 参数列表的第一个位置(不管是否显示调用),都永远是 event 对象的引用。

至于它的缺点,很不幸的就只有在市场份额最大的 IE 浏览器下不可使用这一点。

3、IE Event Module

IE 自己的事件模型跟 W3C 的类似,但主要是通过 attachEvent 和 detachEvent 两个函数来实现的。依旧看几个例子吧—

1
2
window.attachEvent(‘onload’,function(){…});
document.body.attachEvent(‘onkeypress’,myKeyHandler);

可以发现它跟 W3C 的区别是没有第三个参数,而且第一个表示事件类型的参数也必须把’on’给加上。这种方式的优点就是能绑定多个事件处理函数在同一个 DOM 元素上。

至于它的缺点,为什么如今在实际开发中很少见呢?首先 IE 浏览器本身只支持 Bubbling 不支持 Capturing;而且在事件处理的 function 内部 this 关键字也无法使用,因为 this 永远都只想 window object 这个全局对象。要想得到 event 对象必须通过 window.event 方式,最后一点,在别的浏览器中,它显然是无法工作的。

摘要

版本格式:主版本号.次版本号.修订号,版本号递增规则如下:

  1. 主版本号:当你做了不兼容的 API 修改,

  2. 次版本号:当你做了向下兼容的功能性新增,

  3. 修订号:当你做了向下兼容的问题修正。

先行版本号及版本编译元数据可以加到“主版本号.次版本号.修订号”的后面,作为延伸。

简介

在软件管理的领域里存在着被称作“依赖地狱”的死亡之谷,系统规模越大,加入的包越多,你就越有可能在未来的某一天发现自己已深陷绝望之中。

在依赖高的系统中发布新版本包可能很快会成为噩梦。如果依赖关系过高,可能面临版本控制被锁死的风险(必须对每一个依赖包改版才能完成某次升级)。而如果依赖关系过于松散,又将无法避免版本的混乱(假设兼容于未来的多个版本已超出了合理数量)。当你专案的进展因为版本依赖被锁死或版本混乱变得不够简便和可靠,就意味着你正处于依赖地狱之中。

作为这个问题的解决方案之一,我提议用一组简单的规则及条件来约束版本号的配置和增长。这些规则是根据(但不局限于)已经被各种封闭、开放源码软件所广泛使用的惯例所设计。为了让这套理论运作,你必须先有定义好的公共 API 。这可以透过文件定义或代码强制要求来实现。无论如何,这套 API 的清楚明了是十分重要的。一旦你定义了公共 API,你就可以透过修改相应的版本号来向大家说明你的修改。考虑使用这样的版本号格式:X.Y.Z (主版本号.次版本号.修订号)修复问题但不影响 API 时,递增修订号;API 保持向下兼容的新增及修改时,递增次版本号;进行不向下兼容的修改时,递增主版本号。

我称这套系统为“语义化的版本控制”,在这套约定下,版本号及其更新方式包含了相邻版本间的底层代码和修改内容的信息。

语义化版本控制规范(SemVer)
以下关键词 MUST、MUST NOT、REQUIRED、SHALL、SHALL NOT、SHOULD、SHOULD NOT、 RECOMMENDED、MAY、OPTIONAL 依照 RFC 2119 的叙述解读。(译注:为了保持语句顺畅, 以下文件遇到的关键词将依照整句语义进行翻译,在此先不进行个别翻译。)

  1. 使用语义化版本控制的软件必须(MUST)定义公共 API。该 API 可以在代码中被定义或出现于严谨的文件内。无论何种形式都应该力求精确且完整。

  2. 标准的版本号必须(MUST)采用 X.Y.Z 的格式,其中 X、Y 和 Z 为非负的整数,且禁止(MUST NOT)在数字前方补零。X 是主版本号、Y 是次版本号、而 Z 为修订号。每个元素必须(MUST)以数值来递增。例如:1.9.1 -> 1.10.0 -> 1.11.0。

  3. 标记版本号的软件发行后,禁止(MUST NOT)改变该版本软件的内容。任何修改都必须(MUST)以新版本发行。

  4. 主版本号为零(0.y.z)的软件处于开发初始阶段,一切都可能随时被改变。这样的公共 API 不应该被视为稳定版。

  5. 1.0.0 的版本号用于界定公共 API 的形成。这一版本之后所有的版本号更新都基于公共 API 及其修改内容。
    修订号 Z(x.y.Z x > 0)必须(MUST)在只做了向下兼容的修正时才递增。这里的修正指的是针对不正确结果而进行的内部修改。
    次版本号 Y(x.Y.z x > 0)必须(MUST)在有向下兼容的新功能出现时递增。在任何公共 API 的功能被标记为弃用时也必须(MUST)递增。也可以(MAY)在内部程序有大量新功能或改进被加入时递增,其中可以(MAY)包括修订级别的改变。每当次版本号递增时,修订号必须(MUST)归零。
    主版本号 X(X.y.z X > 0)必须(MUST)在有任何不兼容的修改被加入公共 API 时递增。其中可以(MAY)包括次版本号及修订级别的改变。每当主版本号递增时,次版本号和修订号必须(MUST)归零。

  6. 先行版本号可以(MAY)被标注在修订版之后,先加上一个连接号再加上一连串以句点分隔的标识符来修饰。标识符必须(MUST)由 ASCII 字母数字和连接号 [0-9A-Za-z-] 组成,且禁止(MUST NOT)留白。数字型的标识符禁止(MUST NOT)在前方补零。先行版的优先级低于相关联的标准版本。被标上先行版本号则表示这个版本并非稳定而且可能无法满足预期的兼容性需求。范例:1.0.0-alpha、1.0.0-alpha.1、1.0.0-0.3.7、1.0.0-x.7.z.92。

  7. 版本编译元数据可以(MAY)被标注在修订版或先行版本号之后,先加上一个加号再加上一连串以句点分隔的标识符来修饰。标识符必须(MUST)由 ASCII 字母数字和连接号 [0-9A-Za-z-] 组成,且禁止(MUST NOT)留白。当判断版本的优先层级时,版本编译元数据可(SHOULD)被忽略。因此当两个版本只有在版本编译元数据有差别时,属于相同的优先层级。范例:1.0.0-alpha+001、1.0.0+20130313144700、1.0.0-beta+exp.sha.5114f85。

  8. 版本的优先层级指的是不同版本在排序时如何比较。判断优先层级时,必须(MUST)把版本依序拆分为主版本号、次版本号、修订号及先行版本号后进行比较(版本编译元数据不在这份比较的列表中)。由左到右依序比较每个标识符,第一个差异值用来决定优先层级:主版本号、次版本号及修订号以数值比较,例如:1.0.0 < 2.0.0 < 2.1.0 < 2.1.1。当主版本号、次版本号及修订号都相同时,改以优先层级比较低的先行版本号决定。例如:1.0.0-alpha < 1.0.0。有相同主版本号、次版本号及修订号的两个先行版本号,其优先层级必须(MUST)透过由左到右的每个被句点分隔的标识符来比较,直到找到一个差异值后决定:只有数字的标识符以数值高低比较,有字母或连接号时则逐字以 ASCII 的排序来比较。数字的标识符比非数字的标识符优先层级低。若开头的标识符都相同时,栏位比较多的先行版本号优先层级比较高。范例:1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0。

为什么要使用语义化的版本控制?

这并不是一个新的或者革命性的想法。实际上,你可能已经在做一些近似的事情了。问题在于只是“近似”还不够。如果没有某个正式的规范可循,版本号对于依赖的管理并无实质意义。将上述的想法命名并给予清楚的定义,让你对软件使用者传达意向变得容易。一旦这些意向变得清楚,弹性(但又不会太弹性)的依赖规范就能达成。

举个简单的例子就可以展示语义化的版本控制如何让依赖地狱成为过去。假设有个名为“救火车”的函式库,它需要另一个名为“梯子”并已经有使用语义化版本控制的包。当救火车创建时,梯子的版本号为 3.1.0。因为救火车使用了一些版本 3.1.0 所新增的功能, 你可以放心地指定依赖于梯子的版本号大等于 3.1.0 但小于 4.0.0。这样,当梯子版本 3.1.1 和 3.2.0 发布时,你可以将直接它们纳入你的包管理系统,因为它们能与原有依赖的软件兼容。

作为一位负责任的开发者,你理当确保每次包升级的运作与版本号的表述一致。现实世界是复杂的,我们除了提高警觉外能做的不多。你所能做的就是让语义化的版本控制为你提供一个健全的方式来发行以及升级包,而无需推出新的依赖包,节省你的时间及烦恼。

如果你对此认同,希望立即开始使用语义化版本控制,你只需声明你的函式库正在使用它并遵循这些规则就可以了。请在你的 README 文件中保留此页连结,让别人也知道这些规则并从中受益。

测试的类型

  • 单元测试 Unit Test
  • 集成测试 Integration Test
  • 端到端测试 End-to-end Test
  • 用户接受性测试 Acceptance Test

什么是单元测试?

  • “单元测试”是一段由开发人员编写的测试代码,用于测试尽可能小的一个功能。
  • “单元测试”保证了
    • 干的事儿和你想的一样,和需求一致
    • 并且以后也保持如此
  • 根据输入和输出,一个功能可能需要有多个单元测试。

##为什么写单元测试?

  • 实现简单
  • 运行快
  • 稳定性强
  • 投入少回报高

都要测些什么?

  • 测试正面情况(Positive)
  • 测试负面情况(Negative)
  • 测试分支(Branching)
  • 测试边界值(Edge Cases)

##怎么写?

单元测试工具

  • Jest
    • Facebook 出品
    • 适用于任何 JavaScript/TypeScript 项目
    • 快!

基础工具

  • describe() 一个测试套件 (Test Suite)
  • it() 一个测试用例 (Test Case)
  • expect() 一个断言 (assertion)
    • toBe(), toEqual(), toMatch(), …

简单的单元测试

其他工具

  • Jest
    • jest.fn()
  • Sinon
    • fake/stub/mock

复杂的单元测试

越来越多的依赖

  • 依赖注入 Dependency Injection
    • 依赖反转原则 Dependency Inversion

什么是好的单元测试?

  • 跑得快
  • 不容易坏
  • 一目了然
  • 抓得住 Bug!
  • 投入产出比得当

常用原则

  • 一个测试用例测一件事(单一责任原则)
  • 描述清晰,言简意赅
  • 没有 if/for/while…
  • 业务逻辑里没有测试逻辑(测试不该影响业务实现)
  • (未完待续。。。

开头难

  • React/Node/Cocos,结构迥异。
  • 不同类型的测试对象,适用的方法不一样。
  • 每个人都有自己的想法,测试的方式八仙过海。

总结经验

  • 写测试 -> 总结经验 -> 推广好的测试方式。
  • 改进工具,降低测试门槛。
    1. 增加通用的测试工具函数/类 (Utilities)
    2. 利用开发工具自动化
      1. TS Lint 提示/自动生成测试

##测试驱动开发
Test-driven Development

资源

git 支持只 Checkout 部分内容(注意:Git 版本要在 1.7 以上)

1、开启

git config core.sparsecheckout true ,也可以在目录 .git/config 中看到

图片描述

2、写入要获取的文件

echo “/*“ >> .git/info/sparse-checkout ,也可以在目录 .git/info/sparse-checkout 中直接修改需要获取的文件配置

我们 C3 需要的配置如图:

图片描述

方便你更快,可以拷贝一下:

1
2
3
4
5
6
7
8
9
/*
!/assets/courses/C1
!/assets/courses/C2
!/assets/courses/C1.meta
!/assets/courses/C2.meta
!/assets/resources/C1
!/assets/resources/C2
!/assets/resources/C1.meta
!/assets/resources/C2.meta

解释:
通配符

排除项!
目录名称前带斜杠,如/docs/,将只匹配项目根目录下的 docs 目录,
目录名称前不带斜杠,如 docs/,其他目录下如果也有这个名称的目录,如 test/docs/也能被匹配
如果写了多级目录,如 docs/05/,则不管前面是否带有斜杠,都只匹配项目根目录下的目录,如 test/docs/05/不能被匹配。

3、配置完以后生效

git checkout

4、生效以后可能遇到的问题

生效以后你可能还会看到一些空目录仍然存在,比如目录/assets/courses/C1,这时候你可以删除你在.git/info/sparse-checkout

文件中配置的排除项希望去掉的目录。