3.8.3 和 3.8.5 版本适配 16KB 过程记录

Cocos v3.8.3 和 v 3.8.5版本
官方文档( 构建支持 16 KB 设备的 Android 应用)
https://developer.android.com/guide/practices/page-sizes?hl=zh-cn#update-packaging
1、Android Studio 更新到:2025.1.2(按官方文档要求更新到对应版本即可)


2、NDK 升级到: 28.2.13676358 (按官方文档要求更新到对应版本即可)

(1) Cocos红字提示部分不用管
(2) native\engine\android\app\build.gradle 中添加 ndkVersion 依赖项:
android {
ndkVersion “28.2.13676358”
compileSdkVersion PROP_COMPILE_SDK_VERSION.toInteger()
buildToolsVersion PROP_TARGET_SDK_VERSION
ndkPath PROP_NDK_PATH
namespace APPLICATION_ID
.
.
.
3、Android API 已更新到 35(这个很早就已经更新适配了)

4、接下来先构建打包 v3.8.3 版本:
(1) 构建报错1:while ((ALooper_pollAll(_loopTimeOut, nullptr, &events,

解决方案: * NDK 28 变更:ALooper_pollAll 被标注 unavailable(可能忽略唤醒),需改用 ALooper_pollOnce。
源码:
while ((ALooper_pollOnce(_loopTimeOut, nullptr, &events,
reinterpret_cast<void **>(&source))) >= 0) {
// process event
if (source != nullptr) {
source->process(_app, source);
}

        // Exit the game loop when the Activity is destroyed
        if (_app->destroyRequested) {
            break;
        }
    }

(2)构建报错2: xxteaKey.assign(key.begin(), key.end());

(3)构建报错3:const_cast<unsigned char *>(xxteaKey.data()),



jsb_global_init.cpp 文件
解决方案:
1)引入向量容器头文件(Cocos 包装或标准任一都可)
#include “base/std/container/vector.h”
2)static ccstd::vector xxteaKey;
源码位置:
// 原来
//static std::basic_string xxteaKey;
// 替换为
static ccstd::vector xxteaKey;
void jsb_set_xxtea_key(const ccstd::string &key) { //NOLINT
xxteaKey.assign(key.begin(), key.end());
}
3)改两处 xxtea_decrypt 调用参数(删除 const_cast/reinterpret_cast):
uint8_t *data = xxtea_decrypt(fileData.getBytes(),
static_cast<uint32_t>(fileData.getSize()),
xxteaKey.data(),
static_cast<uint32_t>(xxteaKey.size()),
reinterpret_cast<uint32_t *>(&dataLen));
以及
uint8_t *data = xxtea_decrypt(static_cast<uint8_t *>(fileData.getBytes()),
static_cast<uint32_t>(fileData.getSize()),
xxteaKey.data(),
static_cast<uint32_t>(xxteaKey.size()),
&dataLen);
5、替换 V8
(1) 下载官方给的支持16KB的cocos-engine-external:
https://github.com/cocos/cocos-engine-external/tree/v3.8.8
(2) 将下载后的 D:\downloads\cocos-engine-external-3.8.8\android 中如图所示两个文件替换到本地对应的引擎位置:D:\tools\cocos\Creator\3.8.3\resources\resources\3d\engine-native\external\android

更新 arm64-v8a & x86_64 下的libv8_monolith.a
6、适配 V8
(1) cocos 官方修改 PR


如图两个文件修改
native/cocos/core/scene-graph/Node.cpp
native/cocos/platform/android/jni/JniCocosSurfaceView.cpp

(2) 本地引擎 Node.cpp

53 行左右,修改后的代码如下:
#define NODE_SHARED_MEMORY_BYTE_LENGTH (20)
#ifdef clang
#pragma clang diagnostic push
#pragma clang diagnostic ignored “-Winvalid-offsetof”
#endif
static_assert(offsetof(Node, _padding) + sizeof(_padding) - offsetof(Node, _eventMask) == NODE_SHARED_MEMORY_BYTE_LENGTH, “Wrong shared memory size”);
#ifdef clang
#pragma clang diagnostic pop
#endif
_sharedMemoryActor.initialize(&_eventMask, NODE_SHARED_MEMORY_BYTE_LENGTH);
#undef NODE_SHARED_MEMORY_BYTE_LENGTH
_id = idGenerator.getNewId();
if (name.empty()) {
    _name.append("New Node");
} else {
    _name = name;
}
// _eventProcessor = ccnew NodeEventProcessor(this);

}
(3) 本地引擎 JniCocosSurfaceView.cpp



