微信号:guolin_blog

介绍:Android技术分享平台,在这里不仅可以学到各种Android相关的最新技术,还可以将你自己的技术总结分享给其他人,每周定期更新.

使用CMake来进行Android NDK开发

2017-10-10 08:00 宇宝守护神



今日科技快讯


昨日,日本东芝公司日前宣布开发出新一代电动车专用锂电池,快充仅需6分钟。传统电动车锂电池快充30分钟也只能充到约80%的电量,新一代锂电池快充仅需6分钟就能充到90%的电量。东芝公司测试用的电动车充电6分钟后最终跑了约320公里。目前,东芝公司计划对其进行完善,争取在2019年推出正式产品。 


前言


Android NDK 开发可能在平时的项目开发中不常用到,但是这并不代表其不重要,  

相反NDK开发是Android开发人员的进阶过程中必须要掌握的技能。  

Android NDK 是一组允许将C或C++(原生代码)嵌入到Android应用中的工具。  

如果开发者在需要以下操作的时候,使用NDK开发特别有用:  

  • 在平台之间移植其应用

  • 从设备获取卓越性能以用于计算密集型应用,例如游戏或物理模拟。  

  • 重复使用现有库或者提供自己库供重复使用。  

除此之外,对于 ndk 的学习,也有助于加深开发者在阅读框架的源码理解。  

ndk 开发有两种编译方式,一种是通过 ndk-build 来构建;  

一种是通过 CMake 构建原生库。通过 CMake 构建原生库是 Google 新提出来的方式,比较方便、强大。


准备


通过 cmake 进行ndk开发首先有个要求,需要 Android Studio 的版本是2.2以上版本(包含2.2) ,Gradle 的版本需要升到2.2.0及以上。  

满足上面的条件下,我们需要下载 ndk 和构建工具。如下图:

红线标记的三个工具下载好就行。CMake 和 NDK 就不说了,都好理解,  LLDB 呢是一种调试程序,用来调试原生代码的。  


向项目添加原生代码


上面的准备工作做完之后就可以向项目中添加原生代码,构建原生库进行ndk开发了。  

这边有个讨论,向项目中添加原生代码有两种情况:  

  • 一种是新建一个新的项目支持 C/C++;  

  • 一种是在已有项目中添加原生代码。  

所以这边相应的也有两种方式。先说第一种方式。

创建支持C/C++的新项目

创建支持原生代码的新项目跟平常开发中创建一个新项目没有太大的区别,说一下不同的地方。

  • 前者在向导的Config your new project界面需要选中Include C++ Support 复选框。如图1:

  • 前者在向导的Customize C++ Support 界面会有C++ Standard 的选择  意思是你希望使用哪种 C++ 标准。  选择 ToolchainDefault 会使用默认的 CMake 设置。  这里我们选择默认的。  

Exceptions Support:如果你希望启用对 C++ 异常处理的支持,请选中此复选框。  

如果启用此复选框,Android Studio 会将 -fexceptions 标志添加到模块级 build.gradle 文件的 cppFlags 中,Gradle 会将其传递到 CMake。

Runtime Type Information Support:如果你希望支持 RTTI,请选中此复选框。如果启用此复选框,Android Studio 会将 -frtti 标志添加到模块级 build.gradle 文件的   cppFlags 中,Gradle 会将其传递到 CMake。  这里自己看需求选择勾不勾选,这边演示的demo选择勾选。如图2:

创建项目完成之后,在 as 的左侧项目结构目录中的 app 应用模块中可以看到 cpp 文件夹,cpp 文件件里面存放有属于项目的所有原生源文件、标头和预构建库。对于新项目,Android Studio 会创建一个示例 C++ 源文件 native-lib.cpp 除了 cpp 文件夹之外,我们还看一个 CMakeLists.txt 的这样一个文件。这个文件是CMake的构建脚本,下面会详细说,这里就暂且不说。之前有说过新建支持 C/C++ 的项目会提供了一个示例的 c++ 源文件native-lib.cpp,存放在 cpp 文件夹中  ,我们点开看看内容

#include <jni.h> 
#include <string> 

extern "C" JNIEXPORT jstring JNICALL Java_com_example_ndkdemo_MainActivity_stringFromJNI(        JNIEnv *env,        jobject /* this */) { 
    std::string hello = "Hello from C++"; 
    return env->NewStringUTF(hello.c_str()); 
}

恩,是使用 C++ 写的一个方法,返回一个"Hello from C++",我们跟踪进去这个方法,看看哪里调用。

public class MainActivity extends AppCompatActivity { 

