最近几周开始了 IM 项目,因此想在应用层写一套业务隔离的,跨平台的 SDK, 我便瞄准了 libuv 这个库,开始了 Native Hybrid 的研究 —— 为了写一次代码可以同时在 Android 和 iOS 平台上进行编译/运行。
libuv
一开始觉得编译 libuv 很困难,我采用了 gyp 的方案,而且之前对 Android NDK Toolchain 并不了解,导致以前对其尝试的时候,并没有获得我想要的结果,而且就算是 link 出来的包(不正确的方式),运行出错的信息,也不能读懂,所以浪费了我很多时间。总之,学习 Native 编程(此处 Native 指的是使用 C/C++ 进行平台级别的编程)或者说不管学习什么编程,第一个要务就是不能害怕,这个非常重要。
那么经过我的了解,GCC/Clang 编译链接的套路都差不多,无非是指定 源文件/头文件/链接库 进行操作,操作的时候有一些标志位,还有一些预定义
,然后就是一些参数配置了。如果我们知晓了这些,只要一些库一开始做了跨平台开发的准备,那么使用 NDK/Android Toolchain 编译,一般都没有什么问题,那么废话不多说,我们主要就是指定几个环境变量。
CC:C 编译器,可以选择 Android Toolchain 下面的 GCC 或者 Clang
CXX: C++ 编译器
LINKER: 链接器
CFLAGS: CC 编译的时候,一些符号位。
CXXFLAGS: CXX 编译的时候,所需要的符号位
还有其他的环境变量,去参考 https://www.gnu.org/software/...
如果需要的话,我们还要给 GCC 指定一个 sysroot,方便我们的编译程序来寻找 .h 和库文件。
那么整个跨平台编译的流程就是
export CC=xxxxx export CXX=xxxx export CFLAGS=xx export LD=xxx
然后make
完事儿。
如果没有别的需求,那么整个编译流程就是这么简单,一开始我以为 libuv 这个库很复杂,后来证明 libuv 是一个很简单的库 = =。
当然,我们今天的主题是跨平台的编译,因此在这我们选择了 CMake 这个工具用来生成我们的 Makefile 和 xcodeproj。
CMake 的官网是:https://cmake.org/
编写 libuv 的 CMakeList.txt
如果我们使用 gyp 来生成 Makefile 的时候,可以看见 Android 下的 Makefile 内容并不是很多,用到的源文件和头文件其实也屈指可数。
具体可以使用 libuv 下的 android-configure 文件来调用 gyp 生成你的 Makefile 查看。android-configure
这个文件是把 NDK 中的独立工具链安装的 libuv 目录下面,然后导出有用的环境变量,像 CC/CXX/LD 之类的变量让 libuv 可以进行交叉编译。
DEFS_Debug := \ '-D_LARGEFILE_SOURCE' \ '-D_FILE_OFFSET_BITS=64' \ '-DDEBUG' \ '-D_DEBUG' # Flags passed to all source files. CFLAGS_Debug := \ -Wall \ -fvisibility=hidden \ -g \ --std=gnu89 \ -pedantic \ -Wall \ -Wextra \ -Wno-unused-parameter \ -Wstrict-prototypes \ -Wstrict-aliasing \ -g \ -O0 \ -fwrapv \ -fPIE # Flags passed to only C files. CFLAGS_C_Debug := # Flags passed to only C++ files. CFLAGS_CC_Debug := \ -fno-rtti \ -fno-exceptions INCS_Debug := \ -I$(srcdir)/include \ -I$(srcdir)/src DEFS_Release := \ '-D_LARGEFILE_SOURCE' \ '-D_FILE_OFFSET_BITS=64' \ '-DNDEBUG' # Flags passed to all source files. CFLAGS_Release := \ -Wall \ -fvisibility=hidden \ -g \ --std=gnu89 \ -pedantic \ -Wall \ -Wextra \ -Wno-unused-parameter \ -Wstrict-prototypes \ -Wstrict-aliasing \ -O3 \ -fstrict-aliasing \ -fomit-frame-pointer \ -fdata-sections \ -ffunction-sections # Flags passed to only C files. CFLAGS_C_Release := # Flags passed to only C++ files. CFLAGS_CC_Release := \ -fno-rtti \ -fno-exceptions INCS_Release := \ -I$(srcdir)/include \ -I$(srcdir)/src OBJS := \ $(obj).target/$(TARGET)/src/fs-poll.o \ $(obj).target/$(TARGET)/src/inet.o \ $(obj).target/$(TARGET)/src/threadpool.o \ $(obj).target/$(TARGET)/src/uv-common.o \ $(obj).target/$(TARGET)/src/version.o \ $(obj).target/$(TARGET)/src/unix/async.o \ $(obj).target/$(TARGET)/src/unix/core.o \ $(obj).target/$(TARGET)/src/unix/dl.o \ $(obj).target/$(TARGET)/src/unix/fs.o \ $(obj).target/$(TARGET)/src/unix/getaddrinfo.o \ $(obj).target/$(TARGET)/src/unix/getnameinfo.o \ $(obj).target/$(TARGET)/src/unix/loop.o \ $(obj).target/$(TARGET)/src/unix/loop-watcher.o \ $(obj).target/$(TARGET)/src/unix/pipe.o \ $(obj).target/$(TARGET)/src/unix/poll.o \ $(obj).target/$(TARGET)/src/unix/process.o \ $(obj).target/$(TARGET)/src/unix/signal.o \ $(obj).target/$(TARGET)/src/unix/stream.o \ $(obj).target/$(TARGET)/src/unix/tcp.o \ $(obj).target/$(TARGET)/src/unix/thread.o \ $(obj).target/$(TARGET)/src/unix/timer.o \ $(obj).target/$(TARGET)/src/unix/tty.o \ $(obj).target/$(TARGET)/src/unix/udp.o \ $(obj).target/$(TARGET)/src/unix/proctitle.o \ $(obj).target/$(TARGET)/src/unix/linux-core.o \ $(obj).target/$(TARGET)/src/unix/linux-inotify.o \ $(obj).target/$(TARGET)/src/unix/linux-syscalls.o \ $(obj).target/$(TARGET)/src/unix/pthread-fixes.o \ $(obj).target/$(TARGET)/src/unix/android-ifaddrs.o \ $(obj).target/$(TARGET)/src/unix/pthread-barrier.o \ $(obj).target/$(TARGET)/src/unix/procfs-exepath.o \ $(obj).target/$(TARGET)/src/unix/sysinfo-loadavg.o \ $(obj).target/$(TARGET)/src/unix/sysinfo-memory.o
截取这么多,可以看到个大概了,如果想看具体的执行,我们可以调用make V=1
来查看具体执行的flags
。导出以后,我们的CMakeList.txt
大概如下:
cmake_minimum_required(VERSION 3.4.1) PROJECT(uv CXX C) INCLUDE_DIRECTORIES (${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/src) # from uv.gyp MESSAGE(STATUS "in libuv" ${CMAKE_CURRENT_SOURCE_DIR}) SET(SRC_LIST "${CMAKE_CURRENT_SOURCE_DIR}/src/fs-poll.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/inet.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/threadpool.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/uv-common.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/version.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/async.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/core.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/dl.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/fs.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/getaddrinfo.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/getnameinfo.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/loop.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/loop-watcher.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/pipe.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/poll.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/process.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/signal.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/stream.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/tcp.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/thread.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/timer.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/tty.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/udp.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/proctitle.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/pthread-barrier.c") IF (DEFINED ANDROID) ADD_DEFINITIONS(-D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64) SET(SRC_LIST ${SRC_LIST} "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/linux-core.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/linux-inotify.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/linux-syscalls.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/pthread-fixes.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/android-ifaddrs.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/procfs-exepath.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/sysinfo-loadavg.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/sysinfo-memory.c") IF(CMAKE_BUILD_TYPE STREQUAL "Debug") # from common.gypi MESSAGE(STATUS "in Debug") ADD_DEFINITIONS(-DDEBUG -D_DEBUG) SET(COMMON_C_FLAGS "-Wall \ -fvisibility=hidden \ -g \ --std=gnu89 \ -pedantic \ -Wextra \ -Wno-unused-parameter \ -Wstrict-prototypes \ -Wstrict-aliasing \ -g \ -O0 \ -fwrapv \ -fPIE") ELSE() MESSAGE(STATUS "in Release") ADD_DEFINITIONS(-DNDEBUG) SET(COMMON_C_FLAGS "-Wall \ -fvisibility=hidden \ -g \ --std=gnu89 \ -pedantic \ -Wextra \ -Wno-unused-parameter \ -Wstrict-prototypes \ -Wstrict-aliasing \ -O3 \ -fstrict-aliasing \ -fomit-frame-pointer \ -fdata-sections \ -ffunction-sections") ENDIF(CMAKE_BUILD_TYPE STREQUAL "Debug") SET(CMAKE_C_FLAGS_DEBUG ${COMMON_C_FLAGS}) SET(CMAKE_CXX_FLAGS_DEBUG ${COMMON_C_FLAGS} "-fno-rtti -fno-exceptions") SET(CMAKE_C_FLAGS_RELEASE ${COMMON_C_FLAGS}) SET(CMAKE_CXX_FLAGS_RELEASE ${COMMON_C_FLAGS} "-fno-rtti -fno-exceptions") ELSEIF (DEFINED APPLE) SET(SRC_LIST ${SRC_LIST} "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/bsd-ifaddrs.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/darwin-proctitle.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/darwin.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/fsevents.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/unix/kqueue.c") SET(CMAKE_CXX_FLAGS "-fobjc-abi-version=2 \ -fobjc-arc \ ${CMAKE_CXX_FLAGS}") # end of defined android ENDIF(DEFINED ANDROID) ADD_LIBRARY(uv STATIC ${SRC_LIST})
这里利用 CMake 的逻辑判断,分别加入了 iOS 和 Android 不同平台的源文件,我们去查看这些文件会发现他们都是平台相关的 API 实现,共有的一些源文件则是抽象的接口层。
如果想知道 CMake 指令如何使用,可以查看相关文档:
CMake 文档传送门:https://cmake.org/cmake/help/...
开始执行 CMake
当然,光有 CMake 还不够,CMake 提供了一套工具链系统,iOS 和 Android 都有相应的开源工具链插件:
iOS: https://github.com/cristeab/i...
Android :https://github.com/taka-no-me...
指定了 Toolchain 和我们的 CPU ABI 之后,我们就可以为指定的 CPU 架构编译出特定的二进制库了
比如,编译 Android 如下:
cmake -DCMAKE_TOOLCHAIN_FILE=./toolchain/android.toolchain.cmake \ -DCMAKE_BUILD_TYPE=Debug \ -DANDROID_ABI="armeabi-v7a" \ -DANDROID_NATIVE_API_LEVEL="android-21" \ .
编译 iOS 如下:
cmake -DCMAKE_TOOLCHAIN_FILE=./toolchain/darwin.toolchain.cmake \ -DIOS_PLATFORM=OS \ -GXcode \ .
-G
是让 CMake 生成一个 Xcode 的构建系统,也就是 Xcode 项目文件。这样我们就可以启动 Xcode 进行编译了。
我们把这两段脚本写在一个bash
文件里,每次执行的时候,只用调用./build_android.sh
和./build_ios.sh
就行了。
每次执行android
的时候,会生成libxxx.so
的Makefile
,执行ios
的时候,会生成xxx.xcodeproj
,是不是觉得特别炫酷?