C++工程:一文看懂如何使用 C++ 開發 Android、iOS 項目

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++常用庫
  • 綜合
  • json: jsoncppnlohmann/json 都是不錯的選擇。
  • fmt 是一個先進的文本格式庫,具有現代語言的特征,缺點是占用過大。
  • 壓縮:libzip2
  • 網絡:Boost.Asio:用于網絡和底層I/O編程的跨平臺的C++庫。
  • MD5/SHA1:hash-library
  • 調試、單元測試:CMake自帶的 CTestgoogletest;
  • 腳本、虛擬機:
    • V8:Google開發并維護的高性能 JavaScript 引擎;
    • J2V8:針對 V8 的 Java 綁定的JSI,可以在Android上使用;
    • JavaScriptCore:在 WebKit 中提供 JavaScript 引擎的開源框架,iOS7 后集成到了 iPhone 平臺;
    • quickjs:支持多平臺的小型JavaScript引擎,只有210KB大小;
    • Lua:一個小型的lua腳本引擎,可以用于做簡單的邏輯運行和控制;
  • 序列化:protobuf 可以代替json/xml在不同的語言/設備之間傳遞對象序列化數據;
  • 音頻視頻:FFmpeg 用于音視頻的處理;
構建系統
  • cmake:是目前主流的構建系統,簡單易用,也適合構建大型項目,常用的第三方庫基本都支持cmake,可以非常簡單添加第三方依賴,強烈推薦;
  • makefile:是最基本的構建系統
  • CocoaPods:是用于開發OC和Swift項目的構建系統,同樣也支持C++,通常用于iOS應用開發;
  • ndk-build:Android 上一代的構建方式,配置文件為Android.mk,目前Android開發默認的構建方式已經變成了cmake;
  • bazel:Google開源的構建系統,TensorFlow和Flutter都是基于bazel構建,使用復雜,適合大型的項目;
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容