    // Used to load the 'native-lib' library on application startup. 
    static { 
        System.loadLibrary("native-lib"); 
    } 

    @Override 
    protected void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        setContentView(R.layout.activity_main); 

        // Example of a call to a native method 
        TextView tv = (TextView) findViewById(R.id.sample_text); 
        tv.setText(stringFromJNI()); 
    } 

    /**     * A native method that is implemented by the 'native-lib' native library,     * which is packaged with this application.     */ 
    public native String stringFromJNI(); 
}

我们发现,这个原生方法是在MainActivity中调用的,返回的字符串设置给了TextView。  

通过这两部分代码我们发现,原生方法通过native关键字来表示、在使用原生库之前,需要通过 System.loadLibrary(“库名称”)加载、原生方法的方法名命名等常见要点。这边就一笔略过。因为本文重点是CMake构建原生库。 所以ndk的一些基础知识,以及JNI等,就不细说。本文假设读者知道这些。这样的一个新的支持原生代码的项目创建之后,并且还自带demo,开发者就可以很方便在上面进行开发。 假设现在要创建一个原生库进行ndk开发,我们在cpp文件夹下new一个C/C++ Source file ,在你new出来的文件编写你的C/C++代码逻辑,然后在CMakeLists.txt配置文件上稍作配置,即可。下面会详述CMakeLists.txt 的配置。 

向现有项目添加原生代码

向现有项目添加原生代码,进行ndk开发,这里选择一个我的项目,空闲时间写的一个APP,  一款仿今日头条的资讯类软件(求star)-

PalmRead

https://github.com/MRYangY/PalmRead

主要有三个大步骤。第一步:创建新的原生源文件;第二步:创建CMake构建脚本;第三步:将Gradle关联到原生库。现在我们来看第一步,创建新的原生源文件,首先我们要先在应用模块src/main 目录下创建一个cpp文件夹用来存放原生源文件等。然后在cpp文件夹下面创建C/C++源文件New > C/C++ Source File。如图:

创建了一个名为 palmread-lib 文件。第二步,我们创建CMake构建脚本,也就是CMakeLists.txt 文件,在应用模块下new一个file文件,命名为 CMakeLists.txt 即可,注意哦,这个文件的名称不能搞错哦。CMakeLists.txt 创建好打开后,发现没有任何内容,这就对了。。需要我们自己配置的。不像是通过第一种新创建支持原生代码的项目那种,还给你写好,不过我们可以参考第一种方式下,系统给我们默认生成的内容。咱们看一下第一种方式下,系统默认生成的 CMakeLists.txt 文件的内容是什么样子吧。如下:

# Sets the minimum version of CMake required to build the native library. 

cmake_minimum_required(VERSION 3.4.1) 

# Creates and names a library, sets it as either STATIC 
# or SHARED, and provides the relative paths to its source code. 
# You can define multiple libraries, and CMake builds them for you. 
# Gradle automatically packages shared libraries with your APK. 

