Android NDK开发之旅(2):Android Studio中使用CMake进行NDK/JNI开发(初级)

Android NDK开发之旅(2):Android Studio中使用CMake进行NDK/JNI开发(初级)

 (码字不易,转载请声明出处:http://blog.csdn.net/andrexpert/article/details/72904462)

前  言

      上篇文章我们详细介绍了Eclipse中NDK/JNI开发的相关原理,由于谷歌停止了对Eclipse的更新,未来AndroidStudio必然会完全替代Eclipse。基于此,本文将详细介绍下AndroidStudio中使用CMake进行NDK/JNI开发,相比于使用ndk-build编译Android.mk和配置AS中的build.gradle编译的方式,CMake方式显得更加的简洁、高效。

1. 安装CMake、LLDB

     CMake是一款比make更强大的编译自动配置工具,它可以根据不同平台、不同的编译器,并通过CMakeLists.txt文件中简单的语句来描述所有平台的编译过程,生成相应的Makefile文件或project文件。CMake被引入于AndroidStudio2.2,其目的是替代原有的JNI/DNK开发方法,使AS在进行JNI/NDK时更加的方便、高效。CMake的优势如下:

(1)   允许直接在C/C++代码中加入断点,调试;

(2)   在Java层中使用“ctrl+左键”快捷键能够直接进入本地方法对应的C/C++代码中;

(3)   在C/C++中使用“ctrl+鼠标点击”快捷键能够直接进入头文件或库;

(4)   自动生成函数原型头文件,无需配置android.useDeprecatedNdk=true属性;

       LLDB是一个高效的C/C++调试器,它提供了丰富的流程控制和数据检测,有效地帮助我们调试程序。AndroidStudio通过引入LLDB调试器,能够实现对NDK本地代码的调试。

2. 配置NDK路径


3. 创建支持C/C++开发的Android工程

(1)   创建新的Android工程,勾选”IncludeC++ Support”选项;


     在Customize C++ Support部分,可以使用下列选项自定义项目:


   C++ Standard:选择哪一种C++标准,默认选择Toolchain Default选项,其会使用默认的Cmake配置;

   Exceptions Support:是否启用对C++异常处理的支持,如果选中,AS会将-fexceptions标志添加到模块级build.grade文件的cppFlags中;

   Runtime Type Information Support:是否支持RTTI,如果选中,AS会将-frtti标志添加到模块级build.gradle文件的cppFlags中;

(2)  Android工程结构剖析


       红框中的文件是创建工程时自动生成的,其中:

       (1) .externalNativeBuild文件夹:用于存放cmake编译好的文件,包括支持的各种硬件等信息,有点类似于build.gradle文件明确Gradle如何编译APP;

       (2) cpp文件夹:存放C/C++代码文件,native-lib.cpp文件默认生成的;

       (3) CMakeLists.txt:cmake脚本配置文件,cmake会根据该脚本文件中的指令去编译相关的C/C++源文件,并将编译后产物生成共享库或静态块,然后Gradle将其打包到APK中。CMakeLists.txt文件解析如下:

# 指定cmke版本
cmake_minimum_required(VERSION3.4.1)
# add_library()命令用于向CMake添加依赖源文件或库
# 指令需传入三个参数(函数库名称、库类型、依赖源文件相对路径)
add_library(  # 生成函数库的名称,即libnative-lib.so或libnative-lib.a(lib和.so/.a默认缺省)
             native-lib
             # 生成库类型:动态库为SHARED,静态库为STATIC
             SHARED
             # 依赖的c/cpp文件(相对路径)
             src/main/cpp/native-lib.cpp )
# find_library()命令用于定位NDK中的库
# 需传入两个参数(path变量、ndk库名称)
find_library(  # 设置path变量的名称,这里为NDK中的日志库
              log-lib
                            #指定cmake查询库的名称
                            #即在ndk开发包中查询liblog.so函数库,将其路径赋值给log-lib
              log )
#target_link_libraries()命令用于指定要关联到的原生库的库
target_link_libraries(# 指定目标库,与上面指定的函数库名一致
                  native-lib
                  # 链接的库,根据log-lib变量对应liblog.so函数库
                  ${log-lib} )

4. 运行效果

      通过查看native-lib.cpp方法,stringFromJNI目的是向Java层返回一个字符串。如果要在native-lib.cpp文件中添加新的方法,必须添加在extern"C" { } 中,或者在每个方法前加extern"C", 否则会报找不到方法。如果源文件为C,则须将extern“C”部分去掉,因为extern "C"的作用就是告诉编译器以C方式编译。

#include<jni.h>
#include<string>
extern"C"
jstring Java_com_jiangdg_hellojni_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello fromC++";
    return env->NewStringUTF(hello.c_str());
}

