Introduction

如果你是一名 C/C++ 开发人员,正在尝试将 C/C++ 的代码往安卓上迁移,那么这篇文章对你有很大的帮助

如果你是一名 Android 开发人员,正在尝试将外部 so 嵌入到你的 app 中,那么这篇文章对你有很大的帮助

本人属于前一种情况,由于工作的需求,需要把 C/C++ 的 so 库集成至 Android 中进行开发。本人对 Android 开发了解不多,更多是在站在 C/C++ 开发人鱼的角度来描述问题。而对于职业的 Android 开发人员,你们可以从这篇文章中,更加详细地了解 native 侧的细节。

这篇文章中,我将通过一个示例,一步一步地展示如何将 C/C++ 代码嵌入以 so 的形式嵌入至 android 中。

所有代码已上传至 GitHub,大家可以对照着看,比较容易理解Android_Studio_Import_so_Demo

为了让事情变得更简单,我们假设有一个 Adder 类(C++写的),我们要把它的功能嵌入到 Android 中,以下是它的源码:

adder.h

class Adder
{public:static int add(int a, int b);
};

adder.cpp

#include "adder.h"int Adder::add(int a, int b)
{return a + b;
}

1. 交叉编译

想要在 Android 上运行 C/C++ 代码,首先我们要将 C/C++ 代码进行交叉编译。所谓交叉编译就是在一个平台上生成另一个平台的可执行的代码。

例如我们在 Macos 上编译出可以在 Android 运行代码。交叉编译需要用到交叉编译器,这里引用维基百科一段对于交叉编译器的介绍:

交叉编译器(英语:Cross compiler)是指一个在某个系统平台下可以产生另一个系统平台的可执行文件的编译器。交叉编译器在目标系统平台(开发出来的应用程序序所运行的平台)难以或不容易编译时非常有用。

交叉编译器的存在对于从一个开发主机为多个平台编译代码是非常有必要的。直接在平台上编译有时行不通,例如在一个嵌入式系统的单片机 ,因为它们没有操作系统,所以直接编译行不通。

交叉编译器和源代码至源代码编译器不同,交叉编译器用于二进制代码的跨平台软件开发,而源到源编译器是将某种编程语言的程序源代码作为输入,生成以另一种编程语言构成的等效源代码的编译器,但两者都是编程工具。

Android 提供了原生开发套件(NDK, Native Development Kit) 工具。NDK 中就包含了 Android 的交叉编译器。

那么如何利用 NDK 进行交叉编译呢?这里推荐使用 cmake,只需要简单的几个步骤就能完成。下面请跟随我的脚本。

Step 0 安装 NDK

在我们电脑上安装 NDK。这很简单,到 NDK下载页面 下载解压,放到你认为合适的位置就可以了。笔者用的 NDK 21,放在电脑 ~/NDK/ 目录中

Step 1 编写 CMakeLists.txt

Android Studio 编译原生库默认的构建工具是 CMake,想要进行 Native 开发,CMake是绕不过去的。

我们的 CMakeLists.txt 非常简单,add_library生成 so,install 命令将需要用的 so 和 头文件放置到合适的位置。

cmake_minimum_required(VERSION 3.10)
project(native_proj)
include(GNUInstallDirs)include_directories(include)
add_library(adder SHARED src/adder.cpp)# set lib subdir
if(ANDROID)set(LIB_SUBDIR ${ANDROID_ABI})
else()set(LIB_SUBDIR ${CMAKE_SYSTEM_NAME})
endif()# so installation
install(TARGETS adderLIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/${LIB_SUBDIR})# Headers installation
install(DIRECTORY ${CMAKE_SOURCE_DIR}/include/DESTINATION includeFILES_MATCHING PATTERN "*.h"
)

可以简单测试下编译是否正常:

mkdir build
cd build
cmake .. -DCMAKE_INSTALL_PREFIX=../dist
cmake --build .

一切顺利的话,在 dist 目录就会出现编译产物了,目录结构大致如下( 那个 Darwin 就是 mac 操作系统的名字)

├── dist
│   ├── include
│   │   └── adder.h
│   └── lib
│       └── Darwin
│           └── libadder.dylib

Step 2 Android 编译脚本

下一步就是交叉编译了。CMake 支持通过指定 CMAKE_TOOLCHAIN_FILE 来改变编译环境,非常好用的特性。NDK 中提供了一个 toolchain.cmake 文件来帮助我们切换环境。详细的 cmake 语法就不展开了,直接上脚本。

prompt() {echo "Options:-a [arm64-v8a|armeabi-v7a]: android abiexample:$0 -a arm64-v8a"
}if (($#==0)); thenpromptexit 0
fiANDROID_ABI=arm64-v8a
while getopts "a:" arg #选项后面的冒号表示该选项需要参数
docase $arg ina)if [ "$OPTARG" == "arm64-v8a" ]; thenANDROID_ABI=$OPTARGelif [ "$OPTARG" == "armeabi-v7a" ]; thenANDROID_ABI=$OPTARGelseecho "bad argument for android abi"exit 1fi;;?)  #当有不认识的选项的时候arg为?echo "unkonw argument"exit 1;;esac
doneecho "ANDROID_ABI: $ANDROID_ABI"build_dir=build_state_android_$ANDROID_ABI
mkdir -p $build_dir
cd $build_dir || exit 1ANDROID_NDK_HOME=~/NDK/android-ndk-r21/cmake .. \
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=$ANDROID_ABI \
-DANDROID_STL=c++_shared \
-DCMAKE_INSTALL_PREFIX=../dist \
-DCMAKE_BUILD_TYPE=Release \cmake --build . --target install -j8