212行左右,修改后的代码如下:
touchEvent.windowId = windowId;
touchEvent.type = cc::TouchEvent::Type::MOVED;

const int size = env->GetArrayLength(ids);
std::vector<jint> id(size);
std::vector<jfloat> x(size);
std::vector<jfloat> y(size);

env->GetIntArrayRegion(ids, 0, size, id.data());
env->GetFloatArrayRegion(xs, 0, size, x.data());
env->GetFloatArrayRegion(ys, 0, size, y.data());

for (int i = 0; i < size; i++) {
    touchEvent.touches.emplace_back(x[i], y[i], id[i]);
}

249行左右:
touchEvent.windowId = windowId;
touchEvent.type = cc::TouchEvent::Type::CANCELLED;
const int size = env->GetArrayLength(ids);
std::vector id(size);
std::vector x(size);
std::vector y(size);

env->GetIntArrayRegion(ids, 0, size, id.data());
env->GetFloatArrayRegion(xs, 0, size, x.data());
env->GetFloatArrayRegion(ys, 0, size, y.data());
for (int i = 0; i < size; i++) {
    touchEvent.touches.emplace_back(x[i], y[i], id[i]);
}

6、构建打包 .apk 进行验证
(1) 使用 python 脚本进行验证,参考的该研发者的内容:
https://blog.csdn.net/gpf1320253667/article/details/151322842
(2) 安装并初始化 color 处理库,让 Windows 正确解析 ANSI:
pip install colorama
(3) 完整 python 脚本内容如下:
#!/usr/bin/env python3

-- coding: utf-8 --

“”"

check_page_alignment_windows.py

A Windows-friendly script (pure Python) that replicates the behavior of the Bash script:

  • Works on .so files directly

  • Works on directories (recursively finds *.so)

  • Works on .apk files by extracting and scanning embedded *.so files

It does NOT require objdump or unzip; it parses ELF headers and APKs with zipfile.

“”"

import os

import sys

import math

import struct

import tempfile

import zipfile

import argparse

from pathlib import Path

from typing import List, Tuple, Optional

============================= Colors / UI ====================================

try:

import colorama

colorama.just_fix_windows_console()

except Exception:

pass

ANSI colors (Windows 10+ supports these in most terminals; otherwise they’ll show raw codes)