构建和运行APP流程:

  (1) Gradle调用外部构建脚本CmakeLists.txt;

  (2) CMake 按照构建脚本中的命令将 C++ 源文件 native-lib.cpp 编译到共享的对象库中,并命名为 libnative-lib.so,Gradle 随后会将其打包到 APK 中;

  (3) 运行时,应用的 MainActivity 会使用 System.loadLibrary() 加载原生库,至此,调用so中的方法才会生效。


如果需要查看so文件是否生成成功,可以使用Analyze APK 工具来实现。“Build->Analyze APK”,选择app/build/outputs/apk/app-debug.apk查看so文件生成情况:


5. 创建新的C文件

(1)  创建.c或.cpp源文件


(2) 修改CmakeLists.txt



       此时,你会发现在main/cpp目录下并没有刚刚新建的JniLearning.c、JniLearning.h文件,我们需要对Android功能进行同步下(Syncnow)。

(3) 添加新的native方法


(4)生成native方法对应的函数原型

       相比于Eclipse中使用javah命令生成头文件,AndroidStudio就太方便了。我们只需要选中定义的native本地方法,使用快捷键“Alt+Enter”即可自动生成相对应的函数原型。


* 非静态属性

       返回值类型Java_包名_类名_方法名(JNIEnv*,jobject,参数1类型,参数2类型,…)

* 静态属性

       返回值类型Java_包名_类名_方法名(JNIEnv*,jclass,参数1类型,参数2类型,…)


调用JNIUtils.calculate(1,100)运行的结果为:


注:关于JNI数据类型和基本使用,可参看我这篇文章

6. 向现有普通的AS项目添加C/C++代码(以CMake方式构建)

      假如有这么一种情况:由于当初在创建项目时,没有想过在该项目中支持C/C++开发,也就是说在创建项目的时候没有选中“Include C++ support”选项。但是,现在由于业务需要我们需要在本项目中支持C/C++开发,那么又该怎么做?分三步:

(1) 创建新的原生源文件到现有的Android Studio项目中

      将IDE切换到Project视图,右键点击模块(如app模块)的main目录,选择New->Directory创建一个命名为cpp的目录;然后按照第5步(1)创建源文件及其头文件

 

Java层:JnitUtil.class

/**
 * JNI本地方法工具类
 * Created by jiangdongguo on 2018/1/18.
 */

public class JniUtil {

    // 加载原生库,库名由CMakeLists.txt可知
    static {
        System.loadLibrary("HelloJni");
    }

    public native static String getMyName();

    public native int calculate(int a,int b);
}

C/C++层:hello_jni.cpp

//
// Created by jiangdongguo on 2018/1/18.
//

#include "hello_jni.h"

#include <jni.h>