ANDROID_NDK_HOME 指的是 ndk 安装的目录,这里根据你们自己情况进行修改。

ANDROID_ABIarm64-v8aarmeabi-v7a 两种,分别对应 64 位和 32 位。简单测试下,运行./android_build.sh -a arm64-v8a,在 dist 目录下出现编译产物:

├── dist
│   ├── include
│   │   └── adder.h
│   └── lib
│       └── arm64-v8a
│           └── libadder.so

以上三个步骤就完成了对 C/C++ 代码的交叉编译,其编译产物,一些头文件和so文件,将被嵌入至 Android 中。

Android Studio 中引入外部 so 文件

新建一个 Android 项目来简单演示如何将外部 so 引入。

接下来的内容涉及到了 JNI 开发,相关推荐资料包括:

  • Android JNI学习(二)——实战JNI之“hello world”
  • JNI 简明教程之手把手教你入门

Step 0 定义 Native 方法

我们定义一个 native 方法 add,通过add计算两个数的和

public class MainActivity extends AppCompatActivity {private static final String TAG = "MainActivity";@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);Log.d(TAG, "onCreate: " + add(10, 10));}private native int add(int a, int b);
}

Step 1 将交叉编译的产物 copy 到合适位置

将交叉编译头文件 copy 到 main/cpp/include 中,将 so copy 到 main/lib 中。目录结构大致是这样的:

├── main
│   ├── CMakeLists.txt
│   ├── cpp
│   │   ├── include
│   │   │   └── adder.h
│   │   └── jni_src.cpp
│   ├── java
│   │   └── com
│   │       └── bytedance
│   │           └── importso
│   │               └── MainActivity.java
│   ├── lib
│   │   ├── arm64-v8a
│   │   │   └── libadder.so
│   │   └── armeabi-v7a
│   │       └── libadder.so

Step 2 编写 JNI

jni_src.cpp 中我们调用 Adder 这个类来完成功能

#include "adder.h"
#include <jni.h>extern "C"
JNIEXPORT jint JNICALL Java_com_bytedance_importso_MainActivity_add(JNIEnv* env, jobject obj, jint a, jint b)
{return Adder::add(a, b);
}

Step 3 编写 CMakeLists.txt

添加 CMakeLists.txt 在 main 文件夹下(其实位置随意),编写内容为:

cmake_minimum_required(VERSION 3.10)project(import_so_jni_project)include_directories(cpp/include)
add_library(import_so_jni SHARED cpp/jni_src.cpp)find_library(ADDER_LIBNAMES adderPATHS ${PROJECT_SOURCE_DIR}/lib/${ANDROID_ABI}NO_CMAKE_FIND_ROOT_PATH)target_link_libraries(import_so_jniPRIVATE ${ADDER_LIB})

上面的 cmake 中,我们通过 include_directories(cpp/include) 引入头文件,find_library 来引入需要的 so 文件,其中NO_CMAKE_FIND_ROOT_PATH 很重要,记得一定要加上,否则在 Android 环境下没法找到外部的 so

Step 4 修改 build.gradle

修改 build.gradle 文件有两个目的:

  1. 让 AS 去编译 cmake
  2. 让 AS 打包 App 时,把我们的 libadder.so 给打包进去

我们先解决第一个 cmake 编译的问题,在 build.gradle 中添加如下:

