1. 为什么说C方向是鸿蒙开发的“深水区”前阵子和一个转做鸿蒙的同事聊天他问我“我用ArkTS写界面已经挺顺了还需要啃C吗”我当时没有直接回答反问他一句“如果某个模块要处理视频解码、做实时渲染、跑规则引擎你用ArkTS扛得住吗”他沉默了几秒然后说“那确实得用C。”这就是我想说的重点鸿蒙应用开发工程师这个岗位如果只会ArkTS你可能处理的是常规业务但如果加上“C方向”这四个字你负责的就是整个应用里最硬核、最底层、最需要性能的部分。这个岗位不是“会写C”就能胜任而是要理解鸿蒙系统的原生运行机制知道C代码在鸿蒙设备上是怎么被编译、加载、调用和调度的并且能跟ArkTS层顺畅协作。这篇文章适合谁我建议这么几类人重点看第一已经熟悉ArkTS、想往底层深挖的应用开发者第二从Android NDK、iOS原生开发或者其他跨平台引擎转向鸿蒙的C工程师第三正在准备鸿蒙应用开发工程师面试需要系统梳理C方向知识体系的人。无论你是哪一种这篇文章的目标都只有一个——把这摊事讲明白让你看完之后能直接上手干活。在展开之前先把一个概念理清楚鸿蒙的“C方向”到底指什么。它不是什么神秘的特别语言而是指基于鸿蒙原生平台使用C编写底层模块、核心算法、引擎层代码再通过统一的接口层与UI框架通信。这个方向涉及的技术栈集中在三块Native开发环境与构建工具链、ArkTS与C的互操作层、以及基于C的高性能模块设计。后面我会逐个拆开讲。2. 鸿蒙原生开发的核心技术栈拆解不止是NDK那点事2.1 从运行时到框架层一张图看懂C代码在鸿蒙里的位置很多人一听到“鸿蒙C”下意识就会联想到Android的NDK。这个类比有一定道理但不够准确。Android NDK强调的是把C/C代码编译成SO库供Java/Kotlin层通过JNI调用而鸿蒙的C开发面要更宽一些。在鸿蒙系统里C代码可以出现在好几个层级。最顶层是应用层用C做核心能力库比如编解码器、图像处理算法、数据库引擎这一层通过NAPI提供给ArkTS调用。再往下一层是系统框架层比如ArkUI的渲染引擎、动画系统、输入事件分发这些基础设施本身就是C实现的。如果你做的是系统级的开发、硬件适配或者要深度定制某些框架行为那直接接触的就是这一层。更底层则是与内核、驱动相关的部分通常只有设备厂商会深入。对大多数应用开发工程师来说日常主力战场是第一个层级用C写模块通过统一的接口层给上层使用。但理解框架层的存在至关重要因为很多性能问题的根源恰恰是你和框架层的协作方式出了问题比如不必要的跨进程调用、过度频繁的UI刷新通知、或者数据拷贝路径太长。2.2 对号入座C在鸿蒙应用开发里的五个典型场景C不是万金油也不是为了炫技。我在实际项目里总结了一下鸿蒙应用里真正值得用C去做的其实就这五类场景高性能计算与算法比如音视频编解码、图像处理、信号处理、几何计算。这类任务对算力有硬性要求用ArkTS写往往达不到预期帧率或处理速度。跨平台核心逻辑复用如果你手里已经有成熟的C引擎或核心库业务逻辑在多个平台都需要复用那做一套鸿蒙适配层比在ArkTS里重写一遍省力得多也能保持逻辑一致性。内存密集与底层系统交互涉及直接操作文件描述符、共享内存、Socket、Binder等系统资源时C能提供更直接的控制。实时渲染与游戏引擎图形渲染管线、物理引擎、资源调度这几块C基本是唯一务实的选择。安全与加密模块很多加解密算法、密钥管理逻辑用C实现既兼顾性能也便于做代码层的加固和保护。反过来说我强烈不建议什么业务都往C里塞。如果你只是做一个列表页的数据格式化、处理一个JSON配置、算一个简单的业务逻辑那老老实实用ArkTS就好。C引入的成本是多维度的——编译时间变长、内存管理要更小心、跨语言边界要处理类型转换、调试链路也更长。盲目使用只会让项目变复杂不会让项目变快。2.3 给初学者的技术栈学习路径C方向的鸿蒙开发知识体系其实可以分成四层我按学习顺序排一下第一层是基础语法与工程能力现代C特别是C17/20特性、STL、智能指针、移动语义、多线程编程、CMake构建。这些是地基。第二层是鸿蒙平台生态DevEco Studio开发工具、鸿蒙SDK中的Native API、包管理结构、构建签名流程、IDE调试技巧。第三层是互操作机制napi接口、对象生命周期管理、异步任务模型、数据序列化与共享内存传输。第四层是系统底层知识ArkUI渲染管线、IPC机制、内存分配器行为、CPU调度与线程优先级控制。前三层是应用开发工程师必须吃透的第四层可以在碰到具体优化问题时再定向深入。别一开始就想把整个系统源码读完那既不现实也容易打击信心。3. 从零搭建一个可运行的C原生工程3.1 环境准备中最容易被忽略的两个细节工欲善其事必先利其器。搭建环境这件事看起来简单但我见过不少人在第一步就翻车。DevEco Studio装好之后第一件事不是急着建工程而是检查SDK组件里是否安装了Native开发所需的部分。这里有两个容易踩的坑。第一个坑是只安装了默认的SDK包。默认安装通常只涵盖ArkTS应用开发所需的组件而C交叉编译工具链、Native API头文件、CMake工具这些是需要额外勾选的。如果你建工程时发现没有C模板可选或者编译时报找不到napi.h八成就是这个原因。解决方法是进入IDE的SDK Manager找到对应的API版本把Native相关组件补装上。第二个坑是交叉编译环境的配置。鸿蒙主要支持三种CPU架构ARM64、x86_64和较新的64位指令集。制作模拟器镜像时编译目标要选择x86_64真机调试则要选ARM64如果你的库要对外发布给别的开发者用最好打成包含两种架构的通用包。这个配置在模块的build-profile.json5文件里通过abiFilters字段控制。很多人第一次编译通过、装到真机上却提示找不到.so文件就是这一步没配好。3.2 工程创建与CMake配置实战打开DevEco Studio新建工程时选择“Native C”模板。这个模板会自动生成一个最基础的libentry.so供ArkTS侧通过调用一个native接口返回一串字符串。模板工程的主要文件结构大致如下entry/src/main/cpp/ ├── CMakeLists.txt ├── napi_init.cpp ├── types/ │ └── libentry/ │ ├── Index.d.ts │ └── package.jsonCMakeLists.txt是整个原生构建的核心。模板生成的默认配置很简单核心就几条指定最低CMake版本、定义项目名称、添加一个共享库目标、指定源文件、链接必要的鸿蒙原生库。但在真实项目里你很快会发现还需要扩展它引入其他源码目录、链接第三方的SO库或静态库、设置编译选项、控制导出符号。我给你一段实际可用的CMake配置作为参考里面注释了每个关键部分的作用cmake_minimum_required(VERSION 3.5.0) project(my_native_lib) # 设置C标准为C17开启必要的编译优化 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 如果开启GLIBCXX调试在Release构建中不要这样做 if (CMAKE_BUILD_TYPE STREQUAL Release) add_compile_definitions(NDEBUG) endif() # 将cpp子目录的源码全部加入编译 aux_source_directory(. SOURCE_FILES) aux_source_directory(./third_party THIRD_PARTY_SOURCE_FILES) add_library(entry SHARED ${SOURCE_FILES} ${THIRD_PARTY_SOURCE_FILES}) # 链接鸿蒙系统库取决于你的模块需要 target_link_libraries(entry PUBLIC hilog_ndk.z ace_ndk.z napi_ndk.z )注意几个容易出问题的地方第一aux_source_directory会把目录下所有.cpp文件都加进去如果你的目录里有单元测试文件或临时调试文件可能会造成编译冲突建议还是用显式文件列表更可控第二链接鸿蒙系统库时不同API版本的库名可能会有调整要以官方SDK里实际安装的为准第三如果你要导出给其他模块用默认的libentry.so名称和入口符号要跟types目录里的声明保持一致。3.3 第一个napi接口的完整写法模板生成的napi_init.cpp里会有一个简单的示例。我建议你不要直接在那个示例上改而是理解它的结构后自己重写一遍。下面这个例子我加上了完整的错误处理这在开发阶段非常有用因为NAPI层面的失败如果处理不当上层JavaScript很容易被一个莫名其妙的异常卡住。#include napi/native_api.h #include string static napi_value EchoString(napi_env env, napi_callback_info info) { size_t argc 1; napi_value args[1] {nullptr}; napi_get_cb_info(env, info, argc, args, nullptr, nullptr); // 取出传入的字符串 std::string input; size_t len 0; napi_get_value_string_utf8(env, args[0], nullptr, 0, len); input.resize(len); napi_get_value_string_utf8(env, args[0], input[0], len 1, len); // 构造返回值 napi_value result; napi_create_string_utf8(env, input.c_str(), input.size(), result); return result; } EXTERN_C_START static napi_value Init(napi_env env, napi_value exports) { napi_property_descriptor desc[] { {echoString, nullptr, EchoString, nullptr, nullptr, nullptr, napi_default, nullptr} }; napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc); return exports; } EXTERN_C_END static napi_module demoModule { .nm_version 1, .nm_flags 0, .nm_filename nullptr, .nm_register_func Init, .nm_modname entry, .nm_priv ((void*)0), .reserved { 0 }, }; extern C __attribute__((constructor)) void RegisterEntryModule(void) { napi_module_register(demoModule); }注意几个细节nm_modname必须跟ArkTS侧import时的库名完全一致注册函数建议用__attribute__((constructor))确保模块加载时第一时间被注册NAPI接口间的错误码和异常状态要仔细处理不要吞掉异常。3.4 构建、签名、装机的完整闭环流程代码写完之后完整的验证闭环是构建SO库、安装到模拟器或真机、从ArkTS调用、看log排查问题。在DevEco Studio里你可以直接点击Run按钮完成编译、签名、安装、启动的全部步骤。这里我想提醒一个容易让新手困惑的点签名。如果只是跑本地模拟器IDE会自动使用调试签名但如果要在真机上调试你需要先在项目的签名配置里申请并配置调试证书。这一步是国内的开发者必须走的流程建议提前做好别等到真机测试当天才发现跑不了。如果你更习惯命令行操作可以这样做# 在项目根目录执行 hvigorw assembleHap --mode module -p productdefault # 签名后的HAP产物路径通常在 # entry/build/default/outputs/default/entry-default-signed.hap拿到HAP后可以用IDE安装也可以用命令行安装到已连接的设备hdc install entry-default-signed.hap安装启动之后在命令行用日志工具过滤出自己进程的日志验证接口是否正常返回。3.5 弄懂构建产物HAP、HSP 与 .so 的协作关系很多人写了很久的鸿蒙程序对构建产物还是糊里糊涂。这里用最简单的话讲清楚HAP是应用安装包的最小交付单位最终安装到设备上的就是它HSP是共享包相当于模块化的动态库供多个HAP复用代码而你写C编译出来的.so文件要么打包在HAP里要么打包在HSP里。如果你的.so只被自己这个模块用放在entry里就够。如果一套native能力被多个应用模块共享那就应该打成HSP。写types声明文件的时候ensure接口签名和实际实现严格对齐否则上层用起来会出现两个结果导入的接口不清楚或者类型不匹配运行时报错。4. 打通C与ArkTSnapi绑定与数据互传的完整链路4.1 为什么用napi以及它和JNI的底层差异聊到C与上层的互操作很多从Android转过来的朋友会拿JNI做参照物。NAPI的设计思路和JNI确实有相似之处——都是提供一套C API让原生代码与脚本引擎交互。但实际用起来NAPI有几个关键差异更加友好。第一NAPI的设计更接近Node.js的N-API接口风格更统一版本稳定性更好你在API 9上写的代码升级到API 12大概率还能直接用。JNI的版本兼容问题相对更头疼一些。第二NAPI对JavaScript引擎的实现细节做了封装开发者不需要关心底层到底是哪个引擎。第三NAPI暴露的对象模型更贴合脚本语言的思维方式接口命名和参数顺序都更直观。但也要说清楚NAPI依然是C接口你在里面创建的引用要手动释放Java层的垃圾回收不会帮你善后。该懂的内存管理规则一条都逃不掉。4.2 参数传递的三种方式值拷贝、引用传递、共享内存跨语言的数据传递本质上是个“翻译”问题。C侧是内存地址和二进制布局ArkTS侧是对象和引用。翻译方式选得不对性能就会大幅下滑。我按实际场景把传递方式分成三档。第一档是简单值拷贝适用于小数据。字符串、数字、布尔值直接通过napi_create_string_utf8、napi_create_double这些接口把它变成JavaScript可识别的值。优点是简单安全缺点也显而易见——每次调用都要发生拷贝大字符串传一次就够让人肉疼的。第二档是引用传递适用于对象和复杂结构。比如你传一个对象进来C侧拿到的其实是一个napi_value句柄不能直接解引用。要通过napi_get_value_string_utf8等接口把内容读取到C缓冲区。需要明白的是这个读取过程仍然是一次拷贝而且比第一档隐式拷贝更容易被忽略。第三档是共享内存适用于大数据量的高性能场景。比如你要传输一帧视频数据、一批点云数据几千字节到几兆字节的体量。这时候最有效的做法是在C侧分配一段连续内存通过napi_create_external_arraybuffer把这块内存包装成ArrayBuffer交给ArkTS侧两侧直接操作同一块物理内存零拷贝。这要求你非常小心地管理生命周期谁分配、谁释放、什么时候释放必须提前约定清楚。4.3 异步任务模型别把主线程卡死刚接触NAPI的人最容易犯的错误是把耗时操作直接放在接口函数里同步执行。比如一段图像处理要20毫秒你在接口里同步跑完接口返回时页面卡顿一下。如果处理逻辑要100毫秒甚至更久页面就会明显掉帧严重的甚至会导致系统弹出应用无响应。正确做法是用异步任务模型。在NAPI里有两种常见套路一种是创建Promise把任务丢到工作线程里完成后再通过napi_resolve_deferred把结果返回给ArkTS侧另一种是使用napi_thread_safe_function从工作线程回调到JavaScript主线程。我在实际项目里更推荐基于Promise的方案代码可读性强上层写起来自然。下面给一个简化版的思路struct AsyncWork { napi_async_work work; napi_deferred deferred; napi_env env; std::string input; std::string output; }; // 在工作线程中执行耗时逻辑 static void ExecuteWork(napi_env env, void* data) { AsyncWork* work static_castAsyncWork*(data); // 在这里做耗时操作不要碰napi_env work-output HeavyProcess(work-input); } // 回到主线程后解析Promise static void CompleteWork(napi_env env, napi_status status, void* data) { AsyncWork* work static_castAsyncWork*(data); napi_value result; napi_create_string_utf8(env, work-output.c_str(), work-output.size(), result); napi_resolve_deferred(env, work-deferred, result); napi_delete_async_work(env, work-work); delete work; } napi_value StartAsync(napi_env env, napi_callback_info info) { // 创建Promise、封装AsyncWork、napi_create_async_work... return promise; }这里有一个我踩过的坑ExecuteWork里千万不要调用任何napi_env相关的函数因为那个线程不在JavaScript引擎的线程里调用会被阻塞或崩溃。凡是跟引擎相关的操作都要留到CompleteWork里做。4.4 生命周期管理的铁律谁分配谁释放跨语言边界的内存管理是C方向的重灾区。ArkTS侧有垃圾回收机制C侧没有当一块内存被JavaScript对象引用时错误的管理方式会导致两类典型问题一是悬垂指针内存被C侧释放了但JS侧还在引用它一访问就崩溃二是内存泄漏JS侧已经不再引用了但C侧没有释放堆内存白白越积越多。我给自己定了一条铁律**跨边界的内存分配者负最终管理责任但释放时机必须双方协商。**比如ArrayBuffer传给JS之后C侧默认不再管理这块内存。除非通过napi_add_finalizer给这个外部对象挂一个析构回调让引擎在对象被回收时通知C侧做清理。反过来如果数据只是临时传递给C接口做一次性处理那接口内部处理完就该立即释放缓冲绝不要试图长期持有它。4.5 类型映射速查表从ArkTS到C的常见对应关系我在项目里整理过一张对照表新手写接口定义时直接对照能省掉大量出错时间。下面这张表是基本版进阶场景建议在开发中慢慢积累补充。ArkTS 类型napi 接口示例C 落地类型numbernapi_get_value_double / napi_create_doubledouble / int32_tstringnapi_get_value_string_utf8std::stringbooleannapi_get_value_boolboolObjectnapi_get_value_string_utf8 / napi_get_named_property逐字段读取到 structArrayBuffernapi_get_arraybuffer_info指针 长度TypedArraynapi_get_typedarray_info底层连续内存指针Functionnapi_create_reference回调引用需手动管理Promisenapi_create_promisedeferred 句柄一个重要的原则**接口定义要追求朴素。**能传一个int解决的不要传一个对象能传一个数组的不要传一个嵌套对象。因为每一次复杂结构的解析都意味着更多的指令和潜在出错点也意味着上层使用者的心智负担增加。5. 实战中必须知道的性能优化与架构设计5.1 跨语言调用的开销一次调用到底贵在哪在讨论性能优化之前先搞清楚一个问题一次ArkTS到C的函数调用到底贵在哪里首先napi接口调用要经过参数解析和类型转换。JavaScript引擎和C世界的数据布局完全不同每一个参数都要走一遍转换逻辑。其次调用过程会带来引擎的上下文切换和回调机制的开销。最后如果你处理得不好还会有隐式的内存拷贝。三个因素叠加一次napi调用比同进程内的普通C函数调用要慢一到两个数量级这是正常的别幻想它能和本地函数一样快。理解了这一点优化思路就清晰了**减少跨边界的调用次数放大单次调用的数据量。**举个例子你要处理一个数组里的1000个元素与其在ArkTS里循环1000次调用一个native接口处理单个元素不如一次性把整个数组传进去在C里用循环处理完最后返回结果。这样调用次数从1000次降到1次性能提升非常可观。5.2 用C实现高性能模块时怎样设计接口才合理我在设计一个C模块的接口时会先问自己三个问题这个接口会被高频调用吗参数和数据量是固定的还是动态变化的接口语义是同步的还是可以做成异步的想清楚这三个问题之后接口设计的答案基本自动浮现。高频、大数据量、实时性要求高的场景走批量传输和异步回调低频、小数据、逻辑简单的场景同步调用反而更清晰不必为了异步而异步。另外一个常见的架构问题是如何组织多个.so。我见过一些项目把几十个功能模块全部塞进一个entry的SO里结果改动一个函数就得让整个包重新编译发布版体积也大得吓人。反过来合理的做法是把稳定成熟的底层能力放进一个共享SO或HSP把经常迭代的业务模块单独拆成小的SO加载时按需注入。这个道理和代码分层是一样的只是在二进制层面再做一次隔离。5.3 线程模型与CPU调度的配合技巧C侧的多线程设计直接决定了整个应用的稳定性和流畅度。我常用的做法是维护一个线程池专门处理耗时任务。线程数不用多通常等于设备的CPU核心数减一或减二即可多了反而会因为上下文切换损失性能。在线程优先级这件事上我的经验是除非有明确的实时性要求否则不要随意把线程优先级调到最高。高优先级线程会抢占UI线程的时间片导致掉帧。反过来如果任务是典型的低优后台任务比如预加载、日志压缩、数据整理主动调低优先级反而对整个系统的流畅度更友好。还有一个小细节C侧访问共享数据时尽量用细粒度的锁或者干脆用无锁数据结构。锁竞争是性能杀手尤其在手机这种CPU资源本身就不宽裕的设备上一个没优化好的锁可能让整个应用的帧率从60掉到30。5.4 热路径上的典型优化手段所谓热路径就是程序执行频率最高的那条代码路径。针对鸿蒙C开发我总结了几条最实用的优化手段按性价比排序减少不必要的动态内存分配不要在热路径里频繁new/delete用对象池或复用缓冲区。内存分配器不是免费的一次分配可能触发系统调用和锁在循环里的开销会被无限放大。改用连续内存容器std::vector、std::array、预分配的裸缓冲区往往比std::map、std::unordered_map在数据量小时更快因为局部性更好缓存命中率更高。禁止在热路径里打日志日志输出涉及格式化、文件IO或网络传输开销极高。我见过一个性能问题排查了半天最后定位到是某个调试日志在循环里每一帧都打印。善用编译优化Release构建开启O2/O3后很多手工的小优化反而变成了负优化。与其锲而不舍地抠细节不如先让编译器把该做的优化做了。这些手段单独看都很简单但组合起来就是几倍甚至十几倍的性能差距。5.5 稳定性高于一切错误处理与防御式编程性能做完之后还剩下一个更为重要的问题稳定性。一个C模块在真机上闪退不像是UI页面报错那样容易排查往往一个悬垂指针就够你查一整天。我在编写C模块时有几个习惯一直保留着。第一所有入口函数的第一步都是校验参数对空指针、非法长度、负数索引直接返回错误码不要试图“猜”调用者的意图。第二跨语言边界处要捕获所有C异常NAPI接口是C接口C异常不能穿过它否则就是未定义行为。我会在每个napi入口函数里包一层try-catch把异常转成NAPI的错误状态抛给上层。第三避免使用全局非平凡对象因为静态对象的初始化和析构顺序在不同系统版本上可能有微妙差异这是很多偶现崩溃的根源。如果实在需要全局状态建议用函数内的静态局部变量代替。6. 一道高频面试题与我的回答思路6.1 面试官问“C方向与ArkTS方向的区别”时想听什么不少人面试时遇到这道题会从语言层面去回答C是编译型语言、性能高ArkTS是脚本语言、开发效率高。这么说没错但太浅了。面试官想听的其实是你是否理解这条技术路线的分工逻辑以及你在实际项目中是否经历过这种分工的权衡。我的回答思路是这样的首先明确两者的职责边界ArkTS负责UI交互和业务编排C负责算力和底层能力供给。然后举一个具体例子比如图像处理流程UI侧只要负责把图片交给C接口、把结果展示出来而C侧处理像素级操作、性能敏感的计算。最后强调协作层面如何定义接口、如何选择同步异步、如何管理内存。如果能再结合自己项目里踩过的坑比如因为一次同步调用卡了主线程几百毫秒而改造成异步任务那就是加分的实战故事。6.2 这五个追问每一个都指向实战陷阱接下来面试官大概率会顺着你的回答追问细节。这五个问题几乎覆盖了C方向的实战盲区“你的模块里有一个长时间运行的任务你打算怎么设计”——答案核心是异步化丢线程池、回调、Promise千万别同步阻塞。“如果有大量数据要从ArkTS侧传到C侧你怎么传”——先说是ArrayBuffer进一步说明怎么复用缓冲、怎么避免拷贝。“C侧抛出的异常上层怎么感知”——先说要try-catch捕获转成错误码再说明NAPI异常怎么通过napi_throw传递。“你的.so里有一个全局单例生命周期怎么管理”——关键点在初始化顺序、进程生命周期和可能的重复加载场景别让单例变成地雷。“遇到崩溃你的排查思路是什么”——第一步看崩溃栈第二步找日志第三步检查是否和跨边界内存相关第四步用工具复现和定位。6.3 简历上的项目经历怎么描述才加分简历这一关淘汰率很高很多人写“熟悉鸿蒙C开发”就被刷了。问题出在太笼统。我给你一个具体的写法参考不是写“负责底层模块开发”而是写“基于NAPI设计了一套异步批量数据处理框架将图像特征提取的跨语言调用次数从每帧12次降低到1次处理耗时任降至原来的35%”。能量化的尽量量化能让面试官立刻感受到你对这个方向的理解深度。项目规模大不大不重要重要的是你能否把项目里的关键问题、决策过程、最终数据讲成一个完整的故事。我面试过不少候选人项目经历写得花团锦簇细节一问就含糊。真正的实战者解释清楚每一个技术选择背后的妥协根本不需要背稿子。7. 我对C方向未来价值的一点判断过去一年鸿蒙生态的扩张速度是肉眼可见的。随着原生应用越来越多性能敏感型场景的落地需求也在快速增加。视频编辑、实时通信、游戏引擎、工业控制这些领域最终都会需要能把C和鸿蒙平台深度融合的人。这个方向的门槛确实比纯上层开发高但它护城河也更宽。对开发者来说C能力本身是稀缺的再加上对鸿蒙平台的理解这种复合能力在未来的增量市场中会越来越值钱。如果你已经决定往这个方向走我最后再给一个学习方法上的建议不要只停留在看文档的层面去找一个自己感兴趣的小项目比如一个简单的图像滤镜、一个JSON解析器、一个小型物理模拟器从搭建工程开始完整地跑一遍。你真正理解鸿蒙C开发的时刻一定是在实际代码里解决掉一个又一个未知问题的时刻而不是读完某个章节或看完某段教程的瞬间。
阅读完成 · 觉得有帮助?