在移动平台做C++开发这些年,最常被同行问到的其实是两个问题:一个是"移动端都用Kotlin/Swift了,为什么还要写C++",另一个是"我把Windows上的C++代码搬到手机上,怎么就跑不起来了"。这两个问题背后藏着同一件事——移动平台上的C++,从来不是孤立的一门语言,而是一整套围绕交叉编译、内存模型、JNI交互和性能约束的工程实践。这篇东西我会从方案选型、环境搭建、核心语法细节、常用算法落地和问题排查几个方向,把自己踩过的坑和沉淀下来的套路整理出来,给那些想在Android/iOS上用C++做点正经事的人做参考。
开头先交代目标读者:如果你已经在PC端写过C++,想转移动端;或者你主要在移动端做上层业务,但需要接手底层库、游戏引擎、音视频算法这类C++模块;又或者你只是想搞清楚NDK、CMake、JNI这些东西到底怎么协同工作,那么这篇文章应该能帮你省掉很多瞎折腾的时间。内容会尽量贴合实际工程场景,不堆砌语言标准,重点放在"能用、能跑、能调、能上线"这个层面。
1. 移动平台C++开发方案选型与定位
1.1 移动端C++的几个典型应用场景
先说一个最直观的问题:移动平台上为什么还需要C++?答案说到底是三个字:性能、复用、底层访问。性能方面,常规业务逻辑用Kotlin或者Swift写没有问题,但一旦牵扯到每帧循环、大量数学运算、图像处理、编解码、物理碰撞这一类计算密集场景,托管语言的GC停顿和运行时开销就会变得很刺眼。C++在移动端的定位不是替代Kotlin和Swift,而是把关键的、性能敏感的模块下沉到Native层,让上层语言去处理界面和业务逻辑。
复用这块更好理解。不少团队在Windows、macOS或者Linux上已经沉淀了一套跨平台C++库,包含网络、加密、数据模型、业务算法等核心模块。如果移动端全部用托管语言重写,成本极高而且容易出现行为偏差。我在实际项目里见过最典型的方案就是"三个壳一个核":Windows、Android、iOS各做一个薄薄的上层壳,核心逻辑全部用C++共享,这样一套算法在不同平台上的行为是一致的,测试只需要做一遍。
底层访问也是C++不可替代的地方。移动系统很多底层能力,比如OpenGL ES、Vulkan、Metal的调用入口,或者直接操作物理内存、文件映射、共享内存,这些接口本身就是C风格的。要在Android上直接调用这些能力,不走JNI是不可能的。就算你只是用第三方SDK,很多SDK内部也是C++实现,甚至要求你使用C++版本的API接口。游戏领域尤其明显,游戏开发C++和C#在移动端的区别,说白了就是引擎层和业务层的区别——C++负责引擎、渲染、物理、资源管理这类底层,C#负责玩法逻辑和编辑工具。Unity虽然对外是C#,但IL2CPP模式下最终执行的仍然是C++编译产物,只是这个过程对普通开发者透明了。
1.2 决定语言路线前,先想清楚这三个问题
真到了选型阶段,很多团队会犯一个毛病:看别人用C++,自己也跟着上。我觉得立项之前至少要把三个问题想明白。
第一个问题:这块逻辑的生命周期到底有多长?如果是短期验证型功能,比如快速Demo、活动运营页面,托管语言明显更合适。C++最大的成本不在于写,而在于编译、链接、交叉编译、内存调试这些周边环节,这些成本在长期维护的项目里会被摊薄,在短期项目里都是纯亏损。
第二个问题:团队里有没有人能真正Hold住C++?这里说的不是会写for循环和vector,而是能理解移动平台上的内存模型、理解编译器行为、能看懂崩溃堆栈里的符号表信息。移动端C++崩溃时经常出现的情况是,问题不在你写的代码,而在某个第三方C++库的字节对齐、符号冲突、ABI不兼容。没有这方面经验的人排查起来非常头疼。
第三个问题:性能瓶颈到底在哪里?我的建议是永远先做性能分析,再决定要不要用C++。曾经有个项目说登录流程太慢,要求把登录逻辑改成C++,结果排查后发现瓶颈在DNS解析和网络延迟,跟语言没半点关系。真正的性能优化路径通常是:先Profile定位热点,再针对热点模块Native化,而不是一上来就全局重写。这也符合工程上"用数据说话"的原则。
想清楚这三个问题之后,再决定C++在项目里的边界。边界的定义很重要,我一般会在架构文档里明确写出来:哪些模块只能由C++实现,哪些模块可以把核心逻辑放C++但允许上层扩展,哪些模块禁止使用C++。这样可以避免项目中期出现"底层想用C++、上层也想用C++、结果全员在写C++"的失控局面。
2. 开发环境搭建:NDK、CMake与编辑器配置
2.1 从零搭一个NDK工程骨架
不管你是做Android还是做iOS,搭建移动C++工程的第一步都是先搞清楚编译工具链。Android上用的是NDK,它本质上是一个交叉编译工具链的集合,里面包含了编译器、链接器、sysroot、各种平台库和构建工具。iOS上对应的是Xcode自带的toolchain,配合CocoaPods或者Swift Package Manager来管理第三方依赖。
Android端用一个很简单的CMake工程来说明整体结构。目录大致长这样:
app/ src/main/ cpp/ CMakeLists.txt native-lib.cpp core/ core.h core.cppCMakeLists.txt里最核心的是下面这几项:
cmake_minimum_required(VERSION 3.18.1) project(mobile_core) add_library( native-lib SHARED native-lib.cpp core/core.cpp ) find_library( log-lib log ) target_link_libraries( native-lib ${log-lib} )这段配置里,add_library声明生成一个名为native-lib的共享库,find_library把Android系统的log库找出来并链接进去,这样你就能在C++代码里调用__android_log_print输出日志到Logcat。这种做法的好处是可以在Android Studio里直接通过Gradle构建,也能用命令行cmake构建,两个方向都走得通。
iOS端更特殊一点。iOS不推荐你在Xcode之外单独维护一套CMake,比较省心的做法是用Xcode的 framework target 管理C++代码,同时加一个Module.modulemap导出头文件,或者在Swift工程里直接建一个C++头文件桥接。现在Xcode对C++的混编支持已经很成熟,官方推荐通过Objective-C++(.mm文件)做Swift和C++的桥接。
工程骨架确定好之后,下一步是选择编译标准。移动端建议至少用C++17,原因无他——移动平台的编译器对C++17支持已经很完整,而且C++17的std::optional、std::variant、结构化绑定、std::filesystem(部分平台)能明显简化代码。项目里可以统一在CMake里指定:
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON)2.2 VSCode下的编辑、编译与调试配置
很多从PC端转过来的同事习惯用VSCode写C++,到了移动端也一样。VSCode本身不编译代码,它是一个前端,真正干活的是编译器、构建系统和调试器。所以VSCode的价值主要体现在三方面:语法高亮和智能提示、跳转和全局搜索、以及集成调试。
在VSCode里配置移动端C++,核心是三个文件。第一个是c_cpp_properties.json,它控制IntelliSense的行为,告诉VSCode去哪里找头文件、用什么标准去解析代码。一个比较典型的配置长这样:
{ "configurations": [ { "name": "Android", "includePath": [ "${workspaceFolder}/**", "${ndkPath}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include", "${ndkPath}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/c++/v1" ], "defines": [ "ANDROID" ], "cStandard": "c17", "cppStandard": "c++17", "intelliSenseMode": "linux-clang-x64" } ], "version": 4 }这里最容易出问题的是头文件路径的顺序。VSCode智能提示路径优先级很敏感,如果你同时装了多个SDK版本,includePath里靠前的路径会被优先搜索,有时候代码在NDK r23下编译通过,但VSCode却报了头文件找不到,多半就是路径顺序不对或者没把Android的sysroot加进去。
第二个是tasks.json,它负责定义构建命令。比如执行cmake、执行ninja编译等。可以对某个目标执行build,方便在编辑时按快捷键触发编译。
第三个是launch.json,用来做断点调试。这里配置比较讲究,Android真机调试要走lldb的attach模式,必须依赖adb把lldb-server推送到设备上。调试配置里需要指定deviceId、host、port等参数,Android Studio和VSCode在这一点上是相通的。
说实话,如果只是写底层的纯C++逻辑代码,VSCode足够应付。但一旦涉及JNI Java层与C++层交互的调试,还是建议打开Android Studio,它集成的LLDB调试面板可以直接同时看到Java栈和Native栈,排查问题效率高出不少。我的经验是:日常编码用VSCode,复杂问题排查看Android Studio,两个工具配合使用才是完整的移动C++开发姿势。
3. 核心语法细节:字符串、容器与内存管理
3.1 字符串初始化与常见转换
移动端C++开发里,字符串是一个非常容易出岔子的地方。先说初始化。C++字符串数组初始化看着简单,实际上有好几种写法,对应不同的行为:
// 方式一:C风格字符串数组 char oldSchool[] = "hello"; // 方式二:std::string 从C字符串构造 std::string s1("hello"); std::string s2 = "hello"; // 方式三:初始化列表 std::string s3 = {'h', 'e', 'l', 'l', 'o'}; // 方式四:重复字符 std::string s4(5, 'a');方式一和方式二之间最容易被忽视的差异是'\0'的处理。C风格字符串数组"hello"实际占用6个字节,最后一个是字符串结束符;而std::string不依赖'\0'结尾,它内部保存了length信息,所以能处理包含'\0'的字节流。这在处理二进制数据时是一个重要区别,很多从C转过来的人把std::string当作二进制缓冲区用,结果在跨层传递数据时遇到截断问题,源头就在这里。
再说字符串转数组。移动端最常见的两个需求是:std::string转std::vector 、std::string转C风格char*。标准做法是:
std::string src = "mobile native"; std::vector<char> bytes(src.begin(), src.end()); // 或者直接复制底层缓冲区 const char* raw = src.data(); size_t len = src.size();这里必须强调,C++17之前data()返回的指针只保证只读;C++17之后data()返回的char*是允许写操作的,但如果你通过data()修改了字符数据,然后调用非const的成员函数(比如operator[]),需要格外小心字符串的内部缓存失效问题。我在真机上踩过一次坑:从JNI拿到一个jstring转成std::string,然后调用了data()指针直接修改内容,修改完之后又append内容,结果日志和控制台表现不一致,后来定位发现是string对象重新分配了内部缓冲区,旧指针成了悬空指针。
JNI字符串转换也是高频操作。从jstring转到std::string的标准写法是:
std::string jstring2string(JNIEnv* env, jstring jstr) { if (!jstr) return ""; const char* chars = env->GetStringUTFChars(jstr, nullptr); std::string result(chars); env->ReleaseStringUTFChars(jstr, chars); return result; }注意GetStringUTFChars返回的是UTF-8编码的C字符串,如果Java侧传过来的是含有中文的字符串,这里会自动转成UTF-8,这是安全的。但如果Java侧传的是byte[],那就不能走这条路了,需要手动把jbyteArray转成std::vector ,再做编码处理。
3.2 容器选择与排序算法落地
移动C++开发里容器的选择直接影响性能和内存。很多人写C++习惯性地凡是集合都用std::vector,这在PC端问题不大,在移动端有时候会踩到内存碎片和分配耗时过高的坑。移动端内存本身比PC受限,尤其是早期Android设备的低内存场景,频繁的堆分配会带来肉眼可见的卡顿。
我的个人经验是:默认用std::vector,但要根据场景区分两种用法。一种是固定大小、频繁按索引访问的集合,用std::vector配合reserve预留空间;另一种是频繁插入删除的集合,应该考虑std::deque或者std::list,但list的节点分配碎片化问题又很麻烦,所以实际工程里更多是"写标记+批量清除"的延迟删除方案。这里简单说一个排序场景,移动端列表数据排序是刚需。很多人直接调用std::sort,但std::sort是不稳定排序,相同键值的元素顺序会被打乱。如果业务要求相同key的对象保持原顺序,就必须用std::stable_sort,它内部实施了归并排序的思想,代价是额外的内存开销和时间。
排序算法在移动端的另一个大坑是队排序方向。C++默认的operator<是升序,如果你要用降序,惯用法是:
std::sort(v.begin(), v.end(), std::greater<int>());或者用lambda:
std::sort(v.begin(), v.end(), [](const Item& a, const Item& b) { return a.score > b.score; });至于冒泡排序,热词里经常看到"c++ 冒泡排序",不少教程拿它当入门算法讲,但实际工程中应该避免在移动端使用冒泡排序处理大数组,时间复杂度O(n^2)在低端机上处理千级别以上的数据就会看到明显的卡顿。如果只是学习或者处理小数组,冒泡排序可以做点优化:加一个flag表示本轮有没有发生交换,如果没交换说明已经有序,提前退出:
void bubbleSort(std::vector<int>& arr) { int n = arr.size(); for (int i = 0; i < n - 1; ++i) { bool swapped = false; for (int j = 0; j < n - i - 1; ++j) { if (arr[j] > arr[j + 1]) { std::swap(arr[j], arr[j + 1]); swapped = true; } } if (!swapped) break; } }这个优化在局部有序数据上很有效,但整体性能依然差于std::sort,所以我在工程里从来不用手写排序处理真实业务数据,手写排序的场合基本就是面试或者教学。
3.3 内存管理、回调与线程模型
移动平台的内存管理,说到底是四个字:谁new谁delete,或者更现代一点:用智能指针。PC端你可以开着ASan随时测,但移动端漏内存问题非常隐蔽,因为移动系统内存紧张,应用一旦内存异常会被直接杀掉,没有Core Dump可看。
智能指针是移动C++的底线。std::shared_ptr适合真正共享所有权的场景,但它的引用计数是原子操作,多线程环境下会有不小的开销;std::unique_ptr语义更清晰,性能也更好,能用unique_ptr就别用shared_ptr。移动端特别要小心shared_ptr的循环引用,两个对象互相持有shared_ptr会导致引用计数永远不为0,内存泄漏无声无息。
再展开说一个名称规则:C++里的"覆盖"和"隐藏"是两回事。有virtual关键字、且签名匹配的才是覆盖,实现动态绑定;没有virtual、同名同参的,叫隐藏,子类会遮蔽父类的同名函数。这个区别在移动端拿到多态接口时尤其重要,我曾经看一个同事写的代码,父类析构函数不是virtual,子类通过父类指针delete时析构函数根本不调用,直接导致内存泄漏。排查了两天才发现根源。
回调函数在移动C++里的使用频率非常高,尤其在做异步网络、任务队列、事件分发时。现代C++里回调的推荐写法是std::function配合lambda,而不是裸函数指针。原因很简单:lambda可以捕获上下文变量,std::function可以持有任意可调用对象,类型安全,语义清晰。一个典型场景:
using TaskCallback = std::function<void(bool success, std::string errorMsg)>; void startAsyncTask(TaskCallback cb) { // 在线程池上执行耗时任务 std::thread t([cb]() { bool result = doSomething(); std::string error; // 回到主线程执行回调 runOnMainThread([cb, result, error]() { cb(result, error); }); }); t.detach(); }这种写法的坑点在于回调的生命周期管理。如果发起任务的页面已经销毁,回调却还在执行,捕捉到的this指针或者局部状态可能已经失效,从而出现野指针。移动端通用的解法是引入生命周期token,或者在回调里做弱引用检查。Android的Java层有生命周期感知组件,C++层你必须自己维护一套弱回调机制,这是移动C++开发绕不开的自研模块。
关于ABA问题,很多面试题会提到。ABA问题发生在无锁并发编程的CAS操作里:线程A读取值为X,线程B改成了Y,再改回X,线程A的CAS比较发现值还是X,就认为没人动过,实际上状态已经历过变化。移动C++里用到无锁队列、自旋锁时,ABA问题可能出现。解决思路一般是给指针或者值附加一个版本号/标签,CAS时同时比较版本号,版本号变了就说明发生过修改。工程上如果对并发编程掌握不深,最稳妥的做法仍然是使用互斥锁加条件变量,不要在移动端轻易手写无锁结构,无锁代码在弱内存序芯片上的bug很难复现也很难排查。
4. 移动端高频算法实现与优化
4.1 二分查找与边界处理
移动端做搜索排序、断点续传、日志索引时经常用到二分查找。这个算法看起来简单,写对其实不简单,因为它的边界条件种类很多,一不留神就会死循环或者越界。
先用一个标准版本:
int binarySearch(const std::vector<int>& arr, int target) { int left = 0, right = arr.size() - 1; while (left <= right) { int mid = left + (right - left) / 2; if (arr[mid] == target) return mid; else if (arr[mid] < target) left = mid + 1; else right = mid - 1; } return -1; }这里的细节包括:mid的计算用了left + (right - left) / 2,而不是 (left + right) / 2,因为后者在left和right都很大时可能整数溢出。while的条件是left <= right,等于时还要处理一次,这种写法和left < right的版本语义不同。工程上二分查找真正的难点是变体:找第一个等于target的下标、找最后一个小于等于target的下标,这些变体处理不好很容易写出bug。我的经验是不要靠记忆模板,而是画一张指针走向图,把每一轮left和right的移动路径写清楚,再拿去跟测试用例对拍。
提到对拍,就引出另一个工程实践:二分查找这类算法,建议直接无脑用标准库std::lower_bound和std::upper_bound,而不是手写。标准库实现经过了大量测试,边界行为是明确定义的,比自己手写稳妥得多。移动端工程里要的是确定性和可维护性,不是炫技。
4.2 质数判断、快速幂与性能优化
数学类算法在移动端同样有实现价值。比如判断质数,很多人拿到题直接写循环从2到sqrt(n),但细节里坑很多。优化后的写法:
bool isPrime(int n) { if (n < 2) return false; if (n == 2 || n == 3) return true; if (n % 2 == 0 || n % 3 == 0) return false; for (int i = 5; i <= n / i; i += 6) { if (n % i == 0 || n % (i + 2) == 0) return false; } return true; }这里用到了6k±1的数学规律:大于3的质数一定可以写成6的倍数加减1。循环条件用i <= n / i可以避免求sqrt带来的浮点误差和额外开方开销。如果要判断一个范围内所有的质数,再逐个判断就很浪费了,这时应该用埃氏筛或者欧拉筛,一次生成范围内所有的质数表,再O(1)查询。这个思路在移动端做一些批量校验场景很有用。
快速幂同理,很多移动端加密、数据变换的场景都要用到幂运算,直接调pow可能存在浮点精度问题,而快速幂定位是整数取模场景里的核心:
long long modPow(long long base, long long exp, long long mod) { long long result = 1 % mod; base %= mod; while (exp > 0) { if (exp & 1) result = result * base % mod; base = base * base % mod; exp >>= 1; } return result; }这个实现的原理是:把指数拆成二进制位,每一位对应要不要乘上当前base的幂次,base每轮自乘,相当于迭代处理指数的每一位。核心优化点是减少乘法次数:把指数为e的幂运算的乘法次数从O(e)降为O(log e)。移动端做加解密、签名校验时这个算法几乎是标配,面试时考察的几率也极高。
5. 常见问题排查与工程落地经验
5.1 编译环境问题速查表
移动C++开发一半的时间在跟编译器、链接器和依赖库做斗争。结合我自己实测踩过的坑,整理一个速查表:
| 问题现象 | 常见原因 | 解决办法 |
|---|---|---|
| 编译报错 error: microsoft visual c++ 14.0 or greater is required | Windows上用Python或Node安装依赖包时,缺少MSVC编译器 | 在Visual Studio Installer中安装"使用C++的桌面开发",或安装对应版本的Visual C++ Build Tools |
| 运行时提示 Visual C++ Redistributable 未安装 | 发布到目标机器时缺少VC运行库 | 在目标机器安装对应架构的VC_redist.x64/x86.exe |
| NDK编译时报"no rule to make target" | CMakeLists里源文件路径写错 | 检查cpp目录下的相对路径,推荐用${CMAKE_CURRENT_SOURCE_DIR}拼接绝对路径 |
| 链接报undefined reference to '__android_log_print' | 缺少log库链接 | 在CMake中find_library(log-lib log),并在target_link_libraries中加上${log-lib} |
| JNI调用崩溃:JNI ERROR (app bug) | JNI引用类型用错,比如全局引用没有DeleteGlobalRef | 检查所有的NewGlobalRef/NewLocalRef是否成对释放 |
| 真机调试时lldb连接不上 | 设备的adb反向隧道没建好,或手机管家拦截 | adb devices确认设备状态,重启adb server,或换USB调试模式 |
| 多个NDK版本导致智能提示路径混乱 | includePath顺序不对或缓存未刷新 | 清理C/C++插件缓存,重新加载窗口,严格按照sysroot路径配置 |
这张表看起来简单,每一条背后都是真金白银的时间。比如第一条error: microsoft visual c++ 14.0 or greater is required,很多做Python/C++结合的人被它折磨过,其实就是Windows上的编译环境不完整,跟项目代码没有任何关系。遇到这种问题时先冷静分析是什么构建系统在报错、它期望哪个编译器,再针对性安装VC++ Build Tools,不要看到报错就去改代码。
5.2 JNI层交互与内存问题
JNI是移动平台C++开发里绕不开的一层,它承担了Java/Kotlin和C++之间的"翻译"。JNI常见问题的根源其实只有几类:签名不匹配、全局引用泄漏、局部引用表溢出、字符串编码不一致。
签名不匹配是最容易排查的,报错信息会直接告诉你Native方法签名对不上,按照Java声明把签名改成一样就行。难排查的是引用泄漏。JNI每次调用NewLocalRef或者通过GetObjectClass、GetMethodID得到的引用,在函数返回时会自动释放,但如果你在循环里频繁创建局部引用,比如遍历一个很长的Java List,每轮都创建一个jstring,就会触发局部引用表溢出。标准做法是循环体内及时调用DeleteLocalRef释放,或者改用PushLocalFrame/PopLocalFrame批量管理。
全局引用是另一类坑。如果你在C++里缓存了一个Java对象,准备后续回调Java方法用,必须用NewGlobalRef创建全局引用,否则这个对象会在Java侧被GC回收,C++持有的引用变成悬空引用,回调时直接崩溃。我自己的习惯是封装一个JniGlobalRef辅助类,构造时NewGlobalRef,析构时DeleteGlobalRef,利用RAII管理生命周期,这样操作引用就和操作std::unique_ptr一样安全。
JNI回调Java方法还有一点要注意:线程切换。默认情况下,一个Java线程通过JNI调到C++,再想在其他线程调用Java层方法,必须确保该线程已经通过AttachCurrentThread附加到Java虚拟机,然后获取当前线程的JNIEnv,才能安全调用Java方法。很多同事第一次写JNI回调时,在线程池里拿到一个之前的JNIEnv,想都不想就拿来调用Java方法,结果要么崩溃要么卡死。正确做法是在线程入口处调用env = vm->AttachCurrentThread(...),用完之后DetachCurrentThread,这个必须成对出现。
5.3 性能调优的几条经验
移动平台C++性能调优,方向其实很明确:CPU时间、内存带宽、GPU调用、IO。先CPU,用Perfetto或者简单点的debug.startMethodTracing抓调用栈,看哪个函数占据了大量时间。C++层还有一个传统技巧是直接对关键函数做微基准测试,用std::chrono::high_resolution_clock记录耗时,跑几千次求平均值。排查到热点后,优化手段不外乎:减少拷贝、避免虚函数调用、开启编译器优化、使用内联函数,以及最粗暴但有效的——减少分配。
内存带宽这块容易被新手忽视。移动端的DRAM带宽非常有限,如果你在热循环里对一个很大的vector做频繁遍历,即使每次操作很快,总耗时依然可观。我处理过一个案例,一段逐帧遍历数千个粒子做碰撞检测的逻辑,每次遍历都要创建临时对象,结果帧率掉到20帧。优化方案很简单:复用临时对象,避免分配,尽量让内存访问是线性的。改完之后帧率稳定回到60帧。
GPU调用相关的问题,C++层一般管不到,但要注意的是纹理上传和shader编译。C++层如果做了资源加载和预处理,尽量在加载阶段完成纹理解码和格式转换,不要在渲染线程里做这些事情。因为编码格式转换是CPU密集型的,放到渲染线程直接卡帧,放到加载线程就能无缝衔接。
最后说一个容易被忽略的点:构建类型。调试版本和发布版本的C++代码行为差异很大,比如NDK的CMAKE_BUILD_TYPE等于Debug时编译器不开优化,栈上变量可能保留很多调试信息,同时会关掉部分编译器优化,导致性能差异巨大。我在真机调测时测出某个模块耗时20ms,其实同段代码在Release构建下只要3ms。所以做性能评估和上线定位时,一定要以Release构建为准,Debug构建只用于断点排查。
写在最后的个人体会
做移动平台C++开发这几年,最大的体会是这门语言在移动端的价值从来没有被稀释,只是它的舞台变窄了。Kotlin和Swift确实把大部分业务逻辑都包揽了,但越是深入底层,越是发现C++在那个位置依然无可替代。
我最想强调的是工程纪律。C++是一门你越随意、它回报越残忍的语言——内存泄漏、未定义行为、并发竞态,每个问题都足够让你熬几个通宵。在移动端这种资源受限、系统对异常零容忍的环境里,代码规范比写法的炫酷重要得多。如今我在新项目里定下的第一条规定从来不是"能不能用C++",而是"C++代码必须在编译期开-Wall -Wextra -Werror,智能指针管理所有动态资源,JNI引用必须RAII化"。这几条落地到位,80%的坑都会被挡在编译期和代码审查阶段。
如果你正准备从PC端C++转型到移动端,我的建议是先从一个小的NDK模块入手,把JNI、CMake、真机调试这条路走通,再慢慢扩大C++层的边界。别一上来就搞大而全的跨平台底层库,那不是学C++的捷径,而是给自己挖的超大坑。
最后再留一个小技巧:多花点时间把VSCode或者Android Studio里的调试配置调顺。一个能一键编译、一键断点、一键看内存的调试环境,比任何教程都更能帮你理解移动C++的运行机制。工具链顺畅了,后面的事都会顺起来。