先说几句

首先,本篇是对最近所做项目的技术分享、总结。
那什么是 Node.js 呢? Node.js 是一个开源、跨平台的 JavaScript 运行时环境。
正是因为它的跨平台特性,可以运行在前端后端,十分自由。
它还有着丰富的第三方库,使用自带的包管理器 npm 就可以方便的安装所需依赖。
项目在这里:

Run NodeJS on Android with Java API

项目起因

纯粹是被 AutoJS Pro 圈钱了(现在 AutoJS Pro 还处于关服务器状态)。
首先介绍一下 AutoJS,他最开始是开源的,不过后面作者为了圈钱写了个 Pro 版本,就是 AutoJS Pro。
AutoJS 从它名字就能看出来,自动化 JavaScript 实现。这是基于 Android 无障碍 API 的。
Pro 版本凭借着 Node.js 与 Java 的互调用性,以及 Node.js 自带的快照(弄了个自己的打包加密,声称无人破解)。
当然,还有 Node.js 自带的 npm 包管理器,开始圈钱了。
我当时是刚买,结果没过一周 AutoJS Pro 突然删除了无障碍,甚至于是强制更新。
所有买过的开发者(在自动化方面)都没法用了,质问 AutoJS Pro 开发者。
后面 AutoJS Pro 开发者又整了逆天大活,直接说从此 AutoJS Pro 会对代码进行审查。
再往后 AutoJS Pro 直接关闭了后台,现在只能通过自搭建来骗过验证系统。
在这时愤怒的我开始研究起了 Node.js 和 v8,于是有了本项目。
(我目的纯纯就是,让 AutoJS Pro 的 “命根子” 直接开源,让它圈不了钱)

项目细节

首先就是 Node.js,它是基于 v8(一个 JavaScript 引擎) 和 libuv(一个事件循环库)实现的
而 JavaScript 操作部分主要是 v8(Node.js 提供了 Environment 等),下面开始上代码了。

项目实现

重定向 stdout、stderr 至 logcat

由于 Node.js 默认是标准输出,所以在 Android 嵌入项目时就看不到日志,因此第一步就是将输出流重定向到 logcat(这样 console.log 结果也能看了)

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
int pipe_stdout[2];
int pipe_stderr[2];
pthread_t thread_stdout;
pthread_t thread_stderr;
const char *ADBTAG = "NodeJavaNative"; // 可以换成你自己的 TAG

void *thread_stderr_func(void *) {
ssize_t redirect_size;
char buf[2048];
while ((redirect_size = read(pipe_stderr[0], buf, sizeof buf - 1)) > 0) {
if (buf[redirect_size - 1] == '\n')
--redirect_size;
buf[redirect_size] = 0;
__android_log_write(ANDROID_LOG_ERROR, ADBTAG, buf);
}
return 0;
}

void *thread_stdout_func(void *) {
ssize_t redirect_size;
char buf[2048];
while ((redirect_size = read(pipe_stdout[0], buf, sizeof buf - 1)) > 0) {
if (buf[redirect_size - 1] == '\n')
--redirect_size;
buf[redirect_size] = 0;
__android_log_write(ANDROID_LOG_VERBOSE, ADBTAG, buf);
}
return 0;
}

int start_redirecting_stdout_stderr() {
setvbuf(stdout, 0, _IONBF, 0);
pipe(pipe_stdout);
dup2(pipe_stdout[1], STDOUT_FILENO);

setvbuf(stderr, 0, _IONBF, 0);
pipe(pipe_stderr);
dup2(pipe_stderr[1], STDERR_FILENO);

if (pthread_create(&thread_stdout, 0, thread_stdout_func, 0) == -1)
return -1;
pthread_detach(thread_stdout);

if (pthread_create(&thread_stderr, 0, thread_stderr_func, 0) == -1)
return -1;
pthread_detach(thread_stderr);

return 0;
}

初始化 v8 和 Node.js

初始化代码略微复杂,我们在 JNI_OnLoad 里面去注册。
首先先是重定向输出了,随后由于 Node.js 初始化需要参数,我们就给他一个。

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
jint JNI_OnLoad(JavaVM *vm, void *unused) {
Main::vm = vm;
JNIEnv *env;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}

start_redirecting_stdout_stderr();

std::vector<std::string> args = {"node"};

Main::initializationResult =
node::InitializeOncePerProcess(args, {
node::ProcessInitializationFlags::kNoInitializeV8,
node::ProcessInitializationFlags::kNoInitializeNodeV8Platform
}).release();

for (const std::string &error: Main::initializationResult->errors())
LOGE("%s: %s\n", args[0].c_str(), error.c_str());
if (Main::initializationResult->early_return() != 0) {
return Main::initializationResult->exit_code();
}


