ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

StoneAgeMobileApp双端源码解析:共享C++核心与构建实践

StoneAgeMobileApp双端源码解析:共享C++核心与构建实践 简介面向Android与iOS开发者的跨平台源码包 StoneAgeMobileApp以“石器时代”为题材提供了一套完整可参考的移动端工程实践。资源定位于系统开源方向适合移动开发初学者分析项目结构也适合中高级开发者研究跨平台代码组织与底层库集成方式。压缩包共10335个文件约37.05MB其中以C/C头文件与实现2662个h、1826个c、324个cpp、iOS工程文件pbxproj、xcodeproj、Android相关配置java、gradle以及HTML文档和PNG资源为主同时包含大量构建脚本am、mk、sh与第三方库源码便于理解从编译配置到UI资源的完整链路。目前已有1064人学习下载。通过阅读源代码可以学习到Android四大组件与iOS UIKit/SwiftUI的界面构建方式掌握跨平台框架的适配思路还能从预设的工程目录中梳理出模块划分与资源交付逻辑对后续自主开发同类移动应用有直接参考价值。整体内容翔实是一份适合反复研读的开源学习材料。1. 拿到 StoneAgeMobileApp 这套手机端源码先别急着开 Android Studio不少朋友第一次接触 StoneAgeMobileApp是冲着“石器时代手机端 Android iOS 源代码”这几个关键词来的。打开压缩包就习惯性先找 Android 工程、点开 Android Studio 跑一遍再打开 Xcode 看 iOS 那份结果两边界面能起来玩法却对不上。这个标题背后的真实需求通常不是“我要看代码”而是“我要把一套老式游戏客户端改造成能在 Android 和 iOS 上并行维护、并行发布移动应用”。StoneAgeMobileApp 恰恰命名了一类工程结构核心玩法逻辑共享、平台外壳分置。这套思路能不能落地取决于你一开始怎么切分源码。本文就按我做移动端移植时的顺序把源码组织、跨端核心库、构建脚本和排错经验一次讲完新手照着能跑熟手可以避开我踩过的几个大坑。2. 双端源码怎么摊开别再维护“Android 一套、iOS 一套”的老仓库老项目里最要命的结构是AndroidProject/和IOSProject/两个独立 git 仓库。石器时代这类玩法系统复杂战斗、宠物、物品、任务互相引用今天在 Android 里把伤害公式调了明天往往忘记同步到 iOS。StoneAgeMobileApp 要长线维护我的建议是把它整理成“一套核心源、三层壳”的结构。2.1 从“两仓库”变成“一套源、两层壳、共享资源”我一般会把工程整理成下面这个样子StoneAgeMobileApp/ ├── core/ │ ├── include/ │ │ ├── game_core.h │ │ └── exports.h │ └── src/ │ ├── battle.cpp │ ├── pet.cpp │ └── item.cpp ├── assets/ │ ├── source/ │ │ ├── config/ │ │ ├── images/ │ │ └── audio/ │ └── manifest.txt ├── android/ │ ├── app/ │ │ ├── src/main/java/com/stoneage/app/ │ │ ├── src/main/cpp/CMakeLists.txt │ │ └── build.gradle │ └── settings.gradle ├── ios/ │ ├── StoneAgeMobile/ │ │ ├── AppDelegate.swift │ │ ├── GameCoreBridge.mm │ │ └── Resources/ │ └── project.yml └── build_scripts/ ├── copy_assets.sh └── sync_core.shcore/放两端必须行为一致的战斗、宠物、任务状态机逻辑用 C 实现。android/只放 Activity、Fragment、原生 UI 和 JNI 壳工程依赖 Gradle 和 CMake。ios/只放 SwiftUI/UIKit 层与 ObjC 桥接工程建议用 XcodeGen 从project.yml生成。assets/是唯一资源入口图片、音效、配置文件都放source/下构建前用脚本往两个壳里拷贝。这个结构最关键的收益是改动玩法时只改core/Android 和 iOS 重新编译就同时生效。UI 层各自演进不会互相污染。即使你手里的代码已经锁死在两个老仓库里也不用一夜之间重构。常见做法是先把core/目录从两端抽出来让sync_core.sh只同步计算逻辑UI 层继续留在原仓库跑通后再逐步迁移资源。写sync_core.sh时要注意Android 的cpp目录和 iOS 的StoneAgeMobile目录路径不同最好用一个脚本读取两端的文件清单再按清单做镜像同步而不是闭着眼整个目录覆盖。否则某端新增了一个本地头文件下次同步就会被删掉这种坑我遇到不止一次。2.2 资源只维护一份搞定 Android drawable 与 iOS bundle 的差异游戏类 App 的资源量远大于普通应用。石器时代的角色动作帧、地图块、宠物图鉴、音效加起来可能上千个文件。Android 的res/drawable-*要求文件名小写、以字母开头、不能有空格iOS 的Resources则宽容得多。同一个资源入口直接复制到两端Android 编译时会报File name must contain only lowercaseiOS 上却好好的。我选择把原始资源统一放assets/source/用脚本按平台规则拷贝到各自目标目录。一个能直接跑的最小脚本如下#!/usr/bin/env bash # 统一资源入口只维护 assets/source 一份构建前执行 set -euo pipefail SRC_ROOT$(cd $(dirname $0)/.. pwd) SOURCE_DIR$SRC_ROOT/assets/source ANDROID_ASSETS$SRC_ROOT/android/app/src/main/assets IOS_RESOURCES$SRC_ROOT/ios/StoneAgeMobile/Resources # 先清理旧产物避免已删除的资源残留在安装包里 rm -rf $ANDROID_ASSETS $IOS_RESOURCES mkdir -p $ANDROID_ASSETS $IOS_RESOURCES # 拷贝除源文件外的所有资源PSD/AI 只有美术用不进包 rsync -a --exclude*.psd --exclude*.ai --exclude.DS_Store \ $SOURCE_DIR/ $ANDROID_ASSETS/ rsync -a --exclude*.psd --exclude*.ai --exclude.DS_Store \ $SOURCE_DIR/ $IOS_RESOURCES/ # 生成资源清单后续可以用来核对两端是否一致 (cd $SOURCE_DIR find . -type f | sort | shasum) $SRC_ROOT/assets/manifest.txt cp $SRC_ROOT/assets/manifest.txt $ANDROID_ASSETS/manifest.txt cp $SRC_ROOT/assets/manifest.txt $IOS_RESOURCES/manifest.txt echo assets synced这里第一个细节是set -euo pipefail没有它rsync 中途失败脚本还会继续往下走最后打包出来的 App 缺资源查半天查不到原因。第二个细节是清理旧目录而不是往里面覆盖拷贝老项目经常出现图片改名后旧文件残留清空再拷贝能从根上避免。第三个参数是--exclude美术交付的 PSD 源文件不进包否则 final APK 体积会膨胀一倍。注意 Windows 上跑这个脚本需要用 Git Bash如果团队都在 Windows也可以改成用ROBOCOPY /MIR实现同样逻辑但路径分隔符处理起来更麻烦。这个脚本跑完后Android 读取资源用context.assets.open(config/items.json)iOS 读取用Bundle.main.url(forResource: config/items, withExtension: json)两边路径一致才不容易写错。3. 把核心玩法收进 C一份源代码同时喂饱 Android 和 iOS资源能共享后下一步是逻辑。StoneAgeMobileApp 里最不能接受的行为是双端计算出来的战斗结果不一致。比如同一场战斗Android 打完剩 87 点血iOS 打完剩 92 点玩家立刻会去社区发帖说版本有问题。要根治就得把决定性算法从 Java 和 Swift 里挪出来统一放 C。3.1 哪些逻辑适合下沉哪些千万别动不是所有代码都值得搬进core/。我自己的判断标准是跟“数值结果”强相关的放下沉跟“屏幕交互”强相关的留在平台层。下面这个表是我开始做抽取时的分类依据。建议下沉到 C建议留在 Android/iOS战斗伤害公式、暴击判定按钮点击、手势识别、UI 动画宠物成长曲线、经验表查询支付、推送、广告 SDK 调用背包物品排序、堆叠规则图片解码、音效播放、页面跳转任务状态机、每日重置逻辑系统弹窗、权限申请、剪切板操作把 UI 绑进 C 会带来无穷无尽的麻烦。比如某个按钮的点击区域在 Android 上要做成圆形在 iOS 上又做成矩形这类差异如果写进 C最后只能用一堆接口回调往上抛代码比原来还乱。适合下沉的逻辑必须满足两个特点不涉及系统 API输入输出可明确描述。战斗计算就是最典型的例子给它两个角色的攻防属性、技能 ID返回伤害值和命中状态中间不需要读数据库不需要弹 Toast纯函数式最理想。宠物成长、物品排序也类似。抽出来之后Android 用 JNI 调用iOS 用 ObjC 直接调 C本质上都是调用同一个函数体。3.2 Android 的 JNI 绑定和 iOS 的 ObjC 桥接样板Android 侧要先有 Java/Kotlin 的 native 声明。常见做法是建一个GameCore纯静态类package com.stoneage.core; public class GameCore { static { // 模块名要与 CMake 里 add_library 的名字一致 System.loadLibrary(stoneagecore); } // 函数名会按 JNI 规范映射到 C 导出符号 public static native int calcBattleDamage(int attack, int defense); }对应的 C 侧实现#include jni.h #include game_core.h extern C JNIEXPORT jint JNICALL Java_com_stoneage_core_GameCore_calcBattleDamage( JNIEnv *env, jclass clazz, jint attack, jint defense) { // 这里只做转发真正的计算逻辑在 core::Battle::CalcDamage 里 return GameCore::CalcBattleDamage(attack, defense); }这里最容易被忽略的是 JNI 函数名。系统加载libstoneagecore.so后会按Java_包名_类名_方法名去找导出符号包名里的点要替换成下划线。一旦你在 Android Studio 里改了包名这里必须同步改否则运行时只会报UnsatisfiedLinkError。另外extern C不能丢C 编译器会做名字修饰去掉它符号就变成一串乱码Java 侧根本找不到。iOS 侧不需要 JNI因为 Objective-C 文件可以直接调用 C 头文件。我会先包一层 Objective-C 方法给 Swift 调// GameCoreBridge.mm #import GameCoreBridge.h #import game_core.h implementation GameCoreBridge (int)calcBattleDamageWithAttack:(int)attack defense:(int)defense { // 直接调用同一份 C 代码不需要处理 JNI 签名 return GameCore::CalcBattleDamage(attack, defense); } endSwift 侧就变成let damage GameCoreBridge.calcBattleDamage(attack: 120, defense: 80)这套做法比在 Swift 里直接包含 C 头文件舒服因为桥接头文件引入了不少 C 语法噪音Swift 编译器偶尔还会对std::string类型发怵。用 ObjC 做一层隔离后Swift 代码看起来完全像原生 OC 调用类型转换集中在桥接文件里。3.3 export 宏、平台隔离和浮点精度先定规则再动手跨端 C 第一个要解决的问题是符号可见性。Android 上 JNI 函数必须默认导出iOS 上普通 C 函数不需要__attribute__((visibility(default)))也能被链接到但为了安全我会在exports.h里统一定义#if defined(__ANDROID__) #define STONEAGE_EXPORT __attribute__((visibility(default))) #elif defined(__APPLE__) #define STONEAGE_EXPORT #else #define STONEAGE_EXPORT #endif只对 JNI 转接口标记STONEAGE_EXPORT内部函数保持 hidden能减少动态符号表大小也避免核心实现被外部误调用。第二个问题是平台差异代码。C 里偶尔需要拿到当前设备信息比如判断是否真机、获取系统语言这种逻辑不要直接写#ifdef __ANDROID__分散在业务代码里。我习惯把所有平台差异封装成PlatformBridge接口由两端壳层各自实现C 核心只依赖接口。这样核心代码在 Windows 上也能编译跑单元测试调试效率高很多。第三个问题最隐蔽浮点精度。同一份 C 代码在 Android x86 模拟器、Android arm64 真机、iOS 模拟器和 iOS 真机上float运算结果可能不一样。编译器优化级别不同连聚合指令都会变。StoneAgeMobileApp 这类游戏如果战斗伤害差一点玩家一定能发现。我的经验是所有落到数值表、要跨端一致的逻辑一律用double能用整数就不用浮点。更严格的做法是引入定点数但维护成本高我只建议在伤害计算这种核心公式里使用。4. 双端构建脚本与打包让 Android Studio 和 Xcode 都按脚本出活源码结构定了接下来要把 Android Studio 和 Xcode 各自吃到同一份核心库。很多项目卡在这一步C 代码在 Android 里编得通放到 Xcode 链接却报一堆 undefined symbol。这章我把两侧的构建配置分别写出来。4.1 Android 侧用 Gradle 挂 CMake 的最小配置Android 工程只要在app/build.gradle里声明 externalNativeBuildAndroid Studio 就能自动调 CMakeandroid { defaultConfig { externalNativeBuild { cmake { // 老设备兼容 armv7现代设备用 arm64模拟器用 x86_64 abiFilters armeabi-v7a, arm64-v8a, x86_64 arguments -DANDROID_STLc_shared } } } externalNativeBuild { cmake { path src/main/cpp/CMakeLists.txt version 3.22.1 } } packagingOptions { jniLibs { keepDebugSymbols **/*.so } } }CMakeLists 我通常写得尽量薄cmake_minimum_required(VERSION 3.22.1) project(stoneagecore) add_library(stoneagecore SHARED src/battle.cpp src/pet.cpp src/item.cpp ) target_include_directories(stoneagecore PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include ) target_compile_options(stoneagecore PRIVATE -fvisibilityhidden -fexceptions ) # Android 上打日志会用到 log 库不链接会报 undefined reference find_library(ANDROID_LOG_LIBRARY log) target_link_libraries(stoneagecore ${ANDROID_LOG_LIBRARY})abiFilters三个值建议都要问清楚需求和容量armeabi-v7a能覆盖老手机但会增加 so 体积只留arm64-v8a最省心但模拟器上没法跑旧镜像。-DANDROID_STLc_shared也很关键如果用c_static多个 so 同时加载时会重复初始化标准库偶尔会出现诡异崩溃。-fvisibilityhidden配合前面说的STONEAGE_EXPORT只暴露该暴露的接口。改完这一步Android Studio 里点 Sync编译时会在build/intermediates/merged_native_libs/下生成目标 so 文件。要验证 JNI 符号是否真的导出了可以在终端跑nm -D android/app/build/intermediates/merged_native_libs/debug/mergeDebugNativeLibs/out/lib/arm64-v8a/libstoneagecore.so | grep calcBattleDamage能查到Java_com_stoneage_core_GameCore_calcBattleDamage说明 JNI 绑定没问题。4.2 iOS 侧用 XcodeGen 把工程配置文件化iOS 的.xcodeproj是二进制/plist 混合格式多人协作时最容易冲突。StoneAgeMobileApp 这种跨端项目我希望 iOS 工程配置也能被 git 追踪、能 diff。做法是引入 XcodeGen用一个project.yml生成工程文件name: StoneAgeMobile options: bundleIdPrefix: com.stoneage targets: StoneAgeMobile: type: application platform: iOS deploymentTarget: 12.0 sources: - StoneAgeMobile - ../core/include preBuildScripts: - name: Copy Assets script: ../build_scripts/copy_assets.sh settings: base: CLANG_ENABLE_OBJC_ARC: YES OTHER_LDFLAGS: -lcsources里加上../core/includeXcode 才能找到核心头文件.cpp源文件可以直接放进StoneAgeMobile目录也可以作为 External Build 目标单独编译成静态库。我的经验是最初阶段直接参与编译减少链接配置。等核心代码稳定后再抽成libcore.a统一链接。首次配置后执行xcodegen generate会生成StoneAgeMobile.xcodeproj。这个文件不用提交到 git每个人拉完代码先跑 xcodegen再打开工程冲突能少很多。4.3 两端打产物四条命令对应的四种使用场景日常开发中我基本只用这四种组合。场景平台命令产物Android Debug 调试Androidcd android ./gradlew assembleDebugapp-debug.apkAndroid Release 上架Androidcd android ./gradlew bundleReleaseapp.aabiOS 模拟器调试iOSxcodebuild -scheme StoneAgeMobile -configuration Debug -sdk iphonesimulator build.appiOS 真机/上架iOSxcodebuild -scheme StoneAgeMobile -configuration Release -sdk iphoneos build.app/.ipa真机构建前要确认 Code Signing 配置签名用的证书和描述文件不对会卡在最后一个阶段。模拟器构建不强制签名但调试 C 性能时要优先选真机模拟器上的 arm64 翻译层表现和真机差别很大。5. StoneAgeMobileApp 移植路上的 5 个避坑点现象、原因、解决一条条讲这章是给一定会踩坑的人看的。我把过去遇到过的高频问题按“现象 → 原因 → 解决”写出来每一条都来自实际操作不是理论推演。5.1 编译通过运行却报 UnsatisfiedLinkError 找不到 JNI 函数这是 Android 接入 C 最常见的拦路虎。现象是 App 启动时日志里直接出现java.lang.UnsatisfiedLinkError: No implementation found for ... calcBattleDamage但 CMake 编译过程完全正常。原因通常是三种JNI 函数名跟 Java 包名/类名不一致C 文件里漏了extern C或者-fvisibilityhidden把 JNI 符号隐藏了。我见过最隐蔽的是有人把 Java 类从com.stoneage.core.GameCore改到了com.stoneage.game.GameCore却忘了同步修改 C 函数名。JNI 规范要求函数名的包路径和类路径完全对应少一个下划线都找不到。解决办法是先生成 Java 侧的 native 声明再让 Android Studio 自动生成 JNI 函数签名或者用javap -s查看期望的签名。改完后用上面提到的nm -D检查 so 文件导出符号确认函数名匹配。另外不要轻易开-fvisibilityhidden如果开了务必在 JNI 函数前加__attribute__((visibility(default)))。5.2 iOS 模拟器跑通了真机一进战斗就崩这是更让人血压飙升的坑。模拟器上打开石器时代主界面没问题一进战斗调用 C 计算伤害直接闪退真机却可能完全正常或者反过来。原因基本可以锁定在架构不匹配。iOS 模拟器默认构建 x86_64 或 arm64取决于 Mac 芯片真机只认 arm64。如果你在工程里引入了为模拟器单独编译的libcore.a打包时忘了同时包含真机 slice链接器可能不会报错但运行时一调用核心函数就触发非法指令。解决方法是每次 Release 真机打包前检查产物体架构lipo -info libcore.a输出里必须包含arm64。如果只有x86_64说明你编译的是模拟器版本。另一种情况是模拟器能跑但计算奇慢不要怀疑x86_64 上跑 arm64 指令要走翻译层性能差异会让人误判算法复杂度。5.3 Android 资源文件有中文名或空格asset 加载直接找不到老美术资源命名普遍随意宠物_英雄.PNG、map 010.png在 Windows 上也跑得好好的。到了 Android 上就翻车。现象是 Android 端启动后一部分图片加载不出来但控制台不一定报错只是 UI 空白。原因是 Android 的assets目录虽然不像res那么严格限制命名但资源名里含有空格或中文时某些压缩工具和系统解析会出现不一致而且 Java 层读 asset 路径时大小写敏感Windows 文件系统大小写不敏感问题会潜伏到出包后才暴露。我的做法是让copy_assets.sh在拷贝时统一改名文件名全部转小写、空格换成下划线、中文转成拼音或英文同时生成映射表。如果已有资源已经被大量引用可以在拷贝脚本里加入自动改写配置文件的逻辑。改动资源名后一定要清空 App 数据重装避免旧缓存干扰判断。5.4 双端战斗结果不一致Android 剩 87 血iOS 剩 92 血这个坑前面提过值得单独拉出来讲透。现象是你用同一套测试用例分别在 Android 和 iOS 上跑得到的结果不同。一开始我以为是客户端版本没更新后来发现核心代码确实是同一份问题出在编译器对浮点表达式的处理上。Android 的 clang 和 iOS 的 clang 优化策略不完全一样特别是在开启-ffast-math后编译器会把浮点加法重排成乘加融合指令结果低位末位就会产生偏差。如果一款战斗系统只用几次四则运算这个偏差看不出来但石器时代这种反复叠加 buff、多段伤害、多回合累计的机制偏差会被滚雪球式放大。解决标准就三条核心数值计算全部使用double不要开启-ffast-math对最核心的战斗结算用整数定点数实现。定点数看起来很原始其实是端游时代就验证过的手段用整数表示小数比如伤害倍率乘以 1000 后取整所有服务器和客户端都用同一套整数规则从根本上消除浮点误差。5.5 iOS 打包时提示 code object is not signed at all最后一个是 iOS 打包的玄学现象。Xcode 归档能过但上传 App Store Connect 时报警终端里看到code object is not signed at all。原因多数不是核心代码而是你把core编成了静态库或 framework而 framework 没有正确签名。Xcode 对动态 framework 要求显式签名静态库只是链接进主 Mach-O理论上不需要单独签但归档过程如果脚本动了构建产物就可能留下未签名文件。解决的后悔药是尽可能把.cpp文件直接加入 target 参与编译不要为了“工程整洁”非要做成 framework。如果必须用 framework在 Xcode 的 Build Phase 里确认Embed Foundation Extensions和Code Sign On Copy都打开了。真机调试没问题不代表上架没问题最好在出 archive 包时专门检查一下。6. 把双端同步变成每天能跑的验证提交前多花五分钟最后一招也是我最想推荐给大家的习惯让“Android 和 iOS 用的是同一份源码”这件事被脚本自动验证而不是靠人用眼睛比对。我在core/tests/里维护一组纯 C 单测输入一百组战斗参数跑完断言结果必须精确相等。这个测试不依赖 Android/iOS直接用宿主机 g 就能跑mkdir -p build_host g -stdc17 -I core/include \ core/tests/run_tests.cpp \ core/src/battle.cpp \ -o build_host/run_tests ./build_host/run_tests这组测试应该当成核心代码的“安全带”谁改了 C 计算逻辑本地先跑一遍这个测试再提交到共享分支。Android 侧构建可以挂到 Gradle task 里iOS 侧用 Run Script Phase保证一次测试两边受益。再配合一个 git revision 嵌入技巧echo #define GIT_REVISION \$(git rev-parse --short HEAD)\ core/git_revision.h这个命令把当前 commit 短哈希写进git_revision.hC 代码里定义一个全局版本常量Android 和 iOS 启动时把GIT_REVISION打到日志里。以后真机联调先对比两端日志里的 hash不一致就说明有一端没有拉最新核心代码直接回退提交不用再猜。这个动作已经成为我每次提交前必跑的一步。前端调试环境一天重启十几次能把“双端一致性”这种黑匣子问题收敛成版本号比对会省下很多无意义的重复劳动。希望这套思路能帮你在 StoneAgeMobileApp 的移植和开发里少走几段弯路。本文还有配套的精品资源点击获取
返回列表