普通视图

发现新文章,点击刷新页面。
昨天以前崎径 其镜赵安琪的博客

从零配置 VS Code C++ 环境

作者 Anqi Zhao
2026年4月13日 06:32

本文档记录在 Windows 系统上从零配置 VS Code C++ 开发环境的全过程,包括编译、调试和运行。

前置条件

需要的工具

1. MSYS2

MSYS2 是一个 Windows 下的 Linux-like 环境,提供 g++ 编译器和 gdb 调试器。

2. VS Code 扩展

  • C/C++ 扩展:由 Microsoft 提供,支持 IntelliSense、调试等功能
    • 在 VS Code 中搜索安装:ms-vscode.cpptools

安装步骤

步骤 1:安装 MSYS2

  1. 下载 MSYS2 安装包并运行安装程序
  2. 选择安装路径为 C:\msys64
  3. 安装完成后,运行 MSYS2 UCRT64 终端(从开始菜单启动)

步骤 2:更新 MSYS2 并安装工具

在 MSYS2 UCRT64 终端中执行以下命令:

1
2
3
4
5
# 更新包数据库
pacman -Syu

# 安装 g++ 编译器和 gdb 调试器
pacman -S mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-gdb

步骤 3:安装 VS Code 扩展

  1. 打开 VS Code
  2. Ctrl+Shift+X 打开扩展面板
  3. 搜索 C/C++ 并安装 Microsoft 的 C/C++ 扩展

VS Code 配置

在你的 C++ 项目根目录(例如 C:\Dev\leetcode)创建 .vscode 文件夹,并添加以下配置文件:

1. c_cpp_properties.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"configurations": [
{
"name": "windows-gcc-x64",
"includePath": [
"${workspaceFolder}/**"
],
"compilerPath": "C:\\msys64\\ucrt64\\bin\\gcc",
"cStandard": "${default}",
"cppStandard": "${default}",
"intelliSenseMode": "windows-gcc-x64",
"compilerArgs": [
""
]
}
],
"version": 4
}

2. settings.json

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
56
57
58
59
60
61
{
"C_Cpp.default.compilerPath": "c:\\msys64\\ucrt64\\bin\\gcc.exe",
"C_Cpp_Runner.cCompilerPath": "gcc",
"C_Cpp_Runner.cppCompilerPath": "g++",
"C_Cpp_Runner.debuggerPath": "gdb",
"C_Cpp_Runner.cStandard": "",
"C_Cpp_Runner.cppStandard": "",
"C_Cpp_Runner.msvcBatchPath": "C:/Program Files/Microsoft Visual Studio/VR_NR/Community/VC/Auxiliary/Build/vcvarsall.bat",
"C_Cpp_Runner.useMsvc": false,
"C_Cpp_Runner.warnings": [
"-Wall",
"-Wextra",
"-Wpedantic",
"-Wshadow",
"-Wformat=2",
"-Wcast-align",
"-Wconversion",
"-Wsign-conversion",
"-Wnull-dereference"
],
"C_Cpp_Runner.msvcWarnings": [
"/W4",
"/permissive-",
"/w14242",
"/w14287",
"/w14296",
"/w14311",
"/w14826",
"/w44062",
"/w44242",
"/w14905",
"/w14906",
"/w14263",
"/w44265",
"/w14928"
],
"C_Cpp_Runner.enableWarnings": true,
"C_Cpp_Runner.warningsAsError": false,
"C_Cpp_Runner.compilerArgs": [],
"C_Cpp_Runner.linkerArgs": [],
"C_Cpp_Runner.includePaths": [],
"C_Cpp_Runner.includeSearch": [
"*",
"**/*"
],
"C_Cpp_Runner.excludeSearch": [
"**/build",
"**/build/**",
"**/.*",
"**/.*/**",
"**/.vscode",
"**/.vscode/**"
],
"C_Cpp_Runner.useAddressSanitizer": false,
"C_Cpp_Runner.useUndefinedSanitizer": false,
"C_Cpp_Runner.useLeakSanitizer": false,
"C_Cpp_Runner.showCompilationTime": false,
"C_Cpp_Runner.useLinkTimeOptimization": false,
"C_Cpp_Runner.msvcSecureNoWarnings": false,
"terminal.external.windowsExec": "C:\\Windows\\System32\\cmd.exe"
}