Main::platform =
node::MultiIsolatePlatform::Create(4).release();
v8::V8::InitializePlatform(Main::platform);
v8::V8::Initialize();

return JNI_VERSION_1_6;
}

通过 node::MultiIsolatePlatform::Create 函数来创建一个 node::MultiIsolatePlatform(这个 Platform 是支持运行 Worker Thread 的)

创建 Isolate 和 Context

v8::Isolate,也就是隔离。是 v8 虚拟机的实例,它负责为 JavaScript 源码创建执行环境,管理堆栈、编译、执行、context 等所有组件。
由于我们是在 Node.js 环境下,我们可以使用 node::CreateIsolate 来创建一个基于 Node.js 标准的 v8::Isolate 实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Isolate::Isolate() {
allocator = node::CreateArrayBufferAllocator();
loop = uv_loop_new();
self = node::NewIsolate(
allocator,
loop,
Main::platform
);
isolateData = node::CreateIsolateData(
self,
loop,
Main::platform,
allocator
);
}

node::IsolateData 实例包含的信息可以由使用相同 v8::Isolate 的多个 node::Environment 共享。
然后是创建 Context。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Context::Context(Isolate *isolate) {
this->isolate = isolate;

v8::Isolate::Scope isolateScope(isolate->self);
v8::HandleScope handleScope(isolate->self);

self.Reset(isolate->self, node::NewContext(isolate->self));

environment = node::CreateEnvironment(
isolate->isolateData,
self.Get(isolate->self),
Main::initializationResult->args(),
Main::initializationResult->exec_args(),
node::EnvironmentFlags::kOwnsProcessState
);
}

请注意,创建 Context 的时候必须使用 v8::Isolate::Scopev8::HandleScope 限定 v8::Isolate 的范围,否则程序将会崩溃。
随后这里通过 node::NewContext 来创建一个基于 Node.js 标准的 v8::Local<v8::Context> 实例。
随后是创建 node::Environment,这里传入 node::EnvironmentFlags::kOwnsProcessState 这个 Flag 是允许实例更改进程状态(比如使 process.setuid 等等可用,而不是禁用),另外是允许创建多个 node::Environment 实例

运行 JavaScript 代码

通过 v8::Script::Compile 编译 JavaScript 代码,再通过 v8::Script::Run 运行它

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
extern "C"
JNIEXPORT void JNICALL
Java_com_mucheng_nodejava_core_Context_nativeEvaluateScript(JNIEnv *env, jobject thiz,
jstring script) {
Context *context = Context::From(thiz);
Isolate *isolate = context->isolate;

v8::Isolate::Scope isolateScope(isolate->self);
v8::HandleScope handleScope(isolate->self);
v8::Context::Scope contextScope(context->self.Get(isolate->self));

v8::TryCatch tryCatch(isolate->self);
SetProcessExitHandler(context->environment, [](node::Environment *environment, int exitCode) {
Stop(environment, node::StopFlags::kDoNotTerminateIsolate);
LOGE("Process ExitCode: %d", exitCode);
});

v8::MaybeLocal<v8::Script> compiling = v8::Script::Compile(context->self.Get(isolate->self),
v8::String::NewFromUtf8(
isolate->self,
Util::JavaStr2CStr(
script)).ToLocalChecked());
if (compiling.IsEmpty()) {
if (tryCatch.HasCaught()) {
v8::MaybeLocal<v8::Value> stackTrace = v8::TryCatch::StackTrace(
context->self.Get(isolate->self), tryCatch.Exception());
if (stackTrace.IsEmpty()) {
Util::ThrowScriptCompilingException(
*v8::String::Utf8Value(isolate->self, tryCatch.Exception()));
} else {
Util::ThrowScriptCompilingException(
*v8::String::Utf8Value(isolate->self, stackTrace.ToLocalChecked()));
}
}
return;
}

v8::MaybeLocal<v8::Value> running = compiling.ToLocalChecked()->Run(
context->self.Get(isolate->self));

if (running.IsEmpty()) {
if (tryCatch.HasCaught()) {
v8::MaybeLocal<v8::Value> stackTrace = v8::TryCatch::StackTrace(
context->self.Get(isolate->self), tryCatch.Exception());
if (stackTrace.IsEmpty()) {
Util::ThrowScriptRuntimeException(
*v8::String::Utf8Value(isolate->self, tryCatch.Exception()));
} else {
Util::ThrowScriptRuntimeException(
*v8::String::Utf8Value(isolate->self, stackTrace.ToLocalChecked()));
}
}
return;
}
}

注意,请使用 v8::Context::Scope 限定 v8::Local<v8::Context> 的范围,否则程序将会崩溃。
使用 node::SetProcessExitHandler 可以自定义 process.exit 事件,这里是停止当前 Environment。
对于 JavaScript 的异常,你可以使用 v8::TryCatch 进行捕捉与获取 StackTrace。