add_library( # Sets the name of the library. 
             native-lib 

             # Sets the library as a shared library. 
             SHARED 

             # Provides a relative path to your source file(s).             src/main/cpp/native-lib.cpp ) # Searches for a specified prebuilt library and stores the path as a # variable. Because CMake includes system libraries in the search path by # default, you only need to specify the name of the public NDK library # you want to add. CMake verifies that the library exists before # completing its build. find_library( # Sets the name of the path variable.              log-lib              # Specifies the name of the NDK library that              # you want CMake to locate.              log ) # Specifies libraries CMake should link to your target library. You # can link multiple libraries, such as libraries you define in this # build script, prebuilt third-party libraries, or system libraries. target_link_libraries( # Specifies the target library.                       native-lib                       # Links the target library to the log library                       # included in the NDK.                       ${log-lib} )

通过图片我们看到,其实内容也没什么。就cmake_minimum_required()add_library()、find_library、target_link_libraries()。这几个CMake命令,每个CMake命令都有英文介绍很好理解。比如add_library创建和命名一个库,这边名称我们就填palmread-lib,种类分为static和shared,具体区别嘛移步:

static和shared的区别

https://stackoverflow.com/questions/2649334/difference-between-static-and-shared-libraries

这里我们选择 SHARED,然后就是提供一个 library 的相对路径。只有在 CMakeLists.txt 文件中配置了该命令 ,才能找到编译这个库。其他的一些 CMake 命令大家自行搜索了解,都不是很难,很好理解,这里篇幅有限就不细说了。那现在我们可以按照系统提供的模板,基于自己的项目写了一份 CMakeLists.txt 文件,代码如下:

cmake_minimum_required(VERSION 3.4.1)  

add_library(  
             palmread-lib  

             SHARED  

             src/main/cpp/palmread-lib.c )  

find_library( # Sets the name of the path variable.  
              log-lib  

              # Specifies the name of the NDK library that  
              # you want CMake to locate.  
              log )  

target_link_libraries( # Specifies the target library.  
                       palmread-lib  

                       # Links the target library to the log library  
                       # included in the NDK.  
                       ${log-lib} )

随后在 Java 类中加载 palmread-lib 库,这里我这边写了一个 NdkHelper 类,专门用来定义 native 方法的:  

public class NdkHelper {  


    static {  
        System.loadLibrary("palmread-lib");  
    }  

    public static native String GetStringFromC(String str);  
}

这一步做完后,第二步也算告一段落了,现在我们来看第三步:将 Gradle 关联到你的原生库。 将 gradle 关联到原生库有两种方式,这里就先说第一种比较简单的方式,第二种下篇博文再说。 第一种方式是通过as的快捷键来实现的,右键点击你想要关联到原生库的模块(例如 app 模块), 并从菜单中选择 Link C++ Project with Gradle。 BuildSystem选择CMake,projectpath 就是CMakeLists.txt文件的路径。点击ok完成。 

你会在应用模块的build.gradle文件中发现,android闭包里出现了

externalNativeBuild { 
        cmake { 
            path 'CMakeLists.txt' 
        } 
    }

这个语句就跟我们之前说的第二种方式有关系,这里先不说了,后面博文再说。

同步项目后,突然想到之前新建的palmread-lib.c文件好像还没有写C代码呢,呀,这脑子,没事,我们现在写。  写些什么呢?这样吧我们用C来实现这样一个功能,接受一个字符串参数,然后对字符串进行拼接修改操作  ,使得返回一个新的字符串。  

代码如下:

#include "jni.h" #include <stdlib.h> 
#include <string.h> 

JNIEXPORT jstring JNICALL Java_com_example_yangyu_palmread_Logic_NdkHelper_GetStringFromC(JNIEnv *env, jclass type,                                                                jstring str_) { 
    const char *a = (*env)->GetStringUTFChars(env, str_, 0); 
    // TODO 
    char * b = " from c" ; 
    char *result = malloc(strlen(a)+strlen(b)+1); 
    strcpy(result, a); 
    strcat(result, b); 

    (*env)->ReleaseStringUTFChars(env, str_, a); 

    return (*env)->NewStringUTF(env, result); 
}

代码写好之后,就是去调用了,为了方便测试,我们选在在应用的首页activity去调用这个原生方法。  通过toast显示方法返回的值。  

如图:

通过代码,我们可以看出,我们调用了,NdkHelper.GetStringFromC()这个原生方法,传入一个“欢迎来到PalmRead”字符串作为参数。按照之前的 C 逻辑,应该会返回一个新的字符串为  “欢迎来到PalmRead from c”。  那我们运行程序试试效果吧。  

O(∩_∩)O哈!,我们发现toast显示的值确实是我们设想的返回值。这样就架起了Java与原生代码之间的桥梁。 现在我们可以应用模块的build/outputs/apk目录下,打开我们的apk,我们会发现,我们创建的原生文件,编译成了原生库"libpalmread-lib.so"也打包进了apk文件。如图:  


结语


好了,关于CMake来进行 Android NDK 开发,大致的流程就是这样。CMakeLists 的一些命令 还有Gradle关联原生库的第二种通过编辑build.gradle文件的方式 , 通过 CMake 来进行ndk开发之补充篇

http://blog.csdn.net/qq_34902522/article/details/78144127

 有详细说明。

最后给大家做个推荐,隔壁鸿洋新开发了一个Android开发者必看的网站,域名是 http://wanandroid.com ,里面有不少干货文章,大家有兴趣的话可以登录这个网站瞧一瞧。


欢迎长按下图 -> 识别图中二维码

或者 扫一扫 关注我的公众号

 
郭霖 更多文章 基于OkHttp和RxJava封装的Socket长连接开源库 探究Glide的自定义模块功能 大公司知名开源小组及项目介绍 Android地图轨迹抽稀、动态绘制 BouncingBallView 碰撞的小球
猜您喜欢 Folly源码分析系列(一) — ThreadLocalPtr 技术驱动无线新商业 阿里百川加速文娱内容变现 Android JavaPoet 动态生成Java源码(2) 金鸡贺新春,小华给您拜年啦! 高性能网络I/O入门(一)