android {compileSdkVersion 29buildToolsVersion "29.0.3"defaultConfig {...// 0ndk{abiFilters "armeabi-v7a", "arm64-v8a"}// 1 externalNativeBuild{cmake {version "3.10.2"arguments "-DANDROID_STL=c++_shared"}}}...// 2externalNativeBuild{cmake{path "src/main/CMakeLists.txt"}}}

在 0 处添加 abiFilters 指明只需要 “armeabi-v7a”, “arm64-v8a”; 在 1 处添加 cmake 需要的参数;在 2 处指明 CMakeLists.txt 的路径

接着,我们添加 sourceSets,其中 jniLibs.srcDirs 指明外部 so 的位置,这样在打包的时候,会将里面的资源一起打包在 App 中。如果做这一步,app在启动的时候会出现 crash,日志出现 dlopen failed: library "libadder.so" not found 这样的错误

android {...sourceSets{main{jniLibs.srcDirs = ['src/main/lib']}}
}

Step 5 在 Java 中导入 jni 库

public class MainActivity extends AppCompatActivity {private static final String TAG = "MainActivity";// load jni sostatic {System.loadLibrary("import_so_jni");}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);Log.d(TAG, "onCreate: " + add(10, 10));}private native int add(int a, int b);
}

至此,所有工作已经完毕,让我们启动 app,就可以在 logcat 结果了,大功告成

D/MainActivity: onCreate: 20

总结

这篇博客主要描述如何将 so 导入 AS 中,通过一个具体的例子,说明了从交叉编译到AS集成so的具体步骤。
所有代码我已经上传至 GitHub,大家可以参考参考 Android_Studio_Import_so_Demo

Android Studio 导入 so 简明教程:通过一个示例让你理解整个过程相关推荐

  1. Android 开发之Windows环境下Android Studio安装和使用教程(图文详细步骤)

    鉴于谷歌最新推出的Android Studio备受开发者的推崇,所以也跟着体验一下. 一.介绍Android Studio  Android Studio 是一个Android开发环境,基于Intel ...

  2. Android Studio使用技巧系列教程(二)

    尊重劳动成果,转载请注明出处:http://blog.csdn.net/growth58/article/details/46764575 关注新浪微博:@于卫国 邮箱:yuweiguocn@gmai ...

  3. 安卓开发入门篇(一):Android Studio导入ApiDemos

    引言 本人程序员,之前做网站比较多,nodejs/express+html/css+mysql,再之前也做过Java开发,编程上还是有丰富的经验. 在持续的实战中,发现想做产品的话,前端似乎更重要,因 ...

  4. Android Studio导入Eclipse项目的两种方法

    Android Studio导入Eclipse项目有两种方法,一种是直接把Eclipse项目导入Android Studio,另一种是在Eclipse项目里面进行转换,然后再导入Android Stu ...

  5. Mac下Android studio 之NDK配置教程(一)

    Mac下Android studio 之NDK配置教程(一) 1.概述 近期项目全线转移到Mac下使用使用Android studio开发. 遇到关键代码封装到 ***native***层,此时在wi ...

  6. Android Studio导入第三方类库的方法

     Android Studio导入第三方类库的方法 本人也刚刚开始尝试做android app的开发,听说android studio是Google支持的android 应用开发工具,所以想应该肯 ...

  7. Android Studio导入Fresco

    大概一周之前,Facebook开源了专为Android系统定制的图片下载缓存工具,当天该消息就上了各大技术论坛网站的头条,也成为了各个技术群里讨论的最主要的话题.也就在当天stay4it的QQ群里面就 ...

  8. 【Android RTMP】Android Studio 集成 x264 开源库 ( Ubuntu 交叉编译 | Android Studio 导入函数库 )

    文章目录 安卓直播推流专栏博客总结 一. x264 简介 二. x264 交叉编译 三. Android Studio 导入函数库 四. 交叉编译版本 五. GitHub 项目地址 安卓直播推流专栏博 ...

  9. Mac下Android studio 之NDK配置教程(二)

    Mac下Android studio 之NDK配置教程(二) (一)简述 从上一篇NDK配置教程(一) 中,我 简单的阐述了MAC下NDK的基本解压和环境配置步骤. 本节我讲详细描述android s ...

最新文章

  1. 面试者面试官,双向角度的程序员面试指南!
  2. vagrant 简单使用
  3. codeblocks c++ 编译出错
  4. Android官方开发文档Training系列课程中文版:通过NFC共享文件之发送文件到另一台设备
  5. BugkuCTF-MISC题啊哒,白哥的鸽子
  6. MySQL 数据库设计规范
  7. 叫醒你的是闹钟,还是梦想?
  8. 痕迹清理 - Windows
  9. 扫雷游戏网页版_梦幻西游出网页版,王者出新英雄阿古朵,谁在杀死国产游戏的创新...
  10. winrar5.31 专用激活key
  11. 教你如何用python轻轻松松解析XML和PDF,一文就够了,赶紧码住!!!
  12. Linux寻找history命令位置,使用history命令在Linux系统上找到最常用的命令
  13. 不爱,就收起暧昧走开...
  14. 计算机的运作流程的个人感想
  15. 史上讲解最好的 Docker 教程,从入门到精通(建议收藏的教程)
  16. python黑白像素面积占比计算(脏污、白点等)
  17. es如何提升写入性能
  18. 使用JavaCC生成解析器(前言)
  19. cuda pytorch 环境变量_Windows10+CUDA 10.1.0+pytorch安装过程
  20. steam下载捆绑流氓软件??!

热门文章

  1. nodejs html引用js_nodejs做出最简单的网页服务端。【501】
  2. zblog php 调用缩略图,缩略图插件
  3. c++ array容器 传参_C++ 顺序容器基础知识总结
  4. mysql每天销售汇总_MySQL - 所有项目的每个总销售额
  5. 图片链接用src不能被爬虫爬到吗_爬虫:带你一键爬取王者荣耀英雄皮肤壁纸
  6. python类的初始化方法_python学习之-对象的的初始化与__init__方法
  7. 安卓学习笔记14:安卓手势操作编程
  8. 14.图像透视——介绍,坐标系统(Coordinate System),建模投影(Modelling Projection)_1
  9. Linux实战 | 使用Xshell连接Linux_2
  10. ie11浏览器可以下载java吗_如何卸载IE11? 如何安装低版本的IE浏览器?