C/C++是相對底層的語言,相比OC、Swift、Kotlin、Java等都要難,但是C/C++是Android和iOS都支持的語言,我們使用C++主要有一下幾種原因:
- 跨平臺,一套代碼多端使用;
- 安全性,Java是極其容易被反編譯的語言,如果把核心的代碼改成C++可以有效提高安全性,但是在iOS意義就不大了;
- 高性能,在多數的場景下這個優勢并不明確,只有在一些特定的場景下才能發揮他的價值,比如音頻、視頻;
- 調用C++庫API,有部分的庫只提供了C++版本,如果需要訪問那么就要使用C++實現。
項目基本要求
- 一個工程實現Android和iOS工程師一起編碼;
- 可以添加依賴第三方C++庫;
- 可以脫離手機實現代碼調試;
- 可以使用手機實現代碼調試;
項目設計
設計思想參考了 Flutter Plugin 和 protobuf 的工程結構,基于CMake 和 CocoaPods 實現,為大家提供一種多平臺協同開發的思路,本篇文章不會把全部的代碼貼出來,只會列出設計思想和關鍵的代碼,詳細代碼請查看 源碼。
目錄結構
├── CMakeLists.txt
├── android
│ ├── build.gradle
│ └── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java/com/cross/Cross.java
│ └── cpp
│ ├── include
│ ├── cross.cpp
│ └── CMakeLists.txt
├── cross.podspec
├── ios
│ └── Classes
│ ├── Cross.h
│ └── Cross.mm
├── src
│ └── url_signature
│ ├── include/url_signature.h
│ ├── url_signature.cpp
│ └── CMakeLists.txt
├── third_party
│ ├── cxxurl
│ │ ├── include
│ │ ├── src
│ │ └── CMakeLists.txt
│ └── hash
│ ├── include
│ ├── src
│ └── CMakeLists.txt
├── test
│ ├── gtest
│ │ ├── include
│ │ ├── src
│ │ └── CMakeLists.txt
│ ├── main.cpp
│ └── CMakeLists.txt
├── example
│ ├── android #省略
│ └── ios #省略
├── build.gradle
├── gradle.properties
├── settings.gradle
上面的目錄結構主要分為:
- Android代碼(android):雖然C++是通用的語言,但是也有部分代碼并不通用,這里主要是編寫和NDK特定有關的代碼,比如log,除了C++代碼也會在這里編寫JNI接口代碼,提供給Java調用;
- iOS代碼(ios):和上面同理,也是編寫和iOS有特定關聯的代碼,雖然.m改成.mm就可以調用C++代碼,但是為了避免C++代碼混入主項目,這里也要編寫接口層代碼,通過接口調用;
- iOS庫配置文件(cross.podspec):這個文件放到/ios目錄才是比較合理,由于podspec不支持指定上一級的源文件,所以只能放到根目錄;
- 核心C++(src):這里是包含了我們編寫的C++代碼,這里也可以把不同功能的代碼分為多個項目,實現代碼隔離;
- 測試代碼(test):這里主要是包含了我們日常開發調試編寫的測試代碼,也可以包含單元測試代碼;
- 第三方庫(third_party):這里是放第三方庫,每一個庫都有獨立的文件夾和CMakeLists.txt,管理自身的頭文件和代碼;
- 示例(example/android、example/ios)
- Gradle腳本文件:build.gradle/gradle.properties/settings.gradle
C++庫設計
無論是第三方庫還是我們編寫的代碼,都使用同一的目錄結構,都是一個庫,這里以 jsoncpp 為例,每一個庫都包含了CMakeLists.txt、include、src三部分,CMakeLists.txt是庫定義,include是需要暴露的頭文件,src是放實現源碼。
├── CMakeLists.txt
├── include
│ └── json
│ ├── reader.h
│ ├── value.h
│ └── writer.h
└── src
├── json_reader.cpp
├── json_value.cpp
└── json_writer.cpp
在這個項目當中,如果是第三方庫,我會把實現代碼放到src文件夾,如果是我們自己寫的代碼,我會把實現代碼放到和CMakeLists.txt同一級別的目錄。
CMakeLists.txt 的作用主要是暴露當前庫的頭文件,定義需要參與編譯的源碼文件。
# 設置cmake版本要求
cmake_minimum_required(VERSION 3.10.2)
# 定義庫的名字
project(jsoncpp)
# 定義需要參與編譯的源文件
add_library(${PROJECT_NAME} src/json_reader.cpp src/json_value.cpp src/json_writer.cpp)
# 定義需要暴露的頭文件
target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/include)
有了這個C++庫的定義,接下來我們看看根目錄的 CMakeLists.txt
CMakeLists.txt(根目錄)
根目錄的 CMakeLists.txt 是用來關聯各個子項目,需要注意是,如果關聯進來那么編譯成Android的aar文件的都會把代碼包含進來,所以我們要根據不同的場景編寫腳本,比如在Android環境下需要包含android的C++代碼,不要包含測試代碼。
cmake_minimum_required(VERSION 3.10.2)
project(cpp-android-ios-example)
set(CMAKE_CXX_STANDARD 17)
add_subdirectory(third_party/hash)
add_subdirectory(third_party/cxxurl)
add_subdirectory(src/url_signature)
if (ANDROID)
add_subdirectory(android/src/main/cpp)
else ()
add_subdirectory(test/gtest)
add_subdirectory(test)
endif ()
重點:android/build.gradle定義了 CMakeLists.txt 的路徑,而這個路徑必須指向根目錄的 CMakeLists.txt。
externalNativeBuild { cmake { path "../CMakeLists.txt" // 這里需要指向項目根目錄的 CMakeLists.txt version "3.10.2" } }
庫依賴
通過上面的方式把各個子項目關聯起來后,那么我們就可以非常簡單在一個子項目中引用另外一個子項目,這里以 src/url_signature/CMakeLists.txt 為例,我們需要在 url_signature 子項目添加 cxxurl 和 hash 的依賴。
cmake_minimum_required(VERSION 3.10.2)
set(CMAKE_CXX_STANDARD 14)
project(url_signature)
add_library(${PROJECT_NAME} url_signature.cpp)
target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/include)
target_link_libraries(${PROJECT_NAME} cxxurl hash)
Android
以下是android的目錄結構,這部分的代碼主要是實現Java和C++的轉換。
├── build.gradle
├── gradle.properties
├── settings.gradle
└── android
├── build.gradle
└── src/main
├── AndroidManifest.xml
├── cpp
│ ├── CMakeLists.txt
│ └── native-lib.cpp
└── java/com/cross/Cross.java
從上面的目錄可以看到,項目的根目錄有 build.gradle、gradle.properties和settings.gradle,之所以放在根目錄是
android/build.gradle
apply plugin: 'com.android.library'
android {
compileSdkVersion 30
buildToolsVersion "30.0.2"
defaultConfig {
minSdkVersion 16
targetSdkVersion 30
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
// 這里需要指向項目根目錄的 CMakeLists.txt 文件
path "../CMakeLists.txt"
version "3.10.2"
}
}
}
dependencies {
}
android/src/main/cpp/CMakeLists.txt
cmake_minimum_required(VERSION 3.10.2)
project("cross")
include_directories(export_include)
add_library(${PROJECT_NAME} SHARED cross.cpp)
find_library(log-lib log)
target_link_libraries(${PROJECT_NAME} ${log-lib} url_signature)
cross.cpp
#include <jni.h>
#include <string>
#include "url_signature.h"
extern "C"
JNIEXPORT jstring JNICALL
Java_com_cross_Cross_signatureUrl(JNIEnv *env, jclass clazz, jstring url) {
const char *str = env->GetStringUTFChars(url, JNI_FALSE);
std::string result = SignatureUrl(str);
return env->NewStringUTF(result.c_str());
}
Cross.java
package com.cross;
public class Cross {
static {
System.loadLibrary("cross");
}
public static native String signatureUrl(String url);
}
iOS
由于 podspec 無法指定父級的文件,如果放在ios目錄下,那么就無法關聯src和third_party的源文件,所以就把 cross.podspec 放到工程的根目錄。Cross 類主要就是負責對接OC和C++,作為統一的接口類。
├── cross.podspec
├── ios
│ └── Classes
│ ├── Cross.h
│ └── Cross.mm
cross.podspec
Pod::Spec.new do |s|
s.name = 'cross'
s.version = '0.0.1'
s.summary = 'cross library'
s.description = 'Cross library'
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
# 設置源文件,切記不要把測試代碼包含進來
s.source_files = 'ios/Classes/**/*','third_party/**/*.{cc,cpp,h}','src/**/*.{cc,cpp,h}'
# 暴露頭文件,否則引用該spec的項目無法找到頭文件
s.public_header_files = 'ios/Classes/**/*.h','src/url_signature/include/*.h'
s.platform = :ios, '8.0'
# 必須配置HEADER_SEARCH_PATHS屬性,是否會導致項目中C++找不到頭文件
s.xcconfig = {
'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}/third_party/cxxurl/include/" "${PODS_TARGET_SRCROOT}/third_party/hash/include/" "${PODS_TARGET_SRCROOT}/src/url_signature/include/"'
}
end
Cross.h
@interface Cross : NSObject
- (NSString*)signatureUrl:(NSString *)url;
@end
Cross.mm
#import "Cross.h"
#include <string>
#include <url_signature.h>
@implementation Cross
- (NSString*)signatureUrl:(NSString *)url{
std::string str = [url UTF8String];
std::string result = SignatureUrl(str);
NSString *newUrl = [NSString stringWithUTF8String:result.c_str()];
return newUrl;
}
@end
示例設計
Android
├── build.gradle
├── settings.gradle
└── src
└── main
├── AndroidManifest.xml
├── java/com/cross/example/MainActivity.java
└── res
示例就是一個普通的工程,唯一需要注意的是 settings.gradle
rootProject.name = 'example'
include ":cross"
project(":cross").projectDir = new File("../../android")
build.gradle
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:4.1.2"
}
}
apply plugin: 'com.android.application'
android {
...
}
dependencies {
implementation project(path: ':cross')
}
iOS
├── Podfile
├── example
│ ├── AppDelegate.h
│ ├── AppDelegate.m
│ ├── ViewController.h
│ ├── ViewController.m
│ └── main.m
├── example.xcodeproj
Podfile
target 'example' do
use_frameworks!
pod 'cross', :path => '../../'
end
如何編碼?
- 如果是Android開發者,建議使用 Android Studio 打開根目錄,文件目錄選擇 Android 風格的,就可以在一個環境下同時編寫 C++、Java、還有example的代碼。
- 如果是iOS也是同樣打開example/ios項目,記得要先執行 pod install 哦。
- 但多數情況下還是建議使用 vscode 或者 clion 打開 工程的根目錄,在 test 目錄下編寫測試代碼,脫離與平臺關聯進行開發調試,這樣的效率更高。
總結
通常C++部分的代碼不會和APP主工程的代碼放到同一個倉庫,而是獨立開發,比如Android會打包成aar發布到maven倉庫中,而iOS就會發布到內部的CocoaPods倉庫,通過外部庫的引入到主項目當中。
源碼:https://github.com/taoweiji/cpp-android-ios-example
附加資料
集成開發環境(IDE)
- CLion:JetBrains出品,最好用的C++ IDE,對CMake支持非常友好,需要付費的,對開源社區有貢獻的可以申請免費使用;
- AppCode:JetBrains出品,支持Objective-C, Swift, C/C++語言,也是需要付費的;
- Visual Studio Code:微軟出品,支持多種平臺,可以通過插件實現C++編碼,也是一個非常不錯的選擇;
- Xcode:蘋果公司出品,對于iOS開發者非常友好,可以搭配CocoaPods一起開發C++;
- Visual Studio:微軟出品,僅支持Windows,功能非常強大;
- Android Studio:用于Android開發的IDE,同樣也可以編譯C++代碼,適用于Android開發者;
C++常用庫
- 綜合
- boost 提供了很多強大的C++庫,
- Apache C++ Standard Library:主要包含了算法和容器的
- json: jsoncpp 和 nlohmann/json 都是不錯的選擇。
- fmt 是一個先進的文本格式庫,具有現代語言的特征,缺點是占用過大。
- 壓縮:libzip2
- 網絡:Boost.Asio:用于網絡和底層I/O編程的跨平臺的C++庫。
- MD5/SHA1:hash-library
- 調試、單元測試:CMake自帶的 CTest、googletest;
- 腳本、虛擬機:
- 序列化:protobuf 可以代替json/xml在不同的語言/設備之間傳遞對象序列化數據;
- 音頻視頻:FFmpeg 用于音視頻的處理;
構建系統
- cmake:是目前主流的構建系統,簡單易用,也適合構建大型項目,常用的第三方庫基本都支持cmake,可以非常簡單添加第三方依賴,強烈推薦;
- makefile:是最基本的構建系統
- CocoaPods:是用于開發OC和Swift項目的構建系統,同樣也支持C++,通常用于iOS應用開發;
- ndk-build:Android 上一代的構建方式,配置文件為Android.mk,目前Android開發默認的構建方式已經變成了cmake;
- bazel:Google開源的構建系統,TensorFlow和Flutter都是基于bazel構建,使用復雜,適合大型的項目;