转载C++工程:一文看懂如何使用 C++ 开发 AndroidiOS 项目

Posted 字节卷动

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了转载C++工程:一文看懂如何使用 C++ 开发 AndroidiOS 项目相关的知识,希望对你有一定的参考价值。

PS: 本文转载于:https://github.com/taoweiji/cpp-android-ios-example#readme

为什么使用C++

C/C++是相对底层的语言,相比OC、Swift、Kotlin、Java等都要难,但是C/C++是androidios都支持的语言,我们使用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 目录下编写测试代码,脱离与平台关联进行开发调试,这样的效率更高。

导入项目中开发的结构图如下

  • android
    打开Android Studio 导入 这个的项目根目录,如下所示:

  • ios
    打开XCode,打开example/ios项目,如下所示:

    运行效果如下:

总结

通常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构建,使用复杂,适合大型的项目;

以上是关于转载C++工程:一文看懂如何使用 C++ 开发 AndroidiOS 项目的主要内容,如果未能解决你的问题,请参考以下文章

入门科普:一文看懂NLP和中文分词算法(附代码举例)

音视频开发基础:一文掌握现代 C++ 并发编程

Visual C++开发工具与调试技巧整理[2]

​一文看懂数据清洗:缺失值、异常值和重复值的处理

一文看懂:商品分析如何做?

招聘c++开发工程师~~~