C++工程:一文看懂如何使用 C++ 开发 Android、iOS 项目

为什么使用C++

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 j_url) {
    const char *url = env->GetStringUTFChars(j_url, JNI_FALSE);
    auto result = env->NewStringUTF(SignatureUrl(url).c_str());
    env->ReleaseStringUTFChars(j_url, url);
    return result;
}
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 打开 example/android,文件目录选择 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 都是不错的选择。
  • 压缩: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构建,使用复杂,适合大型的项目;