3. tasks.json

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
56
{
"version": "2.0.0",
"tasks": [
{
"type": "cppbuild",
"label": "C/C++: g++.exe build active file",
"command": "C:\\msys64\\ucrt64\\bin\\g++.exe",
"args": [
"-fdiagnostics-color=always",
"-g",
"${file}",
"-o",
"${workspaceFolder}\\build\\Debug\\outDebug.exe"
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [
"$gcc"
],
"group": {
"kind": "build",
"isDefault": true
},
"detail": "Compile current C++ file to build/Debug/outDebug.exe"
},
{
"type": "shell",
"label": "Run C++ Program",
"command": "cmd.exe",
"args": [
"/c",
"start \"\" cmd.exe /k \"${workspaceFolder}\\build\\Debug\\outDebug.exe\""
],
"dependsOn": [
"C/C++: g++.exe build active file"
],
"options": {
"shell": {
"executable": "C:\\Windows\\System32\\cmd.exe"
}
},
"group": {
"kind": "test",
"isDefault": true
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": true,
"panel": "shared"
},
"problemMatcher": []
}
]
}

4. launch.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"version": "0.2.0",
"configurations": [
{
"name": "C/C++ Runner: Debug Session",
"type": "cppdbg",
"request": "launch",
"args": [],
"stopAtEntry": false,
"externalConsole": false,
"cwd": "c:/Dev/leetcode",
"program": "${workspaceFolder}/build/Debug/outDebug.exe",
"MIMode": "gdb",
"miDebuggerPath": "C:\\msys64\\ucrt64\\bin\\gdb.exe",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}

5. keybindings.json(全局配置)

在 VS Code 用户设置中添加快捷键绑定(Ctrl+Shift+P -> Preferences: Open Keyboard Shortcuts (JSON)):

1
2
3
4
5
6
7
8
9
10
[
{
"key": "f6",
"command": "workbench.action.tasks.runTask",
"args": {
"task": "Run C++ Program"
},
"when": "editorTextFocus"
}
]

使用方法

  1. 编译:按 Ctrl+Shift+B 或打开命令面板运行 “Tasks: Run Build Task”
  2. 调试:按 F5,会启动 gdb 调试器
  3. 运行:按 F6,会打开 cmd 黑框窗口运行程序

常见问题

1. 找不到 g++ 或 gdb

  • 确保 MSYS2 已正确安装并更新
  • 检查 PATH 环境变量是否包含 C:\msys64\ucrt64\bin

2. 编译失败

  • 检查代码语法错误
  • 确保头文件路径正确

3. 调试失败

  • 确保已编译生成 build/Debug/outDebug.exe
  • 检查 gdb 路径是否正确

4. 黑框不弹出

  • 重启 VS Code
  • 检查 terminal.external.windowsExec 设置

版本信息

  • MSYS2: 最新版本
  • g++: 15.2.0
  • gdb: 17.1
  • VS Code: 最新版本
  • C/C++ 扩展: 最新版本

参考资料

Unity 游戏的 Google Play 16 kb页面对齐处理

作者 Anqi Zhao
2026年4月4日 01:21

16 KB 内存页面大小的支持,是Google Play 新提出的要求。要在2026年5月31日之前,满足这一条件:

https://play.google.com/console/u/0/developers/9060101706093336387/app/4974577679306649072/policy-center/issues/4988764664083295045/details

Android 15 的 16KB page 检测主要看三件事:

  1. PT_LOAD >= 16 KB
  2. RELRO 必须在 segment 末尾(suffix)
  3. RELRO end 必须 16KB 对齐

对于aab 来说,上面的三个要求,一般只用关注PT_LOAD。因为aab 有压缩率,所以关于压缩的两项检验直接计算是不准确的。Google Play 在分发时会自动处理。

官方文档:

https://developer.android.com/guide/practices/page-sizes?utm_source=chatgpt.com&hl=zh-cn

检测方式

关于so 文件是否合规的检测,主要有以下几种方法:

方法一

可以解压出so文件,然后使用ndk 工具检测(一定使用PowerShell,cmd无法运行):

1
llvm-objdump.exe -p E:\Test\so\arm64-v8a\libanogs.so | Select-String -Pattern "LOAD"

运行结果可以看LOAD off 类型行尾是不是带有212、213。
低于2**14的so文件都不符合。

工具的路径在:

1
{YourNDKPath}\toolchains\llvm\prebuilt\windows-x86_64\bin

但是这里只检查了PT_LOAD。

方法二

Google 官方提供了检测工具:

https://cs.android.com/android/platform/superproject/main/+/main:system/extras/tools/check_elf_alignment.sh?hl=zh-cn

可以直接使用这个脚本进行检测。

1
check_elf_alignment.sh APK_NAME.apk

方法三

最后,如果将Android Studio 更到最新版本,即可使用Apk Analyze 功能进行检测。

前者报错为:

1
4 KB LOAD section alignment, but 16 KB is required

后者报错为:

1
RELRO is not a suffix and its end is not 16 KB aligned

适配方法

Unity 相关so

Unity相关的so,需要通过提升Unity 编辑器版本号的方式进行解决。

第三方SDK

第三方SDK 相关的so,需要联系相关提供商进行SDK 升级。

如果无法完成合规,应该考虑取消对该插件的接入。

自己编译的so

如果是自己使用NDK 编译的so,需要升级NDK工具并在编译中指定相关参数。

1
2
3
4
5
6
7
({NDK Path}/ndk-build ^
NDK_PROJECT_PATH=. ^
APP_BUILD_SCRIPT=Android.mk ^
NDK_APPLICATION_MK=Application.mk ^
APP_ABI="armeabi-v7a x86 arm64-v8a x86_64" ^
APP_LDFLAGS="-Wl,-z,max-page-size=16384 -Wl,-z,common-page-size=16384" ^ // 新增
|| pause) && pause

对于Android.mk 文件和Application.mk 文件也有内容需要修改:

如果旧内容涉及cmd-strip,在打包时会报错。这是因为NDK r23+中,strip 工具路径变成 LLVM 版本。最简单的解决方式是删掉这一行,新版本不需要手动调用strip,会自动调用。

如果文件中存在:APP_STL := gnustl_static,也需要进行修改。
修改为:APP_STL := c++_static

为了兼容RELRO 必须在 segment 末尾(suffix)和 RELRO end 必须 16KB 对齐,需要在Android.mk 中增加:

1
2
LOCAL_LDFLAGS += -Wl,-z,max-page-size=16384
LOCAL_LDFLAGS += -Wl,-z,common-page-size=16384

最后,在旧的代码中,如果使用了PAGE_SIZE / PAGE_MASK 宏,这会报错,在新版本中,NDK 不再提供。
需要增加以下内容:

1
2
3
4
5
6
7
#ifndef PAGE_SIZE
#define PAGE_SIZE 4096
#endif

#ifndef PAGE_MASK
#define PAGE_MASK (~(PAGE_SIZE - 1))
#endif

3. il2cpp处理方式

升级到新版Unity之后,如果不修改代码,可能不会让C++代码重新导出。

也可以删除Library目录下的Bee目录和所有的il2cpp_*目录的缓存,强制生成。

一文详解Hexo 博客搭建

作者 Anqi Zhao
2026年4月3日 22:06

前言

捡起了好久没有更新的博客。

在AI 的帮助下,我完成了整个环境的重新搭建。

由于我使用的hexo 框架和版本都比较老了,所以有些地方可能不适应于新版本的主题,在接入时还请注意。

参考视频: https://www.bilibili.com/video/BV1xTgTemEDU/

视频文档: https://xiamu-ssr.github.io/Hexo/2024/06/19/2024-H1/2024-06-19-12-31-52/

准备工作

使用Github Actions 服务,将hexo 部署到 Github Pages。

与参考内容不同的是,我使用了两个仓库来实现。

参考内容中,使用了同一个项目,两个分支。其中一个分支用于提交博客源文件,另一个分支用于存放生成的静态网页文件。

考虑到将博客源文件内容暴露出来会有一定的风险,于是我的方案是使用两个仓库来实现。其中,Private 的仓库用于存放博客源文件,Public 的仓库用于存放页面。

我之前是有hexo 环境的,会和从头开始实现有所区别,在接下来的内容中,我将会分别说明。

关于Github Actions 服务的用量,Public 仓库可以免费使用,但是Private 的仓库会有一定的限制。但是如果只是作为hexo的资源站,免费的额度也是可以覆盖的。

hexo准备

安装并初始化hexo,如果已有hexo环境,可跳过这一步。

1
2
3
4
npm install -g hexo-cli
hexo init blog
cd blog
npm install

如果是已有hexo环境,在其他地方做过备份的。可以删除node_modules目录和package-lock.json,然后重新安装:

1
2
cd blog
npm install

仓库准备

在github上新建两个仓库,一个Private的blog_base,一个Public 的username.github.io;

已有Github Pages 的,可以不用重新创建username.github.io,只创建博客源文件仓库。

Token准备

使用Github Actions ,需要生成Personal access tokens,而且至少要包含repo 权限。把这个Token 配置给Github Action, 它才有权限去执行自动部署。

首先是Token 的创建:

单击头像 -> Settings -> Developer Settings -> Personal access tokens -> Tokens (classic)

这里是所有Tokens的列表,下面我们继续创建:

Generate new token -> Generate new token (classic)

新打开的界面中,我们需要给这个token 命名,随便命名即可,比如blog_base。

在下面的权限列表中,勾选repo,workflow 两项。

记录token的有效期,有效期到期后需要给Github Actions 配置新的token。

完成创建后,去到blog_base 仓库进行配置。首先打开该仓库,点击上方的Settings -> Secrets and variables -> Actions

点击 New repository secret 创建,Name设置为:GH_TOKEN(可随意设置,后面保持一致即可),Secret 粘贴刚才生成出来的token内容,就是一串以ghp_为开头的字符串。

修改_config.yml

为了设置推送的地址,需要在_config.yml 中配置推送地址。这里选择Public 的Github pages 仓库地址:

1
2
3
4
deploy:
type: git
repo: https://github.com/yourusername/your-repo.git
branch: master

配置Github Actions 工作流

在blog_base 目录创建.github文件夹,再在.github文件夹下创建workflows文件夹,然后创建deploy.yml,内容如下:

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
name: Deploy Hexo to GitHub Pages

on:
push:
branches:
- main # 当推送到 main 分支时触发

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
submodules: false # 禁用子模块检查

- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '12'

- name: Install Dependencies
run: npm install

- name: Install Hexo Git Deployer
run: |
npm install hexo-deployer-git --save
npm install hexo-cli -g

- name: Clean and Generate Static Files
run: |
hexo clean
hexo generate

- name: Configure Git
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'

- name: Deploy to GitHub Pages
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
cd public/
git init
git add -A
git commit -m "Create by workflows"
git branch -M master
git remote add origin https://${{ secrets.GH_TOKEN }}@github.com/yourusername/your-repo.git
git push -f origin master

这里最后一步是将生成的内容,推送到你配置的仓库的master 分支,我的Github Pages就是设置的这个分支。

配置里的node-version 这个参数需要注意。旧版的hexo 使用新版的node 可能会生成失败。我的主题使用的hexo 版本比较老,使用12版本是可以生成的,最新的24 反而无法生成,这里可以按需修改版本。

这个工作流的意思就是,使用ubuntu-latest作为基础环境,然后安装各种依赖,随后hexo generate生成博客网站静态文件夹,最后再推送到设定的仓库和分支。

配置Github Pages

到Github Pages 仓库,点击Settings,设置page来源的分支。

推送并查看是否能够生效。

在这里,介绍一下我的.gitignore文件:

1
2
3
4
5
6
7
.DS_Store
Thumbs.db
db.json
*.log
public/
.deploy*/
node_modules/

.DS_Store文件和Thumbs.db文件。这两个文件分别是Mac系统和Win系统生成的垃圾。

public 目录存放的是本地生成的静态页面,这个也不必上传。Github Actions 会自动生成。

node_modules 目录一定不能上传,需要排除掉。因为它太大了,不方便版本控制。而且不同平台需求的可能都不同。本地环境和运行Github Actions 的虚拟机环境不可能保证一致的。

其他注意事项

如果你之前是有Github Pages的,需要做好备份,防止文件丢失。

我就出现了CNAME文件和其他文件丢失的情况,这里需要注意。

自定义域名

如果嫌username.github.io 的网址不够优雅,可以注册域名,然后配置解析。

在购买域名后,进行以下设置:

Github 配置

进入Github Pages 仓库,单击Settings 切页。

在左侧边栏点击Pages。

Custom domain 配置为:

1
www.yourdomin.xxx

然后勾选Enforce HTTPS

域名解析

在域名解析中,做出以下设置:

1
2
3
4
5
6
7
8
www  CNAME  yourusername.github.io
@ AAAA 2606:50c0:8003::153
@ AAAA 2606:50c0:8002::153
@ AAAA 2606:50c0:8001::153
@ AAAA 2606:50c0:8000::153
@ A 185.199.111.153
@ A 185.199.110.153
@ A 185.199.108.153

这里参考Github的文档:https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site

创建CNAME

需要在Github Pages 网站的根目录创建CNAME文件,内容是在第一步中配置的www.yourdomin.xxx

一文详解Hexo 博客搭建

作者 Anqi Zhao
2026年4月3日 22:06

前言

捡起了好久没有更新的博客。

在AI 的帮助下,我完成了整个环境的重新搭建。

由于我使用的hexo 框架和版本都比较老了,所以有些地方可能不适应于新版本的主题,在接入时还请注意。

参考视频: https://www.bilibili.com/video/BV1xTgTemEDU/

视频文档: https://xiamu-ssr.github.io/Hexo/2024/06/19/2024-H1/2024-06-19-12-31-52/

准备工作

使用Github Action 服务,将hexo 部署到 Github Pages。

与参考内容不同的是,我使用了两个仓库来实现。

参考内容中,使用了同一个项目,两个分支。其中一个分支用于提交博客源文件,另一个分支用于存放生成的静态网页文件。

考虑到将博客源文件内容暴露出来会有一定的风险,于是我的方案是使用两个仓库来实现。其中,Private 的仓库用于存放博客源文件,Public 的仓库用于存放页面。

我之前是有hexo 环境的,会和从头开始实现有所区别,在接下来的内容中,我将会分别说明。

1. hexo准备

安装并初始化hexo,如果已有hexo环境,可跳过这一步。

1
2
3
4
npm install -g hexo-cli
hexo init blog
cd blog
npm install

如果是已有hexo环境,在其他地方做过备份的。可以删除node_modules目录和package-lock.json,然后重新安装:

1
2
cd blog
npm install

2. 仓库准备

在github上新建两个仓库,一个Private的blog_base,一个Public 的username.github.io;

已有Github Pages 的,可以不用重新创建username.github.io,只创建博客源文件仓库。

3. Token准备

使用Github Actions ,需要生成Personal access tokens,而且至少要包含repo 权限。把这个Token 配置给Github Action, 它才有权限去执行自动部署。

首先是Token 的创建:

单击头像 -> Settings -> Developer Settings -> Personal access tokens -> Tokens (classic)

这里是所有Tokens的列表,下面我们继续创建:

Generate new token -> Generate new token (classic)

新打开的界面中,我们需要给这个token 命名,随便命名即可,比如blog_base。

在下面的权限列表中,勾选repo,workflow 两项。

记录token的有效期,有效期到期后需要给Github Actions 配置新的token。

完成创建后,去到blog_base 仓库进行配置。首先打开该仓库,点击上方的Settings -> Secrets and variables -> Actions

点击 New repository secret 创建,Name设置为:GH_TOKEN(可随意设置,后面保持一致即可),Secret 粘贴刚才生成出来的token内容,就是一串以ghp_为开头的字符串。

4. 修改_config.yml

为了设置推送的地址,需要在_config.yml 中配置推送地址。这里选择Public 的Github pages 仓库地址:

1
2
3
4
deploy:
type: git
repo: https://github.com/yourusername/your-repo.git
branch: master

5. 配置Github Actions 工作流

在blog_base 目录创建.github文件夹,再在.github文件夹下创建workflows文件夹,然后创建deploy.yml,内容如下:

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
name: Deploy Hexo to GitHub Pages

on:
push:
branches:
- main # 当推送到 main 分支时触发

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
submodules: false # 禁用子模块检查

- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '12'

- name: Install Dependencies
run: npm install

- name: Install Hexo Git Deployer
run: |
npm install hexo-deployer-git --save
npm install hexo-cli -g

- name: Clean and Generate Static Files
run: |
hexo clean
hexo generate

- name: Configure Git
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'

- name: Deploy to GitHub Pages
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
cd public/
git init
git add -A
git commit -m "Create by workflows"
git branch -M master
git remote add origin https://${{ secrets.GH_TOKEN }}@github.com/yourusername/your-repo.git
git push -f origin master

这里最后一步是将生成的内容,推送到你配置的仓库的master 分支,我的Github Pages就是设置的这个分支。

配置里的node-version 这个参数需要注意。旧版的hexo 使用新版的node 可能会生成失败。我的主题使用的hexo 版本比较老,使用12版本是可以生成的,最新的24 反而无法生成,这里可以按需修改版本。

这个工作流的意思就是,使用ubuntu-latest作为基础环境,然后安装各种依赖,随后hexo generate生成博客网站静态文件夹,最后再推送到设定的仓库和分支。

6. 配置Github Pages

到Github Pages 仓库,点击Settings,设置page来源的分支。

推送并查看是否能够生效。

7. 其他注意事项

如果你之前是有Github Pages的,需要做好备份,防止文件丢失。

我就出现了CNAME文件和其他文件丢失的情况,这里需要注意。

8. 自定义域名

如果嫌username.github.io 的网址不够优雅,可以注册域名,然后配置解析。

在购买域名后,进行以下设置:

8.1 Github 配置

进入Github Pages 仓库,单击Settings 切页。

在左侧边栏点击Pages。

Custom domain 配置为:

1
www.yourdomin.xxx

然后勾选Enforce HTTPS

8.2 域名解析

在域名解析中,做出以下设置:

1
2
3
4
5
6
7
8
www  CNAME  yourusername.github.io
@ AAAA 2606:50c0:8003::153
@ AAAA 2606:50c0:8002::153
@ AAAA 2606:50c0:8001::153
@ AAAA 2606:50c0:8000::153
@ A 185.199.111.153
@ A 185.199.110.153
@ A 185.199.108.153

这里参考Github的文档:https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site

8.3 创建CNAME

需要在Github Pages 网站的根目录创建CNAME文件,内容是在第一步中配置的www.yourdomin.xxx

xlua学习笔记

作者 Anqi Zhao
2021年5月19日 03:05

文件加载

使用LuaEnv中的DoString方法来执行Lua脚本。
因为创建一个LuaEnv相当于创建了一个lua虚拟机,所以一个游戏最好只有一个LuaEnv。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 执行lua代码
env.DoString("print(\"Hello World\")");

// 通过文件执行lua代码
TextAsset ta = Resources.Load<TextAsset>("Lua/helloworld.lua");
env.DoString(ta.text);

// 通过默认Loader执行lua代码
env.DoString("require 'Lua/helloworld'");

// 通过自定义Loader执行lua代码
// require实际上是调一个个的Loader去加载,有一个成功就不再往下尝试,全失败则报文件找不到。
private byte[] LuaLoader(ref string filePath)
{
string path = "Lua/" + filePath + ".lua";
print(path);
TextAsset ta = Resources.Load<TextAsset>(path);
return System.Text.Encoding.UTF8.GetBytes(ta.text);
}

env.AddLoader(LuaLoader);
env.DoString("require 'helloworld'");

官方建议的加载Lua脚本方式是:整个程序就一个DoString(“require ‘main’”),然后在main.lua加载其它脚本(类似lua脚本的命令行执行:lua main.lua)。

C#访问Lua普通全局变量

先加载lua脚本,再通过LuaEnv.Global.Get获取变量。

1
2
3
4
5
6
TextAsset ta = Resources.Load<TextAsset>("Lua/variable.lua");
env.DoString(ta.text);

int g_int = env.Global.Get<int>("g_int");
string g_str = env.Global.Get<string>("g_str");
bool g_bool = env.Global.Get<bool>("g_bool");

C#访问Lua table的四种方法

需要注意的是,在构建C#中对应的结构时,接口一定要是public修饰,否则会报错。类最好也是,但是经测试,没有报错。

映射到普通class或struct

table的属性可以多于或者少于class的属性,也就是不一定都需要做映射。这个过程是值拷贝,如果class比较复杂代价会比较大。而且修改class的字段值不会同步到table,反过来也不会。

1
2
3
4
5
6
7
public class Person
{
public string name;
public int age;
}

Person p = env.Global.Get<Person>("g_tbl");

映射到接口

一定要加[CSharpCallLua]和public,否则会报错。

1
2
3
4
5
6
7
g_int = 1
g_str = "Hello World Variable"
g_bool = false
g_tbl = {name = "Juhnny", age = 12}
g_tbl.say = function(this, str)
print(str)
end

1
2
3
4
5
6
7
8
9
10
[CSharpCallLua]
public interface IPerson
{
string name { get; set; }
int age { get; set; }
public void say(string str);
}

IPerson p = env.Global.Get<IPerson>("g_tbl");
p.say("Yahoo");

映射到Dictionary和List

这种方法较方法2更为轻量级,但是要求table下key和value类型一致。

1
2
g_dic = {apple = "iPhone", flower = "Huawei"}
g_list = {1, 3, 5, 7, 9}

1
2
3
4
5
6
7
8
9
10
11
Dictionary<string, string> dic = env.Global.Get<Dictionary<string, string>>("g_dic");
foreach (var child in dic)
{
print(string.Format("Key is {0}, Value is {1}", child.Key, child.Value));
}

List<int> list = env.Global.Get<List<int>>("g_list");
foreach (var v in list)
{
print(v);
}

映射到LuaTable

这种方法比较慢,而且没有类型检查,不推荐使用。

1
2
3
4
5
6
7
8
LuaTable tab = env.Global.Get<LuaTable>("g_tbl");
string name = tab.Get<string>("name");
int age = tab.Get<int>("age");
print(string.Format("Name is {0}, age is {1}", name, age));
foreach (string key in tab.GetKeys())
{
print(tab.Get<object>(key));
}

C#访问Lua全局函数

使用delegate映射

这种是建议的方式,性能好很多,而且类型安全,但是要生成代码。支持带返回值,多返回值,甚至返回值都可以是delegate。

1
2
3
4
5
6
7
8
g_func1 = function()
print("FF")
end

g_func2 = function(num)
print(num)
return num, "Yoshida", "SE"
end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[CSharpCallLua]
public delegate int Func2(int num, out string name, out string company);
// 声明委托时,一定要加public和[CSharpCallLua]

// 无返回
Action func1 = env.Global.Get<Action>("g_func1");
func1();
func1 = null;

// 单返回值
Func2 func2 = env.Global.Get<Func2>("g_func2");
int num = func2(14);
func2 = null

// 多返回值
Func2 func2 = env.Global.Get<Func2>("g_func2");
string name;
string company;
int num = func2(14, out name, out company);
func2 = null

在过去的一些版本中,需要在不使用后,将接收到的函数销毁,否则会在LuaEnv.Dispose的时候报错。

使用LuaFunction

与上一种方法优缺点相反,不推荐使用。

1
2
LuaFunction func = env.Global.Get<LuaFunction>("g_func2");
object[] res = func.Call(14);

Lua调用C

1
2
3
4
5
6
7
8
9
10
11
// 读静态属性
CS.UnityEngine.Time.deltaTime

// 写静态属性
CS.UnityEngine.Time.timeScale = 0.5

// 调用静态方法
CS.UnityEngine.GameObject.Find('helloworld')

// 读写成员属性类似,但是访问成员方法要使用:
testobj:DMFunc()

其它注意事项

方法的参数处理
Lua调用侧的参数处理规则:C#的普通参数算一个输入形参,ref修饰的算一个输入形参,out不算,然后从左往右对应lua 调用侧的实参列表。

Lua调用侧的返回值处理规则:C#函数的返回值(如果有的话)算一个返回值,out算一个返回值,ref算一个返回值,然后从左往右对应lua的多返回值。

可变参数的处理

1
2
3
4
5
// 对于C#的如下方法:
void VariableParamsFunc(int a, params string[] strs)

// 可以在lua里头这样调用:
testobj:VariableParamsFunc(5, 'hello', 'john')

枚举

1
2
3
4
5
6
7
8
9
10
// 枚举值就像枚举类型下的静态属性一样。

testobj:EnumTestFunc(CS.Tutorial.TestEnum.E1)

上面的EnumTestFunc函数参数是Tutorial.TestEnum类型的。

枚举类支持__CastFrom方法,可以实现从一个整数或者字符串到枚举值的转换,例如:

CS.Tutorial.TestEnum.__CastFrom(1)
CS.Tutorial.TestEnum.__CastFrom('E1')

委托

1
2
3
4
5
6
7
8
-- 使用+-号来实现委托
-- 比如testobj里头有个事件定义是这样:public event Action TestEvent;

-- 增加事件回调
testobj:TestEvent('+', lua_event_callback)

--移除事件回调
testobj:TestEvent('-', lua_event_callback)

Lua新知

作者 Anqi Zhao
2021年5月10日 19:42

两年没有碰过Lua了,总觉得得要捡起来。现在从头到尾再过一遍,总结一些东西。之前根据菜鸟学的时候,使用了5.1.1的版本。与现在常用的版本相悖。因此这次我使用了:zerobrane studio,它可以方便切换版本,能实现很好的练习效果。

关于多行注释的安全问题

如果直接使用默认的–[[–]]来进行多行注释的时候,如果遇到table包table的情况会直接结束注释:

1
2
3
4
5
6
--[[
a = [];
b = [a];
a[b[1]]
a = 1
--]]

可以使用另一种方法来进行多行注释,避免这个问题:

1
2
3
4
5
6
--[=[
a = [];
b = [a];
a[b[1]]
a = 1
]=]

当然,最安全的方法还是使用IDE的快捷键进行注释.IDE会在每行的前面加单行注释,避免错误的发生.

多行字符串安全问题

多行字符串,也会出现类似于上面的情况:

1
2
3
4
5
6
7
8
9
10
str = 
[[
话说
这个东西
好像打印不出来
就是这个
table[table[idx]]
咋办啊
]]
print(str)

运算符相关

#运算符取的是最大索引的值,如果删除了中间的值,获得的结果不会改变:

1
2
3
4
5
tb_a = {1, 2, 3}
print (#tb_a)

tb_a[2] = nil
print (#tb_a)

但是这种情况仅限于Lua 5.1 ,Lua5.3和Lua5.4结果是1。

模拟三目运算符的and…or…,在第二个参数为false时,始终返回c,会出现问题:

1
2
a = true
print (a and false or true)

输出结果会是true 而不是三目运算符应该返回的false。
这时候,可以将三目运算符的后面两个部分转换成table,运算结束后再取第一个元素,即可实现:

1
2
3
4
5
6
7
a = true
b = (a and {false} or {true})[1]
if b then
print("true")
else
print("false")
end

自定义迭代器

自定义迭代器的格式:

1
2
3
for 变量列表 in 迭代函数, 状态变量, 控制变量 do
-- 循环体
end

自定义无状态迭代器

1
2
3
4
5
6
7
8
9
10
11
function square(iteratorMaxCount, currentNumber)
if currentNumber < iteratorMaxCount then
currentNumber = currentNumber + 1
return currentNumber, currentNumber * currentNumber
end
end

for i, n in square, 3, 0
do
print(i, n)
end

多状态迭代器:
多个状态可以存放在table,因此只需要一个参数即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
array = {"Google", "Runoob"}

function elementIterator (collection)
local index = 0
local count = #collection
-- 闭包函数
return function ()
index = index + 1
if index <= count then
-- 返回迭代器的当前元素
return collection[index]
end
end
end

for element in elementIterator(array) do
print(element)
end

循环时的goto语句

可以使用goto语句来实现continue:

1
2
3
4
5
6
7
8
9
for i=1, 3 do
if i <= 2 then
print(i, "yes continue")
goto continue
end
print(i, " no continue")
::continue::
print([[i'm end]])
end

元表

Lua可以通过设置元表,定义元方法的方式,改变table的一些行为:

index元方法,可以修改按索引取值的逻辑。
index内容是一个表的话,如果table里没有值,会从元表中对应的索引去取:

1
2
3
4
5
6
7
8
9
10
11
t_table = setmetatable({k1 = "v1"}, {__index = {k2 = "v2"}})
print(t_table["k1"])
print(t_table["k2"])
t_table["k2"] = "v3"
print(t_table["k2"])
--[[
输出是:
v1
v2
v3
--]]

若__index内容是一个函数,则可以定义返回逻辑:

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
t_table = setmetatable({}, {__index =
function(t_table, key)
if key == 1 then
return "true"
else
return "false"
end
end
})

print(t_table[1])
print(t_table[2])
print(t_table[3])

t_table = {1, 2, 3}

print(t_table[1])
print(t_table[2])
print(t_table[3])
--[[
输出是:
true
false
false
1
2
3
--]]

__newindex元方法,可以修改追加索引时的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
t_table = setmetatable({k1 = "v1"}, {
__newindex =
function(t_table, key, value)
rawset(t_table, key, "\"v" .. value .. "\"")
end
})

t_table["k2"] = 2

print(t_table["k1"])
print(t_table["k2"])

--[[
输出是:
v1
"v2"
--]]

其它的规则:
|元表|运算符|
|:–:|:–:|
|add|+|
|
sub|-|
|mul|*|
|
div|/|
|mod|%|
|
unm|-(相反数)|
|concat|..|
|
eq|==|
|lt|<|
|
le|<=|

add和tostring:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
t_table = setmetatable({1, 2, 3}, {__add =
function(t_table, data)
t_table[#t_table + 1] = data
return t_table
end,
__tostring =
function(t_table)
local res = ""
for _, v in pairs(t_table) do
res = res .. v
end
return res
end
})
print(t_table)
print(t_table + 4)
--[[
输出:
123
1234
--]]

需要注意的是,关系运算符,比较的两端,元表必须相同。如果只有一方有元表,另一方没有,又或者是两方拥有不同的元表,会导致比较报错:

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
a_metatable =
{
__gt = function(a_table, b_table)
return a_table[1] > b_table[1]
end,
__lt = function(a_table, b_table)
return a_table[1] < b_table[1]
end
}
b_metatable =
{
__gt = function(a_table, b_table)
return b_table[1] > a_table[1]
end,
__lt = function(a_table, b_table)
return b_table[1] < a_table[1]
end
}
a_table = setmetatable({0}, a_metatable)
b_table = setmetatable({1}, b_metatable)
print(a_table<b_table)
--[[
报错内容是:
attempt to compare two table values
--]]

重写取反操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
a_metatable =
{
__unm = function(a_table)
local _temp_table = {}
for i = #a_table, 1, -1 do
_temp_table[#a_table + 1 - i] = a_table[i]
end
return _temp_table
end
}
p_metatable =
{
__tostring =
function(a_table)
local res = ""
for _, v in pairs(a_table) do
res = res .. v
end
return res
end
}
a_table = setmetatable({1, 2, 3}, a_metatable)
b_table = setmetatable(-a_table, p_metatable)
print(b_table)

__call比较有意思,可以让table可以变得和函数一样可以执行。因此可以写成如下的极其过分的形式:

1
2
3
4
5
6
7
8
9
10
function_content =
{
__call = function(a_table, a_data)
print(a_data)
end
}


func_print = setmetatable({}, function_content)
b = func_print(4)

协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function sleep(n)
local t0 = os.clock()
while os.clock() - t0 <= n do end
end
-- 这里的sleep会占用大量资源 正常逻辑不能使用

test_coroutine = coroutine.create(
function()
for i = 1, 10, 1 do
print(i)
sleep(0.1)
coroutine.yield()
end
end
)

function do_cro(co)
while(coroutine.status(co) ~="dead") do
coroutine.resume(co)
end
end

do_cro(test_coroutine)

面向对象

用table实现了类与继承

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
-- 类
-- 类管理器
CClassManager = {}
function CClassManager.ctor(cls, ...)
local this = {}
setmetatable(this, cls)
cls.__index = cls -- 将元表的__index设为自身,访问表的属性不存在时会搜索元表
cls.init(this, ...)
return this
end

function CClassManager.VInit(self, ...)
end

function CClassManager.init(self, ...)
self:VInit(...)
end

function CClassManager.dtor(cls)
-- do sth
end


-- 通过继承实现的类的定义
ClassT = CClassManager:ctor()
function ClassT.VInit(self, word)
self.word = word
end

function ClassT.say(self)
print(self.word)
end

test = ClassT:ctor("B")
test:say()
test:dtor()
test:say()

EFK日志分析系统的搭建

作者 Anqi Zhao
2021年5月7日 01:22

简介

EFK 是三个开源软件的缩写,Elasticsearch,FileBeat,Kibana。其中 ELasticsearch 负责日志分析和存储,FileBeat 负责日志收集,Kibana 负责界面展示。它们之间互相配合使用,完美衔接,高效的满足了很多场合的应用,是目前主流的一种日志分析系统解决方案。

EFK 和 ELK 只有一个区别, 收集日志的组件由 Logstash 替换成了 FileBeat,因为 Filebeat 相对于 Logstash 来说有2个好处:
1.侵入低,无需修改 elasticsearch 和 kibana 的配置;
2.性能高,IO 占用率比 logstash 小太多

当然 Logstash 相比于 FileBeat 也有一定的优势,比如 Logstash 对于日志的格式化处理能力,FileBeat 只是将日志从日志文件中读取出来,当然如果收集的日志本身是有一定格式的,FileBeat 也可以格式化,但是相对于Logstash 来说,效果差很多。

部署步骤

1 . 安装Docker

2 . 下载所需镜像

1
2
3
docker pull elasticsearch:7.12.0
docker pull kibana:7.12.0
docker pull store/elastic/filebeat:7.12.0

3 . 创建自定义网络

1
docker network create elk_net

4 . 启动Elasticsearch和Kibana

1
2
docker run -d --name elasticsearch --net elk_net -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch:7.12.0
docker run -d --name kibana --net elk_net -p 5601:5601 kibana:7.12.0

如果希望使用中文界面(中文化不完全,不推荐使用),可以进入kibana的容器里,修改排至文件,在现有配置文件中加一行中文相关的配置即可:

1
2
3
4
5
6
server.name: kibana
server.host: "0"
elasticsearch.hosts: [ "http://elasticsearch:9200" ]
monitoring.ui.container.elasticsearch.enabled: true
// 设置语言为中文↓
i18n.locale: "zh-CN"

进入docker容器的指令为:

1
docker exec -it kibana /bin/bash

5 . 配置日志解析规则
日志解析的规则,是存放在Elasticsearch中的。只要通过curl命令即可实现向es中插入数据。
新建json文件:

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
{
"grok": {
"field": "message",
"patterns": [
"%{TIMESTAMP_ISO8601:Time}\\s\\s*\\[%{DATA:ServerType}\\]*\\[%{DATA:ServerID}\\]*\\[%{LOGLEVEL:Level}\\s+\\]*\\[%{DATA:Func}\\]%{GREEDYDATA:Log}"
],
"pattern_definitions": {
"ALL_CODE": "(.|\\n)*"
}
}
},
{
"date": {
"field": "Time",
"formats": [
"ISO8601"
],
"target_field": "@timestamp",
"timezone": "Asia/Shanghai"
}
},
{
"remove": {
"field": "message"
}
},
{
"remove": {
"field": "Time"
}
}

patterns部分为解析每行日志的正则表达式,pattern_definitions是处理换行标志。
测试可以使用kibana自带的grok测试工具,在设置界面会有。
下方的date部分,是通过对上方产生的Time字段,存放到默认的时间戳上,用于处理排序。
处理完成后,把多余的字段删除,特别是输入的message,有利于缩减入库数据的量。
需要注意的是,json文件里的反斜杠一定要进行转义。

执行命令:

1
curl -H "Content-Type: application/json" -XPUT 'http://192.168.3.67:9200/_ingest/pipeline/lmyzpip' -d@/home/docker/pipline/pip.json

除此之外,可以登录Kibana界面,通过可视化界面配置解析。

  • 点击左侧的三条线按钮,进入Stack Management->Ingest Node Pipelines->Create a pipeline
  • 输入名称和描述
  • 点击下方Add a processor,增加处理方式
  • 填写相关参数,点击Add保存

右侧Add documents可以填写测试用例,测试相关配置是否正确(通过直接写入ES的配置也可以在此测试)。

测试数据的格式为:

1
2
3
4
5
6
[
{"_index":"index",
"_id":"id",
"_source":{"message":"2021-04-29T15:42:05.000000 [Game][4][DEBUG ][CNetManager::_SendServerConfigRequest]Request for server-config, Stream-{Game: 4-0}. ServerConfig<2> DBConfig<0> RedisConfig<0>"}
}
]

6 . 安装filebeat
新增配置文件filebeat.docker.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#=========================== Filebeat inputs ==============
filebeat.inputs:

- type: log

enabled: true
##配置你要收集的日志目录,可以配置多个目录
paths:
- /home/docker/logs/*.log

## 设置kibana的地址,开始filebeat的可视化
setup.kibana.host: "http://kibana:5601"
setup.dashboards.enabled: true
#-------------------------- Elasticsearch output ---------
output.elasticsearch:
hosts: ["elasticsearch:9200"]
pipeline: "lmyzpip"

setup.template.name: "my-log"
setup.template.pattern: "my-log-*"
json.keys_under_root: false
json.overwrite_keys: true

其中需要注意的是,如果filebeat和前面的EK两个东西,没有部署在同一台机器上,需要把容器名改成对应的ip地址。

运行Filebeat:

1
docker run -d --net elk_net -v /home/docker/filebeat/filebeat.docker.yml:/usr/share/filebeat/filebeat.yml -v /home/docker/logs/:/home/docker/logs/ --link elasticsearch:elasticsearch --link kibana:kibana  --name filebeat docker.io/store/elastic/filebeat:7.12.0

使用贝塞尔曲线实现道具随机飞动效果

作者 Anqi Zhao
2020年12月13日 05:03

工作中,遇到了一个需求是要实现获得道具和货币的飞动效果:

  1. 根据道具的多少生成不同数目的道具
  2. 货币首先要有一个炸开的效果,然后向一个点汇集;道具从原位置沿直线飞过去
  3. balabala

这里只讨论货币飞行路线的问题。

贝塞尔曲线(Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。PS中的钢笔工具就是使用贝塞尔曲线来绘制矢量曲线的。

这里使用简单的,四个点确定的贝塞尔曲线来实现飞行轨迹。首先p0和p3分别是移动的起点和终点。为了模拟爆炸效果,可以通过随机选取以p0为圆心,长度为R的圆上的点,调整曲线的第一次弯折位置实现。这一点定为P1。最后,根据P1点和P0点之间的关系,进行x轴或者y轴的修正,实现曲线平滑延申到p3,这一点是p2点。

p1点(爆炸点)的获取方式:

1
2
3
4
5
private static Vector3 GetRandomPosition(Vector3 vec)
{
float sita = UnityEngine.Random.Range(-Mathf.PI, Mathf.PI);
return new Vector3(vec.x + Mathf.Cos(sita) * Screen.width / 8, vec.y + Mathf.Sin(sita) * Screen.width / 8);
}

p2点(修正点)的获取方式:

1
2
3
4
private static Vector3 GetFixedPoint(Vector3 start, Vector3 end)
{
return new Vector3(start.x + 3*(end.x - start.x)/4, start.y + 3 * (end.y - start.y) / 4 + p1.y > start.y ? Screen.height / 15 : - Screen.height / 15);
}

根据已知的四个点,实现物体延贝塞尔曲线移动的效果。

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
/// <summary>
/// 延贝塞尔曲线移动
/// </summary>
private static IEnumerator MoveBezier(GComponent gcom, float time, Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3)
{
for (float t = 0; t < time; t += Time.deltaTime)
{
gcom.position = CalculateCubicBezierPoint(t / time, p0, p1, p2, p3);
yield return 0;
}
gcom.position = p3;
gcom.Dispose();
}

/// <summary>
/// 计算贝塞尔曲线点的坐标
/// </summary>
private static Vector3 CalculateCubicBezierPoint(float t, Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3)
{
float u = 1 - t;
float tt = t * t;
float uu = u * u;
float uuu = uu * u;
float ttt = tt * t;

Vector3 p = uuu * p0;
p += 3 * uu * t * p1;
p += 3 * u * tt * p2;
p += ttt * p3;

return p;
}

震惊,JS不加分号会造成错误!?

作者 Anqi Zhao
2019年12月2日 08:12

在之前的工作中,我遇到了一个奇怪的问题。明明在语法上没有问题,找同事也看了,但是程序依旧会产生奇怪的错误。最后通过一步一步断点,定为了错误位置,才找到了造成这个错误的原因———在一个不需要分号的语言中,句末不加分号居然报错了。

不卖关子了,这个错误是多返回值函数造成了对上一句值的影响。下面举个例子:

1
2
3
4
5
6
7
8
9
10
function func() {
return [1, 2]
}

let b = 0
let c = 0
let a = 3
[b, c] = func();
console.log("a is :" + a)
console.log("b is :" + b + " c is :" + c)

运行的结果将会是这样:

a is :1,2
b is :0 c is :0

很容易看到,我们函数的返回值给了上一行的a。这是因为我们的编译器将代码认为了:

1
let a = 3 ,[b, c] = func();

避免这种情况,还是要多加分号吧——虽然它并不会报错。

Linux升级Python

作者 Anqi Zhao
2019年11月28日 20:12

11月底,腾讯云搞了一波双11返场活动,我买了三年的服务器。

和买了新的电脑或者做了新系统一样,得先把生产环境搞好。

距离Python2.x停止维护大概只有5个月了吧,所以第一要务是升级Python的版本。但是yum是依赖Python2的,所以升级还是会有一些顾虑的。下面是升级的过程:

下载、解压

1
2
wget https://www.python.org/ftp/python/3.8.0/Python-3.8.0.tgz
tar -zxvf Python-3.8.0.tgz

安装编译环境

1
2
yum install make build-essential libssl-dev zlib1g-dev libbz2-dev
yum install zlib

编译、安装

1
2
3
4
5
6
yum install -y make build-essential libssl-dev zlib1g-dev libbz2-dev
yum -y install zlib

cd Python-3.8.0
./configure
make && make install

备份与配置

1
2
mv /usr/bin/python /usr/bin/python.bak
ln -s /usr/local/bin/python3 /usr/bin/python

保证yum可用

将下面文件中的配置修改为Python2.x版本的路径。

1
2
/usr/bin/yum
/usr/libexec/urlgrabber-ext-down

安装pip

pip需要依赖setuptools,所以要先安装setuptools。

1
2
3
4
5
6
7
8
9
10
11
wget https://files.pythonhosted.org/packages/ce/1d/96320b9784b04943c924a9f1c6fa49124a1542039ce098a5f9a369227bad/setuptools-42.0.1.zip
unzip setuptools-42.0.1.zip
cd setuptools-42.0.1
python setup.py build
python setup.py install

wget https://files.pythonhosted.org/packages/ce/ea/9b445176a65ae4ba22dce1d93e4b5fe182f953df71a145f557cffaffc1bf/pip-19.3.1.tar.gz
tar -zxvf pip-19.3.1.tar.gz
cd pip-19.3.1
python setup.py build
python setup.py install

参考资料

https://www.cnblogs.com/zhangym/p/6226435.html
https://www.cnblogs.com/fjping0606/p/9156344.html
https://www.cnblogs.com/fyly/p/11112169.html

Github图床工具

作者 Anqi Zhao
2019年11月18日 02:32

这是一个上传图片到github的工具,目前还不是很成熟,不过已经可以实现压缩并上传图片的目的了,对于写博客来说已经够用了。

使用步骤

  1. 创建todo目录
  2. 创建pic目录,在github上创建一个空项目,然后pull到这里
  3. 修改配置文件,将git_url修改为上一步新建的项目
  4. 将图片放到todo文件夹下
  5. 执行工具

需要注意的是,因为图床需要一个git目录,同时代码也需要。在使用部分git管理工具时会禁止这种目录的嵌套。因此最好将代码独立运行。可以自己打包,也可以使用我打好的可执行程序。

在这里需要提一句,一般如果只想要下载github上项目的一个目录或者一个文件,可以使用svn进行下载。将文件路径中的文件名/master替换为trunk即可使用下载。

经过配置之后,以后只需要将图片放到todo目录下,执行脚本即可。

编译时需要安装以下运行库

1
2
3
pip install pyyaml
pip install gitpython
pip install pillow

更新文档

Ver.0.2 2019-11-17 提交编译的可执行程序;增加配置文件

Ver.0.1 2019-11-12 提交项目

JS使用replace()函数全部替换

作者 Anqi Zhao
2019年11月17日 06:03

在处理爬虫爬取下来的数据时,遇到了在文字中出现了经过转义的换行符,在文中显示出了\n,很影响观赏效果。因此,我对内容做了处理。

但是在刷库的过程中,我发现,我总不能一次处理完所有的数据。后来发现是JavaScript的Replace函数的问题,这个函数默认只能替换第一个匹配到的项目。如果需要处理全部的,需要使用正则表达式:

1
string.replace(/\\n/g, "\n")

除此之外,下面是一些处理爬取内容常用操作,包含了html的转义:

1
2
3
4
5
6
7
8
9
10
string.replace(/&nbsp;/g, ' ')
string.replace(/&lt;/g, '<')
string.replace(/&gt;/g, '>')
string.replace(/&amp;/g, '&')
string.replace(/&quot;/g, '"')
string.replace(/&#x3D;/g, '=')
string.replace(/\[.*?\]/g,'')
string.replace("\\n","\n")
string.replace("\\t","")
string.replace("\\r","")

JS使用Splice()函数操作数组

作者 Anqi Zhao
2019年11月17日 05:59

在js的使用过程中,有一次需要对数组进行各种操作,一时间迫使我想要去使用链表。后来通过查阅资料,总结了下面的一些方法,主要使用了splice()函数。

下面的方法主要是使用下标进行操作。如果是用值的话,可以通过indexOf()函数来获取下标。若不存在则返回-1。

值交换

1
2
3
4
function swap_arr(a_list, index1, index2) {
a_list[index1] = a_list.splice(index2, 1, a_list[index1])[0];
return a_list;
}

置顶

1
2
3
4
5
function up_arr(a_list, index){
    if(index!=0 && index!=-1){
a_list[index] = a_list.splice(index-1, 1, a_list[index])[0];
    }
}

下移

1
2
3
4
5
function down_arr(a_list, index) {
    if(index!=a_list.length-1 && index!=-1){
a_list[index] = a_list.splice(index+1, 1, a_list[index])[0];
    }
}

插入

1
2
3
4
5
6
7
8
9
10
11
function ins_arr(a_list, index, a_data) {
if(index!=-1){
if (a_list.indexOf(a_data)==-1) {
a_list.splice(index+1, 0, a_data);
return true;
}
else {
return false;
}
}
}

在顶部插入

1
2
3
function topins_arr(a_list, a_data) {
a_list.splice(0, 0, a_data)
}

删除元素

1
2
3
4
5
6
7
8
function del_arr(a_list, a_data_list) {
for (const ele of a_data_list) {
let index = a_list.indexOf(ele);
if(index!=-1){
a_list.splice(index, 1);
}
}
}

当你的程序连接Mysql然后崩溃时

作者 Anqi Zhao
2019年11月17日 05:43

之前写过一个监控mysql数据库更新状态的预警程序,总是莫名其妙的报一个连接错误的错,然后程序死掉。后来在系统趋于稳定之后,我就没再继续维护这个工具了。

但是最近我在写另一个工具时,遇到了一个奇怪的问题,就是:tick总在27000多左右的时候崩溃。

我进行了一系列的猜测,比如tick的代码,或者是逻辑有问题,最后我把思路放在了之前遇到的这个错误上。查阅资料后发现,MySQL数据库在连接之后,如果超过一个设定的时间戳之后,会断开。这个值叫WAIT_TIMEOUT,默认值是28800,也就是说如果连上MySQL数据库之后,8小时内没有进行操作,这个连接便会断开。

网上很多连接MySQL数据库的代码没有处理过超时连接的问题,就连JS的官方代码好像也是在17年之后才更新的。以往这个问题,大家都是通过修改这个值来进行规避的。比如改成300天。修改有两种方式,一种是修改配置文件,这样在启动时便会使用这个配置;另一种是修改这个值,或者全局,或者当次生效。

下面是我在使用JavaScript语言链接MySQL数据库时,处理超时重连问题的代码:

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
this.config = {
"host": "x.x.x.x",
"port": xxxx,
"user": "root",
"password": "pass",
"database": "name""
}

async connect() {
let self = this;
console.log("connect mysql success with", JSON.stringify(this.config))
// 创建连接
this.db_mysql = mysql.createConnection(
this.config
);
// 连接数据库
await this.db_mysql.connect();
// 错误处理
this.db_mysql.on('error', function(err) {
if (err) {
if (err.code === 'PROTOCOL_CONNECTION_LOST') {
// 处理超时
console.warning("start reconnect mysql");
self.connect();
} else {
console.error(err.stack || err);
console.warning("start reconnect mysql");
self.connect();
}
}
});
}

安卓应用闪屏

作者 Anqi Zhao
2019年11月17日 05:19

去年在接入安卓SDK时,会有部分渠道有要求手写闪屏的情况,下面是当时的笔记,这只是最简单的一种方法。

参考资料:

很好的例子:

https://www.jianshu.com/p/a609f510b19a

https://blog.csdn.net/l799069596/article/details/47094731

安卓动画:https://blog.csdn.net/IO_Field/article/details/53101499

背景:

除去游戏本身的闪屏之外,有的渠道会要求,有额外的渠道闪屏。为了使用一套资源出不同渠道包,我们可以对接渠道的AS工程进行处理,单独设置闪屏。

首先,创建一个闪屏Activity,为你的主Activity,这样在游戏的一开始你就可以看到闪屏了。

这里需要注意的是,你原先的Activit也需要在Manifest中注册打开日志,否则在打包的时候会找不到,报错:

https://blog.csdn.net/qq_28301007/article/details/52265775

1
<activity android:name="...Activity"/>

下面是主Activity,也就是闪屏Activity的代码,需要根据AS的提示import缺少的部分。

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
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.view.WindowManager;
import android.widget.ImageView;

public class SplashActivity extends Activity {
private final int TimeAnimDurning = 2000;
private int displayDeviceWidth;
ImageView iv_splash;
private ObjectAnimator objAnim;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
displayDeviceWidth = getResources().getDisplayMetrics().widthPixels;
setContentView(R.layout.activity_splash);
iv_splash = (ImageView) findViewById(R.id.splash);
objAnim = ObjectAnimator.ofFloat(iv_splash,"alpha",1,0);
objAnim.setDuration(TimeAnimDurning);

new Handler().postDelayed(new Runnable() {
@Override
public void run() {
objAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if((int)animation.getAnimatedFraction() == 1){
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
startActivity(new Intent(SplashActivity.this , .YouActivity.class));
finish();
return ;
}
}
});
objAnim.start();
}

}, 2000);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
tools:context="com.unity3d.player.SplashActivity">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible">

<ImageView
android:id="@+id/splash"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:scaleType="fitXY"
android:src="@drawable/splash" />

</LinearLayout>
</RelativeLayout>

在上面控制闪屏格式的style.xml中,可以看到闪屏的背景色设置为了白色。这里有一些常用的颜色xml:https://blog.csdn.net/sundaysunshine/article/details/53509854

除了这两处之外,还需要根据style.xml中的配置,放好闪屏图片,设置闪屏背景。

整个闪屏的原理就是创建一个动画,在动画播放完成之后,去执行一个新的activity。在补全报错的部分之后,还是有一些细节部分需要注意的。

首先是结束时间的判定。判定时机总共有两种,一种是获取动画的进度,就像这里的例子,使用(int)animation.getAnimatedFraction()进行获取一个从0~1的数,来表示目前的动画的播放进度。除此之外还可以获取播放的时间,这个函数是:getAnimatedValue(),它可以获取属性的当前值。使用这两个函数可以很方便地控制动画的时间和动作。

除此之外,在调起另一个Activity之后,我结束了这个Activity。这是因为如果使用默认的LaunchMode,在重新唤醒应用时,闪屏会再次启动,然后走完动画,应用重启。这就造成了应用无法关闭的状况,只能后台强制杀掉。解决办法就是让闪屏只执行一次。

安卓各渠道SDK接入体验

作者 Anqi Zhao
2019年11月13日 07:00

去年的这个时候,我在忙于接入各种SDK。接渠道SDK,是一件十分薛定谔的事情。你觉得很容易,的确很容易,但是,也很容易遇到问题。然后我就陷入了长期的自闭状态,再加上偷懒,然后博客就断更了一年。现在回头来回忆一下,去年的这个时候,接入SDK时的那些体验。

1.酷派

充值之后,服务器收不到消息,对接也没有人。

现在看来很明显,已然黄了。

2.应用宝

传说中的5000人大群只有2个技术的大渠道。每天上午问问题会施舍你两句,下午是肯定不会回答问题的,团建能团建半个月。这个渠道我是使用聚合sdk接入的。需要注意的是,接入时有测试阶段,和正式阶段之分,游戏货币名不能修改,同时还必须接入腾讯的信鸽推送SDK,否则无法过审,手动接入成本极高。

3.金立

高版本会造成初次进入闪退,主动获取权限也不行,必须低版本编译。
华为手机会出现渲染错误,游戏变成紫红色。
需要安装支付插件。
现在也应该没有接入的必要了。

4.华为

相当棒的渠道,文档详细,对接起来体验也很不错。充值错误的时候,每一步骤,原因都会有显示。
但是不支持第三方工具接入。
需要安装支付插件。

5.魅族

商品id配置不明,会出现莫名的变动,很不靠谱。
支付回调生效需要1天的时间,需要对商品进行映射,对接很麻烦的渠道。
需要安装支付插件。

6.360

包体最大,足足有50多k的方法数,不分包就是死。

7.百度

方法数排名第二,仅次于360。
高版本编译会无法使用闪屏。
提审体验极差。说好的SDK不强用更,但是等到提审后告诉你不合格。

8.联想

商品id为自动生成,需要做好映射。
AnySDK接入需要注意参数顺序。

9.UC

无法使用第三方工具进行接入。
闪屏比较蛋疼,在sdk初始化时自动播放。首次运行时无法正常显示,时机不一定,容易和应用闪屏覆盖。

10.OPPO、VIVO

无法使用第三方工具进行接入。
需要安装支付插件。

11.小七

文档描述不明确,注意对登陆回调的处理方式,注重切换账号的测试。
SDK的Manifest中,最高宽高比设置为2.2。如果游戏中有对这个参数进行修改,需要进行统一。

12.拇指玩

sdk默认背景为透明,会造成有些版本的手机唤起sdk时会显示桌面为背景。或者是切换到主屏后,再回来,只有单独的sdk页面。
解决方式是把style.xml中的windowIsTranslucent值置为为false。
回调处理方式与其他sdk略有不同,Log与执行功能部分的代码进行了分离。
目前版本,无法使用qq登陆等方式,在登陆界面仍没有去掉该入口。

其他:

在运营过程中,小米、UC、应用宝和魅族会不同频率出现无法登录的问题,属正常现象,是他们的SDK服务器抽风了,会报一些很可怕的错误,比如应用不存在,应用id无效之类的。这时候只需要稳定好用户的心态即可,没有任何解决办法。

某微信爬虫工具多开方案

作者 Anqi Zhao
2019年11月13日 05:15

之前因为需求找到了这个超级好用的微信爬虫工具https://github.com/striver-ing/wechat-spider,目前已经开源。工具可以很方便地实现爬取微信文章,获取点赞、评论等功能。

最近,微信针对文章历史接口做了调整:PC版限制了爬取的次数,访问间隔应该控制在8分钟以上,移动端则是在两个月前直接干掉了这个功能。文章评论则没有改变。因此,这个工具目前最好的使用方式就是多开,分别爬不同的文章,再单开一个用来爬需要爬的评论。

工具的具体使用方法在原工程中都有提到,这里就不再赘述了。

在工具的使用方法中,我们知道,作者是使用全局代理,将所有的https消息都强制走了本机的8080端口,然后通过Python的mitmproxy来截取消息内容来实现的这个工具。那么,我可以使用局部代理,将制定的微信客户端,走制定端口,即可实现工具的多开。

有了这个思路,那么我们就只需要解决两个问题:

  1. 微信的多开
  2. 多个微信走多个局部代理

这里先说一下,我们无法使用微信PC客户端自带的代理功能。因为一旦这个功能开启,微信的所有链接都将进行加密,你得到的只会是一个格式如:https://xxx.xxx.xx.xx/mmtls/xxxxxx的加密链接。

微信的多开

微信多开的实现比较简单,直接使用bat脚本打开多个微信即可。需要注意的是,要以管理员模式运行。

1
2
3
4
5
6
7
@echo off
start /d "C:\Program Files (x86)\Tencent\WeChat\" WeChat.exe
start /d "C:\Program Files (x86)\Tencent\WeChat\" WeChat.exe
start /d "C:\Program Files (x86)\Tencent\WeChat\" WeChat.exe
start /d "C:\Program Files (x86)\Tencent\WeChat\" WeChat.exe
start /d "C:\Program Files (x86)\Tencent\WeChat\" WeChat.exe
exit

这样虽然可以打开多个微信,但是在设置局部代理时,工具无法区相同路径下的相同可执行文件。因此,我们需要把微信客户端拷贝多份,以应对后面的步骤。

1
2
3
4
5
6
7
@echo off
start /d "C:\Program Files (x86)\Tencent\WeChat\" WeChat.exe
start /d "C:\Program Files (x86)\Tencent\WeChatb\" WeChatb.exe
start /d "C:\Program Files (x86)\Tencent\WeChatc\" WeChatc.exe
start /d "C:\Program Files (x86)\Tencent\WeChatd\" WeChatd.exe
start /d "C:\Program Files (x86)\Tencent\WeChate\" WeChate.exe
exit

需要注意的是,这样配置之后的脚本将不一定百分百执行成功,可能只打开一个客户端。一般第二次即可执行成功。

局部代理

这里,我使用了Proxifier工具,进行局部代理。这个工具很容易获取,x度上很容易就可以获取到免费破解汉化的版本。

安装之后,我们开始配置工作。

1.配置文件->代理服务器 这里地址填本机127.0.0.1,端口填你想要转发的端口,比如8080,8081,协议类型是HTTPS。你想开多少,就填多少个。

图1
图2

2.配置文件->代理规则 这里我们把默认的全局代理给关闭,双击条目,将“是否有效”取消勾选即可。然后添加微信的代理规则,点击添加,名称随意,应用程序浏览到微信的exe文件,目标主机清空,端口清空,然后在最下方的动作中选择你上一步配置的一个端口。那么这个路径下的客户端执行时,便会代理到这个端口下了。

图3
图4

微信多开之后,我们会发现多开的这几个的微信的进程名是一样的,无法进行区分。这时候可以在任务管理器中使用切换到、最小化等功能确定哪个窗口是哪个进程,转到本地文件来确定他是哪个目录下的。在开多个的时候,一定要注意区分,以防登错账号,影响爬取。

爬虫的配置文件中,我们最好使用不同的mysql数据库,以免产影响。当然,如果你修改了原工具的代码,那就另当别论了。

U3D问题总结(六) 优化

作者 Anqi Zhao
2019年10月30日 07:00

请简述GC(垃圾回收)产生的原因,并描述如何避免(?

GC回收堆上的内存
避免:1.减少new产生对象的次数
2.使用公用的对象(静态成员)
3.将String换为StringBuilder

如何优化内存?

1.压缩自带类库;
2.将暂时不用的以后还需要使用的物体隐藏起来而不是直接Destroy掉;
3.释放AssetBundle占用的资源;
4.降低模型的片面数,降低模型的骨骼数量,降低贴图的大小;
5.使用光照贴图,使用多层次细节(LOD),使用着色器(Shader),使用预设(Prefab)。
6.代码中少产生临时变量

UNITY3d在移动设备上的一些优化资源的方法

1.使用assetbundle,实现资源分离和共享,将内存控制到200m之内,同时也可以实现资源的在线更新
2.顶点数对渲染无论是cpu还是gpu都是压力最大的贡献者,降低顶点数到8万以下,fps稳定到了30帧左右
3.只使用一盏动态光,不是用阴影,不使用光照探头
粒子系统是cpu上的大头
4.剪裁粒子系统
5.合并同时出现的粒子系统
6.自己实现轻量级的粒子系统
animator也是一个效率奇差的地方
7.把不需要跟骨骼动画和动作过渡的地方全部使用animation,控制骨骼数量在30根以下
8.animator出视野不更新
9.删除无意义的animator
10.animator的初始化很耗时(粒子上能不能尽量不用animator)
11.除主角外都不要跟骨骼运动apply root motion
12.绝对禁止掉那些不带刚体带包围盒的物体(static collider )运动
NUGI的代码效率很差,基本上runtime的时候对cpu的贡献和render不相上下
13每帧递归的计算finalalpha改为只有初始化和变动时计算
14去掉法线计算
15不要每帧计算viewsize 和windowsize
16filldrawcall时构建顶点缓存使用array.copy
17.代码剪裁:使用strip level ,使用.net2.0 subset
18.尽量减少smooth group
19.给美术定一个严格的经过科学验证的美术标准,并在U3D里面配以相应的检查工具

场景优化

1.遮挡剔除(Occlusion Culling) 不显示被遮挡住的物体
2.LOD 根据相机距离远近显示不同精细程度的模型
3.大场景可以调节相机可视距离
4.小物体可以适当隐藏掉
5.使用光照贴图 避免动态实时的进行光照计算,提高效率

UI优化

1.将同一画面图片放到同一图集中
2.图片和文字尽量不要交叉,会产生多余drawcall(相同材质和纹理的UI元素是可以合并的)
3.UI层级尽量不要重叠太多
4.取消勾选不必要的射线检测RaycastTarget
5.将动态的UI元素和静态的UI元素放在不同的Canvas中,减少canvas网格重构频率

GC优化

1.字符串使用StringBuilder而不是string,stringBuilder在创建时会自动获取一个容量存储并逐渐扩充,string每一次改变都会创建一个新的对象。
2.访问物体tag的时候尽量使用Gameobject.CompareTag(),因为访问物体的tag属性会在堆上额外的分配空间
3.使用对象池缓存大量创建的物体
4.用for代替foreach,foreach每次迭代产生24字节垃圾内存

❌
❌