RED = “\033[0;31m”

GREEN = “\033[0;32m”

BLUE = “\033[0;34m”

YELLOW= “\033[1;33m”

NC = “\033[0m”

def hrule(width: int = 120) -> str:

return "=" * width

def print_table_header():

print(f"{BLUE}{'Status':<8} {'File Path':<50} {'Architecture':<12} {'Alignment':<12} {'Notes'}{NC}")

print(hrule())

def print_table_row(symbol: str, filepath: str, arch: str, alignment: str, notes: str):

color = GREEN if symbol == "✅" else RED if symbol == "❌" else NC

print(f"{color}{symbol:<8}{NC} {filepath:<50} {arch:<12} {alignment:<12} {notes}")

============================= ELF parsing ====================================

ELF_MAGIC = b"\x7fELF"

ELFCLASS32 = 1

ELFCLASS64 = 2

ELFDATA2LSB = 1

ELFDATA2MSB = 2

PT_LOAD = 1

e_machine values

EM_386 = 3

EM_ARM = 40

EM_X86_64 = 62

EM_AARCH64 = 183

def arch_name(e_machine: int) -> str:

if e_machine == EM_AARCH64:

    return "ARM64"

if e_machine == EM_X86_64:

    return "x86_64"

if e_machine == EM_386:

    return "x86"

if e_machine == EM_ARM:

    return "ARM v7"

return "unknown"

def read_elf_info(path: str) -> Tuple[Optional[int], Optional[int], Optional[int], Optional[int], Optional[int]]:

"""

Returns (elf_class, data_encoding, e_machine, e_phoff, e_phnum) or (None,... ) if not ELF.

"""

with open(path, "rb") as f:

    head = f.read(64)  # enough for both 32/64 headers

    if len(head) < 16 or head[:4] != ELF_MAGIC:

        return (None, None, None, None, None)

    elf_class = head[4]

    data_enc  = head[5]  # 1 little, 2 big

    # e_machine is at offset 18 (0x12), 2 bytes

    if data_enc == ELFDATA2LSB:

        e_machine = struct.unpack_from("<H", head, 18)[0]

    elif data_enc == ELFDATA2MSB:

        e_machine = struct.unpack_from(">H", head, 18)[0]

    else:

        return (None, None, None, None, None)

    # e_phoff, e_phentsize, e_phnum differ by class

    if elf_class == ELFCLASS32:

        # Offsets: e_phoff @ 0x1C (4), e_phentsize @ 0x2A (2), e_phnum @ 0x2C (2)

        f.seek(0)

        ehdr = f.read(52)  # 32-bit ELF header size

        if data_enc == ELFDATA2LSB:

            e_phoff     = struct.unpack_from("<I", ehdr, 0x1C)[0]

            e_phentsize = struct.unpack_from("<H", ehdr, 0x2A)[0]

            e_phnum     = struct.unpack_from("<H", ehdr, 0x2C)[0]

        else:

            e_phoff     = struct.unpack_from(">I", ehdr, 0x1C)[0]

            e_phentsize = struct.unpack_from(">H", ehdr, 0x2A)[0]

            e_phnum     = struct.unpack_from(">H", ehdr, 0x2C)[0]

    elif elf_class == ELFCLASS64:

        # Offsets: e_phoff @ 0x20 (8), e_phentsize @ 0x36 (2), e_phnum @ 0x38 (2)

        f.seek(0)

        ehdr = f.read(64)  # 64-bit ELF header size

        if data_enc == ELFDATA2LSB:

            e_phoff     = struct.unpack_from("<Q", ehdr, 0x20)[0]

            e_phentsize = struct.unpack_from("<H", ehdr, 0x36)[0]

            e_phnum     = struct.unpack_from("<H", ehdr, 0x38)[0]

        else:

            e_phoff     = struct.unpack_from(">Q", ehdr, 0x20)[0]

            e_phentsize = struct.unpack_from(">H", ehdr, 0x36)[0]

            e_phnum     = struct.unpack_from(">H", ehdr, 0x38)[0]

    else:

        return (None, None, None, None, None)

    return (elf_class, data_enc, e_machine, e_phoff, e_phnum)

def max_load_align_exponent(path: str) -> Optional[int]:

"""

Iterate program headers and return max log2(p_align) for PT_LOAD segments.

If p_align is 0, treat as 0 (i.e., 2**0).

"""

info = read_elf_info(path)

if info[0] is None:

    return None

elf_class, data_enc, e_machine, e_phoff, e_phnum = info

if e_phoff is None or e_phnum is None:

    return None

# Program header entry sizes:

if elf_class == ELFCLASS32:

    # p_type(4), p_offset(4), p_vaddr(4), p_paddr(4), p_filesz(4), p_memsz(4), p_flags(4), p_align(4)

    ph_size = 32

    if data_enc == ELFDATA2LSB:

        fmt = "<IIIIIIII"

    else:

        fmt = ">IIIIIIII"

    p_type_idx = 0

    p_align_idx = 7

else:

    # 64-bit: p_type(4), p_flags(4), p_offset(8), p_vaddr(8), p_paddr(8), p_filesz(8), p_memsz(8), p_align(8)

    ph_size = 56

    if data_enc == ELFDATA2LSB:

        fmt = "<IIQQQQQQ"

    else:

        fmt = ">IIQQQQQQ"

    p_type_idx = 0

    p_align_idx = 7

max_exp = None

with open(path, "rb") as f:

    f.seek(e_phoff)

    for _ in range(e_phnum):

        data = f.read(ph_size)

        if len(data) != ph_size:

            break

        fields = struct.unpack(fmt, data)

        p_type = fields[p_type_idx]

        if p_type == PT_LOAD:

            p_align = fields[p_align_idx]

            if p_align == 0:

                exp = 0

            else:

                # p_align can be any number; compute floor(log2(p_align))

                exp = int(math.log2(p_align)) if p_align > 0 else 0

            max_exp = exp if max_exp is None else max(max_exp, exp)

return max_exp

def get_arch(path: str) -> str:

info = read_elf_info(path)

if info[0] is None:

    return "Unknown"

e_machine = info[2]

return arch_name(e_machine)

def is_arm_v7(path: str) -> bool:

info = read_elf_info(path)

return info[0] is not None and info[2] == EM_ARM

============================= Core logic =====================================

Result = Tuple[str, str, str, str, str] # (status_type, notes, symbol, alignment, filepath, arch) - aligns w/ Bash script idea

def check_so_alignment(so_file: str, relative_path: str) -> Result:

if not os.path.isfile(so_file):

    return ("ERROR", "File not found", "❌", "0B", relative_path, "Unknown")

# Detect ELF

with open(so_file, "rb") as f:

    magic = f.read(4)

if magic != ELF_MAGIC:

    return ("ERROR", "Not an ELF file", "❌", "Unknown", relative_path, "Unknown")

arch = get_arch(so_file)

if is_arm_v7(so_file):

    return ("PASS", "ARM v7 - 16KB not required", "✅", "N/A", relative_path, arch)

max_exp = max_load_align_exponent(so_file)

if max_exp is None:

    return ("ERROR", "Unknown alignment", "❌", "Unknown", relative_path, arch)

# Human display for alignment

if max_exp >= 10:

    kb = 1 << (max_exp - 10)

    alignment_disp = f"{kb}KB"

else:

    b = 1 << max_exp

    alignment_disp = f"{b}B"

if max_exp >= 14:

    return ("PASS", ">=16KB aligned", "✅", alignment_disp, relative_path, arch)

else:

    return ("FAIL", "Need 16KB alignment", "❌", alignment_disp, relative_path, arch)

def process_results(results: List[Result]):

total_so = 0

aligned_so = 0

failed_so = 0

passes: List[Tuple[str,str,str,str,str]] = []

fails:  List[Tuple[str,str,str,str,str]] = []

for r in results:

    if not r:

        continue

    total_so += 1

    status_type, notes, symbol, alignment, filepath, arch = r

    if status_type == "PASS":

        aligned_so += 1

        passes.append((symbol, filepath, arch, alignment, notes))

    else:

        failed_so += 1

        fails.append((symbol, filepath, arch, alignment, notes))

if passes:

    print(f"\n{GREEN}✅ Compatible Libraries{NC}")

    print_table_header()

    for symbol, filepath, arch, alignment, notes in passes:

        print_table_row(symbol, filepath, arch, alignment, notes)

if fails:

    print(f"\n{RED}❌ Not Compatible Libraries{NC}")

    print_table_header()

    for symbol, filepath, arch, alignment, notes in fails:

        print_table_row(symbol, filepath, arch, alignment, notes)

print("")

print(hrule())

print(f"{BLUE}📊 SUMMARY{NC}")

print(f"Total .so files: {total_so}")

print(f"16KB Aligned (>=): {GREEN}{aligned_so}{NC}")

print(f"Not aligned: {RED}{failed_so}{NC}")

if failed_so == 0:

    print(f"\n{GREEN}🎉 All .so files are properly aligned!{NC}")

else:

    print(f"\n{RED}⚠️  Some .so files need alignment fixes{NC}")

def process_apk(apk_file: str):

print(f"{BLUE}📱 Processing APK: {apk_file}{NC}")

print("=================================================")

results: List[Result] = []

with tempfile.TemporaryDirectory() as temp_dir:

    try:

        with zipfile.ZipFile(apk_file, 'r') as z:

            z.extractall(temp_dir)

    except Exception as e:

        print(f"{RED}❌ Failed to extract APK file: {e}{NC}")

        return

    for root, _, files in os.walk(temp_dir):

        for name in files:

            if name.lower().endswith(".so"):

                so_path = os.path.join(root, name)

                rel_path = os.path.relpath(so_path, temp_dir)

                results.append(check_so_alignment(so_path, rel_path))

if not results:

    print(f"{YELLOW}⚠️  No .so files found in APK{NC}")

    return

process_results(results)

def process_single_so(so_file: str):

print(f"{BLUE}🔍 Processing single .so file: {so_file}{NC}")

print("=================================================")

result = check_so_alignment(so_file, os.path.basename(so_file))

process_results([result])

def process_directory(directory: str):

print(f"{BLUE}📁 Processing directory: {directory}{NC}")

print("=================================================")

results: List[Result] = []

for root, _, files in os.walk(directory):

    for name in files:

        if name.lower().endswith(".so"):

            so_path = os.path.join(root, name)

            rel_path = os.path.relpath(so_path, directory)

            results.append(check_so_alignment(so_path, rel_path))

if not results:

    print(f"{YELLOW}⚠️  No .so files found in directory{NC}")

    return

process_results(results)

def main():

parser = argparse.ArgumentParser(description="Check .so PT_LOAD alignment (>=16KB) inside ELF files, dirs, or APKs.")

parser.add_argument("input", help="Path to .apk, .so, or a directory")

args = parser.parse_args()

inp = args.input

if not os.path.exists(inp):

    print(f"{RED}❌ Invalid input: not found{NC}")

    sys.exit(1)

if os.path.isfile(inp):

    lower = inp.lower()

    if lower.endswith(".apk"):

        process_apk(inp)

    elif lower.endswith(".so"):

        process_single_so(inp)

    else:

        print(f"{RED}❌ Unsupported file type (expected .apk or .so){NC}")

        sys.exit(1)

elif os.path.isdir(inp):

    process_directory(inp)

else:

    print(f"{RED}❌ Invalid input{NC}")

    sys.exit(1)

if name == “main”:

main()

(4) 运行命令:
python check_page_alignment_python.py xxx.apk
(5) 运行结果:


从截图看,arm64-v8a 的 .so 都“>=16KB aligned”,是符合 16KB 页对齐要求的;“ARM v7 – 16KB not required” 表示这些 32 位 armeabi-v7a 库不受 16KB 对齐强制要求的约束,属于豁免项,显示 N/A 属于预期。
7、使用 zipalign 检测
zipalign 是 Android SDK Build-Tools 自带的命令行工具,用来给 APK 做 ZIP 条目对齐。其作用是让 APK 中未压缩的资源(尤其是 .so)在设备上按页面边界直接映射读取,减少内存拷贝。为支持 16KB 页面大小,需使用带 -P 选项的新版本 zipalign(build-tools 35+)对齐/校验未压缩的 .so 为 16KB ZIP 对齐。参考官方文档:支持 16 KB 页面大小 · 更新打包方式。https://developer.android.com/guide/practices/page-sizes?hl=zh-cn#update-packaging
(1) 用 Android Studio 图形界面:安装 “Android SDK Build-Tools 35.0.0”(或更高)

  • 安装后 zipalign 位于:

  • C:\Users<你的用户名>\AppData\Local\Android\Sdk\build-tools\35.0.0\zipalign.exe

1) 先确认 zipalign 版本支持 -P

.\zipalign.exe -h


应看到 “-P <pagesize_kb>”

2)校验 APK 是否为 16KB ZIP 对齐(把路径换成你的 APK;路径有空格要用引号)

.\zipalign.exe -v -c -P 16 4 “D:\你的路径\xxx.apk”


出现 “Verification successful” 即通过。

3)若未通过:先对齐未签名包,再签名

.\zipalign.exe -f -p 16384 “D:\path\app-release-unsigned.apk” “D:\path\app-release-zipaligned.apk”
.\apksigner.bat sign --ks “D:\path\your.jks” --ks-key-alias yourAlias “D:\path\app-release-zipaligned.apk”

8、3.8.5 版本适配 16KB
(1) 开始步骤和 3.8.3 一致,3.8.5 引擎需要修改两个文件
1)AndroidPlatform.cpp



856行左右:将 ALooper_pollAll 改为 ALooper_pollOnce
2)jsb_global_init.cpp

