
1. 为什么现在还要啃 CMake 这块硬骨头如果你最近两年才开始接触 Android 上的原生开发可能会觉得 CMake 这东西有点老派——毕竟现在很多新项目直接上ndk-build的替代方案或者干脆用 Rust 那套构建体系。但只要你真正做过需要跨平台复用 C/C 代码的项目尤其是那种要在 Android、iOS、桌面端同时维护一套核心库的场景CMake 依然是绕不开的基础设施。我在几个实际项目里反复折腾过 CMake 和 NDK 的配合踩过的坑足够写满一个笔记本今天就把这些经验系统性地梳理一遍。先说清楚这篇文章适合谁看。如果你是完全没碰过 NDK 的新手建议先补一下 JNI 的基本概念再回来因为 CMake 在 Android 里的角色是构建工具它解决的是怎么把一堆 C/C 源文件编译成 Android 能加载的 .so 库这个问题而不是Java 怎么调 C的问题。如果你已经能用ndk-build跑通 Hello World但一遇到多模块、第三方库依赖、ABI 过滤就头大那这篇就是写给你的。如果你已经在用 CMake 但总是被各种链接错误、找不到头文件、运行时崩溃搞得焦头烂额那更好我会把那些最容易翻车的地方一个个拆开讲。CMake 本质上是一个构建系统生成器它自己不编译代码而是根据你写的CMakeLists.txt生成对应平台的构建文件比如 Ninja 或 Makefile然后再调用编译器去干活。这个间接层带来的好处是跨平台同一份CMakeLists.txt在 Android 上生成 NDK 工具链的构建脚本在 Linux 上生成 GCC 的在 Windows 上生成 MSVC 的。代价就是多了一层抽象出问题的时候报错信息往往不那么直观。Android NDK 从 r18 开始正式把 CMake 作为默认推荐方案到现在 r26、r27CMake 的支持已经非常成熟但成熟不代表简单很多细节还是得自己趟。接下来我会从整体设计思路讲起然后逐个拆解核心配置项再给出一套可以直接抄的实操流程最后把那些年我遇到过的典型问题和排查方法整理成速查表。全程尽量说人话该给参数给参数该解释为什么就解释为什么。2. CMake 在 Android NDK 中的整体设计思路2.1 为什么 NDK 最终选择了 CMake 而不是继续推 ndk-build早期 Android 原生开发基本是ndk-buildAndroid.mk的天下这套东西是 NDK 自己的一套 Makefile 方言写起来啰嗦跨平台能力几乎为零。你写了一个Android.mk想拿到 Linux 桌面端复用对不起得重写一份Makefile。CMake 的优势就在于它本身就是为跨平台设计的NDK 团队从 r12 左右开始试验性支持到 r18 正式转正再到后来ndk-build虽然还在维护但官方文档里 CMake 的篇幅明显更重。从工程角度看选择 CMake 意味着你的原生代码可以更容易地和桌面端、服务端共享。我做过一个图像处理库核心算法用 C 写Android 端通过 JNI 调用桌面端用 Qt 调用服务端用命令行工具调用。如果当初用ndk-buildAndroid 这边就得单独维护一套构建脚本用 CMake 之后核心的CMakeLists.txt只写一份各平台通过不同的 toolchain 文件来适配维护成本直接砍半。2.2 CMake 构建 Android 原生库的完整链路理解这条链路对排查问题至关重要。当你在 Android Studio 里点下Run按钮或者命令行执行./gradlew assembleDebug时背后发生的事情大致是这样的Gradle 的 Android 插件读取你build.gradle里的externalNativeBuild配置找到CMakeLists.txt的路径然后调用 NDK 里自带的 CMake 可执行文件。这个 CMake 会加载 NDK 提供的 toolchain 文件android.toolchain.cmake这个文件里定义了目标平台是 Android、目标 ABI 是什么、使用哪个版本的 Clang 编译器、sysroot 在哪里等等。CMake 根据这些信息生成 Ninja 构建文件然后调用 Ninja 去实际编译和链接最终产出.so文件。这些.so会被打包进 APK 的lib/abi/目录下。这条链路里任何一环出问题都会导致构建失败而且报错信息可能来自 CMake、来自 Clang、来自 Ninja、甚至来自 Gradle 的封装层。我后面会专门讲怎么定位问题出在哪一环。2.3 一个典型项目的目录结构应该怎么组织在动手写CMakeLists.txt之前先把目录结构定好能省掉后面很多麻烦。我习惯的结构是这样的project-root/ ├── app/ │ ├── src/main/cpp/ │ │ ├── CMakeLists.txt # 主构建脚本 │ │ ├── native-lib.cpp # JNI 入口 │ │ └── core/ # 核心算法模块 │ │ ├── CMakeLists.txt │ │ ├── include/ │ │ └── src/ │ └── build.gradle ├── third_party/ # 第三方库源码 │ └── some-lib/ │ ├── CMakeLists.txt │ └── ... └── ...把 JNI 入口和核心算法分开核心算法单独成一个 CMake 子项目好处是核心算法可以独立编译测试不依赖 Android 环境。第三方库统一放third_party通过add_subdirectory引入而不是直接改它的源码。这个结构在项目变大之后优势非常明显。3. CMakeLists.txt 核心配置逐项拆解3.1 最低版本声明与项目定义每个CMakeLists.txt开头都得有这两行cmake_minimum_required(VERSION 3.22.1) project(native-lib LANGUAGES CXX C)cmake_minimum_required这个版本号不是随便写的。Android Studio 不同版本捆绑的 CMake 版本不一样NDK 不同版本要求的 CMake 最低版本也不一样。比如 NDK r26 要求 CMake 至少 3.22.1如果你写了个更低的版本号CMake 会以兼容模式运行某些新特性就用不了写太高了如果本地装的 CMake 版本不够直接报错。我的建议是查一下你当前 NDK 版本对应的推荐 CMake 版本然后照着写。可以在$ANDROID_SDK/cmake/目录下看到实际安装了哪些版本。project里的LANGUAGES参数指定项目涉及哪些语言。如果你的项目只有 C写CXX就行如果还有纯 C 文件得把C也加上。这个参数影响 CMake 会去检测哪些编译器漏写会导致某些源文件被忽略而且报错信息往往很隐晦。3.2 设置 C 标准与编译选项set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF)这三行几乎是标配。CMAKE_CXX_STANDARD指定用哪个 C 标准NDK 的 Clang 对 C17 和 C20 支持都很好新项目建议直接上 17 或 20。CMAKE_CXX_STANDARD_REQUIRED ON表示如果编译器不支持这个标准就直接报错而不是悄悄降级——这个很重要否则你可能在本地用着 C17 的特性换台机器编译时编译器悄悄用了 C14然后一堆莫名其妙的错误。CMAKE_CXX_EXTENSIONS OFF表示禁用编译器特有的扩展强制使用标准 C。这个在跨平台项目里尤其重要因为 GCC 和 Clang 的扩展语法不完全一样禁掉扩展能提前暴露兼容性问题。编译选项方面Debug 和 Release 要分开设置set(CMAKE_CXX_FLAGS_DEBUG -O0 -g -DDEBUG) set(CMAKE_CXX_FLAGS_RELEASE -O2 -DNDEBUG)-O0关闭优化方便调试-g生成调试符号。Release 用-O2是性能和编译时间的平衡点-O3有时候会带来意外的代码膨胀。-DNDEBUG会禁用assert宏这个在 Release 里是标准做法。3.3 添加库目标add_library 的三种形态add_library是 CMake 里最核心的命令之一它决定了你要产出什么类型的库。在 Android 场景下主要有三种用法# 共享库最终产出 .so 文件 add_library(native-lib SHARED native-lib.cpp) # 静态库编译时链接进其他库 add_library(core-lib STATIC core/src/algorithm.cpp) # 接口库只有头文件和依赖声明不产出二进制 add_library(header-only-lib INTERFACE) target_include_directories(header-only-lib INTERFACE core/include)SHARED库是 Android 上最常见的因为 Java 层通过System.loadLibrary加载的就是.so。STATIC库适合把核心算法打包成静态库再链接进最终的共享库这样可以隐藏内部符号减小最终.so的体积。INTERFACE库适合纯头文件的工具库它不编译任何东西只是把包含路径和依赖关系传递出去。这里有个容易踩的坑如果你有多个SHARED库它们之间的符号默认是互相可见的可能导致符号冲突。解决办法是用-fvisibilityhidden编译选项只导出必要的符号set(CMAKE_CXX_VISIBILITY_PRESET hidden) set(CMAKE_VISIBILITY_INLINES_HIDDEN ON)然后在需要导出的函数上加__attribute__((visibility(default)))或者用 JNI 的JNIEXPORT宏。3.4 头文件路径与源文件收集target_include_directories(native-lib PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/core/include ${CMAKE_CURRENT_SOURCE_DIR}/third_party/some-lib/include )PRIVATE表示这些头文件路径只对当前目标可见不会传递给依赖它的其他目标。对应的还有PUBLIC和INTERFACE。这个可见性控制是 CMake 现代用法target-based的核心比老式的include_directories全局设置要清晰得多。源文件收集方面我强烈建议显式列出每个源文件而不是用file(GLOB ...)add_library(native-lib SHARED native-lib.cpp core/src/algorithm.cpp core/src/utils.cpp )file(GLOB)看起来方便但它有个致命问题CMake 只在配置阶段执行 GLOB如果你新增了一个源文件但没有重新运行 CMake 配置新文件不会被编译进去。在 IDE 里可能感觉不到但在 CI 环境里经常导致本地能编译CI 上找不到符号的诡异问题。显式列出虽然麻烦但可靠。3.5 链接库与依赖传递target_link_libraries(native-lib PRIVATE core-lib android log )android和log是 NDK 提供的系统库分别对应 Android 的 native 接口和日志接口。core-lib是我们自己定义的静态库。PRIVATE表示这些依赖不会传递给依赖native-lib的其他目标。链接顺序在 CMake 里通常不需要手动关心因为 CMake 会自动处理依赖关系。但如果你直接链接第三方预编译的.so可能会遇到顺序问题。这时候可以用IMPORTED目标来包装add_library(prebuilt-lib SHARED IMPORTED) set_target_properties(prebuilt-lib PROPERTIES IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/libs/${ANDROID_ABI}/libprebuilt.so INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/libs/include )用${ANDROID_ABI}变量来区分不同架构的预编译库这是处理第三方二进制库的标准做法。4. 与 Gradle 的集成配置详解4.1 build.gradle 里的 externalNativeBuild 块CMake 本身不关心 Android是 Gradle 插件把它拉进来的。在app/build.gradle里android { defaultConfig { externalNativeBuild { cmake { cppFlags -stdc17 -fexceptions -frtti arguments -DANDROID_STLc_shared } } ndk { abiFilters arm64-v8a, armeabi-v7a } } externalNativeBuild { cmake { path file(src/main/cpp/CMakeLists.txt) version 3.22.1 } } }cppFlags里的-fexceptions和-frtti需要特别注意。NDK 默认是关闭 C 异常和 RTTI 的因为早期 Android 运行时对异常的支持不完善而且开启这两个特性会增加二进制体积。但现在 Android 对异常的支持已经很好了如果你的代码用了try/catch或者dynamic_cast必须显式开启否则编译能过但运行时行为异常。arguments里的-DANDROID_STLc_shared指定使用共享的 C 标准库。默认是c_static每个.so都静态链接一份 STL多个.so之间传递 STL 对象时会出问题。用c_shared的话所有.so共享同一个libc_shared.so但需要确保这个库被打包进 APK。Gradle 插件通常会自动处理但如果你的项目结构比较特殊可能需要手动确认。4.2 ABI 过滤与产物路径abiFilters决定了编译哪些架构的.so。现在主流是arm64-v8a和armeabi-v7ax86和x86_64主要用于模拟器。全架构编译会显著增加构建时间开发阶段建议只保留当前测试设备对应的架构。编译产物默认在app/build/intermediates/cmake/variant/obj/abi/下最终打包进 APK 的lib/abi/。如果你想在构建后自动拷贝.so到某个目录可以用 Gradle 的 task 来做tasks.register(copyNativeLibs, Copy) { from layout.buildDirectory.dir(intermediates/cmake/debug/obj) into layout.projectDirectory.dir(libs) }4.3 CMake 版本与 NDK 版本的匹配这是新手最容易翻车的地方之一。Android Studio 里可以指定 CMake 版本但如果你指定的版本本地没装它会尝试下载下载失败就报错。更稳妥的做法是让 Gradle 用 SDK 里已经装好的版本或者干脆不指定version让它用默认的。NDK 版本同理android.ndkVersion可以指定不指定的话用 SDK 里最新的。但不同 NDK 版本对 CMake 的要求不一样比如 NDK r23 之后移除了一些旧的 toolchain 行为如果你的CMakeLists.txt依赖了那些旧行为升级 NDK 就会炸。我的建议是在项目里显式锁定 NDK 和 CMake 版本写在build.gradle和 CI 配置里避免我本地能编译的经典问题。5. 完整实操流程从零搭一个可用的原生模块5.1 环境准备与版本确认先确认本地环境。打开终端# 查看已安装的 NDK 版本 ls $ANDROID_SDK/ndk/ # 查看已安装的 CMake 版本 ls $ANDROID_SDK/cmake/ # 查看 CMake 实际版本 $ANDROID_SDK/cmake/3.22.1/bin/cmake --version假设你看到 NDK 有25.2.9519653和26.1.10909125CMake 有3.22.1。那就在build.gradle里锁定android { ndkVersion 26.1.10909125 externalNativeBuild { cmake { version 3.22.1 } } }锁定版本的好处是团队协作和 CI 环境一致坏处是升级时需要手动改。但比起随机崩溃这点麻烦完全值得。5.2 编写第一个可运行的 CMakeLists.txt从最简单的开始确保能跑通再逐步加复杂度cmake_minimum_required(VERSION 3.22.1) project(native-lib LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_library(native-lib SHARED native-lib.cpp ) target_link_libraries(native-lib PRIVATE android log )对应的native-lib.cpp#include jni.h #include android/log.h #include string #define LOG_TAG NativeLib #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) extern C JNIEXPORT jstring JNICALL Java_com_example_myapp_MainActivity_stringFromJNI(JNIEnv* env, jobject /* this */) { std::string hello Hello from C; LOGI(stringFromJNI called); return env-NewStringUTF(hello.c_str()); }Java 层对应public class MainActivity extends AppCompatActivity { static { System.loadLibrary(native-lib); } public native String stringFromJNI(); }这个最小可运行版本跑通之后再往里加东西。很多人一上来就搞复杂结构结果一个链接错误排查半天不如先确保基础链路是通的。5.3 引入第三方库的两种方式第一种是源码引入用add_subdirectoryadd_subdirectory(third_party/some-lib) target_link_libraries(native-lib PRIVATE some-lib)前提是third_party/some-lib里有自己的CMakeLists.txt。这种方式的好处是第三方库也跟着你的编译选项走ABI 和 STL 都一致。坏处是如果第三方库的CMakeLists.txt写得不规范可能会污染你的构建环境。第二种是预编译库引入用IMPORTED目标add_library(prebuilt SHARED IMPORTED) set_target_properties(prebuilt PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libprebuilt.so INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_SOURCE_DIR}/libs/include ) target_link_libraries(native-lib PRIVATE prebuilt)这种方式适合那些不提供源码或者编译很耗时的库。注意${ANDROID_ABI}变量在 CMake 配置阶段是可用的NDK toolchain 会设置它。5.4 编译、打包与验证配置完成后执行./gradlew assembleDebug如果一切正常在app/build/intermediates/cmake/debug/obj/arm64-v8a/下能看到libnative-lib.so。用readelf检查一下$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-readelf -d libnative-lib.so看看NEEDED项里有没有libc_shared.so如果有确认 APK 里也打包了这个库。可以用unzip -l app-debug.apk | grep libc检查。安装到设备后用adb logcat看日志确认stringFromJNI called打印出来说明 JNI 调用链路是通的。6. 常见问题与排查技巧实录6.1 找不到头文件target_include_directories 的可见性陷阱最常见的报错就是fatal error: xxx.h file not found。排查步骤先确认头文件路径是否正确用message(STATUS Include dir: ${CMAKE_CURRENT_SOURCE_DIR}/core/include)打印出来看看。然后确认target_include_directories是加在正确的目标上而且可见性修饰符PRIVATE/PUBLIC/INTERFACE符合预期。如果 A 目标依赖 B 目标B 的头文件路径是PRIVATE的那 A 就看不到 B 的头文件得改成PUBLIC或INTERFACE。还有一种情况是头文件路径里包含了变量但变量为空比如${ANDROID_ABI}在某些上下文里没定义导致路径变成/libs//include自然找不到。6.2 链接错误undefined reference 的几种典型原因undefined reference to xxx这个错误信息量很大。可能的原因一是源文件没被编译进去。检查add_library里是否列出了所有需要的源文件。二是链接顺序问题虽然 CMake 通常能处理但涉及循环依赖时可能需要手动调整。三是符号被隐藏了如果开了-fvisibilityhidden需要导出的符号必须显式标记。四是 C/C 混编时的extern C问题C 代码调用 C 函数时C 函数的声明必须包在extern C里否则链接器找的是 C 修饰后的符号名。排查方法是用nm或llvm-nm查看.so里的符号llvm-nm -D libnative-lib.so | grep xxx-D只看动态符号。如果符号存在但类型是Uundefined说明它依赖别的库如果是t或T说明定义在当前库里。6.3 运行时崩溃UnsatisfiedLinkError 的排查路径java.lang.UnsatisfiedLinkError: dlopen failed: library libxxx.so not found这个错误说明.so没被打包进 APK或者 ABI 不匹配。先检查 APK 里的lib/目录unzip -l app-debug.apk | grep \.so如果某个 ABI 的.so缺失检查abiFilters配置。如果.so存在但还是报错可能是依赖的libc_shared.so没打包或者.so本身有未满足的依赖。用llvm-readelf -d看NEEDED项逐个确认依赖库都在。还有一种情况是设备 ABI 和 APK 里的 ABI 不匹配比如设备是arm64-v8a但 APK 里只有armeabi-v7a虽然理论上可以兼容运行但如果armeabi-v7a的库依赖了arm64特有的东西就会崩。6.4 构建缓存导致的诡异问题CMake 和 Ninja 都有缓存机制有时候改了CMakeLists.txt但构建结果没变化或者报错信息指向已经删掉的文件。这时候清理缓存./gradlew clean rm -rf app/.cxxapp/.cxx目录是 CMake 配置和构建的缓存删掉它强制重新配置。这个操作我几乎每周都要做几次尤其是切换分支或者改构建脚本之后。6.5 常见问题速查表报错信息可能原因排查方法fatal error: xxx.h file not found头文件路径未配置或可见性不对检查target_include_directories的路径和修饰符undefined reference to xxx源文件未编译、链接顺序、符号隐藏、extern C 缺失用llvm-nm查符号检查源文件列表UnsatisfiedLinkError: library not found.so未打包、ABI 不匹配、依赖缺失解压 APK 检查lib/目录用llvm-readelf -d查依赖error: undefined symbol: __cxa_throw未开启 C 异常支持在cppFlags加-fexceptionserror: cannot use dynamic_cast未开启 RTTI在cppFlags加-frtti构建结果与预期不符CMake 缓存未更新删除app/.cxx目录重新构建CMake Error: Could not find toolchain fileNDK 路径或版本配置错误检查ndkVersion和ANDROID_NDK环境变量7. 进阶技巧与性能优化7.1 减小 .so 体积的几种手段原生库体积直接影响 APK 大小尤其是多 ABI 打包时。几个有效的手段开启链接时优化LTOset(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE ON)LTO 让编译器在链接阶段做跨编译单元的优化能去掉未使用的代码但会显著增加链接时间。Release 构建可以开Debug 就别开了。剥离调试符号set(CMAKE_CXX_FLAGS_RELEASE ${CMAKE_CXX_FLAGS_RELEASE} -s)-s告诉链接器在最终产物里去掉符号表能减小不少体积。但注意这会让你无法用addr2line解析崩溃堆栈所以最好保留一份带符号的版本用于崩溃分析。使用-ffunction-sections -fdata-sections配合-Wl,--gc-sectionsset(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -ffunction-sections -fdata-sections) set(CMAKE_SHARED_LINKER_FLAGS ${CMAKE_SHARED_LINKER_FLAGS} -Wl,--gc-sections)这组选项让每个函数和数据单独成段链接器可以精确地回收未引用的段。实测在一个中等规模的项目里能减掉 15% 到 25% 的体积。7.2 多模块项目的依赖管理策略项目变大之后CMakeLists.txt会变得难以维护。我的做法是每个模块一个CMakeLists.txt顶层只负责add_subdirectory和全局配置。模块之间的依赖通过target_link_libraries显式声明避免隐式的全局依赖。对于跨模块的公共配置比如编译选项、C 标准可以定义一个INTERFACE库来统一管理add_library(project-defaults INTERFACE) target_compile_features(project-defaults INTERFACE cxx_std_17) target_compile_options(project-defaults INTERFACE -Wall -Wextra)然后每个模块target_link_libraries(mymodule PRIVATE project-defaults)这样改一处就能全局生效。7.3 构建速度优化实践原生构建慢是常态几个提速手段用 Ninja 而不是 Make。NDK 默认就用 Ninja但如果你手动配置过确认一下CMAKE_GENERATOR是Ninja。开启并行编译。Gradle 里设置android { externalNativeBuild { cmake { arguments -DCMAKE_BUILD_PARALLEL_LEVEL8 } } }数字根据机器 CPU 核心数调整一般是核心数的 1 到 1.5 倍。用ccache。如果团队里多人反复编译同一份代码ccache能缓存编译结果第二次编译快很多。配置方式是在CMakeLists.txt里find_program(CCACHE_PROGRAM ccache) if(CCACHE_PROGRAM) set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE_PROGRAM}) endif()不过 Android NDK 的交叉编译场景下ccache命中率不一定高因为编译命令里包含大量路径信息换台机器或换个目录就失效了。适合在 CI 环境里用固定路径构建。8. 我踩过的那些坑和最后的经验说几个印象深刻的翻车经历。有一次项目里同时用了两个第三方库都静态链接了c_static结果运行时崩溃堆栈指向 STL 内部。排查了半天才发现是两个库各自带了一份 STL内存分配和释放跨了不同的 STL 实例。改成c_shared之后问题消失。这个坑的教训是只要项目里有多个.so就老老实实用c_shared别省那点体积。还有一次是 CI 上构建失败报错说找不到某个头文件但本地完全正常。查了半天发现是 CI 环境的 CMake 版本比本地低不支持某个语法。从那以后我在项目根目录放了一个cmake-version.txtCI 脚本里读这个文件来安装对应版本确保环境一致。另一个常见的坑是file(GLOB)导致的新增文件不编译。有个同事加了个新的.cpp文件本地 IDE 里能跑提交后 CI 构建出来的.so里就是没有那个文件的符号。原因就是 CI 是全新配置GLOB 执行时文件已经存在所以能扫到但本地是增量构建CMake 没有重新配置。后来我们统一改成显式列出源文件这个问题再也没出现过。最后分享一个调试技巧当你不确定 CMake 实际用了哪些编译选项时在CMakeLists.txt里加set(CMAKE_EXPORT_COMPILE_COMMANDS ON)这会在构建目录生成compile_commands.json里面记录了每个源文件实际的编译命令。用这个文件可以精确知道编译器收到了什么参数排查选项相关的问题非常高效。配合 IDE 的 clangd 插件还能获得准确的代码补全和跳转。CMake 这东西入门容易精通难但大部分项目用到的就是那些核心功能。把add_library、target_include_directories、target_link_libraries这三个命令吃透再理解 toolchain 和 Gradle 的集成方式基本就能应付绝大多数场景了。剩下的就是遇到问题查文档、看报错、动手试踩的坑多了自然就熟了。