extern "C"
JNIEXPORT jstring JNICALL
Java_com_jiangdg_androidndk_JniUtil_getMyName(JNIEnv *env, jclass type) {
    return env->NewStringUTF("Jiang dong guo");
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_jiangdg_androidndk_JniUtil_calculate(JNIEnv *env, jobject instance, jint a, jint b) {
    return (a+b);
}
说明:关于本地函数(native方法映射函数)的生成,可以通过"Alt+Enter"实现,通过会自动在app目录下生成一个jni目录并创建一个新的映射函数,而我们只需要将映射函数拷贝到hello_jni.cpp源文件中,然后再将整个jni目录删除。有了以上这一步,下一次我们使用"Alt+Enter"生成映射函数时,AS会自动找到hello_jni.cpp文件。此外,如果你对JNI开发足够熟悉,也可以根据native方法相关信息直接在hello_jni.cpp文件中敲,通常映射函数的格式是“JNIEXPORT 返回类型 JNICALL Java_包名_类名_函数名(JNIEnv *env,jobject jobj,参数列表...)”,需要注意的是如果native方法为静态方法,那么映射函数的第二个参数为jclass类型,如果为非静态方法,则为jobject类型。另外,extern "C"的作用实现C++代码调用其他C语言代码,加上该标志会指示编译器这部分代码按C语言的进行编译,而不是C++的。

(2) 创建CMake构建脚本

       将IDE切换到Project视图,右键点击模块(如app模块)的根目录,选择New->File创建一个命名为CMakeLists.txt的文件。CmakeLists.txt内容如下:

#设置工程支持的Cmake最小版本
cmake_minimum_required(VERSION 3.4.1)

#向CMake构建脚本添加源文件,并指定库名称和库类型
add_library( # 指定library名称,Cmake将创建一个名为libHelloJni.so文件
             HelloJni
             # 指定library类型
             SHARED
             # 源文件路径
             src/main/cpp/hello_jni.cpp
            )
# 指定头文件路径
include_directories(src/main/cpp/include/)
# 查找要引用DNK库,并将其路径保存到log-lib变量中
find_library(# 变量,用于保存DNK库(log)的路径
             log-lib
             # NDK库的名称,CMake将会根据名称定位到NDK库的log库
             log
            )
# 将log库关联到原生库
target_link_libraries( # Specifies the target library.
                       HelloJni

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

说明:Android NDK 提供了一套实用的原生 API 和库,预构建的 NDK 库已经存在于 Android 平台上,因此无需再构建或将其打包到 APK 中。甚至,我们不需要在本地NDK安装中指定库的位置,只需要向CMake提供希望使用库的名称,并将其关联到自己的原生库即可(可通过find_library()命令和target_link_libraries()命令实现)。

(3) 将Gradle关联到原生库,修改模块(比如app)如下所示:

android {
    compileSdkVersion 26
    defaultConfig {
        applicationId "com.jiangdg.helloworld"
        minSdkVersion 16
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        // 指定ABI
        ndk {
            abiFilters 'x86', 'x86_64', 'armeabi', 'armeabi-v7a',
                    'arm64-v8a'
        }
    }

    // externalNativeBuild用于封装外部原生构建配置
    externalNativeBuild {
        // 将Gradle关联到原生库,如果是ndk-build项目(下一节讨论),则用ndkBuild {...}代替
        cmake {
            path "CMakeLists.txt"
        }
    }
    
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

点击"Sync now",Project视图的app目录中会自动生成一个.externalNativeBuild目录,即可说明Gradle关联到原生库成功。然后,执行“”Run“”操作,你就会看到在工程的app/build/.../cmake目录下生成的so原生库:

                         

LogCat也会打印出以下结果,说明so库被调用成功:

01-18 09:32:24.820 7812-7812/com.jiangdg.androidndk I/AndroidNDKDemo: JniUtil.getMyName() = Jiang dong guo
01-18 09:32:24.820 7812-7812/com.jiangdg.androidndk I/AndroidNDKDemo: JniUtil.calculate(1,2) = 3

    写到这里,一个新的NDK工程算是构建完毕了,但是,当我们在编写cpp源码的时候,发现有一个很不好的体验,就是编写代码时居然没有代码补全和提示功能。那么,接下来还需要作最后一步,以实现编写C/C++代码时,代码自动提示功能。

首先,右击选中src/main/cpp目录,选择"Link C++ Project with Gradle"


然后,浏览选择本项目中的CmakeLists.txt文件,使cpp目录与之绑定即可


7. 向现有普通的AS项目添加C/C++代码(以ndkBuild方式构建)

    或许你会说:我对Cmake构建方式不熟悉,平时都是在Eclipse中通过ndk-build来构建原生库,对Android.mk比较熟悉,该怎么样在AS中以ndk-build方式构建呢?嗯,不捉急,在这一小节我们将阐述如何在现有的AS项目中以ndkBuild方式构建C/C++工程。

(1) 切换到Project视图,在app模块的main目录下创建一个jni目录(右键点击main目录,File->New->Folder->Jni Folder)。然后,按照第6小节的(1)创建JniUtils.class、hello_jni.cpp源文件,并分别创建Android.mk、Application.mk文件。整个视图如下:


Android.mk源码如下:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

LOCAL_MODULE := HelloJni2
LOCAL_SRC_FILES := hello_jni2.cpp

include $(BUILD_SHARED_LIBRARY)
Application.mk源码如下:

APP_MODULES := HelloJni2
# 指定生成哪些cpu架构的库
APP_ABI := armeabi  armeabi-v7a  x86 # all 即所有平台
(2) 修改app模块级gradle.build。很多博客都提到我们只需要在defaultConfig中添加ndk{...}模块,Android Studio会自动帮我们生成Android.mk文件而无需自己编写Android.mk,同时生成so原生库。这个方法我试过,会报C++....错误,究其原因是这种方式的ndkCompile已经被谷歌废弃了,因此无法对C++模块进行处理。因此,我们需要在sourceSets字段中使用jni.srcDirs = []禁止AS使用ndk-build编译生成Android.mk文件。当然,Google官方也提供了另外一种方式使用ndkBuild来构建,即通过externalNativeBuild{...}将Gradle关联到Android.mk,确实是可以的,但是相关的配置还不是很熟。这里直接使用执行"ndk-build"命令来构建。

apply plugin: 'com.android.application'

android {
    compileSdkVersion 26
    defaultConfig {
        applicationId "com.jiangdg.androidndkdemo2"
        minSdkVersion 15
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    sourceSets {
        main {
            // 禁止自动使用ndk-build编译生成Android.mk文件
            jni.srcDirs = []
            // 指定so文件生成的路径
            jniLibs.srcDirs = ['libs']
        }
    }
    // 指定mk文件的路径
    // externalNativeBuild {
    //    ndkBuild {
    //        path 'src/main/jni/Android.mk'
    //    }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
}

(3) 好了,C/C++源文件、Android.mk、Application.mk都已经做好了,然后就是修炼so原生库了。打开工具栏的"Terminal",使用cd命令切换到项目的jni目录,执行ndk-build命令,最后就会在app/main/libs目录生成指定平台的so原生库。

 

(4) 最后,将main/libs目录下所有平台架构的so,拷贝到app/libs目录下,这样app运行时才能真正调用到so,否则会提示unsatisefied异常


8. 本地代码调试

   众所周知,当使用Eclipse进行NDK/JNI开发时,基本无法对C/C++本地代码调试,而AndroidStudio却非常容易。AS通过借助LLDB调试工具,可以非常方便地对C/C++本地代码进行调试。LLDB调试的步骤基本与调试Java层代码一致,只是断点标志在C/C++函数中。


     从上面的图中可以看到除了Variables的Tab页以外,还有一个Tab页就是LLDB,点击进入可以看到(lldb)的命令行,在命令行里面可以输入LLDB的命令,LLDB命令有很多强大的能力,比如,打印,寻址,调用堆栈等,通过这些命令可以有效的帮助调试NDK程序。

GitHub项目地址:https://github.com/jiangdongguo/JniLearning


  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值