52行左右:
将 static std::basic_string xxteaKey; 改为 static std::vector xxteaKey;
(2) 16 KB 进行检测
1)zipalign 检测(直接将 apk 拖进来即可)
.\zipalign.exe -v -c -P 16 4 D:\你的路径\xxx.apk

2) python 脚本检测
python check_page_alignment_python.py xxx.apk

6赞

过两天要用。先mark下。

先mark下

Mark it first

第2步里我是改成了static ccstd::vector<unsigned char> xxteaKey; 要不有报错

1赞

3.8.5版本吗?你之前是不是改动过其他地方?


这个地方的删除reinterpret_cast 是说删除



这6个吗?


在修改JiCocosSurfaceview.cpp中的249行左右的这个id,x,y 会报错吗?

应该是报错行替换成这个

,你上面的改完下面的对应就报红了,应该是只改一行。

不会会的应该,你参考官方的 pr 修改:

确实,我说怎么报错呢

3.8.5 改完后是这样: static std::vector xxteaKey; 有报错嘛?


您好,我用3.8.5又重新按照上面的步骤改完了,遇到了这个问题,谁遇到了吗?

3.8.3会有报错,改成static ccstd::vector xxteaKey; 才可以,在Androidstudio里改完可以直接看到有没有报错的,不需要构建也能看到,如果有红线就说明会报错


这个是3.8.5我改完的,根据3.8.8里的源码看着改完,然后就没报错了

3.8.3是这样改的:


static ccstd::vector xxteaKey; 这个是 3.8.5 的改法

这样引入会报错

图上的和你说的3.8.5的改法是完全一样的啊,你没看出来吗。static ccstd::vector xxteaKey

是不是因为 vector 是模板类,需要指定元素类型。?