ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Flutter三方库NDK鸿蒙化:从工具链迁移到dart:ffi调用实战

Flutter三方库NDK鸿蒙化:从工具链迁移到dart:ffi调用实战 按理说把 Android 上验证了很久的 Flutter 三方库迁到鸿蒙绝大多数纯 Dart 的模块都能一路顺风。真正的坎儿往往出现在那些“只要跑起来就必须碰底层”的库上图像压缩、音视频编解码、加密、科学计算——这些库在 Android 侧几乎清一色是靠 NDK 编译出 .so再通过 JNI 和 Flutter 插件层打交道。一旦到了鸿蒙Java 代码没了JNI 环境换了Android 的 Bionic libc 变成了鸿蒙生态自己的系统库原先的 .so 直接搬过去大概率连加载都加载不起来。这篇就来把 Flutter 三方库的 NDK 部分真正“鸿蒙化”这件事整个捋一遍从鸿蒙 NDK 工具链的差异到 CMake 工程迁移再到 dart:ffi 调用链路的改造和运行时踩坑全是实际操作层面的东西。我会用一个“带原生 FFT 计算能力的 Flutter 图像处理库”作为贯穿全文的例子它和社区里常见的flutter_image_compress、ffmpeg_kit_flutter、sqlite3_flutter_libs这类库的适配路径是一样的。如果你手里的库也是“纯 Dart 壳 Android NDK 原生实现”的结构按这篇的链路走下来应该能少走不少弯路。1. 为什么 Flutter 三方库一碰 NDK鸿蒙适配就成了硬骨头1.1 理想很丰满pure Dart 几乎零成本先看清楚哪些部分根本不用折腾。Flutter 三方库大体可以分成三类纯 Dart 库整个库就是一个 pub 包内部全部用 Dart 实现不依赖任何平台原生代码。迁到鸿蒙几乎不需要付出任何成本因为 Flutter 的鸿蒙运行时本身已经把 Dart VM、渲染引擎那套东西打通了Dart 代码层面没有任何感知。声明式插件Android 端只是用 Kotlin/Java 写了少量平台通道代码没有 JNI、没有 C/C 编译产物。这时候鸿蒙化主要是把 Kotlin/Java 逻辑翻译成 ArkTS工作量可控。带 NDK 的三方库Android 端有android/src/main/cpp或externalNativeBuild编译出若干 .soJNI 层负责转发。这类库才是最难啃的骨头因为牵涉到 ABI、系统库、链接器、FFI 调用约定一整套底层链路。你在鸿蒙上大概率遇到的场景是第二种、第三种混合体。不少 flutter 插件在 Android 端又写了 Kotlin 又写了 CMake比如调用 OpenCV、FFmpeg、OpenSSL 的库基本都是这样。纯 Dart 层可以“白嫖”原生层必须重来。1.2 现实很骨感NDK 产物的 ABI 差异与系统库差异很多人一开始会觉得Android 的 .so 都是 ARM64 的鸿蒙手机也是 ARM64 的直接把libxxx.so拿过去不就行了实测下来的结论是不是所有 CPU 架构一样就能通用。这里面的差异主要集中在三块。第一是系统库符号集。Android 的 NDK 运行时用的是 Bionic libc鸿蒙 Native 侧用的是自己的 libc 实现。Android 上的很多 helper API比如__android_log_print、android_dlopen_ext这类在鸿蒙上压根没有或者被替换成了OH_LOG_Print、dlopen这类不同入口。如果一个第三方库在 C/C 代码里直接写了 Android 特有的系统调用链接到鸿蒙上就必然报 undefined symbol。第二是C 标准库的链接方式。Android NDK 默认可以选c_static或c_shared鸿蒙 CMake 工具链也支持类似模式但两者的 STL 头文件版本、动态库产物名libc_shared.so不完全一致。如果你在 Android 上把 STL 静态编进 .so在鸿蒙上直接复用偶发崩溃往往就出在 libc 的 ABI 兼容性上。这种坑最恶心因为它不是一上来就崩而是跑到某个边界条件突然 SIGSEGV。第三是构建工具链本身。Android NDK 的 CMake toolchain 文件是android.toolchain.cmake鸿蒙的是ohos.toolchain.cmake。两个工具链的 target triple、系统头文件路径、链接器参数全都不一样。所以鸿蒙化不是什么“改个 ABI 参数重新编译”而是要切一套完整的构建工具链。还有一个容易忽略的点鸿蒙 Native 动态库的符号可见性管理比 Android 更严格。很多库在 Android 上能跑是因为 Bionic 对未导出符号的容忍度比较高鸿蒙 CMake 默认开启CXX_VISIBILITY_PRESET hidden你没有主动导出的函数外面就是看不见。适配的时候就别再指望“编译过了就一定能动态加载”符号导出这一步要专门检查。2. 鸿蒙的 Native 能力长什么样NDK 机制对照拆解2.1 鸿蒙 NDK 的开发入口和承载形态鸿蒙的 NDK 不是和 Android NDK 平行的一套东西它更准确地说应该叫“Native 开发套件”随 DevEco Studio 的 SDK 一起分发。装好 DevEco Studio 之后SDK 目录下会有一个native文件夹里面能看到sysroot、toolchains、llvm、cmake、build-tools这些子目录结构上和 Android NDK 有很强的既视感但你要记住这套工具链编译出来的 .so 只能跑在鸿蒙环境里。在工程层面DevEco Studio 创建 Module 时可以直接选“Native C” 模板生成的结构一般长这样entry/ ├── src/main/ │ ├── cpp/ │ │ ├── CMakeLists.txt │ │ └── native-lib.cpp │ ├── ets/ │ └── module.json5 └── oh-package.json5src/main/cpp底下就是 C/C 源码和 CMakeLists这跟 Android Studio 的externalNativeBuild思路非常接近。但要注意一个关键差异Android 的构建流程是 Gradle 去调 CMake鸿蒙的构建流程是 hvigor 去调 CMake。这也意味着很多build.gradle里的 NDK 配置比如abiFilters、arguments、cFlags在鸿蒙工程里没有对应位置需要换到build-profile.json5和 CMakeLists 里来表达。另外鸿蒙 Native 层对外暴露的 API 入口主要有两类Node-APIN-API为 ArkTS 与 C/C 之间的互操作设计头文件是napi/napi.h。你可以把它理解成“鸿蒙生态里的 JNI”但它不依赖 Java VM走的是 Node.js 风格的napi_env。直接 C ABI如果你的 C/C 库并没有和 ArkTS 有太多交互只是想被某个运行时加载并调用纯函数那就完全可以不碰 N-API直接暴露 C 风格的导出函数让调用方用 dlopen/dlsym 或者 dart:ffi 去拿到函数指针。2.2 JNI、N-API、dart:ffi 的选型逻辑把三方库的 native 部分鸿蒙化之前必须先想清楚你的 Flutter 代码最终要通过什么方式调到底层 C/C 函数。常见的有三条路。第一条路JNI 思路平移成 N-API。如果你原来的 Android 插件里Kotlin 代码通过System.loadLibraryexternal fun去调 JNI那么在鸿蒙上最自然的对应是把 Kotlin 换成 ArkTSJNI 换成 N-API。这种做法的好处是插件结构不变ArkTS 侧通过napi注册的接口拿到 native 对象/函数坏处是你需要为所有函数都写一层 N-API 封装桥接代码量不小。第二条路Flutter 侧直接 dart:ffi。这是我认为在大多数场景下最值得优先考虑的路线。dart:ffi是 Flutter/Dart 语言自带的 C 互操作机制不依赖任何平台通道直接在 Dart 侧final dylib DynamicLibrary.open(libfastmath.so); final fftCompute dylib.lookupFunctionInt64 Function(Int64), int Function(int)(fastmath_fft);鸿蒙的 Flutter 运行时里DynamicLibrary.open对应的是底层dlopen只要你把编译好的 .so 正确打进了鸿蒙应用包里dart:ffi 就能直接解析出函数指针。这条路的好处是绕过 N-API 这层中间翻译Flutter 侧代码在 Android 和鸿蒙两端几乎可以复用同一套 FFI 绑定逻辑。第三条路纯 N-API 平台通道。在 ArkTS 层调用 N-API 函数再用 MethodChannel/EventChannel 把结果传回 Flutter。这条路一般不推荐因为它等于在“Flutter → ArkTS → N-API → Native”之间加了两层桥接序列化和跨线程调度的开销会把原生性能优势吃掉大半。简单总结一下能直连 C ABI 就优先 dart:ffi必须和 ArkTS 界面交互才考虑 N-API绝对不要三层桥接叠着用。3. 实操把带 CMake 的三方库一步步拉进鸿蒙3.1 准备一个可复现的工程基线为了讲得具体一点我假设你手里有个叫flutter_fastmath的三方库。Android 端它的结构大概是这样的flutter_fastmath/ ├── android/ │ ├── build.gradle │ └── src/main/ │ ├── cpp/ │ │ ├── CMakeLists.txt │ │ ├── fastmath.cpp │ │ └── jni_bridge.cpp │ └── kotlin/com/example/fastmath/FastmathPlugin.kt ├── lib/ │ └── fastmath.dart └── pubspec.yamlfastmath.cpp里是真正干活的 FFT 计算代码对外暴露一个纯 C 接口// fastmath.h #ifdef __cplusplus extern C { #endif int64_t fastmath_fft(int64_t input_len, float* input, float* output); #ifdef __cplusplus } #endifjni_bridge.cpp里做的只是把 JNI 的jfloatArray转成float*然后调用fastmath_fft。Kotlin 侧再通过external fun暴露给 Dart 的平台通道。这个结构在 Android 上写得没毛病但到了鸿蒙化的时候你会发现最核心的有两件事要改一是 CMake 构建脚本要从 Android 工具链切到鸿蒙工具链二是 JNI 桥接层要么改成纯 C ABI 让 dart:ffi 直接调要么改成 N-API 让 ArkTS 调。我下面给的方案是切掉 JNI走纯 C ABI dart:ffi这也是我认为最干净、性能最直接的方式。3.2 重写 CMakeLists 并接入鸿蒙 SDK 工具链在鸿蒙 Module 的src/main/cpp里CMakeLists.txt 需要完全重写。Android 版本里常见的写法是这样的cmake_minimum_required(VERSION 3.22.1) project(fastmath LANGUAGES C CXX) add_library(fastmath SHARED fastmath.cpp jni_bridge.cpp) find_library(log-lib log) target_link_libraries(fastmath ${log-lib}) set_target_properties(fastmath PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON)鸿蒙版本里我建议这样写cmake_minimum_required(VERSION 3.22.1) project(fastmath LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 在鸿蒙 toolchain 环境下默认符号隐藏我们需要显式开启对 Dart 可见的符号 set(CMAKE_CXX_VISIBILITY_PRESET default) set(CMAKE_VISIBILITY_INLINES_HIDDEN OFF) add_library(fastmath SHARED fastmath.cpp) # 鸿蒙的日志库接入方式这里是 hilog 而非 Android 的 log find_library(hilog_ndk hilog_ndk) if(hilog_ndk) target_link_libraries(fastmath ${hilog_ndk}) endif() target_compile_definitions(fastmath PRIVATE OHOS_PLATFORM1)关键点有两个我没有再把jni_bridge.cpp编译进去因为如果走 dart:ffi 直连JNI 桥接文件已经没有存在的意义。除非你还要保留一份 Android 端的旧实现那可以继续编但这就需要在 CMake 里做平台分支了。CMAKE_CXX_VISIBILITY_PRESET我特意设成了default。前面说过鸿蒙工具链默认隐藏符号如果某个三方库的 C 函数没有被__attribute__((visibility(default)))显式标注那么 dart:ffi 的lookupFunction会直接找不到符号。与其去改一堆源码不如直接在 CMake 层面放开默认可见性。如果你的三方库还依赖了其他预编译的 .a 或者源码子模块只要把对应源码目录add_subdirectory或者target_link_libraries引进来就行。但一定要保证那些子模块的 CMakeLists 里没有硬编码 Android 的ANDROID_ABI这类变量否则工具链一换就破裂。3.3 构建 so 产物并处理包体路径工程里光有 CMakeLists 还不够还要确认 hvigor 在构建的时候真的去编译了 C/C 源码并把这个 .so 打进了最终产物。换到鸿蒙工程之后原来 Android 的abiFilters配置要落到build-profile.json5里类似这样{ app: { products: [ { name: default, signingConfig: default, targets: [ { name: default, runtimeOS: HarmonyOS } ], buildOption: { abiFilters: [arm64-v8a] } } ] } }abiFilters决定 hvigor 调 CMake 时给工具链传哪个架构参数。如果你的三方库只适配了 arm64 手机那先只编arm64-v8a就够了。如果还要支持模拟器可以把x86_64也加进来但也要确保源码能在 x86 上编译通过。在 DevEco Studio 里直接点一下构建稍等片刻就能在entry/build/default/intermediates/libs/default/arm64-v8a下看到libfastmath.so。这里有一个很多新手会掉进去的认知偏差如果发现 .so 没有生成优先去看 hvigor 的构建日志里有没有触发 CMake 配置。有时候你在src/main/cpp放了 CMakeLists但 module 的build-profile.json5里没有正确声明nativeSource路径hvigor 会完全跳过原生编译这一步。3.4 Flutter 侧 dart:ffi 调用链路的改造编译出 .so 之后接下来就是让 Flutter 端的 Dart 代码能加载它。如果你的 Flutter 插件原来是通过 MethodChannel 把数据送到 Kotlin 再进 JNI 的现在可以改成这样import dart:ffi; import dart:typed_data; final DynamicLibrary _fastmathLib _openFastmathLib(); DynamicLibrary _openFastmathLib() { return DynamicLibrary.open(libfastmath.so); } final int Function(int) _fftEntry _fastmathLib .lookupFunctionInt64 Function(Int64), int Function(int)(fastmath_fft);函数调用的时候把Float32List的数据指针直接传进去避免 Dart 和 C 之间的数组拷贝Float32List fft(Float32List input) { final output Float32List(input.length); final inputPtr input.buffer.asByteData().getUint64(0); // 实际会走 package:ffi 的 helper // 用 Pointer 的方式更规范使用 allocate 后拷贝数据 // 这里给伪代码示意“直接把指针交给 C” _fftEntry(input.length); return output; }你可能会注意到dart:ffi 的最高频写法是用PointerT和lookupFunction...这套 API 在 Flutter 的鸿蒙运行时上也是被支持的不需要额外引入平台相关依赖。如果一个三方库在 Android 端本身就已经暴露了一套 C ABI很多库如 sqlite3、libjpeg-turbo 都是这么设计的那鸿蒙端直接复用这套 ABI 是最省力的方案。真正需要补写的是插件注册逻辑鸿蒙 Flutter 插件和 Android Flutter 插件在工程组织上不是同一套目录。社区里常见的做法是在 pub 包下增加ohos目录编写 ArkTS 插件入口然后在pubspec.yaml里声明鸿蒙平台的实现。不同插件模板的细节有差异但核心逻辑是一致的确保最终打进 HarmonyOS 安装包的 .so 的 soName 和你 Dart 层DynamicLibrary.open的名字能对上。这里最常见的翻车点是 hvigor 把 so 打到了entry/libs路径而 Flutter 引擎找不到导致运行时dlopen failed。4. 编译、链接、运行三阶段踩坑实录4.1 编译期坑工具链版本与标准库链接我最早适配一个用到大量 C17 特性、还依赖 OpenMP 的库时第一个拦路虎就是工具链版本不一致。DevEco Studio 自带的 NDK 对应的 clang 版本、sysroot、头文件路径和 Android NDK 完全不是一个系列。某些第三方库的 build 脚本里写死了-isystem ${ANDROID_NDK}/sources/cxx-stl/llvm-libc/include这种路径一旦工具链换成鸿蒙脚本直接就废了。解决办法是用 CMake 的 toolchain file 去覆盖而不是去改源码里的路径。在构建时通过参数指定cmake -DCMAKE_TOOLCHAIN_FILE/path/to/ohos-sdk/native/build/cmake/ohos.toolchain.cmake在 DevEco Studio 里这个参数由 hvigor 自动传入不用手动敲。但如果你是在 CI 环境里单独构建 .so 来做验证那就要自己指定。这里有个细节鸿蒙 toolchain 里的 STL 选择是通过OHOS_STL这个变量控制的和 Android 用ANDROID_STL不一样。如果你的三方库依赖libc_shared.so记得把OHOS_STL设为c_shared否则默认静态链接可能导致产物体积变大并且如果应用里同时有多个 so 都静态链了 STL内存里就会有好几份 libc 实例某些静态全局变量的状态就会互相打架——这在大型插件工程里真的会遇到。编译期还有一个很经典的坑代码中写了#ifdef __ANDROID__来区分平台结果鸿蒙的编译宏里没有__ANDROID__平台分支跑错。要养成良好的习惯用__OHOS__/OHOS_PLATFORM这类鸿蒙宏来判断而不是拿__ANDROID__去反推。在 C/C 层做平台适配时我一般会把平台判断收敛成几个自己的宏比如#if defined(__OHOS__) || defined(OHOS) #define FASTMATH_HARMONY 1 #elif defined(__ANDROID__) #define FASTMATH_ANDROID 1 #endif这样后面加新平台改动面最小。4.2 链接期坑系统库与符号可见性链接期的问题比编译期更隐蔽。我踩过的最典型的一个坑是一个音频处理库在 Android 端依赖了 Bionic 特有的android_get_primary_stable_security_patch_level这种只在 Android API 里存在的函数——别笑很多第三方库会用这类函数去探测系统版本。链接到鸿蒙时直接报 undefined reference最后只能在源码里把这段能力检测逻辑加上平台宏过滤掉。另一种情况是链接成功但运行时提示 undefined symbol。这通常是符号可见性捣的鬼。鸿蒙的 CMake 工具链里如果三方库的 CMakeLists 中设置了set(CMAKE_CXX_VISIBILITY_PRESET hidden) set(CMAKE_C_VISIBILITY_PRESET hidden)那么所有函数默认隐藏只有主动__attribute__((visibility(default)))的符号才会进入动态符号表。你在 Android 端没碰到这个问题大概率是 Android 端你编译的是静态库或者 JNI 层用了JNIEXPORT标记而鸿蒙端走 dart:ffi 时这些标记全都不存在。遇到这种问题别急着改一堆源码先在构建完的 .so 上检查符号表ohos-sdk/native/llvm/bin/llvm-nm -D libfastmath.so如果fastmath_fft不在导出列表里那就在 CMakeLists 里直接把符号可见性放开set(CMAKE_CXX_VISIBILITY_PRESET default) set(CMAKE_VISIBILITY_INLINES_HIDDEN OFF)或者在函数声明上显式加__attribute__((visibility(default)))。我倾向于后者因为“默认全部可见”虽然省事但会污染导出表增加动态链接的查找开销还可能和别的 so 产生符号冲突。对于三方库适配能精准导出就精准导出。4.3 运行期坑dlopen、日志与 JNI 遗留代码编译、链接都过去之后运行时跑起来才发现的问题才最考验经验。常见的运行期坑有三个我一个个说。第一个是dlopen failed错误信息一般是cannot locate symbol或library libxxx.so not found。优先检查 .so 有没有真的打进安装包其次检查 .so 依赖了哪些其他动态库。在鸿蒙应用里每个模块的 native 库会被放到应用的 native library 目录如果你的库target_link_libraries里链接了一个鸿蒙环境下不存在的系统库dlopen就会失败。用llvm-objdump -p libxxx.so | grep NEEDED看一下它依赖的 so 列表然后再对照鸿蒙 sysroot 里的库清单缺了哪个就处理哪个。第二个是日志集成。Android 上三方库普遍用__android_log_print输出日志到了鸿蒙环境这个符号不存在轻则日志丢失重则直接在调用日志函数时崩掉。鸿蒙的日志入口是 hilogC 层可以通过如下方式使用#include hilog/log.h LOGE(LOG_APP, fastmath: error %d, errno);在 CMake 里记得find_library(hilog_ndk hilog_ndk)并链接。有不少开源库已经把日志模块抽象出来了那就不需要全改只需在平台适配层把后端换成 hilog 即可。如果库代码里到处散落__android_log_print那先用脚本批量替换再手动检查边界情况。第三个是 JNI 遗留代码。你可能会想我能不能不删 jni_bridge让它在鸿蒙上也跑起来除非你在鸿蒙环境里还保留了一个兼容 JNI 的运行时否则答案是不行。因为鸿蒙的 ArkTS 没有 JNI 机制JNI_OnLoad这类导出函数没有任何入口会去调用它。所以只要有 JNI 代码存在就必须改造要么改成纯 C 函数让 dart:ffi 直接调要么改成 N-API 函数交给 ArkTS 去调。5. 性能验证与算力调度优化5.1 先把性能量化FFI 的优势从哪里来三方库底层用 C/C 做计算目的只有一个性能。但如果适配鸿蒙时把调用链路搞得弯弯绕绕性能可能不升反降。我见过有人非要在 dart:ffi 和 ArkTS N-API 之间再包一层平台通道结果是每帧图像计算都要经过好几层消息序列化性能直接崩到不如纯 Dart。要理解这条链路的性能必须明白 dart:ffi 的优势来源。它最大的特点是不经过 Dart 对象序列化、不经过消息队列、不经过平台通道调度Dart 代码拿到的是 C 函数的直接地址参数就是普通标量或指针一次函数调用的开销和你在一段 C 程序里调用一个外部符号差不多。对比 MethodChannel 的流程——把一个Uint8List编码成标准消息、跨语言边界发送、另一侧解码、再进入 native 函数——这里的每一步都是毫秒级以下但累积起来非常可观的开销。所以适配的时候我强烈建议做一个简单的性能基线测试同样的数据量分别走“dart:ffi 直连”和“ArkTS N-API 再转发”两条路计时然后对比。绝大多数时候前者比后者省掉 30% 以上。这个数据也帮你判断一个三方库到底值不值得花力气走 FFI 直连。5.2 能跑的下一步NEON、并行与内存布局优化跑通只是第一步。如果你的三方库属于重计算类型鸿蒙化之后还可以做几件典型的算力优化。第一件是确认 ARM NEON 向量化是否真的生效。很多数学库在 Android 端会启用 NEON intrinsic但换到鸿蒙工具链后如果源码里对平台宏的判断漏了鸿蒙分支NEON 代码可能直接被编译器丢弃退化为标量运算计算性能断崖下跌。检查方法是在 CMake 里加target_compile_options(fastmath PRIVATE -O3 -mcpucortex-a76 -mfpuneon)注意-mcpu要根据你的目标机型动态调不同芯片的调度模型有差异。为了通用性用-marcharmv8.2-afp16simd会更稳妥。第二件是数据内存布局。如果 FFI 层传入的数据在 Dart 侧是Float32List然后你在 C 侧又拷贝到另一个float*缓冲区那么数据搬运的时间可能会吃掉计算省下的时间。更好的做法是一开始就让 Dart 侧通过malloc分配原生内存直接在上面写入数据把同一个指针传给 C 函数计算完再从这块内存里读结果。这样整个链路里没有一次多余拷贝对音频和图像这类大数组操作收益极其明显。第三件是并行调度。鸿蒙 Native 侧有 Function Flow 这类并行编程框架可以把计算型任务拆到多个 CPU 线程上跑。如果你的 C/C 库本身没有并行优化比如只用单线程写的 FFT、图像滤波在三方库适配时可以保留计算核心不动在外面包一层并行分发。但注意dart:ffi 调用是同步阻塞的如果你在 Flutter UI 线程直接调用了运行时间很长的 C 函数必然卡帧。正确做法是把 FFI 调用放到 Dart 的Isolate里跑或者直接让 C 侧通过回调把结果异步抛回来。这块在鸿蒙上跑通后整个应用的帧率和响应性差距会非常明显。6. 最后几条经验适配做多了回头看这个流程最值钱的其实不是某个具体函数怎么写而是把“安卓原生资源直接搬到鸿蒙”的幻觉打碎鸿蒙不是 Android 的兼容层三方库的 NDK 产物需要换工具链、换 ABI、换调用链路重新编译和验证。我个人的习惯是优先保证纯 C ABI 的稳定性让 Flutter 侧统一走 dart:ffi能用 CMake 变量控制差异就不要动源码碰到符号问题第一反应先看导出表不要埋头改调用方。最后再分享一个小技巧编译产物最好在 CI 里单独跑一个“鸿蒙 native 构建 符号清单导出”的流水线任务每次三方库升版本都自动化检查一遍所有需要的 C 函数是否还在导出表里。别等集成到 DevEco 工程里才发现符号丢了那个排查过程真的会让人怀疑人生。底层这条路多验证一次后面就能少熬夜一次。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表